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