001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.plot;
034
035import java.awt.Color;
036import java.awt.Font;
037import java.io.Serializable;
038import java.util.ArrayList;
039import java.util.List;
040
041import org.jfree.chart3d.internal.Args;
042import org.jfree.chart3d.Chart3D;
043import org.jfree.chart3d.Chart3DFactory;
044import org.jfree.chart3d.ChartElementVisitor;
045import org.jfree.chart3d.data.DataUtils;
046import org.jfree.chart3d.data.ItemKey;
047import org.jfree.chart3d.data.KeyedValuesItemKey;
048import org.jfree.chart3d.data.PieDataset3D;
049import org.jfree.chart3d.graphics3d.Dimension3D;
050import org.jfree.chart3d.graphics3d.Dot3D;
051import org.jfree.chart3d.graphics3d.Object3D;
052import org.jfree.chart3d.graphics3d.World;
053import org.jfree.chart3d.label.PieLabelGenerator;
054import org.jfree.chart3d.label.StandardPieLabelGenerator;
055import org.jfree.chart3d.legend.LegendItemInfo;
056import org.jfree.chart3d.legend.StandardLegendItemInfo;
057
058/**
059 * A plot for creating 3D pie charts.  To create a pie chart, you can use the 
060 * {@code createPieChart()} method in the {@link Chart3DFactory} class.  
061 * A typical pie chart will look like this:  
062 * <div>
063 * <img src="../../../../../doc-files/PieChart3DDemo1.svg" alt="PieChart3DDemo1.svg" 
064 * width="500" height="359">
065 * </div>
066 * <br><br>
067 * NOTE: This class is serializable, but the serialization format is subject 
068 * to change in future releases and should not be relied upon for persisting 
069 * instances of this class. 
070 */
071@SuppressWarnings("serial")
072public class PiePlot3D extends AbstractPlot3D implements Serializable {
073    
074    /** The default font for section labels on the chart. */
075    public static final Font DEFAULT_SECTION_LABEL_FONT 
076            = new Font("Dialog", Font.PLAIN, 14);
077    
078    /** The dataset. */
079    private PieDataset3D<? extends Comparable> dataset;
080
081    /** The radius of the pie chart. */
082    private double radius; 
083  
084    /** The depth of the pie chart. */
085    private double depth;
086  
087    /** The section color source. */
088    private ColorSource sectionColorSource;
089
090    /** The section label generator. */
091    private PieLabelGenerator sectionLabelGenerator;
092    
093    /** The font source used to determine the font for section labels. */
094    private FontSource sectionLabelFontSource;
095
096    /** 
097     * The color source used to determine the foreground color for section 
098     * labels. 
099     */
100    private ColorSource sectionLabelColorSource;
101    
102    /** The legend label generator. */
103    private PieLabelGenerator legendLabelGenerator;
104    
105    /** 
106     * The tool tip generator (can be null, in which case there will be no
107     * tool tips. */
108    private PieLabelGenerator toolTipGenerator;
109    
110    /** 
111     * The number of segments used to render 360 degrees of the pie.  A higher
112     * number will give better output but slower performance.
113     */
114    private int segments = 40;
115  
116    /**
117     * Creates a new pie plot in 3D.
118     * 
119     * @param dataset  the dataset ({@code null} not permitted). 
120     */
121    public PiePlot3D(PieDataset3D<? extends Comparable> dataset) {
122        Args.nullNotPermitted(dataset, "dataset");
123        this.dataset = dataset;
124        this.dataset.addChangeListener(this);
125        this.radius = 4.0;    
126        this.depth = 0.5;
127        this.sectionColorSource = new StandardColorSource();
128        this.sectionLabelGenerator = new StandardPieLabelGenerator(
129                StandardPieLabelGenerator.KEY_ONLY_TEMPLATE);
130        this.sectionLabelFontSource = new StandardFontSource(
131                DEFAULT_SECTION_LABEL_FONT);
132        this.sectionLabelColorSource = new StandardColorSource(Color.BLACK);
133        this.legendLabelGenerator = new StandardPieLabelGenerator();
134        this.toolTipGenerator = new StandardPieLabelGenerator(
135                StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP);
136    }
137
138    /**
139     * Returns the dataset.
140     * 
141     * @return The dataset (never {@code null}). 
142     */
143    public PieDataset3D<? extends Comparable> getDataset() {
144        return this.dataset;
145    }
146
147    /**
148     * Sets the dataset and notifies registered listeners that the dataset has
149     * been updated.
150     * 
151     * @param dataset  the dataset ({@code null} not permitted). 
152     */
153    public void setDataset(PieDataset3D<? extends Comparable> dataset) {
154        Args.nullNotPermitted(dataset, "dataset");
155        this.dataset.removeChangeListener(this);
156        this.dataset = dataset;
157        this.dataset.addChangeListener(this);
158        fireChangeEvent(true);
159    }
160
161    /**
162     * Returns the radius of the pie (the default value is 8.0).
163     * 
164     * @return The radius of the pie. 
165     */
166    public double getRadius() {
167        return this.radius;
168    }
169  
170    /**
171     * Sets the radius of the pie chart and sends a change event to all 
172     * registered listeners.
173     * 
174     * @param radius  the radius. 
175     */
176    public void setRadius(double radius) {
177        this.radius = radius;
178        fireChangeEvent(true);
179    }
180  
181    /**
182     * Returns the depth of the pie (the default value is 2.0).
183     * 
184     * @return The depth of the pie. 
185     */
186    public double getDepth() {
187        return this.depth;
188    }
189
190    /**
191     * Sets the depth of the pie chart and sends a change event to all 
192     * registered listeners.
193     * 
194     * @param depth  the depth. 
195     */
196    public void setDepth(double depth) {
197        this.depth = depth;
198        fireChangeEvent(true);
199    }
200    
201    /**
202     * Returns the color source for section colors.
203     * 
204     * @return The color source (never {@code null}).
205     */
206    public ColorSource getSectionColorSource() {
207        return this.sectionColorSource;
208    }
209    
210    /**
211     * Sets the color source and sends a {@link Plot3DChangeEvent} to all 
212     * registered listeners.
213     * 
214     * @param source  the color source ({@code null} not permitted). 
215     */
216    public void setSectionColorSource(ColorSource source) {
217        Args.nullNotPermitted(source, "source");
218        this.sectionColorSource = source;
219        fireChangeEvent(true);
220    }
221    
222    /**
223     * Sets a new color source for the plot using the specified colors and
224     * sends a {@link Plot3DChangeEvent} to all registered listeners. This 
225     * is a convenience method that is equivalent to 
226     * {@code setSectionColorSource(new StandardColorSource(colors))}.
227     * 
228     * @param colors  one or more colors ({@code null} not permitted).
229     * 
230     * @since 1.2
231     */
232    public void setSectionColors(Color... colors) {
233        setSectionColorSource(new StandardColorSource(colors));
234    }
235
236    /**
237     * Returns the object that creates labels for each section of the pie
238     * chart.
239     * 
240     * @return The section label generator (never {@code null}).
241     * 
242     * @since 1.2
243     */
244    public PieLabelGenerator getSectionLabelGenerator() {
245        return this.sectionLabelGenerator;    
246    }
247    
248    /**
249     * Sets the object that creates labels for each section of the pie chart,
250     * and sends a {@link Plot3DChangeEvent} to all registered listeners.
251     * 
252     * @param generator  the generator ({@code null} not permitted).
253     * 
254     * @since 1.2
255     */
256    public void setSectionLabelGenerator(PieLabelGenerator generator) {
257        Args.nullNotPermitted(generator, "generator");
258        this.sectionLabelGenerator = generator;
259        fireChangeEvent(false);
260    }
261    
262    /**
263     * Returns the font source that is used to determine the font to use for 
264     * the section labels.
265     * 
266     * @return The font source for the section labels (never {@code null}). 
267     */
268    public FontSource getSectionLabelFontSource() {
269        return this.sectionLabelFontSource; 
270    }
271    
272    /**
273     * Sets the font source and sends a {@link Plot3DChangeEvent} to all
274     * registered listeners.
275     * 
276     * @param source  the source ({@code null} not permitted). 
277     */
278    public void setSectionLabelFontSource(FontSource source) {
279        Args.nullNotPermitted(source, "source");
280        this.sectionLabelFontSource = source;
281        fireChangeEvent(false);
282    }
283
284    /**
285     * Returns the color source for section labels.  The default value is
286     * an instance of {@link StandardColorSource} that always returns
287     * {@code Color.BLACK}.
288     * 
289     * @return The color source (never {@code null}).
290     * 
291     * @see #setSectionLabelColorSource(ColorSource) 
292     */
293    public ColorSource getSectionLabelColorSource() {
294        return this.sectionLabelColorSource;
295    }
296    
297    /**
298     * Sets the color source for the section labels and sends a 
299     * {@link Plot3DChangeEvent} to all registered listeners.
300     * 
301     * @param source  the color source. 
302     * 
303     * @see #getSectionLabelColorSource() 
304     */
305    public void setSectionLabelColorSource(ColorSource source) {
306        Args.nullNotPermitted(source, "source");
307        this.sectionLabelColorSource = source;
308        fireChangeEvent(false);
309    }
310    
311    /**
312     * Returns the object that creates legend labels for each section of the pie
313     * chart.
314     * 
315     * @return The legend label generator (never {@code null}).
316     * 
317     * @since 1.2
318     */
319    public PieLabelGenerator getLegendLabelGenerator() {
320        return this.legendLabelGenerator;
321    }
322    
323    /**
324     * Sets the object that creates legend labels for each section of the pie 
325     * chart, and sends a {@link Plot3DChangeEvent} to all registered 
326     * listeners.
327     * 
328     * @param generator  the generator ({@code null} not permitted).
329     * 
330     * @since 1.2
331     */
332    public void setLegendLabelGenerator(PieLabelGenerator generator) {
333        Args.nullNotPermitted(generator, "generator");
334        this.legendLabelGenerator = generator;
335        fireChangeEvent(false);
336    }
337    
338    /**
339     * Returns the tool tip generator.
340     * 
341     * @return The tool tip generator (possibly {@code null}).
342     * 
343     * @since 1.3
344     */
345    public PieLabelGenerator getToolTipGenerator() {
346        return this.toolTipGenerator;
347    }
348    
349    /**
350     * Sets the tool tip generator and sends a change event to all registered
351     * listeners.
352     * 
353     * @param generator  the generator ({@code null} permitted).
354     * 
355     * @since 1.3
356     */
357    public void setToolTipGenerator(PieLabelGenerator generator) {
358        this.toolTipGenerator = generator;
359        fireChangeEvent(false);
360    }
361
362    /**
363     * Returns the dimensions for the plot.  For the pie chart, it is more 
364     * natural to specify the dimensions in terms of a radius and a depth, so
365     * we use those values to calculate the dimensions here.
366     * 
367     * @return The dimensions for the plot. 
368     */
369    @Override
370    public Dimension3D getDimensions() {
371        return new Dimension3D(this.radius * 2, this.depth, this.radius * 2);
372    }
373 
374    /**
375     * Returns the number of segments used when composing the 3D objects
376     * representing the pie chart.  The default value is {@code 40}.
377     * 
378     * @return The number of segments used to compose the pie chart. 
379     */
380    public int getSegmentCount() {
381        return this.segments;
382    }
383    
384    /**
385     * Sets the number of segments used when composing the pie chart and 
386     * sends a {@link Plot3DChangeEvent} to all registered listeners.  A higher
387     * number will result in a more rounded pie chart, but will take longer
388     * to render.
389     * 
390     * @param count  the count. 
391     */
392    public void setSegmentCount(int count) {
393        this.segments = count;
394        fireChangeEvent(true);
395    }
396    
397    /**
398     * Returns a list containing legend item info, typically one item for
399     * each series in the chart.  This is intended for use in the construction
400     * of a chart legend.
401     * 
402     * @return A list containing legend item info.
403     */
404    @Override @SuppressWarnings("unchecked")
405    public List<LegendItemInfo> getLegendInfo() {
406        List<LegendItemInfo> result = new ArrayList<>();
407        for (Comparable<?> key : this.dataset.getKeys()) {
408            String label = this.legendLabelGenerator.generateLabel(dataset, 
409                    key);
410            LegendItemInfo info = new StandardLegendItemInfo(key, 
411                    label, this.sectionColorSource.getColor(key));
412            result.add(info);
413        }
414        return result;
415    }
416    
417    /**
418     * Adds 3D objects representing the current data for the plot to the 
419     * specified world.  After the world has been populated (or constructed) in
420     * this way, it is ready for rendering.  This method is called by the
421     * {@link Chart3D} class, you won't normally call it directly.
422     * 
423     * @param world  the world ({@code null} not permitted).
424     * @param xOffset  the x-offset.
425     * @param yOffset  the y-offset.
426     * @param zOffset  the z-offset.
427     */
428    @Override
429    @SuppressWarnings("unchecked")
430    public void compose(World world, double xOffset, double yOffset, 
431            double zOffset) {
432        double total = DataUtils.total(this.dataset);
433        double r = 0.0;
434        int count = this.dataset.getItemCount();
435        for (int i = 0; i < count; i++) {
436            Comparable<?> key = this.dataset.getKey(i);
437            Number n = this.dataset.getValue(i);
438            if (n != null) {
439                double angle = Math.PI * 2 * (n.doubleValue() / total);
440                Color c = this.sectionColorSource.getColor(
441                        this.dataset.getKey(i));
442                Object3D segment = Object3D.createPieSegment(this.radius, 0.0, 
443                        yOffset, this.depth, r, r + angle, 
444                        Math.PI / this.segments, c);
445                segment.setProperty(Object3D.ITEM_KEY, 
446                        new KeyedValuesItemKey(key));
447                world.add(segment);
448                r = r + angle;
449            }
450        }
451    }
452  
453    /**
454     * Returns a list of label faces for the plot.  These are non-visible 
455     * objects added to the 3D model of the pie chart to track the positions 
456     * for labels (which are added after the plot is projected and rendered).  
457     * <br><br>
458     * NOTE: This method is public so that it can be called by the 
459     * {@link Chart3D} class - you won't normally call it directly.
460     * 
461     * @param xOffset  the x-offset.
462     * @param yOffset  the y-offset.
463     * @param zOffset  the z-offset.
464     * 
465     * @return A list of label faces.
466     */
467    public List<Object3D> getLabelFaces(double xOffset, double yOffset, 
468            double zOffset) {
469        double total = DataUtils.total(this.dataset);
470        List<Object3D> result = new ArrayList<>();
471        // this adds the centre points
472        result.add(new Dot3D(0.0f, 0.0f, 0.0f, Color.RED));
473        result.add(new Dot3D(0.0f, (float) yOffset, 0.0f, Color.RED));
474        double r = 0.0;
475        int count = this.dataset.getItemCount();
476        for (int i = 0; i < count; i++) {
477            Number n = this.dataset.getValue(i);
478            double angle = 0.0;
479            if (n != null) {
480                angle = Math.PI * 2 * (n.doubleValue() / total);
481            }
482            result.addAll(Object3D.createPieLabelMarkers(this.radius * 1.2,
483                    0.0, yOffset - this.depth * 0.05, this.depth * 1.1, r, 
484                    r + angle));
485            r = r + angle;
486        }
487        return result;
488    }
489
490    @Override
491    public String generateToolTipText(ItemKey itemKey) {
492        if (!(itemKey instanceof KeyedValuesItemKey)) {
493            throw new IllegalArgumentException(
494                    "The itemKey must be a ValuesItemKey instance.");
495        }
496        KeyedValuesItemKey vik = (KeyedValuesItemKey) itemKey;
497        return this.toolTipGenerator.generateLabel(this.dataset, vik.getKey());
498    }
499
500    /**
501     * Receives a visitor.  This is a general purpose mechanism, but the main
502     * use is to apply chart style changes across all the elements of a 
503     * chart.
504     * 
505     * @param visitor  the visitor ({@code null} not permitted).
506     * 
507     * @since 1.2
508     */
509    @Override
510    public void receive(ChartElementVisitor visitor) { 
511        visitor.visit(this);
512    }
513
514    /**
515     * Tests this plot for equality with an arbitrary object.  Note that the
516     * plot's dataset is NOT considered in the equality test.
517     * 
518     * @param obj  the object ({@code null} not permitted).
519     * 
520     * @return A boolean. 
521     */
522    @Override
523    public boolean equals(Object obj) {
524        if (obj == this) {
525            return true;
526        }
527        if (!(obj instanceof PiePlot3D)) {
528            return false;
529        }
530        PiePlot3D that = (PiePlot3D) obj;
531        if (this.radius != that.radius) {
532            return false;
533        }
534        if (this.depth != that.depth) {
535            return false;
536        }
537        if (!this.sectionColorSource.equals(that.sectionColorSource)) {
538            return false;
539        }
540        if (!this.sectionLabelGenerator.equals(that.sectionLabelGenerator)) {
541            return false;
542        }
543        if (!this.sectionLabelFontSource.equals(that.sectionLabelFontSource)) {
544            return false;
545        }
546        if (!this.sectionLabelColorSource.equals(
547                that.sectionLabelColorSource)) {
548            return false;
549        }
550        if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) {
551            return false;
552        }
553        if (!this.toolTipGenerator.equals(that.toolTipGenerator)) {
554            return false;
555        }
556        if (this.segments != that.segments) {
557            return false;
558        }
559        return super.equals(obj);
560    }
561
562    @Override
563    public int hashCode() {
564        int hash = 5;
565        hash = 97 * hash + (int) (Double.doubleToLongBits(this.radius) 
566                ^ (Double.doubleToLongBits(this.radius) >>> 32));
567        hash = 97 * hash + (int) (Double.doubleToLongBits(this.depth) 
568                ^ (Double.doubleToLongBits(this.depth) >>> 32));
569        hash = 97 * hash + this.sectionColorSource.hashCode();
570        hash = 97 * hash + this.sectionLabelGenerator.hashCode();
571        hash = 97 * hash + this.sectionLabelFontSource.hashCode();
572        hash = 97 * hash + this.sectionLabelColorSource.hashCode();
573        hash = 97 * hash + this.segments;
574        return hash;
575    }
576
577}