    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
    006 *
    007 * http://www.object-refinery.com/orsoncharts/index.html
    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 com.orsoncharts.plot;
    034
    035import java.awt.Color;
    036import java.awt.Font;
    037import java.io.Serializable;
    038import java.util.ArrayList;
    039import java.util.List;
    040
    041import com.orsoncharts.Chart3D;
    042import com.orsoncharts.data.PieDataset3D;
    043import com.orsoncharts.util.ArgChecks;
    044import com.orsoncharts.graphics3d.Dimension3D;
    045import com.orsoncharts.graphics3d.Dot3D;
    046import com.orsoncharts.graphics3d.Object3D;
    047import com.orsoncharts.graphics3d.World;
    048import com.orsoncharts.legend.LegendItemInfo;
    049import com.orsoncharts.legend.StandardLegendItemInfo;
    050import com.orsoncharts.Chart3DFactory;
    051import com.orsoncharts.ChartElementVisitor;
    052import com.orsoncharts.data.DataUtils;
    053import com.orsoncharts.data.ItemKey;
    054import com.orsoncharts.data.KeyedValuesItemKey;
    055import com.orsoncharts.label.PieLabelGenerator;
    056import com.orsoncharts.label.StandardPieLabelGenerator;
    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 * <object id="ABC" data="../../../doc-files/PieChart3DDemo1.svg"
    064 * type="image/svg+xml" width="500" height="359">
    065 * </object>
    066 * </div>
    067 * <br><br>
    068 * NOTE: This class is serializable, but the serialization format is subject
    069 * to change in future releases and should not be relied upon for persisting
    070 * instances of this class.
    071 */
    072@SuppressWarnings("serial")
    073public class PiePlot3D extends AbstractPlot3D implements Serializable {
    074
    075    /** The default font for section labels on the chart. */
    076    public static final Font DEFAULT_SECTION_LABEL_FONT
    077            = new Font("Dialog", Font.PLAIN, 14);
    078
    079    /** The dataset. */
    080    private PieDataset3D dataset;
    081
    082    /** The radius of the pie chart. */
    083    private double radius;
    084
    085    /** The depth of the pie chart. */
    086    private double depth;
    087
    088    /** The section color source. */
    089    private ColorSource sectionColorSource;
    090
    091    /** The section label generator. */
    092    private PieLabelGenerator sectionLabelGenerator;
    093
    094    /** The font source used to determine the font for section labels. */
    095    private FontSource sectionLabelFontSource;
    096
    097    /**
    098     * The color source used to determine the foreground color for section
    099     * labels.
    100     */
    101    private ColorSource sectionLabelColorSource;
    102
    103    /** The legend label generator. */
    104    private PieLabelGenerator legendLabelGenerator;
    105
    106    /**
    107     * The tool tip generator (can be null, in which case there will be no
    108     * tool tips. */
    109    private PieLabelGenerator toolTipGenerator;
    110
    111    /**
    112     * The number of segments used to render 360 degrees of the pie.  A higher
    113     * number will give better output but slower performance.
    114     */
    115    private int segments = 40;
    116
    117    /**
    118     * Creates a new pie plot in 3D.
    119     *
    120     * @param dataset  the dataset ({@code null} not permitted).
    121     */
    122    public PiePlot3D(PieDataset3D dataset) {
    123        ArgChecks.nullNotPermitted(dataset, "dataset");
    124        this.dataset = dataset;
    125        this.dataset.addChangeListener(this);
    126        this.radius = 4.0;
    127        this.depth = 0.5;
    128        this.sectionColorSource = new StandardColorSource();
    129        this.sectionLabelGenerator = new StandardPieLabelGenerator(
    130                StandardPieLabelGenerator.KEY_ONLY_TEMPLATE);
    131        this.sectionLabelFontSource = new StandardFontSource(
    132                DEFAULT_SECTION_LABEL_FONT);
    133        this.sectionLabelColorSource = new StandardColorSource(Color.BLACK);
    134        this.legendLabelGenerator = new StandardPieLabelGenerator();
    135        this.toolTipGenerator = new StandardPieLabelGenerator(
    136                StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP);
    137    }
    138
    139    /**
    140     * Returns the dataset.
    141     *
    142     * @return The dataset (never {@code null}).
    143     */
    144    public PieDataset3D getDataset() {
    145        return this.dataset;
    146    }
    147
    148    /**
    149     * Sets the dataset and notifies registered listeners that the dataset has
    150     * been updated.
    151     *
    152     * @param dataset  the dataset ({@code null} not permitted).
    153     */
    154    public void setDataset(PieDataset3D dataset) {
    155        ArgChecks.nullNotPermitted(dataset, "dataset");
    156        this.dataset.removeChangeListener(this);
    157        this.dataset = dataset;
    158        this.dataset.addChangeListener(this);
    159        fireChangeEvent(true);
    160    }
    161
    162    /**
    163     * Returns the radius of the pie (the default value is 8.0).
    164     *
    165     * @return The radius of the pie.
    166     */
    167    public double getRadius() {
    168        return this.radius;
    169    }
    170
    171    /**
    172     * Sets the radius of the pie chart and sends a change event to all
    173     * registered listeners.
    174     *
    175     * @param radius  the radius.
    176     */
    177    public void setRadius(double radius) {
    178        this.radius = radius;
    179        fireChangeEvent(true);
    180    }
    181
    182    /**
    183     * Returns the depth of the pie (the default value is 2.0).
    184     *
    185     * @return The depth of the pie.
    186     */
    187    public double getDepth() {
    188        return this.depth;
    189    }
    190
    191    /**
    192     * Sets the depth of the pie chart and sends a change event to all
    193     * registered listeners.
    194     *
    195     * @param depth  the depth.
    196     */
    197    public void setDepth(double depth) {
    198        this.depth = depth;
    199        fireChangeEvent(true);
    200    }
    201
    202    /**
    203     * Returns the color source for section colors.
    204     *
    205     * @return The color source (never {@code null}).
    206     */
    207    public ColorSource getSectionColorSource() {
    208        return this.sectionColorSource;
    209    }
    210
    211    /**
    212     * Sets the color source and sends a {@link Plot3DChangeEvent} to all
    213     * registered listeners.
    214     *
    215     * @param source  the color source ({@code null} not permitted).
    216     */
    217    public void setSectionColorSource(ColorSource source) {
    218        ArgChecks.nullNotPermitted(source, "source");
    219        this.sectionColorSource = source;
    220        fireChangeEvent(true);
    221    }
    222
    223    /**
    224     * Sets a new color source for the plot using the specified colors and
    225     * sends a {@link Plot3DChangeEvent} to all registered listeners. This
    226     * is a convenience method that is equivalent to
    227     * {@code setSectionColorSource(new StandardColorSource(colors))}.
    228     *
    229     * @param colors  one or more colors ({@code null} not permitted).
    230     *
    231     * @since 1.2
    232     */
    233    public void setSectionColors(Color... colors) {
    234        setSectionColorSource(new StandardColorSource(colors));
    235    }
    236
    237    /**
    238     * Returns the object that creates labels for each section of the pie
    239     * chart.
    240     *
    241     * @return The section label generator (never {@code null}).
    242     *
    243     * @since 1.2
    244     */
    245    public PieLabelGenerator getSectionLabelGenerator() {
    246        return this.sectionLabelGenerator;
    247    }
    248
    249    /**
    250     * Sets the object that creates labels for each section of the pie chart,
    251     * and sends a {@link Plot3DChangeEvent} to all registered listeners.
    252     *
    253     * @param generator  the generator ({@code null} not permitted).
    254     *
    255     * @since 1.2
    256     */
    257    public void setSectionLabelGenerator(PieLabelGenerator generator) {
    258        ArgChecks.nullNotPermitted(generator, "generator");
    259        this.sectionLabelGenerator = generator;
    260        fireChangeEvent(false);
    261    }
    262
    263    /**
    264     * Returns the font source that is used to determine the font to use for
    265     * the section labels.
    266     *
    267     * @return The font source for the section labels (never {@code null}).
    268     */
    269    public FontSource getSectionLabelFontSource() {
    270        return this.sectionLabelFontSource;
    271    }
    272
    273    /**
    274     * Sets the font source and sends a {@link Plot3DChangeEvent} to all
    275     * registered listeners.
    276     *
    277     * @param source  the source ({@code null} not permitted).
    278     */
    279    public void setSectionLabelFontSource(FontSource source) {
    280        ArgChecks.nullNotPermitted(source, "source");
    281        this.sectionLabelFontSource = source;
    282        fireChangeEvent(false);
    283    }
    284
    285    /**
    286     * Returns the color source for section labels.  The default value is
    287     * an instance of {@link StandardColorSource} that always returns
    288     * {@code Color.BLACK}.
    289     *
    290     * @return The color source (never {@code null}).
    291     *
    292     * @see #setSectionLabelColorSource(ColorSource)
    293     */
    294    public ColorSource getSectionLabelColorSource() {
    295        return this.sectionLabelColorSource;
    296    }
    297
    298    /**
    299     * Sets the color source for the section labels and sends a
    300     * {@link Plot3DChangeEvent} to all registered listeners.
    301     *
    302     * @param source  the color source.
    303     *
    304     * @see #getSectionLabelColorSource()
    305     */
    306    public void setSectionLabelColorSource(ColorSource source) {
    307        ArgChecks.nullNotPermitted(source, "source");
    308        this.sectionLabelColorSource = source;
    309        fireChangeEvent(false);
    310    }
    311
    312    /**
    313     * Returns the object that creates legend labels for each section of the pie
    314     * chart.
    315     *
    316     * @return The legend label generator (never {@code null}).
    317     *
    318     * @since 1.2
    319     */
    320    public PieLabelGenerator getLegendLabelGenerator() {
    321        return this.legendLabelGenerator;
    322    }
    323
    324    /**
    325     * Sets the object that creates legend labels for each section of the pie
    326     * chart, and sends a {@link Plot3DChangeEvent} to all registered
    327     * listeners.
    328     *
    329     * @param generator  the generator ({@code null} not permitted).
    330     *
    331     * @since 1.2
    332     */
    333    public void setLegendLabelGenerator(PieLabelGenerator generator) {
    334        ArgChecks.nullNotPermitted(generator, "generator");
    335        this.legendLabelGenerator = generator;
    336        fireChangeEvent(false);
    337    }
    338
    339    /**
    340     * Returns the tool tip generator.
    341     *
    342     * @return The tool tip generator (possibly {@code null}).
    343     *
    344     * @since 1.3
    345     */
    346    public PieLabelGenerator getToolTipGenerator() {
    347        return this.toolTipGenerator;
    348    }
    349
    350    /**
    351     * Sets the tool tip generator and sends a change event to all registered
    352     * listeners.
    353     *
    354     * @param generator  the generator ({@code null} permitted).
    355     *
    356     * @since 1.3
    357     */
    358    public void setToolTipGenerator(PieLabelGenerator generator) {
    359        this.toolTipGenerator = generator;
    360        fireChangeEvent(false);
    361    }
    362
    363    /**
    364     * Returns the dimensions for the plot.  For the pie chart, it is more
    365     * natural to specify the dimensions in terms of a radius and a depth, so
    366     * we use those values to calculate the dimensions here.
    367     *
    368     * @return The dimensions for the plot.
    369     */
    370    @Override
    371    public Dimension3D getDimensions() {
    372        return new Dimension3D(this.radius * 2, this.depth, this.radius * 2);
    373    }
    374
    375    /**
    376     * Returns the number of segments used when composing the 3D objects
    377     * representing the pie chart.  The default value is {@code 40}.
    378     *
    379     * @return The number of segments used to compose the pie chart.
    380     */
    381    public int getSegmentCount() {
    382        return this.segments;
    383    }
    384
    385    /**
    386     * Sets the number of segments used when composing the pie chart and
    387     * sends a {@link Plot3DChangeEvent} to all registered listeners.  A higher
    388     * number will result in a more rounded pie chart, but will take longer
    389     * to render.
    390     *
    391     * @param count  the count.
    392     */
    393    public void setSegmentCount(int count) {
    394        this.segments = count;
    395        fireChangeEvent(true);
    396    }
    397
    398    /**
    399     * Returns a list containing legend item info, typically one item for
    400     * each series in the chart.  This is intended for use in the construction
    401     * of a chart legend.
    402     *
    403     * @return A list containing legend item info.
    404     */
    405    @Override
    406    public List<LegendItemInfo> getLegendInfo() {
    407        List<LegendItemInfo> result = new ArrayList<LegendItemInfo>();
    408        for (Comparable<?> key : this.dataset.getKeys()) {
    409            String label = this.legendLabelGenerator.generateLabel(dataset,
    410                    key);
    411            LegendItemInfo info = new StandardLegendItemInfo(key,
    412                    label, this.sectionColorSource.getColor(key));
    413            result.add(info);
    414        }
    415        return result;
    416    }
    417
    418    /**
    419     * Adds 3D objects representing the current data for the plot to the
    420     * specified world.  After the world has been populated (or constructed) in
    421     * this way, it is ready for rendering.  This method is called by the
    422     * {@link Chart3D} class, you won't normally call it directly.
    423     *
    424     * @param world  the world ({@code null} not permitted).
    425     * @param xOffset  the x-offset.
    426     * @param yOffset  the y-offset.
    427     * @param zOffset  the z-offset.
    428     */
    429    @Override
    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<Object3D>();
    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}