    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
    052    package org.jfree.chart.plot;
    053
    054    import java.awt.BasicStroke;
    055    import java.awt.Color;
    056    import java.awt.Graphics2D;
    057    import java.awt.Paint;
    058    import java.awt.Shape;
    059    import java.awt.Stroke;
    060    import java.awt.geom.Arc2D;
    061    import java.awt.geom.GeneralPath;
    062    import java.awt.geom.Line2D;
    063    import java.awt.geom.Rectangle2D;
    064    import java.io.IOException;
    065    import java.io.ObjectInputStream;
    066    import java.io.ObjectOutputStream;
    067    import java.io.Serializable;
    068
    069    import org.jfree.chart.entity.EntityCollection;
    070    import org.jfree.chart.entity.PieSectionEntity;
    071    import org.jfree.chart.event.PlotChangeEvent;
    072    import org.jfree.chart.labels.PieToolTipGenerator;
    073    import org.jfree.chart.urls.PieURLGenerator;
    074    import org.jfree.data.general.PieDataset;
    075    import org.jfree.io.SerialUtilities;
    076    import org.jfree.ui.RectangleInsets;
    077    import org.jfree.util.ObjectUtilities;
    078    import org.jfree.util.PaintUtilities;
    079    import org.jfree.util.Rotation;
    080    import org.jfree.util.ShapeUtilities;
    081    import org.jfree.util.UnitType;
    082
    083    /**
    084     * A customised pie plot that leaves a hole in the middle.
    085     */
    086    public 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    }