001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, 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 * [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 * JFreeChart.java
029 * ---------------
030 * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Andrzej Porebski;
034 *                   David Li;
035 *                   Wolfgang Irler;
036 *                   Christian W. Zuckschwerdt;
037 *                   Klaus Rheinwald;
038 *                   Nicolas Brodu;
039 *                   Peter Kolb (patch 2603321);
040 *
041 * NOTE: The above list of contributors lists only the people that have
042 * contributed to this source file (JFreeChart.java) - for a list of ALL
043 * contributors to the project, please see the README.md file.
044 *
045 */
046
047package org.jfree.chart;
048
049import java.awt.AlphaComposite;
050import java.awt.BasicStroke;
051import java.awt.Color;
052import java.awt.Composite;
053import java.awt.Font;
054import java.awt.Graphics2D;
055import java.awt.Image;
056import java.awt.Paint;
057import java.awt.RenderingHints;
058import java.awt.Shape;
059import java.awt.Stroke;
060import java.awt.geom.AffineTransform;
061import java.awt.geom.Point2D;
062import java.awt.geom.Rectangle2D;
063import java.awt.image.BufferedImage;
064import java.io.IOException;
065import java.io.ObjectInputStream;
066import java.io.ObjectOutputStream;
067import java.io.Serializable;
068import java.util.ArrayList;
069import java.util.HashMap;
070import java.util.Iterator;
071import java.util.List;
072import java.util.Map;
073import java.util.Objects;
074import javax.swing.UIManager;
075import javax.swing.event.EventListenerList;
076
077import org.jfree.chart.block.BlockParams;
078import org.jfree.chart.block.EntityBlockResult;
079import org.jfree.chart.block.LengthConstraintType;
080import org.jfree.chart.block.RectangleConstraint;
081import org.jfree.chart.entity.EntityCollection;
082import org.jfree.chart.entity.JFreeChartEntity;
083import org.jfree.chart.event.ChartChangeEvent;
084import org.jfree.chart.event.ChartChangeListener;
085import org.jfree.chart.event.ChartProgressEvent;
086import org.jfree.chart.event.ChartProgressListener;
087import org.jfree.chart.event.PlotChangeEvent;
088import org.jfree.chart.event.PlotChangeListener;
089import org.jfree.chart.event.TitleChangeEvent;
090import org.jfree.chart.event.TitleChangeListener;
091import org.jfree.chart.plot.CategoryPlot;
092import org.jfree.chart.plot.Plot;
093import org.jfree.chart.plot.PlotRenderingInfo;
094import org.jfree.chart.plot.XYPlot;
095import org.jfree.chart.title.LegendTitle;
096import org.jfree.chart.title.TextTitle;
097import org.jfree.chart.title.Title;
098import org.jfree.chart.ui.Align;
099import org.jfree.chart.ui.Drawable;
100import org.jfree.chart.ui.HorizontalAlignment;
101import org.jfree.chart.ui.RectangleEdge;
102import org.jfree.chart.ui.RectangleInsets;
103import org.jfree.chart.ui.Size2D;
104import org.jfree.chart.ui.VerticalAlignment;
105import org.jfree.chart.util.PaintUtils;
106import org.jfree.chart.util.Args;
107import org.jfree.chart.util.SerialUtils;
108import org.jfree.data.Range;
109
110/**
111 * A chart class implemented using the Java 2D APIs.  The current version
112 * supports bar charts, line charts, pie charts and xy plots (including time
113 * series data).
114 * <P>
115 * JFreeChart coordinates several objects to achieve its aim of being able to
116 * draw a chart on a Java 2D graphics device: a list of {@link Title} objects
117 * (which often includes the chart's legend), a {@link Plot} and a
118 * {@link org.jfree.data.general.Dataset} (the plot in turn manages a
119 * domain axis and a range axis).
120 * <P>
121 * You should use a {@link ChartPanel} to display a chart in a GUI.
122 * <P>
123 * The {@link ChartFactory} class contains static methods for creating
124 * 'ready-made' charts.
125 *
126 * @see ChartPanel
127 * @see ChartFactory
128 * @see Title
129 * @see Plot
130 */
131public class JFreeChart implements Drawable, TitleChangeListener,
132        PlotChangeListener, Serializable, Cloneable {
133
134    /** For serialization. */
135    private static final long serialVersionUID = -3470703747817429120L;
136
137    /** The default font for titles. */
138    public static final Font DEFAULT_TITLE_FONT
139            = new Font("SansSerif", Font.BOLD, 18);
140
141    /** The default background color. */
142    public static final Paint DEFAULT_BACKGROUND_PAINT
143            = UIManager.getColor("Panel.background");
144
145    /** The default background image. */
146    public static final Image DEFAULT_BACKGROUND_IMAGE = null;
147
148    /** The default background image alignment. */
149    public static final int DEFAULT_BACKGROUND_IMAGE_ALIGNMENT = Align.FIT;
150
151    /** The default background image alpha. */
152    public static final float DEFAULT_BACKGROUND_IMAGE_ALPHA = 0.5f;
153
154    /**
155     * The key for a rendering hint that can suppress the generation of a 
156     * shadow effect when drawing the chart.  The hint value must be a 
157     * Boolean.
158     */
159    public static final RenderingHints.Key KEY_SUPPRESS_SHADOW_GENERATION
160            = new RenderingHints.Key(0) {
161        @Override
162        public boolean isCompatibleValue(Object val) {
163            return val instanceof Boolean;
164        }
165    };
166    
167    /**
168     * Rendering hints that will be used for chart drawing.  This should never
169     * be {@code null}.
170     */
171    private transient RenderingHints renderingHints;
172
173    /** The chart id (optional, will be used by JFreeSVG export). */
174    private String id;
175    
176    /** A flag that controls whether or not the chart border is drawn. */
177    private boolean borderVisible;
178
179    /** The stroke used to draw the chart border (if visible). */
180    private transient Stroke borderStroke;
181
182    /** The paint used to draw the chart border (if visible). */
183    private transient Paint borderPaint;
184
185    /** The padding between the chart border and the chart drawing area. */
186    private RectangleInsets padding;
187
188    /** The chart title (optional). */
189    private TextTitle title;
190
191    /**
192     * The chart subtitles (zero, one or many).  This field should never be
193     * {@code null}.
194     */
195    private List subtitles;
196
197    /** Draws the visual representation of the data. */
198    private Plot plot;
199
200    /** Paint used to draw the background of the chart. */
201    private transient Paint backgroundPaint;
202
203    /** An optional background image for the chart. */
204    private transient Image backgroundImage;  // todo: not serialized yet
205
206    /** The alignment for the background image. */
207    private int backgroundImageAlignment = Align.FIT;
208
209    /** The alpha transparency for the background image. */
210    private float backgroundImageAlpha = 0.5f;
211
212    /** Storage for registered change listeners. */
213    private transient EventListenerList changeListeners;
214
215    /** Storage for registered progress listeners. */
216    private transient EventListenerList progressListeners;
217
218    /**
219     * A flag that can be used to enable/disable notification of chart change
220     * events.
221     */
222    private boolean notify;
223
224    /** 
225     * A flag that controls whether or not rendering hints that identify
226     * chart element should be added during rendering.  This defaults to false
227     * and it should only be enabled if the output target will use the hints.
228     * JFreeSVG is one output target that supports these hints.
229     */
230    private boolean elementHinting;
231    
232    /**
233     * Creates a new chart based on the supplied plot.  The chart will have
234     * a legend added automatically, but no title (although you can easily add
235     * one later).
236     * <br><br>
237     * Note that the  {@link ChartFactory} class contains a range
238     * of static methods that will return ready-made charts, and often this
239     * is a more convenient way to create charts than using this constructor.
240     *
241     * @param plot  the plot ({@code null} not permitted).
242     */
243    public JFreeChart(Plot plot) {
244        this(null, null, plot, true);
245    }
246
247    /**
248     * Creates a new chart with the given title and plot.  A default font
249     * ({@link #DEFAULT_TITLE_FONT}) is used for the title, and the chart will
250     * have a legend added automatically.
251     * <br><br>
252     * Note that the {@link ChartFactory} class contains a range
253     * of static methods that will return ready-made charts, and often this
254     * is a more convenient way to create charts than using this constructor.
255     *
256     * @param title  the chart title ({@code null} permitted).
257     * @param plot  the plot ({@code null} not permitted).
258     */
259    public JFreeChart(String title, Plot plot) {
260        this(title, JFreeChart.DEFAULT_TITLE_FONT, plot, true);
261    }
262
263    /**
264     * Creates a new chart with the given title and plot.  The
265     * {@code createLegend} argument specifies whether or not a legend
266     * should be added to the chart.
267     * <br><br>
268     * Note that the  {@link ChartFactory} class contains a range
269     * of static methods that will return ready-made charts, and often this
270     * is a more convenient way to create charts than using this constructor.
271     *
272     * @param title  the chart title ({@code null} permitted).
273     * @param titleFont  the font for displaying the chart title
274     *                   ({@code null} permitted).
275     * @param plot  controller of the visual representation of the data
276     *              ({@code null} not permitted).
277     * @param createLegend  a flag indicating whether or not a legend should
278     *                      be created for the chart.
279     */
280    public JFreeChart(String title, Font titleFont, Plot plot,
281                      boolean createLegend) {
282
283        Args.nullNotPermitted(plot, "plot");
284        this.id = null;
285        plot.setChart(this);
286        
287        // create storage for listeners...
288        this.progressListeners = new EventListenerList();
289        this.changeListeners = new EventListenerList();
290        this.notify = true;  // default is to notify listeners when the
291                             // chart changes
292
293        this.renderingHints = new RenderingHints(
294                RenderingHints.KEY_ANTIALIASING,
295                RenderingHints.VALUE_ANTIALIAS_ON);
296        // added the following hint because of 
297        // http://stackoverflow.com/questions/7785082/
298        this.renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
299                RenderingHints.VALUE_STROKE_PURE);
300        
301        this.borderVisible = false;
302        this.borderStroke = new BasicStroke(1.0f);
303        this.borderPaint = Color.BLACK;
304
305        this.padding = RectangleInsets.ZERO_INSETS;
306
307        this.plot = plot;
308        plot.addChangeListener(this);
309
310        this.subtitles = new ArrayList();
311
312        // create a legend, if requested...
313        if (createLegend) {
314            LegendTitle legend = new LegendTitle(this.plot);
315            legend.setMargin(new RectangleInsets(1.0, 1.0, 1.0, 1.0));
316            legend.setBackgroundPaint(Color.WHITE);
317            legend.setPosition(RectangleEdge.BOTTOM);
318            this.subtitles.add(legend);
319            legend.addChangeListener(this);
320        }
321
322        // add the chart title, if one has been specified...
323        if (title != null) {
324            if (titleFont == null) {
325                titleFont = DEFAULT_TITLE_FONT;
326            }
327            this.title = new TextTitle(title, titleFont);
328            this.title.addChangeListener(this);
329        }
330
331        this.backgroundPaint = DEFAULT_BACKGROUND_PAINT;
332
333        this.backgroundImage = DEFAULT_BACKGROUND_IMAGE;
334        this.backgroundImageAlignment = DEFAULT_BACKGROUND_IMAGE_ALIGNMENT;
335        this.backgroundImageAlpha = DEFAULT_BACKGROUND_IMAGE_ALPHA;
336    }
337
338    /**
339     * Returns the ID for the chart.
340     * 
341     * @return The ID for the chart (possibly {@code null}).
342     */
343    public String getID() {
344        return this.id;
345    }
346    
347    /**
348     * Sets the ID for the chart.
349     * 
350     * @param id  the id ({@code null} permitted).
351     */
352    public void setID(String id) {
353        this.id = id;
354    }
355    
356    /**
357     * Returns the flag that controls whether or not rendering hints 
358     * ({@link ChartHints#KEY_BEGIN_ELEMENT} and 
359     * {@link ChartHints#KEY_END_ELEMENT}) that identify chart elements are 
360     * added during rendering.  The default value is {@code false}.
361     * 
362     * @return A boolean.
363     * 
364     * @see #setElementHinting(boolean) 
365     */
366    public boolean getElementHinting() {
367        return this.elementHinting;
368    }
369    
370    /**
371     * Sets the flag that controls whether or not rendering hints 
372     * ({@link ChartHints#KEY_BEGIN_ELEMENT} and 
373     * {@link ChartHints#KEY_END_ELEMENT}) that identify chart elements are 
374     * added during rendering.
375     * 
376     * @param hinting  the new flag value.
377     * 
378     * @see #getElementHinting() 
379     */
380    public void setElementHinting(boolean hinting) {
381        this.elementHinting = hinting;
382    }
383    
384    /**
385     * Returns the collection of rendering hints for the chart.
386     *
387     * @return The rendering hints for the chart (never {@code null}).
388     *
389     * @see #setRenderingHints(RenderingHints)
390     */
391    public RenderingHints getRenderingHints() {
392        return this.renderingHints;
393    }
394
395    /**
396     * Sets the rendering hints for the chart.  These will be added (using the
397     * {@code Graphics2D.addRenderingHints()} method) near the start of the
398     * {@code JFreeChart.draw()} method.
399     *
400     * @param renderingHints  the rendering hints ({@code null} not permitted).
401     *
402     * @see #getRenderingHints()
403     */
404    public void setRenderingHints(RenderingHints renderingHints) {
405        Args.nullNotPermitted(renderingHints, "renderingHints");
406        this.renderingHints = renderingHints;
407        fireChartChanged();
408    }
409
410    /**
411     * Returns a flag that controls whether or not a border is drawn around the
412     * outside of the chart.
413     *
414     * @return A boolean.
415     *
416     * @see #setBorderVisible(boolean)
417     */
418    public boolean isBorderVisible() {
419        return this.borderVisible;
420    }
421
422    /**
423     * Sets a flag that controls whether or not a border is drawn around the
424     * outside of the chart.
425     *
426     * @param visible  the flag.
427     *
428     * @see #isBorderVisible()
429     */
430    public void setBorderVisible(boolean visible) {
431        this.borderVisible = visible;
432        fireChartChanged();
433    }
434
435    /**
436     * Returns the stroke used to draw the chart border (if visible).
437     *
438     * @return The border stroke.
439     *
440     * @see #setBorderStroke(Stroke)
441     */
442    public Stroke getBorderStroke() {
443        return this.borderStroke;
444    }
445
446    /**
447     * Sets the stroke used to draw the chart border (if visible).
448     *
449     * @param stroke  the stroke.
450     *
451     * @see #getBorderStroke()
452     */
453    public void setBorderStroke(Stroke stroke) {
454        this.borderStroke = stroke;
455        fireChartChanged();
456    }
457
458    /**
459     * Returns the paint used to draw the chart border (if visible).
460     *
461     * @return The border paint.
462     *
463     * @see #setBorderPaint(Paint)
464     */
465    public Paint getBorderPaint() {
466        return this.borderPaint;
467    }
468
469    /**
470     * Sets the paint used to draw the chart border (if visible).
471     *
472     * @param paint  the paint.
473     *
474     * @see #getBorderPaint()
475     */
476    public void setBorderPaint(Paint paint) {
477        this.borderPaint = paint;
478        fireChartChanged();
479    }
480
481    /**
482     * Returns the padding between the chart border and the chart drawing area.
483     *
484     * @return The padding (never {@code null}).
485     *
486     * @see #setPadding(RectangleInsets)
487     */
488    public RectangleInsets getPadding() {
489        return this.padding;
490    }
491
492    /**
493     * Sets the padding between the chart border and the chart drawing area,
494     * and sends a {@link ChartChangeEvent} to all registered listeners.
495     *
496     * @param padding  the padding ({@code null} not permitted).
497     *
498     * @see #getPadding()
499     */
500    public void setPadding(RectangleInsets padding) {
501        Args.nullNotPermitted(padding, "padding");
502        this.padding = padding;
503        notifyListeners(new ChartChangeEvent(this));
504    }
505
506    /**
507     * Returns the main chart title.  Very often a chart will have just one
508     * title, so we make this case simple by providing accessor methods for
509     * the main title.  However, multiple titles are supported - see the
510     * {@link #addSubtitle(Title)} method.
511     *
512     * @return The chart title (possibly {@code null}).
513     *
514     * @see #setTitle(TextTitle)
515     */
516    public TextTitle getTitle() {
517        return this.title;
518    }
519
520    /**
521     * Sets the main title for the chart and sends a {@link ChartChangeEvent}
522     * to all registered listeners.  If you do not want a title for the
523     * chart, set it to {@code null}.  If you want more than one title on
524     * a chart, use the {@link #addSubtitle(Title)} method.
525     *
526     * @param title  the title ({@code null} permitted).
527     *
528     * @see #getTitle()
529     */
530    public void setTitle(TextTitle title) {
531        if (this.title != null) {
532            this.title.removeChangeListener(this);
533        }
534        this.title = title;
535        if (title != null) {
536            title.addChangeListener(this);
537        }
538        fireChartChanged();
539    }
540
541    /**
542     * Sets the chart title and sends a {@link ChartChangeEvent} to all
543     * registered listeners.  This is a convenience method that ends up calling
544     * the {@link #setTitle(TextTitle)} method.  If there is an existing title,
545     * its text is updated, otherwise a new title using the default font is
546     * added to the chart.  If {@code text} is {@code null} the chart
547     * title is set to {@code null}.
548     *
549     * @param text  the title text ({@code null} permitted).
550     *
551     * @see #getTitle()
552     */
553    public void setTitle(String text) {
554        if (text != null) {
555            if (this.title == null) {
556                setTitle(new TextTitle(text, JFreeChart.DEFAULT_TITLE_FONT));
557            } else {
558                this.title.setText(text);
559            }
560        }
561        else {
562            setTitle((TextTitle) null);
563        }
564    }
565
566    /**
567     * Adds a legend to the plot and sends a {@link ChartChangeEvent} to all
568     * registered listeners.
569     *
570     * @param legend  the legend ({@code null} not permitted).
571     *
572     * @see #removeLegend()
573     */
574    public void addLegend(LegendTitle legend) {
575        addSubtitle(legend);
576    }
577
578    /**
579     * Returns the legend for the chart, if there is one.  Note that a chart
580     * can have more than one legend - this method returns the first.
581     *
582     * @return The legend (possibly {@code null}).
583     *
584     * @see #getLegend(int)
585     */
586    public LegendTitle getLegend() {
587        return getLegend(0);
588    }
589
590    /**
591     * Returns the nth legend for a chart, or {@code null}.
592     *
593     * @param index  the legend index (zero-based).
594     *
595     * @return The legend (possibly {@code null}).
596     *
597     * @see #addLegend(LegendTitle)
598     */
599    public LegendTitle getLegend(int index) {
600        int seen = 0;
601        Iterator iterator = this.subtitles.iterator();
602        while (iterator.hasNext()) {
603            Title subtitle = (Title) iterator.next();
604            if (subtitle instanceof LegendTitle) {
605                if (seen == index) {
606                    return (LegendTitle) subtitle;
607                }
608                else {
609                    seen++;
610                }
611            }
612        }
613        return null;
614    }
615
616    /**
617     * Removes the first legend in the chart and sends a
618     * {@link ChartChangeEvent} to all registered listeners.
619     *
620     * @see #getLegend()
621     */
622    public void removeLegend() {
623        removeSubtitle(getLegend());
624    }
625
626    /**
627     * Returns the list of subtitles for the chart.
628     *
629     * @return The subtitle list (possibly empty, but never {@code null}).
630     *
631     * @see #setSubtitles(List)
632     */
633    public List getSubtitles() {
634        return new ArrayList(this.subtitles);
635    }
636
637    /**
638     * Sets the title list for the chart (completely replaces any existing
639     * titles) and sends a {@link ChartChangeEvent} to all registered
640     * listeners.
641     *
642     * @param subtitles  the new list of subtitles ({@code null} not
643     *                   permitted).
644     *
645     * @see #getSubtitles()
646     */
647    public void setSubtitles(List subtitles) {
648        if (subtitles == null) {
649            throw new NullPointerException("Null 'subtitles' argument.");
650        }
651        setNotify(false);
652        clearSubtitles();
653        Iterator iterator = subtitles.iterator();
654        while (iterator.hasNext()) {
655            Title t = (Title) iterator.next();
656            if (t != null) {
657                addSubtitle(t);
658            }
659        }
660        setNotify(true);  // this fires a ChartChangeEvent
661    }
662
663    /**
664     * Returns the number of titles for the chart.
665     *
666     * @return The number of titles for the chart.
667     *
668     * @see #getSubtitles()
669     */
670    public int getSubtitleCount() {
671        return this.subtitles.size();
672    }
673
674    /**
675     * Returns a chart subtitle.
676     *
677     * @param index  the index of the chart subtitle (zero based).
678     *
679     * @return A chart subtitle.
680     *
681     * @see #addSubtitle(Title)
682     */
683    public Title getSubtitle(int index) {
684        if ((index < 0) || (index >= getSubtitleCount())) {
685            throw new IllegalArgumentException("Index out of range.");
686        }
687        return (Title) this.subtitles.get(index);
688    }
689
690    /**
691     * Adds a chart subtitle, and notifies registered listeners that the chart
692     * has been modified.
693     *
694     * @param subtitle  the subtitle ({@code null} not permitted).
695     *
696     * @see #getSubtitle(int)
697     */
698    public void addSubtitle(Title subtitle) {
699        Args.nullNotPermitted(subtitle, "subtitle");
700        this.subtitles.add(subtitle);
701        subtitle.addChangeListener(this);
702        fireChartChanged();
703    }
704
705    /**
706     * Adds a subtitle at a particular position in the subtitle list, and sends
707     * a {@link ChartChangeEvent} to all registered listeners.
708     *
709     * @param index  the index (in the range 0 to {@link #getSubtitleCount()}).
710     * @param subtitle  the subtitle to add ({@code null} not permitted).
711     */
712    public void addSubtitle(int index, Title subtitle) {
713        if (index < 0 || index > getSubtitleCount()) {
714            throw new IllegalArgumentException(
715                    "The 'index' argument is out of range.");
716        }
717        Args.nullNotPermitted(subtitle, "subtitle");
718        this.subtitles.add(index, subtitle);
719        subtitle.addChangeListener(this);
720        fireChartChanged();
721    }
722
723    /**
724     * Clears all subtitles from the chart and sends a {@link ChartChangeEvent}
725     * to all registered listeners.
726     *
727     * @see #addSubtitle(Title)
728     */
729    public void clearSubtitles() {
730        Iterator iterator = this.subtitles.iterator();
731        while (iterator.hasNext()) {
732            Title t = (Title) iterator.next();
733            t.removeChangeListener(this);
734        }
735        this.subtitles.clear();
736        fireChartChanged();
737    }
738
739    /**
740     * Removes the specified subtitle and sends a {@link ChartChangeEvent} to
741     * all registered listeners.
742     *
743     * @param title  the title.
744     *
745     * @see #addSubtitle(Title)
746     */
747    public void removeSubtitle(Title title) {
748        this.subtitles.remove(title);
749        fireChartChanged();
750    }
751
752    /**
753     * Returns the plot for the chart.  The plot is a class responsible for
754     * coordinating the visual representation of the data, including the axes
755     * (if any).
756     *
757     * @return The plot.
758     */
759    public Plot getPlot() {
760        return this.plot;
761    }
762
763    /**
764     * Returns the plot cast as a {@link CategoryPlot}.
765     * <p>
766     * NOTE: if the plot is not an instance of {@link CategoryPlot}, then a
767     * {@code ClassCastException} is thrown.
768     *
769     * @return The plot.
770     *
771     * @see #getPlot()
772     */
773    public CategoryPlot getCategoryPlot() {
774        return (CategoryPlot) this.plot;
775    }
776
777    /**
778     * Returns the plot cast as an {@link XYPlot}.
779     * <p>
780     * NOTE: if the plot is not an instance of {@link XYPlot}, then a
781     * {@code ClassCastException} is thrown.
782     *
783     * @return The plot.
784     *
785     * @see #getPlot()
786     */
787    public XYPlot getXYPlot() {
788        return (XYPlot) this.plot;
789    }
790
791    /**
792     * Returns a flag that indicates whether or not anti-aliasing is used when
793     * the chart is drawn.
794     *
795     * @return The flag.
796     *
797     * @see #setAntiAlias(boolean)
798     */
799    public boolean getAntiAlias() {
800        Object val = this.renderingHints.get(RenderingHints.KEY_ANTIALIASING);
801        return RenderingHints.VALUE_ANTIALIAS_ON.equals(val);
802    }
803
804    /**
805     * Sets a flag that indicates whether or not anti-aliasing is used when the
806     * chart is drawn.
807     * <P>
808     * Anti-aliasing usually improves the appearance of charts, but is slower.
809     *
810     * @param flag  the new value of the flag.
811     *
812     * @see #getAntiAlias()
813     */
814    public void setAntiAlias(boolean flag) {
815        Object hint = flag ? RenderingHints.VALUE_ANTIALIAS_ON 
816                : RenderingHints.VALUE_ANTIALIAS_OFF;
817        this.renderingHints.put(RenderingHints.KEY_ANTIALIASING, hint);
818        fireChartChanged();
819    }
820
821    /**
822     * Returns the current value stored in the rendering hints table for
823     * {@link RenderingHints#KEY_TEXT_ANTIALIASING}.
824     *
825     * @return The hint value (possibly {@code null}).
826     *
827     * @see #setTextAntiAlias(Object)
828     */
829    public Object getTextAntiAlias() {
830        return this.renderingHints.get(RenderingHints.KEY_TEXT_ANTIALIASING);
831    }
832
833    /**
834     * Sets the value in the rendering hints table for
835     * {@link RenderingHints#KEY_TEXT_ANTIALIASING} to either
836     * {@link RenderingHints#VALUE_TEXT_ANTIALIAS_ON} or
837     * {@link RenderingHints#VALUE_TEXT_ANTIALIAS_OFF}, then sends a
838     * {@link ChartChangeEvent} to all registered listeners.
839     *
840     * @param flag  the new value of the flag.
841     *
842     * @see #getTextAntiAlias()
843     * @see #setTextAntiAlias(Object)
844     */
845    public void setTextAntiAlias(boolean flag) {
846        if (flag) {
847            setTextAntiAlias(RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
848        }
849        else {
850            setTextAntiAlias(RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
851        }
852    }
853
854    /**
855     * Sets the value in the rendering hints table for
856     * {@link RenderingHints#KEY_TEXT_ANTIALIASING} and sends a
857     * {@link ChartChangeEvent} to all registered listeners.
858     *
859     * @param val  the new value ({@code null} permitted).
860     *
861     * @see #getTextAntiAlias()
862     * @see #setTextAntiAlias(boolean)
863     */
864    public void setTextAntiAlias(Object val) {
865        this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, val);
866        notifyListeners(new ChartChangeEvent(this));
867    }
868
869    /**
870     * Returns the paint used for the chart background.
871     *
872     * @return The paint (possibly {@code null}).
873     *
874     * @see #setBackgroundPaint(Paint)
875     */
876    public Paint getBackgroundPaint() {
877        return this.backgroundPaint;
878    }
879
880    /**
881     * Sets the paint used to fill the chart background and sends a
882     * {@link ChartChangeEvent} to all registered listeners.
883     *
884     * @param paint  the paint ({@code null} permitted).
885     *
886     * @see #getBackgroundPaint()
887     */
888    public void setBackgroundPaint(Paint paint) {
889
890        if (this.backgroundPaint != null) {
891            if (!this.backgroundPaint.equals(paint)) {
892                this.backgroundPaint = paint;
893                fireChartChanged();
894            }
895        }
896        else {
897            if (paint != null) {
898                this.backgroundPaint = paint;
899                fireChartChanged();
900            }
901        }
902
903    }
904
905    /**
906     * Returns the background image for the chart, or {@code null} if
907     * there is no image.
908     *
909     * @return The image (possibly {@code null}).
910     *
911     * @see #setBackgroundImage(Image)
912     */
913    public Image getBackgroundImage() {
914        return this.backgroundImage;
915    }
916
917    /**
918     * Sets the background image for the chart and sends a
919     * {@link ChartChangeEvent} to all registered listeners.
920     *
921     * @param image  the image ({@code null} permitted).
922     *
923     * @see #getBackgroundImage()
924     */
925    public void setBackgroundImage(Image image) {
926
927        if (this.backgroundImage != null) {
928            if (!this.backgroundImage.equals(image)) {
929                this.backgroundImage = image;
930                fireChartChanged();
931            }
932        }
933        else {
934            if (image != null) {
935                this.backgroundImage = image;
936                fireChartChanged();
937            }
938        }
939
940    }
941
942    /**
943     * Returns the background image alignment. Alignment constants are defined
944     * in the {@link Align} class.
945     *
946     * @return The alignment.
947     *
948     * @see #setBackgroundImageAlignment(int)
949     */
950    public int getBackgroundImageAlignment() {
951        return this.backgroundImageAlignment;
952    }
953
954    /**
955     * Sets the background alignment.  Alignment options are defined by the
956     * {@link org.jfree.chart.ui.Align} class.
957     *
958     * @param alignment  the alignment.
959     *
960     * @see #getBackgroundImageAlignment()
961     */
962    public void setBackgroundImageAlignment(int alignment) {
963        if (this.backgroundImageAlignment != alignment) {
964            this.backgroundImageAlignment = alignment;
965            fireChartChanged();
966        }
967    }
968
969    /**
970     * Returns the alpha-transparency for the chart's background image.
971     *
972     * @return The alpha-transparency.
973     *
974     * @see #setBackgroundImageAlpha(float)
975     */
976    public float getBackgroundImageAlpha() {
977        return this.backgroundImageAlpha;
978    }
979
980    /**
981     * Sets the alpha-transparency for the chart's background image.
982     * Registered listeners are notified that the chart has been changed.
983     *
984     * @param alpha  the alpha value.
985     *
986     * @see #getBackgroundImageAlpha()
987     */
988    public void setBackgroundImageAlpha(float alpha) {
989        if (this.backgroundImageAlpha != alpha) {
990            this.backgroundImageAlpha = alpha;
991            fireChartChanged();
992        }
993    }
994
995    /**
996     * Returns a flag that controls whether or not change events are sent to
997     * registered listeners.
998     *
999     * @return A boolean.
1000     *
1001     * @see #setNotify(boolean)
1002     */
1003    public boolean isNotify() {
1004        return this.notify;
1005    }
1006
1007    /**
1008     * Sets a flag that controls whether or not listeners receive
1009     * {@link ChartChangeEvent} notifications.
1010     *
1011     * @param notify  a boolean.
1012     *
1013     * @see #isNotify()
1014     */
1015    public void setNotify(boolean notify) {
1016        this.notify = notify;
1017        // if the flag is being set to true, there may be queued up changes...
1018        if (notify) {
1019            notifyListeners(new ChartChangeEvent(this));
1020        }
1021    }
1022
1023    /**
1024     * Draws the chart on a Java 2D graphics device (such as the screen or a
1025     * printer).
1026     * <P>
1027     * This method is the focus of the entire JFreeChart library.
1028     *
1029     * @param g2  the graphics device.
1030     * @param area  the area within which the chart should be drawn.
1031     */
1032    @Override
1033    public void draw(Graphics2D g2, Rectangle2D area) {
1034        draw(g2, area, null, null);
1035    }
1036
1037    /**
1038     * Draws the chart on a Java 2D graphics device (such as the screen or a
1039     * printer).  This method is the focus of the entire JFreeChart library.
1040     *
1041     * @param g2  the graphics device.
1042     * @param area  the area within which the chart should be drawn.
1043     * @param info  records info about the drawing (null means collect no info).
1044     */
1045    public void draw(Graphics2D g2, Rectangle2D area, ChartRenderingInfo info) {
1046        draw(g2, area, null, info);
1047    }
1048
1049    /**
1050     * Draws the chart on a Java 2D graphics device (such as the screen or a
1051     * printer).
1052     * <P>
1053     * This method is the focus of the entire JFreeChart library.
1054     *
1055     * @param g2  the graphics device.
1056     * @param chartArea  the area within which the chart should be drawn.
1057     * @param anchor  the anchor point (in Java2D space) for the chart
1058     *                ({@code null} permitted).
1059     * @param info  records info about the drawing (null means collect no info).
1060     */
1061    public void draw(Graphics2D g2, Rectangle2D chartArea, Point2D anchor,
1062             ChartRenderingInfo info) {
1063
1064        notifyListeners(new ChartProgressEvent(this, this,
1065                ChartProgressEvent.DRAWING_STARTED, 0));
1066        
1067        if (this.elementHinting) {
1068            Map m = new HashMap<String, String>();
1069            if (this.id != null) {
1070                m.put("id", this.id);
1071            }
1072            m.put("ref", "JFREECHART_TOP_LEVEL");            
1073            g2.setRenderingHint(ChartHints.KEY_BEGIN_ELEMENT, m);            
1074        }
1075        
1076        EntityCollection entities = null;
1077        // record the chart area, if info is requested...
1078        if (info != null) {
1079            info.clear();
1080            info.setChartArea(chartArea);
1081            entities = info.getEntityCollection();
1082        }
1083        if (entities != null) {
1084            entities.add(new JFreeChartEntity((Rectangle2D) chartArea.clone(),
1085                    this));
1086        }
1087
1088        // ensure no drawing occurs outside chart area...
1089        Shape savedClip = g2.getClip();
1090        g2.clip(chartArea);
1091
1092        g2.addRenderingHints(this.renderingHints);
1093
1094        // draw the chart background...
1095        if (this.backgroundPaint != null) {
1096            g2.setPaint(this.backgroundPaint);
1097            g2.fill(chartArea);
1098        }
1099
1100        if (this.backgroundImage != null) {
1101            Composite originalComposite = g2.getComposite();
1102            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1103                    this.backgroundImageAlpha));
1104            Rectangle2D dest = new Rectangle2D.Double(0.0, 0.0,
1105                    this.backgroundImage.getWidth(null),
1106                    this.backgroundImage.getHeight(null));
1107            Align.align(dest, chartArea, this.backgroundImageAlignment);
1108            g2.drawImage(this.backgroundImage, (int) dest.getX(),
1109                    (int) dest.getY(), (int) dest.getWidth(),
1110                    (int) dest.getHeight(), null);
1111            g2.setComposite(originalComposite);
1112        }
1113
1114        if (isBorderVisible()) {
1115            Paint paint = getBorderPaint();
1116            Stroke stroke = getBorderStroke();
1117            if (paint != null && stroke != null) {
1118                Rectangle2D borderArea = new Rectangle2D.Double(
1119                        chartArea.getX(), chartArea.getY(),
1120                        chartArea.getWidth() - 1.0, chartArea.getHeight()
1121                        - 1.0);
1122                g2.setPaint(paint);
1123                g2.setStroke(stroke);
1124                g2.draw(borderArea);
1125            }
1126        }
1127
1128        // draw the title and subtitles...
1129        Rectangle2D nonTitleArea = new Rectangle2D.Double();
1130        nonTitleArea.setRect(chartArea);
1131        this.padding.trim(nonTitleArea);
1132
1133        if (this.title != null && this.title.isVisible()) {
1134            EntityCollection e = drawTitle(this.title, g2, nonTitleArea,
1135                    (entities != null));
1136            if (e != null && entities != null) {
1137                entities.addAll(e);
1138            }
1139        }
1140
1141        Iterator iterator = this.subtitles.iterator();
1142        while (iterator.hasNext()) {
1143            Title currentTitle = (Title) iterator.next();
1144            if (currentTitle.isVisible()) {
1145                EntityCollection e = drawTitle(currentTitle, g2, nonTitleArea,
1146                        (entities != null));
1147                if (e != null && entities != null) {
1148                    entities.addAll(e);
1149                }
1150            }
1151        }
1152
1153        Rectangle2D plotArea = nonTitleArea;
1154
1155        // draw the plot (axes and data visualisation)
1156        PlotRenderingInfo plotInfo = null;
1157        if (info != null) {
1158            plotInfo = info.getPlotInfo();
1159        }
1160        this.plot.draw(g2, plotArea, anchor, null, plotInfo);
1161        g2.setClip(savedClip);
1162        if (this.elementHinting) {         
1163            g2.setRenderingHint(ChartHints.KEY_END_ELEMENT, Boolean.TRUE);            
1164        }
1165
1166        notifyListeners(new ChartProgressEvent(this, this,
1167                ChartProgressEvent.DRAWING_FINISHED, 100));
1168    }
1169
1170    /**
1171     * Creates a rectangle that is aligned to the frame.
1172     *
1173     * @param dimensions  the dimensions for the rectangle.
1174     * @param frame  the frame to align to.
1175     * @param hAlign  the horizontal alignment.
1176     * @param vAlign  the vertical alignment.
1177     *
1178     * @return A rectangle.
1179     */
1180    private Rectangle2D createAlignedRectangle2D(Size2D dimensions,
1181            Rectangle2D frame, HorizontalAlignment hAlign,
1182            VerticalAlignment vAlign) {
1183        double x = Double.NaN;
1184        double y = Double.NaN;
1185        if (hAlign == HorizontalAlignment.LEFT) {
1186            x = frame.getX();
1187        }
1188        else if (hAlign == HorizontalAlignment.CENTER) {
1189            x = frame.getCenterX() - (dimensions.width / 2.0);
1190        }
1191        else if (hAlign == HorizontalAlignment.RIGHT) {
1192            x = frame.getMaxX() - dimensions.width;
1193        }
1194        if (vAlign == VerticalAlignment.TOP) {
1195            y = frame.getY();
1196        }
1197        else if (vAlign == VerticalAlignment.CENTER) {
1198            y = frame.getCenterY() - (dimensions.height / 2.0);
1199        }
1200        else if (vAlign == VerticalAlignment.BOTTOM) {
1201            y = frame.getMaxY() - dimensions.height;
1202        }
1203
1204        return new Rectangle2D.Double(x, y, dimensions.width,
1205                dimensions.height);
1206    }
1207
1208    /**
1209     * Draws a title.  The title should be drawn at the top, bottom, left or
1210     * right of the specified area, and the area should be updated to reflect
1211     * the amount of space used by the title.
1212     *
1213     * @param t  the title ({@code null} not permitted).
1214     * @param g2  the graphics device ({@code null} not permitted).
1215     * @param area  the chart area, excluding any existing titles
1216     *              ({@code null} not permitted).
1217     * @param entities  a flag that controls whether or not an entity
1218     *                  collection is returned for the title.
1219     *
1220     * @return An entity collection for the title (possibly {@code null}).
1221     */
1222    protected EntityCollection drawTitle(Title t, Graphics2D g2,
1223                                         Rectangle2D area, boolean entities) {
1224
1225        Args.nullNotPermitted(t, "t");
1226        Args.nullNotPermitted(area, "area");
1227        Rectangle2D titleArea;
1228        RectangleEdge position = t.getPosition();
1229        double ww = area.getWidth();
1230        if (ww <= 0.0) {
1231            return null;
1232        }
1233        double hh = area.getHeight();
1234        if (hh <= 0.0) {
1235            return null;
1236        }
1237        RectangleConstraint constraint = new RectangleConstraint(ww,
1238                new Range(0.0, ww), LengthConstraintType.RANGE, hh,
1239                new Range(0.0, hh), LengthConstraintType.RANGE);
1240        Object retValue = null;
1241        BlockParams p = new BlockParams();
1242        p.setGenerateEntities(entities);
1243        if (position == RectangleEdge.TOP) {
1244            Size2D size = t.arrange(g2, constraint);
1245            titleArea = createAlignedRectangle2D(size, area,
1246                    t.getHorizontalAlignment(), VerticalAlignment.TOP);
1247            retValue = t.draw(g2, titleArea, p);
1248            area.setRect(area.getX(), Math.min(area.getY() + size.height,
1249                    area.getMaxY()), area.getWidth(), Math.max(area.getHeight()
1250                    - size.height, 0));
1251        } else if (position == RectangleEdge.BOTTOM) {
1252            Size2D size = t.arrange(g2, constraint);
1253            titleArea = createAlignedRectangle2D(size, area,
1254                    t.getHorizontalAlignment(), VerticalAlignment.BOTTOM);
1255            retValue = t.draw(g2, titleArea, p);
1256            area.setRect(area.getX(), area.getY(), area.getWidth(),
1257                    area.getHeight() - size.height);
1258        } else if (position == RectangleEdge.RIGHT) {
1259            Size2D size = t.arrange(g2, constraint);
1260            titleArea = createAlignedRectangle2D(size, area,
1261                    HorizontalAlignment.RIGHT, t.getVerticalAlignment());
1262            retValue = t.draw(g2, titleArea, p);
1263            area.setRect(area.getX(), area.getY(), area.getWidth()
1264                    - size.width, area.getHeight());
1265        } else if (position == RectangleEdge.LEFT) {
1266            Size2D size = t.arrange(g2, constraint);
1267            titleArea = createAlignedRectangle2D(size, area,
1268                    HorizontalAlignment.LEFT, t.getVerticalAlignment());
1269            retValue = t.draw(g2, titleArea, p);
1270            area.setRect(area.getX() + size.width, area.getY(), area.getWidth()
1271                    - size.width, area.getHeight());
1272        }
1273        else {
1274            throw new RuntimeException("Unrecognised title position.");
1275        }
1276        EntityCollection result = null;
1277        if (retValue instanceof EntityBlockResult) {
1278            EntityBlockResult ebr = (EntityBlockResult) retValue;
1279            result = ebr.getEntityCollection();
1280        }
1281        return result;
1282    }
1283
1284    /**
1285     * Creates and returns a buffered image into which the chart has been drawn.
1286     *
1287     * @param width  the width.
1288     * @param height  the height.
1289     *
1290     * @return A buffered image.
1291     */
1292    public BufferedImage createBufferedImage(int width, int height) {
1293        return createBufferedImage(width, height, null);
1294    }
1295
1296    /**
1297     * Creates and returns a buffered image into which the chart has been drawn.
1298     *
1299     * @param width  the width.
1300     * @param height  the height.
1301     * @param info  carries back chart state information ({@code null}
1302     *              permitted).
1303     *
1304     * @return A buffered image.
1305     */
1306    public BufferedImage createBufferedImage(int width, int height,
1307                                             ChartRenderingInfo info) {
1308        return createBufferedImage(width, height, BufferedImage.TYPE_INT_ARGB,
1309                info);
1310    }
1311
1312    /**
1313     * Creates and returns a buffered image into which the chart has been drawn.
1314     *
1315     * @param width  the width.
1316     * @param height  the height.
1317     * @param imageType  the image type.
1318     * @param info  carries back chart state information ({@code null}
1319     *              permitted).
1320     *
1321     * @return A buffered image.
1322     */
1323    public BufferedImage createBufferedImage(int width, int height,
1324            int imageType, ChartRenderingInfo info) {
1325        BufferedImage image = new BufferedImage(width, height, imageType);
1326        Graphics2D g2 = image.createGraphics();
1327        draw(g2, new Rectangle2D.Double(0, 0, width, height), null, info);
1328        g2.dispose();
1329        return image;
1330    }
1331
1332    /**
1333     * Creates and returns a buffered image into which the chart has been drawn.
1334     *
1335     * @param imageWidth  the image width.
1336     * @param imageHeight  the image height.
1337     * @param drawWidth  the width for drawing the chart (will be scaled to
1338     *                   fit image).
1339     * @param drawHeight  the height for drawing the chart (will be scaled to
1340     *                    fit image).
1341     * @param info  optional object for collection chart dimension and entity
1342     *              information.
1343     *
1344     * @return A buffered image.
1345     */
1346    public BufferedImage createBufferedImage(int imageWidth,
1347                                             int imageHeight,
1348                                             double drawWidth,
1349                                             double drawHeight,
1350                                             ChartRenderingInfo info) {
1351
1352        BufferedImage image = new BufferedImage(imageWidth, imageHeight,
1353                BufferedImage.TYPE_INT_ARGB);
1354        Graphics2D g2 = image.createGraphics();
1355        double scaleX = imageWidth / drawWidth;
1356        double scaleY = imageHeight / drawHeight;
1357        AffineTransform st = AffineTransform.getScaleInstance(scaleX, scaleY);
1358        g2.transform(st);
1359        draw(g2, new Rectangle2D.Double(0, 0, drawWidth, drawHeight), null,
1360                info);
1361        g2.dispose();
1362        return image;
1363    }
1364
1365    /**
1366     * Handles a 'click' on the chart.  JFreeChart is not a UI component, so
1367     * some other object (for example, {@link ChartPanel}) needs to capture
1368     * the click event and pass it onto the JFreeChart object.
1369     * If you are not using JFreeChart in a client application, then this
1370     * method is not required.
1371     *
1372     * @param x  x-coordinate of the click (in Java2D space).
1373     * @param y  y-coordinate of the click (in Java2D space).
1374     * @param info  contains chart dimension and entity information
1375     *              ({@code null} not permitted).
1376     */
1377    public void handleClick(int x, int y, ChartRenderingInfo info) {
1378        // pass the click on to the plot...
1379        // rely on the plot to post a plot change event and redraw the chart...
1380        this.plot.handleClick(x, y, info.getPlotInfo());
1381    }
1382
1383    /**
1384     * Registers an object for notification of changes to the chart.
1385     *
1386     * @param listener  the listener ({@code null} not permitted).
1387     *
1388     * @see #removeChangeListener(ChartChangeListener)
1389     */
1390    public void addChangeListener(ChartChangeListener listener) {
1391        Args.nullNotPermitted(listener, "listener");
1392        this.changeListeners.add(ChartChangeListener.class, listener);
1393    }
1394
1395    /**
1396     * Deregisters an object for notification of changes to the chart.
1397     *
1398     * @param listener  the listener ({@code null} not permitted)
1399     *
1400     * @see #addChangeListener(ChartChangeListener)
1401     */
1402    public void removeChangeListener(ChartChangeListener listener) {
1403        Args.nullNotPermitted(listener, "listener");
1404        this.changeListeners.remove(ChartChangeListener.class, listener);
1405    }
1406
1407    /**
1408     * Sends a default {@link ChartChangeEvent} to all registered listeners.
1409     * <P>
1410     * This method is for convenience only.
1411     */
1412    public void fireChartChanged() {
1413        ChartChangeEvent event = new ChartChangeEvent(this);
1414        notifyListeners(event);
1415    }
1416
1417    /**
1418     * Sends a {@link ChartChangeEvent} to all registered listeners.
1419     *
1420     * @param event  information about the event that triggered the
1421     *               notification.
1422     */
1423    protected void notifyListeners(ChartChangeEvent event) {
1424        if (this.notify) {
1425            Object[] listeners = this.changeListeners.getListenerList();
1426            for (int i = listeners.length - 2; i >= 0; i -= 2) {
1427                if (listeners[i] == ChartChangeListener.class) {
1428                    ((ChartChangeListener) listeners[i + 1]).chartChanged(
1429                            event);
1430                }
1431            }
1432        }
1433    }
1434
1435    /**
1436     * Registers an object for notification of progress events relating to the
1437     * chart.
1438     *
1439     * @param listener  the object being registered.
1440     *
1441     * @see #removeProgressListener(ChartProgressListener)
1442     */
1443    public void addProgressListener(ChartProgressListener listener) {
1444        this.progressListeners.add(ChartProgressListener.class, listener);
1445    }
1446
1447    /**
1448     * Deregisters an object for notification of changes to the chart.
1449     *
1450     * @param listener  the object being deregistered.
1451     *
1452     * @see #addProgressListener(ChartProgressListener)
1453     */
1454    public void removeProgressListener(ChartProgressListener listener) {
1455        this.progressListeners.remove(ChartProgressListener.class, listener);
1456    }
1457
1458    /**
1459     * Sends a {@link ChartProgressEvent} to all registered listeners.
1460     *
1461     * @param event  information about the event that triggered the
1462     *               notification.
1463     */
1464    protected void notifyListeners(ChartProgressEvent event) {
1465        Object[] listeners = this.progressListeners.getListenerList();
1466        for (int i = listeners.length - 2; i >= 0; i -= 2) {
1467            if (listeners[i] == ChartProgressListener.class) {
1468                ((ChartProgressListener) listeners[i + 1]).chartProgress(event);
1469            }
1470        }
1471    }
1472
1473    /**
1474     * Receives notification that a chart title has changed, and passes this
1475     * on to registered listeners.
1476     *
1477     * @param event  information about the chart title change.
1478     */
1479    @Override
1480    public void titleChanged(TitleChangeEvent event) {
1481        event.setChart(this);
1482        notifyListeners(event);
1483    }
1484
1485    /**
1486     * Receives notification that the plot has changed, and passes this on to
1487     * registered listeners.
1488     *
1489     * @param event  information about the plot change.
1490     */
1491    @Override
1492    public void plotChanged(PlotChangeEvent event) {
1493        event.setChart(this);
1494        notifyListeners(event);
1495    }
1496
1497    /**
1498     * Tests this chart for equality with another object.
1499     *
1500     * @param obj  the object ({@code null} permitted).
1501     *
1502     * @return A boolean.
1503     */
1504    @Override
1505    public boolean equals(Object obj) {
1506        if (obj == this) {
1507            return true;
1508        }
1509        if (!(obj instanceof JFreeChart)) {
1510            return false;
1511        }
1512        JFreeChart that = (JFreeChart) obj;
1513        if (!this.renderingHints.equals(that.renderingHints)) {
1514            return false;
1515        }
1516        if (this.borderVisible != that.borderVisible) {
1517            return false;
1518        }
1519        if (!Objects.equals(this.borderStroke, that.borderStroke)) {
1520            return false;
1521        }
1522        if (!PaintUtils.equal(this.borderPaint, that.borderPaint)) {
1523            return false;
1524        }
1525        if (!this.padding.equals(that.padding)) {
1526            return false;
1527        }
1528        if (!Objects.equals(this.title, that.title)) {
1529            return false;
1530        }
1531        if (!Objects.equals(this.subtitles, that.subtitles)) {
1532            return false;
1533        }
1534        if (!Objects.equals(this.plot, that.plot)) {
1535            return false;
1536        }
1537        if (!PaintUtils.equal(
1538            this.backgroundPaint, that.backgroundPaint
1539        )) {
1540            return false;
1541        }
1542        if (!Objects.equals(this.backgroundImage, that.backgroundImage)) {
1543            return false;
1544        }
1545        if (this.backgroundImageAlignment != that.backgroundImageAlignment) {
1546            return false;
1547        }
1548        if (this.backgroundImageAlpha != that.backgroundImageAlpha) {
1549            return false;
1550        }
1551        if (this.notify != that.notify) {
1552            return false;
1553        }
1554        return true;
1555    }
1556
1557    /**
1558     * Provides serialization support.
1559     *
1560     * @param stream  the output stream.
1561     *
1562     * @throws IOException  if there is an I/O error.
1563     */
1564    private void writeObject(ObjectOutputStream stream) throws IOException {
1565        stream.defaultWriteObject();
1566        SerialUtils.writeStroke(this.borderStroke, stream);
1567        SerialUtils.writePaint(this.borderPaint, stream);
1568        SerialUtils.writePaint(this.backgroundPaint, stream);
1569    }
1570
1571    /**
1572     * Provides serialization support.
1573     *
1574     * @param stream  the input stream.
1575     *
1576     * @throws IOException  if there is an I/O error.
1577     * @throws ClassNotFoundException  if there is a classpath problem.
1578     */
1579    private void readObject(ObjectInputStream stream)
1580        throws IOException, ClassNotFoundException {
1581        stream.defaultReadObject();
1582        this.borderStroke = SerialUtils.readStroke(stream);
1583        this.borderPaint = SerialUtils.readPaint(stream);
1584        this.backgroundPaint = SerialUtils.readPaint(stream);
1585        this.progressListeners = new EventListenerList();
1586        this.changeListeners = new EventListenerList();
1587        this.renderingHints = new RenderingHints(
1588                RenderingHints.KEY_ANTIALIASING,
1589                RenderingHints.VALUE_ANTIALIAS_ON);
1590        this.renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
1591                RenderingHints.VALUE_STROKE_PURE);
1592        
1593        // register as a listener with sub-components...
1594        if (this.title != null) {
1595            this.title.addChangeListener(this);
1596        }
1597
1598        for (int i = 0; i < getSubtitleCount(); i++) {
1599            getSubtitle(i).addChangeListener(this);
1600        }
1601        this.plot.addChangeListener(this);
1602    }
1603
1604    /**
1605     * Clones the object, and takes care of listeners.
1606     * Note: caller shall register its own listeners on cloned graph.
1607     *
1608     * @return A clone.
1609     *
1610     * @throws CloneNotSupportedException if the chart is not cloneable.
1611     */
1612    @Override
1613    public Object clone() throws CloneNotSupportedException {
1614        JFreeChart chart = (JFreeChart) super.clone();
1615
1616        chart.renderingHints = (RenderingHints) this.renderingHints.clone();
1617        // private boolean borderVisible;
1618        // private transient Stroke borderStroke;
1619        // private transient Paint borderPaint;
1620
1621        if (this.title != null) {
1622            chart.title = (TextTitle) this.title.clone();
1623            chart.title.addChangeListener(chart);
1624        }
1625
1626        chart.subtitles = new ArrayList();
1627        for (int i = 0; i < getSubtitleCount(); i++) {
1628            Title subtitle = (Title) getSubtitle(i).clone();
1629            chart.subtitles.add(subtitle);
1630            subtitle.addChangeListener(chart);
1631        }
1632
1633        if (this.plot != null) {
1634            chart.plot = (Plot) this.plot.clone();
1635            chart.plot.addChangeListener(chart);
1636        }
1637
1638        chart.progressListeners = new EventListenerList();
1639        chart.changeListeners = new EventListenerList();
1640        return chart;
1641    }
1642
1643}