001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -------------
028 * RingPlot.java
029 * -------------
030 * (C) Copyright 2004-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Christoph Beck (bug 2121818);
034 *
035 */
036
037package org.jfree.chart.plot;
038
039import java.awt.BasicStroke;
040import java.awt.Color;
041import java.awt.Font;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.Stroke;
046import java.awt.geom.Arc2D;
047import java.awt.geom.GeneralPath;
048import java.awt.geom.Line2D;
049import java.awt.geom.Rectangle2D;
050import java.io.IOException;
051import java.io.ObjectInputStream;
052import java.io.ObjectOutputStream;
053import java.io.Serializable;
054import java.text.DecimalFormat;
055import java.text.Format;
056import java.util.Objects;
057
058import org.jfree.chart.entity.EntityCollection;
059import org.jfree.chart.entity.PieSectionEntity;
060import org.jfree.chart.labels.PieToolTipGenerator;
061import org.jfree.chart.text.TextUtils;
062import org.jfree.chart.ui.RectangleInsets;
063import org.jfree.chart.ui.TextAnchor;
064import org.jfree.chart.urls.PieURLGenerator;
065import org.jfree.chart.util.LineUtils;
066import org.jfree.chart.util.PaintUtils;
067import org.jfree.chart.util.Args;
068import org.jfree.chart.util.Rotation;
069import org.jfree.chart.util.SerialUtils;
070import org.jfree.chart.util.ShapeUtils;
071import org.jfree.chart.util.UnitType;
072import org.jfree.data.general.PieDataset;
073
074/**
075 * A customised pie plot that leaves a hole in the middle.
076 */
077public class RingPlot extends PiePlot implements Cloneable, Serializable {
078
079    /** For serialization. */
080    private static final long serialVersionUID = 1556064784129676620L;
081
082    /** The center text mode. */
083    private CenterTextMode centerTextMode = CenterTextMode.NONE;
084    
085    /** 
086     * Text to display in the middle of the chart (used for 
087     * CenterTextMode.FIXED). 
088     */
089    private String centerText;
090    
091    /**
092     * The formatter used when displaying the first data value from the
093     * dataset (CenterTextMode.VALUE).
094     */
095    private Format centerTextFormatter = new DecimalFormat("0.00");
096    
097    /** The font used to display the center text. */
098    private Font centerTextFont;
099    
100    /** The color used to display the center text. */
101    private Color centerTextColor;
102    
103    /**
104     * A flag that controls whether or not separators are drawn between the
105     * sections of the chart.
106     */
107    private boolean separatorsVisible;
108
109    /** The stroke used to draw separators. */
110    private transient Stroke separatorStroke;
111
112    /** The paint used to draw separators. */
113    private transient Paint separatorPaint;
114
115    /**
116     * The length of the inner separator extension (as a percentage of the
117     * depth of the sections).
118     */
119    private double innerSeparatorExtension;
120
121    /**
122     * The length of the outer separator extension (as a percentage of the
123     * depth of the sections).
124     */
125    private double outerSeparatorExtension;
126
127    /**
128     * The depth of the section as a percentage of the diameter.
129     */
130    private double sectionDepth;
131
132    /**
133     * Creates a new plot with a {@code null} dataset.
134     */
135    public RingPlot() {
136        this(null);
137    }
138
139    /**
140     * Creates a new plot for the specified dataset.
141     *
142     * @param dataset  the dataset ({@code null} permitted).
143     */
144    public RingPlot(PieDataset dataset) {
145        super(dataset);
146        this.centerTextMode = CenterTextMode.NONE;
147        this.centerText = null;
148        this.centerTextFormatter = new DecimalFormat("0.00");
149        this.centerTextFont = DEFAULT_LABEL_FONT;
150        this.centerTextColor = Color.BLACK;
151        this.separatorsVisible = true;
152        this.separatorStroke = new BasicStroke(0.5f);
153        this.separatorPaint = Color.GRAY;
154        this.innerSeparatorExtension = 0.20;  // 20%
155        this.outerSeparatorExtension = 0.20;  // 20%
156        this.sectionDepth = 0.20; // 20%
157    }
158
159    /**
160     * Returns the mode for displaying text in the center of the plot.  The
161     * default value is {@link CenterTextMode#NONE} therefore no text
162     * will be displayed by default.
163     * 
164     * @return The mode (never {@code null}).
165     */
166    public CenterTextMode getCenterTextMode() {
167        return this.centerTextMode;
168    }
169    
170    /**
171     * Sets the mode for displaying text in the center of the plot and sends 
172     * a change event to all registered listeners.  For
173     * {@link CenterTextMode#FIXED}, the display text will come from the 
174     * {@code centerText} attribute (see {@link #getCenterText()}).
175     * For {@link CenterTextMode#VALUE}, the center text will be the value from
176     * the first section in the dataset.
177     * 
178     * @param mode  the mode ({@code null} not permitted).
179     */
180    public void setCenterTextMode(CenterTextMode mode) {
181        Args.nullNotPermitted(mode, "mode");
182        this.centerTextMode = mode;
183        fireChangeEvent();
184    }
185    
186    /**
187     * Returns the text to display in the center of the plot when the mode
188     * is {@link CenterTextMode#FIXED}.
189     * 
190     * @return The text (possibly {@code null}).
191     */
192    public String getCenterText() {
193        return this.centerText;
194    }
195    
196    /**
197     * Sets the text to display in the center of the plot and sends a
198     * change event to all registered listeners.  If the text is set to 
199     * {@code null}, no text will be displayed.
200     * 
201     * @param text  the text ({@code null} permitted).
202     */
203    public void setCenterText(String text) {
204        this.centerText = text;
205        fireChangeEvent();
206    }
207    
208    /**
209     * Returns the formatter used to format the center text value for the mode
210     * {@link CenterTextMode#VALUE}.  The default value is 
211     * {@code DecimalFormat("0.00")}.
212     * 
213     * @return The formatter (never {@code null}).
214     */
215    public Format getCenterTextFormatter() {
216        return this.centerTextFormatter;
217    }
218    
219    /**
220     * Sets the formatter used to format the center text value and sends a
221     * change event to all registered listeners.
222     * 
223     * @param formatter  the formatter ({@code null} not permitted).
224     */
225    public void setCenterTextFormatter(Format formatter) {
226        Args.nullNotPermitted(formatter, "formatter");
227        this.centerTextFormatter = formatter;
228    }
229    
230    /**
231     * Returns the font used to display the center text.  The default value
232     * is {@link PiePlot#DEFAULT_LABEL_FONT}.
233     * 
234     * @return The font (never {@code null}).
235     */
236    public Font getCenterTextFont() {
237        return this.centerTextFont;
238    }
239    
240    /**
241     * Sets the font used to display the center text and sends a change event
242     * to all registered listeners.
243     * 
244     * @param font  the font ({@code null} not permitted).
245     */
246    public void setCenterTextFont(Font font) {
247        Args.nullNotPermitted(font, "font");
248        this.centerTextFont = font;
249        fireChangeEvent();
250    }
251    
252    /**
253     * Returns the color for the center text.  The default value is
254     * {@code Color.BLACK}.
255     * 
256     * @return The color (never {@code null}). 
257     */
258    public Color getCenterTextColor() {
259        return this.centerTextColor;
260    }
261    
262    /**
263     * Sets the color for the center text and sends a change event to all 
264     * registered listeners.
265     * 
266     * @param color  the color ({@code null} not permitted).
267     */
268    public void setCenterTextColor(Color color) {
269        Args.nullNotPermitted(color, "color");
270        this.centerTextColor = color;
271        fireChangeEvent();
272    }
273    
274    /**
275     * Returns a flag that indicates whether or not separators are drawn between
276     * the sections in the chart.
277     *
278     * @return A boolean.
279     *
280     * @see #setSeparatorsVisible(boolean)
281     */
282    public boolean getSeparatorsVisible() {
283        return this.separatorsVisible;
284    }
285
286    /**
287     * Sets the flag that controls whether or not separators are drawn between
288     * the sections in the chart, and sends a change event to all registered 
289     * listeners.
290     *
291     * @param visible  the flag.
292     *
293     * @see #getSeparatorsVisible()
294     */
295    public void setSeparatorsVisible(boolean visible) {
296        this.separatorsVisible = visible;
297        fireChangeEvent();
298    }
299
300    /**
301     * Returns the separator stroke.
302     *
303     * @return The stroke (never {@code null}).
304     *
305     * @see #setSeparatorStroke(Stroke)
306     */
307    public Stroke getSeparatorStroke() {
308        return this.separatorStroke;
309    }
310
311    /**
312     * Sets the stroke used to draw the separator between sections and sends
313     * a change event to all registered listeners.
314     *
315     * @param stroke  the stroke ({@code null} not permitted).
316     *
317     * @see #getSeparatorStroke()
318     */
319    public void setSeparatorStroke(Stroke stroke) {
320        Args.nullNotPermitted(stroke, "stroke");
321        this.separatorStroke = stroke;
322        fireChangeEvent();
323    }
324
325    /**
326     * Returns the separator paint.
327     *
328     * @return The paint (never {@code null}).
329     *
330     * @see #setSeparatorPaint(Paint)
331     */
332    public Paint getSeparatorPaint() {
333        return this.separatorPaint;
334    }
335
336    /**
337     * Sets the paint used to draw the separator between sections and sends a
338     * change event to all registered listeners.
339     *
340     * @param paint  the paint ({@code null} not permitted).
341     *
342     * @see #getSeparatorPaint()
343     */
344    public void setSeparatorPaint(Paint paint) {
345        Args.nullNotPermitted(paint, "paint");
346        this.separatorPaint = paint;
347        fireChangeEvent();
348    }
349
350    /**
351     * Returns the length of the inner extension of the separator line that
352     * is drawn between sections, expressed as a percentage of the depth of
353     * the section.
354     *
355     * @return The inner separator extension (as a percentage).
356     *
357     * @see #setInnerSeparatorExtension(double)
358     */
359    public double getInnerSeparatorExtension() {
360        return this.innerSeparatorExtension;
361    }
362
363    /**
364     * Sets the length of the inner extension of the separator line that is
365     * drawn between sections, as a percentage of the depth of the
366     * sections, and sends a change event to all registered listeners.
367     *
368     * @param percent  the percentage.
369     *
370     * @see #getInnerSeparatorExtension()
371     * @see #setOuterSeparatorExtension(double)
372     */
373    public void setInnerSeparatorExtension(double percent) {
374        this.innerSeparatorExtension = percent;
375        fireChangeEvent();
376    }
377
378    /**
379     * Returns the length of the outer extension of the separator line that
380     * is drawn between sections, expressed as a percentage of the depth of
381     * the section.
382     *
383     * @return The outer separator extension (as a percentage).
384     *
385     * @see #setOuterSeparatorExtension(double)
386     */
387    public double getOuterSeparatorExtension() {
388        return this.outerSeparatorExtension;
389    }
390
391    /**
392     * Sets the length of the outer extension of the separator line that is
393     * drawn between sections, as a percentage of the depth of the
394     * sections, and sends a change event to all registered listeners.
395     *
396     * @param percent  the percentage.
397     *
398     * @see #getOuterSeparatorExtension()
399     */
400    public void setOuterSeparatorExtension(double percent) {
401        this.outerSeparatorExtension = percent;
402        fireChangeEvent();
403    }
404
405    /**
406     * Returns the depth of each section, expressed as a percentage of the
407     * plot radius.
408     *
409     * @return The depth of each section.
410     *
411     * @see #setSectionDepth(double)
412     */
413    public double getSectionDepth() {
414        return this.sectionDepth;
415    }
416
417    /**
418     * The section depth is given as percentage of the plot radius.
419     * Specifying 1.0 results in a straightforward pie chart.
420     *
421     * @param sectionDepth  the section depth.
422     *
423     * @see #getSectionDepth()
424     */
425    public void setSectionDepth(double sectionDepth) {
426        this.sectionDepth = sectionDepth;
427        fireChangeEvent();
428    }
429
430    /**
431     * Initialises the plot state (which will store the total of all dataset
432     * values, among other things).  This method is called once at the
433     * beginning of each drawing.
434     *
435     * @param g2  the graphics device.
436     * @param plotArea  the plot area ({@code null} not permitted).
437     * @param plot  the plot.
438     * @param index  the secondary index ({@code null} for primary
439     *               renderer).
440     * @param info  collects chart rendering information for return to caller.
441     *
442     * @return A state object (maintains state information relevant to one
443     *         chart drawing).
444     */
445    @Override
446    public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
447            PiePlot plot, Integer index, PlotRenderingInfo info) {
448        PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
449        state.setPassesRequired(3);
450        return state;
451    }
452
453    /**
454     * Draws a single data item.
455     *
456     * @param g2  the graphics device ({@code null} not permitted).
457     * @param section  the section index.
458     * @param dataArea  the data plot area.
459     * @param state  state information for one chart.
460     * @param currentPass  the current pass index.
461     */
462    @Override
463    protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea,
464            PiePlotState state, int currentPass) {
465
466        PieDataset dataset = getDataset();
467        Number n = dataset.getValue(section);
468        if (n == null) {
469            return;
470        }
471        double value = n.doubleValue();
472        double angle1 = 0.0;
473        double angle2 = 0.0;
474
475        Rotation direction = getDirection();
476        if (direction == Rotation.CLOCKWISE) {
477            angle1 = state.getLatestAngle();
478            angle2 = angle1 - value / state.getTotal() * 360.0;
479        }
480        else if (direction == Rotation.ANTICLOCKWISE) {
481            angle1 = state.getLatestAngle();
482            angle2 = angle1 + value / state.getTotal() * 360.0;
483        }
484        else {
485            throw new IllegalStateException("Rotation type not recognised.");
486        }
487
488        double angle = (angle2 - angle1);
489        if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
490            Comparable key = getSectionKey(section);
491            double ep = 0.0;
492            double mep = getMaximumExplodePercent();
493            if (mep > 0.0) {
494                ep = getExplodePercent(key) / mep;
495            }
496            Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
497                    state.getExplodedPieArea(), angle1, angle, ep);
498            Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
499                    Arc2D.OPEN);
500
501            // create the bounds for the inner arc
502            double depth = this.sectionDepth / 2.0;
503            RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
504                depth, depth, depth, depth);
505            Rectangle2D innerArcBounds = new Rectangle2D.Double();
506            innerArcBounds.setRect(arcBounds);
507            s.trim(innerArcBounds);
508            // calculate inner arc in reverse direction, for later
509            // GeneralPath construction
510            Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
511                    + angle, -angle, Arc2D.OPEN);
512            GeneralPath path = new GeneralPath();
513            path.moveTo((float) arc.getStartPoint().getX(),
514                    (float) arc.getStartPoint().getY());
515            path.append(arc.getPathIterator(null), false);
516            path.append(arc2.getPathIterator(null), true);
517            path.closePath();
518
519            Line2D separator = new Line2D.Double(arc2.getEndPoint(),
520                    arc.getStartPoint());
521
522            if (currentPass == 0) {
523                Paint shadowPaint = getShadowPaint();
524                double shadowXOffset = getShadowXOffset();
525                double shadowYOffset = getShadowYOffset();
526                if (shadowPaint != null && getShadowGenerator() == null) {
527                    Shape shadowArc = ShapeUtils.createTranslatedShape(
528                            path, (float) shadowXOffset, (float) shadowYOffset);
529                    g2.setPaint(shadowPaint);
530                    g2.fill(shadowArc);
531                }
532            }
533            else if (currentPass == 1) {
534                Paint paint = lookupSectionPaint(key);
535                g2.setPaint(paint);
536                g2.fill(path);
537                Paint outlinePaint = lookupSectionOutlinePaint(key);
538                Stroke outlineStroke = lookupSectionOutlineStroke(key);
539                if (getSectionOutlinesVisible() && outlinePaint != null 
540                        && outlineStroke != null) {
541                    g2.setPaint(outlinePaint);
542                    g2.setStroke(outlineStroke);
543                    g2.draw(path);
544                }
545                
546                if (section == 0) {
547                    String nstr = null;
548                    if (this.centerTextMode.equals(CenterTextMode.VALUE)) {
549                        nstr = this.centerTextFormatter.format(n);
550                    } else if (this.centerTextMode.equals(CenterTextMode.FIXED)) {
551                        nstr = this.centerText;
552                    }
553                    if (nstr != null) {
554                        g2.setFont(this.centerTextFont);
555                        g2.setPaint(this.centerTextColor);
556                        TextUtils.drawAlignedString(nstr, g2, 
557                            (float) dataArea.getCenterX(), 
558                            (float) dataArea.getCenterY(),  
559                            TextAnchor.CENTER);                        
560                    }
561                }
562
563                // add an entity for the pie section
564                if (state.getInfo() != null) {
565                    EntityCollection entities = state.getEntityCollection();
566                    if (entities != null) {
567                        String tip = null;
568                        PieToolTipGenerator toolTipGenerator
569                                = getToolTipGenerator();
570                        if (toolTipGenerator != null) {
571                            tip = toolTipGenerator.generateToolTip(dataset,
572                                    key);
573                        }
574                        String url = null;
575                        PieURLGenerator urlGenerator = getURLGenerator();
576                        if (urlGenerator != null) {
577                            url = urlGenerator.generateURL(dataset, key,
578                                    getPieIndex());
579                        }
580                        PieSectionEntity entity = new PieSectionEntity(path,
581                                dataset, getPieIndex(), section, key, tip,
582                                url);
583                        entities.add(entity);
584                    }
585                }
586            }
587            else if (currentPass == 2) {
588                if (this.separatorsVisible) {
589                    Line2D extendedSeparator = LineUtils.extendLine(
590                            separator, this.innerSeparatorExtension,
591                            this.outerSeparatorExtension);
592                    g2.setStroke(this.separatorStroke);
593                    g2.setPaint(this.separatorPaint);
594                    g2.draw(extendedSeparator);
595                }
596            }
597        }
598        state.setLatestAngle(angle2);
599    }
600
601    /**
602     * This method overrides the default value for cases where the ring plot
603     * is very thin.  This fixes bug 2121818.
604     *
605     * @return The label link depth, as a percentage of the plot's radius.
606     */
607    @Override
608    protected double getLabelLinkDepth() {
609        return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
610    }
611
612    /**
613     * Tests this plot for equality with an arbitrary object.
614     *
615     * @param obj  the object to test against ({@code null} permitted).
616     *
617     * @return A boolean.
618     */
619    @Override
620    public boolean equals(Object obj) {
621        if (this == obj) {
622            return true;
623        }
624        if (!(obj instanceof RingPlot)) {
625            return false;
626        }
627        RingPlot that = (RingPlot) obj;
628        if (!this.centerTextMode.equals(that.centerTextMode)) {
629            return false;
630        }
631        if (!Objects.equals(this.centerText, that.centerText)) {
632            return false;
633        }
634        if (!this.centerTextFormatter.equals(that.centerTextFormatter)) {
635            return false;
636        }
637        if (!this.centerTextFont.equals(that.centerTextFont)) {
638            return false;
639        }
640        if (!this.centerTextColor.equals(that.centerTextColor)) {
641            return false;
642        }
643        if (this.separatorsVisible != that.separatorsVisible) {
644            return false;
645        }
646        if (!Objects.equals(this.separatorStroke,
647                that.separatorStroke)) {
648            return false;
649        }
650        if (!PaintUtils.equal(this.separatorPaint, that.separatorPaint)) {
651            return false;
652        }
653        if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
654            return false;
655        }
656        if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
657            return false;
658        }
659        if (this.sectionDepth != that.sectionDepth) {
660            return false;
661        }
662        return super.equals(obj);
663    }
664
665    /**
666     * Provides serialization support.
667     *
668     * @param stream  the output stream.
669     *
670     * @throws IOException  if there is an I/O error.
671     */
672    private void writeObject(ObjectOutputStream stream) throws IOException {
673        stream.defaultWriteObject();
674        SerialUtils.writeStroke(this.separatorStroke, stream);
675        SerialUtils.writePaint(this.separatorPaint, stream);
676    }
677
678    /**
679     * Provides serialization support.
680     *
681     * @param stream  the input stream.
682     *
683     * @throws IOException  if there is an I/O error.
684     * @throws ClassNotFoundException  if there is a classpath problem.
685     */
686    private void readObject(ObjectInputStream stream)
687        throws IOException, ClassNotFoundException {
688        stream.defaultReadObject();
689        this.separatorStroke = SerialUtils.readStroke(stream);
690        this.separatorPaint = SerialUtils.readPaint(stream);
691    }
692
693}