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