    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * -------------
    028 * RingPlot.java
    029 * -------------
    030 * (C) Copyright 2004-2011, by Object Refinery Limited.
    031 *
    032 * Original Author:  David Gilbert (for Object Refinery Limtied);
    033 * Contributor(s):   Christoph Beck (bug 2121818);
    034 *
    035 * Changes
    036 * -------
    037 * 08-Nov-2004 : Version 1 (DG);
    038 * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
    039 * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
    040 *               GradientPaint (DG);
    041 * ------------- JFREECHART 1.0.x ---------------------------------------------
    042 * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
    043 * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
    044 * 12-Oct-2006 : Added configurable section depth (DG);
    045 * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
    046 * 23-Sep-2008 : Fix for bug 2121818 by Christoph Beck (DG);
    047 * 13-Jul-2009 : Added support for shadow generator (DG);
    048 * 11-Oct-2011 : Check sectionOutlineVisible - bug 3237879 (DG);
    049 *
    050 */
    051
    052package org.jfree.chart.plot;
    053
    054import java.awt.BasicStroke;
    055import java.awt.Color;
    056import java.awt.Graphics2D;
    057import java.awt.Paint;
    058import java.awt.Shape;
    059import java.awt.Stroke;
    060import java.awt.geom.Arc2D;
    061import java.awt.geom.GeneralPath;
    062import java.awt.geom.Line2D;
    063import java.awt.geom.Rectangle2D;
    064import java.io.IOException;
    065import java.io.ObjectInputStream;
    066import java.io.ObjectOutputStream;
    067import java.io.Serializable;
    068
    069import org.jfree.chart.entity.EntityCollection;
    070import org.jfree.chart.entity.PieSectionEntity;
    071import org.jfree.chart.event.PlotChangeEvent;
    072import org.jfree.chart.labels.PieToolTipGenerator;
    073import org.jfree.chart.urls.PieURLGenerator;
    074import org.jfree.data.general.PieDataset;
    075import org.jfree.io.SerialUtilities;
    076import org.jfree.ui.RectangleInsets;
    077import org.jfree.util.ObjectUtilities;
    078import org.jfree.util.PaintUtilities;
    079import org.jfree.util.Rotation;
    080import org.jfree.util.ShapeUtilities;
    081import org.jfree.util.UnitType;
    082
    083/**
    084 * A customised pie plot that leaves a hole in the middle.
    085 */
    086public class RingPlot extends PiePlot implements Cloneable, Serializable {
    087
    088    /** For serialization. */
    089    private static final long serialVersionUID = 1556064784129676620L;
    090
    091    /**
    092     * A flag that controls whether or not separators are drawn between the
    093     * sections of the chart.
    094     */
    095    private boolean separatorsVisible;
    096
    097    /** The stroke used to draw separators. */
    098    private transient Stroke separatorStroke;
    099
    100    /** The paint used to draw separators. */
    101    private transient Paint separatorPaint;
    102
    103    /**
    104     * The length of the inner separator extension (as a percentage of the
    105     * depth of the sections).
    106     */
    107    private double innerSeparatorExtension;
    108
    109    /**
    110     * The length of the outer separator extension (as a percentage of the
    111     * depth of the sections).
    112     */
    113    private double outerSeparatorExtension;
    114
    115    /**
    116     * The depth of the section as a percentage of the diameter.
    117     */
    118    private double sectionDepth;
    119
    120    /**
    121     * Creates a new plot with a <code>null</code> dataset.
    122     */
    123    public RingPlot() {
    124        this(null);
    125    }
    126
    127    /**
    128     * Creates a new plot for the specified dataset.
    129     *
    130     * @param dataset  the dataset (<code>null</code> permitted).
    131     */
    132    public RingPlot(PieDataset dataset) {
    133        super(dataset);
    134        this.separatorsVisible = true;
    135        this.separatorStroke = new BasicStroke(0.5f);
    136        this.separatorPaint = Color.gray;
    137        this.innerSeparatorExtension = 0.20;  // twenty percent
    138        this.outerSeparatorExtension = 0.20;  // twenty percent
    139        this.sectionDepth = 0.20; // 20%
    140    }
    141
    142    /**
    143     * Returns a flag that indicates whether or not separators are drawn between
    144     * the sections in the chart.
    145     *
    146     * @return A boolean.
    147     *
    148     * @see #setSeparatorsVisible(boolean)
    149     */
    150    public boolean getSeparatorsVisible() {
    151        return this.separatorsVisible;
    152    }
    153
    154    /**
    155     * Sets the flag that controls whether or not separators are drawn between
    156     * the sections in the chart, and sends a {@link PlotChangeEvent} to all
    157     * registered listeners.
    158     *
    159     * @param visible  the flag.
    160     *
    161     * @see #getSeparatorsVisible()
    162     */
    163    public void setSeparatorsVisible(boolean visible) {
    164        this.separatorsVisible = visible;
    165        fireChangeEvent();
    166    }
    167
    168    /**
    169     * Returns the separator stroke.
    170     *
    171     * @return The stroke (never <code>null</code>).
    172     *
    173     * @see #setSeparatorStroke(Stroke)
    174     */
    175    public Stroke getSeparatorStroke() {
    176        return this.separatorStroke;
    177    }
    178
    179    /**
    180     * Sets the stroke used to draw the separator between sections and sends
    181     * a {@link PlotChangeEvent} to all registered listeners.
    182     *
    183     * @param stroke  the stroke (<code>null</code> not permitted).
    184     *
    185     * @see #getSeparatorStroke()
    186     */
    187    public void setSeparatorStroke(Stroke stroke) {
    188        if (stroke == null) {
    189            throw new IllegalArgumentException("Null 'stroke' argument.");
    190        }
    191        this.separatorStroke = stroke;
    192        fireChangeEvent();
    193    }
    194
    195    /**
    196     * Returns the separator paint.
    197     *
    198     * @return The paint (never <code>null</code>).
    199     *
    200     * @see #setSeparatorPaint(Paint)
    201     */
    202    public Paint getSeparatorPaint() {
    203        return this.separatorPaint;
    204    }
    205
    206    /**
    207     * Sets the paint used to draw the separator between sections and sends a
    208     * {@link PlotChangeEvent} to all registered listeners.
    209     *
    210     * @param paint  the paint (<code>null</code> not permitted).
    211     *
    212     * @see #getSeparatorPaint()
    213     */
    214    public void setSeparatorPaint(Paint paint) {
    215        if (paint == null) {
    216            throw new IllegalArgumentException("Null 'paint' argument.");
    217        }
    218        this.separatorPaint = paint;
    219        fireChangeEvent();
    220    }
    221
    222    /**
    223     * Returns the length of the inner extension of the separator line that
    224     * is drawn between sections, expressed as a percentage of the depth of
    225     * the section.
    226     *
    227     * @return The inner separator extension (as a percentage).
    228     *
    229     * @see #setInnerSeparatorExtension(double)
    230     */
    231    public double getInnerSeparatorExtension() {
    232        return this.innerSeparatorExtension;
    233    }
    234
    235    /**
    236     * Sets the length of the inner extension of the separator line that is
    237     * drawn between sections, as a percentage of the depth of the
    238     * sections, and sends a {@link PlotChangeEvent} to all registered
    239     * listeners.
    240     *
    241     * @param percent  the percentage.
    242     *
    243     * @see #getInnerSeparatorExtension()
    244     * @see #setOuterSeparatorExtension(double)
    245     */
    246    public void setInnerSeparatorExtension(double percent) {
    247        this.innerSeparatorExtension = percent;
    248        fireChangeEvent();
    249    }
    250
    251    /**
    252     * Returns the length of the outer extension of the separator line that
    253     * is drawn between sections, expressed as a percentage of the depth of
    254     * the section.
    255     *
    256     * @return The outer separator extension (as a percentage).
    257     *
    258     * @see #setOuterSeparatorExtension(double)
    259     */
    260    public double getOuterSeparatorExtension() {
    261        return this.outerSeparatorExtension;
    262    }
    263
    264    /**
    265     * Sets the length of the outer extension of the separator line that is
    266     * drawn between sections, as a percentage of the depth of the
    267     * sections, and sends a {@link PlotChangeEvent} to all registered
    268     * listeners.
    269     *
    270     * @param percent  the percentage.
    271     *
    272     * @see #getOuterSeparatorExtension()
    273     */
    274    public void setOuterSeparatorExtension(double percent) {
    275        this.outerSeparatorExtension = percent;
    276        fireChangeEvent();
    277    }
    278
    279    /**
    280     * Returns the depth of each section, expressed as a percentage of the
    281     * plot radius.
    282     *
    283     * @return The depth of each section.
    284     *
    285     * @see #setSectionDepth(double)
    286     * @since 1.0.3
    287     */
    288    public double getSectionDepth() {
    289        return this.sectionDepth;
    290    }
    291
    292    /**
    293     * The section depth is given as percentage of the plot radius.
    294     * Specifying 1.0 results in a straightforward pie chart.
    295     *
    296     * @param sectionDepth  the section depth.
    297     *
    298     * @see #getSectionDepth()
    299     * @since 1.0.3
    300     */
    301    public void setSectionDepth(double sectionDepth) {
    302        this.sectionDepth = sectionDepth;
    303        fireChangeEvent();
    304    }
    305
    306    /**
    307     * Initialises the plot state (which will store the total of all dataset
    308     * values, among other things).  This method is called once at the
    309     * beginning of each drawing.
    310     *
    311     * @param g2  the graphics device.
    312     * @param plotArea  the plot area (<code>null</code> not permitted).
    313     * @param plot  the plot.
    314     * @param index  the secondary index (<code>null</code> for primary
    315     *               renderer).
    316     * @param info  collects chart rendering information for return to caller.
    317     *
    318     * @return A state object (maintains state information relevant to one
    319     *         chart drawing).
    320     */
    321    public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
    322            PiePlot plot, Integer index, PlotRenderingInfo info) {
    323
    324        PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
    325        state.setPassesRequired(3);
    326        return state;
    327
    328    }
    329
    330    /**
    331     * Draws a single data item.
    332     *
    333     * @param g2  the graphics device (<code>null</code> not permitted).
    334     * @param section  the section index.
    335     * @param dataArea  the data plot area.
    336     * @param state  state information for one chart.
    337     * @param currentPass  the current pass index.
    338     */
    339    protected void drawItem(Graphics2D g2,
    340                            int section,
    341                            Rectangle2D dataArea,
    342                            PiePlotState state,
    343                            int currentPass) {
    344
    345        PieDataset dataset = getDataset();
    346        Number n = dataset.getValue(section);
    347        if (n == null) {
    348            return;
    349        }
    350        double value = n.doubleValue();
    351        double angle1 = 0.0;
    352        double angle2 = 0.0;
    353
    354        Rotation direction = getDirection();
    355        if (direction == Rotation.CLOCKWISE) {
    356            angle1 = state.getLatestAngle();
    357            angle2 = angle1 - value / state.getTotal() * 360.0;
    358        }
    359        else if (direction == Rotation.ANTICLOCKWISE) {
    360            angle1 = state.getLatestAngle();
    361            angle2 = angle1 + value / state.getTotal() * 360.0;
    362        }
    363        else {
    364            throw new IllegalStateException("Rotation type not recognised.");
    365        }
    366
    367        double angle = (angle2 - angle1);
    368        if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
    369            Comparable key = getSectionKey(section);
    370            double ep = 0.0;
    371            double mep = getMaximumExplodePercent();
    372            if (mep > 0.0) {
    373                ep = getExplodePercent(key) / mep;
    374            }
    375            Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
    376                    state.getExplodedPieArea(), angle1, angle, ep);
    377            Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
    378                    Arc2D.OPEN);
    379
    380            // create the bounds for the inner arc
    381            double depth = this.sectionDepth / 2.0;
    382            RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
    383                depth, depth, depth, depth);
    384            Rectangle2D innerArcBounds = new Rectangle2D.Double();
    385            innerArcBounds.setRect(arcBounds);
    386            s.trim(innerArcBounds);
    387            // calculate inner arc in reverse direction, for later
    388            // GeneralPath construction
    389            Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
    390                    + angle, -angle, Arc2D.OPEN);
    391            GeneralPath path = new GeneralPath();
    392            path.moveTo((float) arc.getStartPoint().getX(),
    393                    (float) arc.getStartPoint().getY());
    394            path.append(arc.getPathIterator(null), false);
    395            path.append(arc2.getPathIterator(null), true);
    396            path.closePath();
    397
    398            Line2D separator = new Line2D.Double(arc2.getEndPoint(),
    399                    arc.getStartPoint());
    400
    401            if (currentPass == 0) {
    402                Paint shadowPaint = getShadowPaint();
    403                double shadowXOffset = getShadowXOffset();
    404                double shadowYOffset = getShadowYOffset();
    405                if (shadowPaint != null && getShadowGenerator() == null) {
    406                    Shape shadowArc = ShapeUtilities.createTranslatedShape(
    407                            path, (float) shadowXOffset, (float) shadowYOffset);
    408                    g2.setPaint(shadowPaint);
    409                    g2.fill(shadowArc);
    410                }
    411            }
    412            else if (currentPass == 1) {
    413                Paint paint = lookupSectionPaint(key);
    414                g2.setPaint(paint);
    415                g2.fill(path);
    416                Paint outlinePaint = lookupSectionOutlinePaint(key);
    417                Stroke outlineStroke = lookupSectionOutlineStroke(key);
    418                if (getSectionOutlinesVisible() && outlinePaint != null
    419                        && outlineStroke != null) {
    420                    g2.setPaint(outlinePaint);
    421                    g2.setStroke(outlineStroke);
    422                    g2.draw(path);
    423                }
    424
    425                // add an entity for the pie section
    426                if (state.getInfo() != null) {
    427                    EntityCollection entities = state.getEntityCollection();
    428                    if (entities != null) {
    429                        String tip = null;
    430                        PieToolTipGenerator toolTipGenerator
    431                                = getToolTipGenerator();
    432                        if (toolTipGenerator != null) {
    433                            tip = toolTipGenerator.generateToolTip(dataset,
    434                                    key);
    435                        }
    436                        String url = null;
    437                        PieURLGenerator urlGenerator = getURLGenerator();
    438                        if (urlGenerator != null) {
    439                            url = urlGenerator.generateURL(dataset, key,
    440                                    getPieIndex());
    441                        }
    442                        PieSectionEntity entity = new PieSectionEntity(path,
    443                                dataset, getPieIndex(), section, key, tip,
    444                                url);
    445                        entities.add(entity);
    446                    }
    447                }
    448            }
    449            else if (currentPass == 2) {
    450                if (this.separatorsVisible) {
    451                    Line2D extendedSeparator = extendLine(separator,
    452                        this.innerSeparatorExtension,
    453                        this.outerSeparatorExtension);
    454                    g2.setStroke(this.separatorStroke);
    455                    g2.setPaint(this.separatorPaint);
    456                    g2.draw(extendedSeparator);
    457                }
    458            }
    459        }
    460        state.setLatestAngle(angle2);
    461    }
    462
    463    /**
    464     * This method overrides the default value for cases where the ring plot
    465     * is very thin.  This fixes bug 2121818.
    466     *
    467     * @return The label link depth, as a percentage of the plot's radius.
    468     */
    469    protected double getLabelLinkDepth() {
    470        return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
    471    }
    472
    473    /**
    474     * Tests this plot for equality with an arbitrary object.
    475     *
    476     * @param obj  the object to test against (<code>null</code> permitted).
    477     *
    478     * @return A boolean.
    479     */
    480    public boolean equals(Object obj) {
    481        if (this == obj) {
    482            return true;
    483        }
    484        if (!(obj instanceof RingPlot)) {
    485            return false;
    486        }
    487        RingPlot that = (RingPlot) obj;
    488        if (this.separatorsVisible != that.separatorsVisible) {
    489            return false;
    490        }
    491        if (!ObjectUtilities.equal(this.separatorStroke,
    492                that.separatorStroke)) {
    493            return false;
    494        }
    495        if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
    496            return false;
    497        }
    498        if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
    499            return false;
    500        }
    501        if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
    502            return false;
    503        }
    504        if (this.sectionDepth != that.sectionDepth) {
    505            return false;
    506        }
    507        return super.equals(obj);
    508    }
    509
    510    /**
    511     * Creates a new line by extending an existing line.
    512     *
    513     * @param line  the line (<code>null</code> not permitted).
    514     * @param startPercent  the amount to extend the line at the start point
    515     *                      end.
    516     * @param endPercent  the amount to extend the line at the end point end.
    517     *
    518     * @return A new line.
    519     */
    520    private Line2D extendLine(Line2D line, double startPercent,
    521                              double endPercent) {
    522        if (line == null) {
    523            throw new IllegalArgumentException("Null 'line' argument.");
    524        }
    525        double x1 = line.getX1();
    526        double x2 = line.getX2();
    527        double deltaX = x2 - x1;
    528        double y1 = line.getY1();
    529        double y2 = line.getY2();
    530        double deltaY = y2 - y1;
    531        x1 = x1 - (startPercent * deltaX);
    532        y1 = y1 - (startPercent * deltaY);
    533        x2 = x2 + (endPercent * deltaX);
    534        y2 = y2 + (endPercent * deltaY);
    535        return new Line2D.Double(x1, y1, x2, y2);
    536    }
    537
    538    /**
    539     * Provides serialization support.
    540     *
    541     * @param stream  the output stream.
    542     *
    543     * @throws IOException  if there is an I/O error.
    544     */
    545    private void writeObject(ObjectOutputStream stream) throws IOException {
    546        stream.defaultWriteObject();
    547        SerialUtilities.writeStroke(this.separatorStroke, stream);
    548        SerialUtilities.writePaint(this.separatorPaint, stream);
    549    }
    550
    551    /**
    552     * Provides serialization support.
    553     *
    554     * @param stream  the input stream.
    555     *
    556     * @throws IOException  if there is an I/O error.
    557     * @throws ClassNotFoundException  if there is a classpath problem.
    558     */
    559    private void readObject(ObjectInputStream stream)
    560        throws IOException, ClassNotFoundException {
    561        stream.defaultReadObject();
    562        this.separatorStroke = SerialUtilities.readStroke(stream);
    563        this.separatorPaint = SerialUtilities.readPaint(stream);
    564    }
    565
    566}