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