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