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