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