/* =========================================================== * Orson Charts : a 3D chart library for the Java(tm) platform * =========================================================== * * (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved. * * http://www.object-refinery.com/orsoncharts/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * Orson Charts home page: * * http://www.object-refinery.com/orsoncharts/index.html * */ package com.orsoncharts.plot; import java.awt.Color; import java.awt.Font; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.orsoncharts.Chart3D; import com.orsoncharts.data.PieDataset3D; import com.orsoncharts.util.ArgChecks; import com.orsoncharts.graphics3d.Dimension3D; import com.orsoncharts.graphics3d.Dot3D; import com.orsoncharts.graphics3d.Object3D; import com.orsoncharts.graphics3d.World; import com.orsoncharts.legend.LegendItemInfo; import com.orsoncharts.legend.StandardLegendItemInfo; import com.orsoncharts.Chart3DFactory; import com.orsoncharts.ChartElementVisitor; import com.orsoncharts.data.DataUtils; import com.orsoncharts.data.ItemKey; import com.orsoncharts.data.KeyedValuesItemKey; import com.orsoncharts.label.PieLabelGenerator; import com.orsoncharts.label.StandardPieLabelGenerator; /** * A plot for creating 3D pie charts. To create a pie chart, you can use the * {@code createPieChart()} method in the {@link Chart3DFactory} class. * A typical pie chart will look like this: * <div> * <object id="ABC" data="../../../doc-files/PieChart3DDemo1.svg" * type="image/svg+xml" width="500" height="359"> * </object> * </div> * <br><br> * NOTE: This class is serializable, but the serialization format is subject * to change in future releases and should not be relied upon for persisting * instances of this class. */ @SuppressWarnings("serial") public class PiePlot3D extends AbstractPlot3D implements Serializable { /** The default font for section labels on the chart. */ public static final Font DEFAULT_SECTION_LABEL_FONT = new Font("Dialog", Font.PLAIN, 14); /** The dataset. */ private PieDataset3D<? extends Comparable> dataset; /** The radius of the pie chart. */ private double radius; /** The depth of the pie chart. */ private double depth; /** The section color source. */ private ColorSource sectionColorSource; /** The section label generator. */ private PieLabelGenerator sectionLabelGenerator; /** The font source used to determine the font for section labels. */ private FontSource sectionLabelFontSource; /** * The color source used to determine the foreground color for section * labels. */ private ColorSource sectionLabelColorSource; /** The legend label generator. */ private PieLabelGenerator legendLabelGenerator; /** * The tool tip generator (can be null, in which case there will be no * tool tips. */ private PieLabelGenerator toolTipGenerator; /** * The number of segments used to render 360 degrees of the pie. A higher * number will give better output but slower performance. */ private int segments = 40; /** * Creates a new pie plot in 3D. * * @param dataset the dataset ({@code null} not permitted). */ public PiePlot3D(PieDataset3D<? extends Comparable> dataset) { ArgChecks.nullNotPermitted(dataset, "dataset"); this.dataset = dataset; this.dataset.addChangeListener(this); this.radius = 4.0; this.depth = 0.5; this.sectionColorSource = new StandardColorSource(); this.sectionLabelGenerator = new StandardPieLabelGenerator( StandardPieLabelGenerator.KEY_ONLY_TEMPLATE); this.sectionLabelFontSource = new StandardFontSource( DEFAULT_SECTION_LABEL_FONT); this.sectionLabelColorSource = new StandardColorSource(Color.BLACK); this.legendLabelGenerator = new StandardPieLabelGenerator(); this.toolTipGenerator = new StandardPieLabelGenerator( StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP); } /** * Returns the dataset. * * @return The dataset (never {@code null}). */ public PieDataset3D<? extends Comparable> getDataset() { return this.dataset; } /** * Sets the dataset and notifies registered listeners that the dataset has * been updated. * * @param dataset the dataset ({@code null} not permitted). */ public void setDataset(PieDataset3D<? extends Comparable> dataset) { ArgChecks.nullNotPermitted(dataset, "dataset"); this.dataset.removeChangeListener(this); this.dataset = dataset; this.dataset.addChangeListener(this); fireChangeEvent(true); } /** * Returns the radius of the pie (the default value is 8.0). * * @return The radius of the pie. */ public double getRadius() { return this.radius; } /** * Sets the radius of the pie chart and sends a change event to all * registered listeners. * * @param radius the radius. */ public void setRadius(double radius) { this.radius = radius; fireChangeEvent(true); } /** * Returns the depth of the pie (the default value is 2.0). * * @return The depth of the pie. */ public double getDepth() { return this.depth; } /** * Sets the depth of the pie chart and sends a change event to all * registered listeners. * * @param depth the depth. */ public void setDepth(double depth) { this.depth = depth; fireChangeEvent(true); } /** * Returns the color source for section colors. * * @return The color source (never {@code null}). */ public ColorSource getSectionColorSource() { return this.sectionColorSource; } /** * Sets the color source and sends a {@link Plot3DChangeEvent} to all * registered listeners. * * @param source the color source ({@code null} not permitted). */ public void setSectionColorSource(ColorSource source) { ArgChecks.nullNotPermitted(source, "source"); this.sectionColorSource = source; fireChangeEvent(true); } /** * Sets a new color source for the plot using the specified colors and * sends a {@link Plot3DChangeEvent} to all registered listeners. This * is a convenience method that is equivalent to * {@code setSectionColorSource(new StandardColorSource(colors))}. * * @param colors one or more colors ({@code null} not permitted). * * @since 1.2 */ public void setSectionColors(Color... colors) { setSectionColorSource(new StandardColorSource(colors)); } /** * Returns the object that creates labels for each section of the pie * chart. * * @return The section label generator (never {@code null}). * * @since 1.2 */ public PieLabelGenerator getSectionLabelGenerator() { return this.sectionLabelGenerator; } /** * Sets the object that creates labels for each section of the pie chart, * and sends a {@link Plot3DChangeEvent} to all registered listeners. * * @param generator the generator ({@code null} not permitted). * * @since 1.2 */ public void setSectionLabelGenerator(PieLabelGenerator generator) { ArgChecks.nullNotPermitted(generator, "generator"); this.sectionLabelGenerator = generator; fireChangeEvent(false); } /** * Returns the font source that is used to determine the font to use for * the section labels. * * @return The font source for the section labels (never {@code null}). */ public FontSource getSectionLabelFontSource() { return this.sectionLabelFontSource; } /** * Sets the font source and sends a {@link Plot3DChangeEvent} to all * registered listeners. * * @param source the source ({@code null} not permitted). */ public void setSectionLabelFontSource(FontSource source) { ArgChecks.nullNotPermitted(source, "source"); this.sectionLabelFontSource = source; fireChangeEvent(false); } /** * Returns the color source for section labels. The default value is * an instance of {@link StandardColorSource} that always returns * {@code Color.BLACK}. * * @return The color source (never {@code null}). * * @see #setSectionLabelColorSource(ColorSource) */ public ColorSource getSectionLabelColorSource() { return this.sectionLabelColorSource; } /** * Sets the color source for the section labels and sends a * {@link Plot3DChangeEvent} to all registered listeners. * * @param source the color source. * * @see #getSectionLabelColorSource() */ public void setSectionLabelColorSource(ColorSource source) { ArgChecks.nullNotPermitted(source, "source"); this.sectionLabelColorSource = source; fireChangeEvent(false); } /** * Returns the object that creates legend labels for each section of the pie * chart. * * @return The legend label generator (never {@code null}). * * @since 1.2 */ public PieLabelGenerator getLegendLabelGenerator() { return this.legendLabelGenerator; } /** * Sets the object that creates legend labels for each section of the pie * chart, and sends a {@link Plot3DChangeEvent} to all registered * listeners. * * @param generator the generator ({@code null} not permitted). * * @since 1.2 */ public void setLegendLabelGenerator(PieLabelGenerator generator) { ArgChecks.nullNotPermitted(generator, "generator"); this.legendLabelGenerator = generator; fireChangeEvent(false); } /** * Returns the tool tip generator. * * @return The tool tip generator (possibly {@code null}). * * @since 1.3 */ public PieLabelGenerator getToolTipGenerator() { return this.toolTipGenerator; } /** * Sets the tool tip generator and sends a change event to all registered * listeners. * * @param generator the generator ({@code null} permitted). * * @since 1.3 */ public void setToolTipGenerator(PieLabelGenerator generator) { this.toolTipGenerator = generator; fireChangeEvent(false); } /** * Returns the dimensions for the plot. For the pie chart, it is more * natural to specify the dimensions in terms of a radius and a depth, so * we use those values to calculate the dimensions here. * * @return The dimensions for the plot. */ @Override public Dimension3D getDimensions() { return new Dimension3D(this.radius * 2, this.depth, this.radius * 2); } /** * Returns the number of segments used when composing the 3D objects * representing the pie chart. The default value is {@code 40}. * * @return The number of segments used to compose the pie chart. */ public int getSegmentCount() { return this.segments; } /** * Sets the number of segments used when composing the pie chart and * sends a {@link Plot3DChangeEvent} to all registered listeners. A higher * number will result in a more rounded pie chart, but will take longer * to render. * * @param count the count. */ public void setSegmentCount(int count) { this.segments = count; fireChangeEvent(true); } /** * Returns a list containing legend item info, typically one item for * each series in the chart. This is intended for use in the construction * of a chart legend. * * @return A list containing legend item info. */ @Override @SuppressWarnings("unchecked") public List<LegendItemInfo> getLegendInfo() { List<LegendItemInfo> result = new ArrayList<LegendItemInfo>(); for (Comparable<?> key : (List<Comparable<?>>) this.dataset.getKeys()) { String label = this.legendLabelGenerator.generateLabel(dataset, key); LegendItemInfo info = new StandardLegendItemInfo(key, label, this.sectionColorSource.getColor(key)); result.add(info); } return result; } /** * Adds 3D objects representing the current data for the plot to the * specified world. After the world has been populated (or constructed) in * this way, it is ready for rendering. This method is called by the * {@link Chart3D} class, you won't normally call it directly. * * @param world the world ({@code null} not permitted). * @param xOffset the x-offset. * @param yOffset the y-offset. * @param zOffset the z-offset. */ @Override @SuppressWarnings("unchecked") public void compose(World world, double xOffset, double yOffset, double zOffset) { double total = DataUtils.total(this.dataset); double r = 0.0; int count = this.dataset.getItemCount(); for (int i = 0; i < count; i++) { Comparable<?> key = this.dataset.getKey(i); Number n = (Number) this.dataset.getValue(i); if (n != null) { double angle = Math.PI * 2 * (n.doubleValue() / total); Color c = this.sectionColorSource.getColor( this.dataset.getKey(i)); Object3D segment = Object3D.createPieSegment(this.radius, 0.0, yOffset, this.depth, r, r + angle, Math.PI / this.segments, c); segment.setProperty(Object3D.ITEM_KEY, new KeyedValuesItemKey(key)); world.add(segment); r = r + angle; } } } /** * Returns a list of label faces for the plot. These are non-visible * objects added to the 3D model of the pie chart to track the positions * for labels (which are added after the plot is projected and rendered). * <br><br> * NOTE: This method is public so that it can be called by the * {@link Chart3D} class - you won't normally call it directly. * * @param xOffset the x-offset. * @param yOffset the y-offset. * @param zOffset the z-offset. * * @return A list of label faces. */ public List<Object3D> getLabelFaces(double xOffset, double yOffset, double zOffset) { double total = DataUtils.total(this.dataset); List<Object3D> result = new ArrayList<Object3D>(); // this adds the centre points result.add(new Dot3D(0.0f, 0.0f, 0.0f, Color.RED)); result.add(new Dot3D(0.0f, (float) yOffset, 0.0f, Color.RED)); double r = 0.0; int count = this.dataset.getItemCount(); for (int i = 0; i < count; i++) { Number n = (Number) this.dataset.getValue(i); double angle = 0.0; if (n != null) { angle = Math.PI * 2 * (n.doubleValue() / total); } result.addAll(Object3D.createPieLabelMarkers(this.radius * 1.2, 0.0, yOffset - this.depth * 0.05, this.depth * 1.1, r, r + angle)); r = r + angle; } return result; } @Override public String generateToolTipText(ItemKey itemKey) { if (!(itemKey instanceof KeyedValuesItemKey)) { throw new IllegalArgumentException( "The itemKey must be a ValuesItemKey instance."); } KeyedValuesItemKey vik = (KeyedValuesItemKey) itemKey; return this.toolTipGenerator.generateLabel(this.dataset, vik.getKey()); } /** * Receives a visitor. This is a general purpose mechanism, but the main * use is to apply chart style changes across all the elements of a * chart. * * @param visitor the visitor ({@code null} not permitted). * * @since 1.2 */ @Override public void receive(ChartElementVisitor visitor) { visitor.visit(this); } /** * Tests this plot for equality with an arbitrary object. Note that the * plot's dataset is NOT considered in the equality test. * * @param obj the object ({@code null} not permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof PiePlot3D)) { return false; } PiePlot3D that = (PiePlot3D) obj; if (this.radius != that.radius) { return false; } if (this.depth != that.depth) { return false; } if (!this.sectionColorSource.equals(that.sectionColorSource)) { return false; } if (!this.sectionLabelGenerator.equals(that.sectionLabelGenerator)) { return false; } if (!this.sectionLabelFontSource.equals(that.sectionLabelFontSource)) { return false; } if (!this.sectionLabelColorSource.equals( that.sectionLabelColorSource)) { return false; } if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) { return false; } if (!this.toolTipGenerator.equals(that.toolTipGenerator)) { return false; } if (this.segments != that.segments) { return false; } return super.equals(obj); } @Override public int hashCode() { int hash = 5; hash = 97 * hash + (int) (Double.doubleToLongBits(this.radius) ^ (Double.doubleToLongBits(this.radius) >>> 32)); hash = 97 * hash + (int) (Double.doubleToLongBits(this.depth) ^ (Double.doubleToLongBits(this.depth) >>> 32)); hash = 97 * hash + this.sectionColorSource.hashCode(); hash = 97 * hash + this.sectionLabelGenerator.hashCode(); hash = 97 * hash + this.sectionLabelFontSource.hashCode(); hash = 97 * hash + this.sectionLabelColorSource.hashCode(); hash = 97 * hash + this.segments; return hash; } }