    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}