001/* ======================================================
002 * Orson : a free chart beans library based on JFreeChart
003 * ======================================================
004 *
005 * (C) Copyright 2007, by Object Refinery Limited.
006 *
007 * Project Info:  http://www.jfree.org/orson/
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 * 
027 */
028
029package org.jfree.beans;
030
031import java.awt.Color;
032import java.awt.Dimension;
033import java.awt.Font;
034import java.awt.Graphics;
035import java.awt.Graphics2D;
036import java.awt.GraphicsConfiguration;
037import java.awt.Image;
038import java.awt.Insets;
039import java.awt.Paint;
040import java.awt.Point;
041import java.awt.Stroke;
042import java.awt.Transparency;
043import java.awt.event.ActionEvent;
044import java.awt.event.ActionListener;
045import java.awt.event.MouseEvent;
046import java.awt.event.MouseListener;
047import java.awt.event.MouseMotionListener;
048import java.awt.geom.AffineTransform;
049import java.awt.geom.Point2D;
050import java.awt.geom.Rectangle2D;
051import java.beans.PropertyChangeEvent;
052import java.beans.PropertyEditorManager;
053import java.io.File;
054import java.io.IOException;
055
056import javax.swing.JComponent;
057import javax.swing.JFileChooser;
058import javax.swing.JMenuItem;
059import javax.swing.JPopupMenu;
060import javax.swing.ToolTipManager;
061import javax.swing.event.EventListenerList;
062
063import org.jfree.beans.editors.AxisLocationEditor;
064import org.jfree.beans.editors.LegendPositionEditor;
065import org.jfree.beans.editors.PaintEditor;
066import org.jfree.beans.editors.PlotOrientationEditor;
067import org.jfree.beans.editors.RectangleEdgeEditor;
068import org.jfree.beans.editors.StrokeEditor;
069import org.jfree.beans.events.LegendClickEvent;
070import org.jfree.beans.events.LegendClickListener;
071import org.jfree.chart.ChartRenderingInfo;
072import org.jfree.chart.ChartUtilities;
073import org.jfree.chart.JFreeChart;
074import org.jfree.chart.axis.AxisLocation;
075import org.jfree.chart.entity.ChartEntity;
076import org.jfree.chart.entity.EntityCollection;
077import org.jfree.chart.entity.LegendItemEntity;
078import org.jfree.chart.event.ChartChangeEvent;
079import org.jfree.chart.event.ChartChangeListener;
080import org.jfree.chart.plot.Plot;
081import org.jfree.chart.plot.PlotOrientation;
082import org.jfree.chart.plot.PlotRenderingInfo;
083import org.jfree.chart.plot.Zoomable;
084import org.jfree.chart.title.LegendTitle;
085import org.jfree.chart.title.TextTitle;
086import org.jfree.ui.ExtensionFileFilter;
087import org.jfree.ui.HorizontalAlignment;
088import org.jfree.ui.RectangleEdge;
089import org.jfree.ui.RectangleInsets;
090
091/**
092 * A base class for creating chart beans.
093 */
094public abstract class AbstractChart extends JComponent 
095        implements ChartChangeListener, ActionListener, MouseListener, 
096        MouseMotionListener {
097
098    // here we register some custom editors that make the chart beans a little
099    // easier to work with...
100    static {
101        PropertyEditorManager.registerEditor(Paint.class, PaintEditor.class);
102        PropertyEditorManager.registerEditor(Stroke.class, StrokeEditor.class);
103        PropertyEditorManager.registerEditor(PlotOrientation.class, 
104                PlotOrientationEditor.class);
105        PropertyEditorManager.registerEditor(RectangleEdge.class,
106                RectangleEdgeEditor.class);
107        PropertyEditorManager.registerEditor(AxisLocation.class, 
108                AxisLocationEditor.class);
109        PropertyEditorManager.registerEditor(LegendPosition.class,
110                LegendPositionEditor.class);
111    }
112
113    /** The underlying chart. */
114    protected JFreeChart chart;
115    
116    /** 
117     * The chart's legend.  We keep a separate reference to this, so that
118     * the legend can be added/removed from the chart.
119     */
120    protected LegendTitle legend;
121    
122    /**
123     * The current legend position (TOP, BOTTOM, LEFT, RIGHT or NONE).
124     */
125    protected LegendPosition legendPosition;
126  
127    /** 
128     * A subtitle for the chart.
129     */
130    protected TextTitle subtitle;
131    
132    /**
133     * A subtitle that shows the data source.
134     */
135    protected TextTitle sourceSubtitle;
136    
137    /** 
138     * The chart rendering info, which is used for tooltips and mouse 
139     * events. 
140     */
141    protected ChartRenderingInfo info;
142    
143    /** Storage for registered listeners. */
144    protected EventListenerList listeners;
145
146    /** A buffer for the rendered chart. */
147    protected Image chartBuffer;
148
149    /** The height of the chart buffer. */
150    protected int chartBufferHeight;
151
152    /** The width of the chart buffer. */
153    protected int chartBufferWidth;
154
155    /** A flag that indicates that the buffer should be refreshed. */
156    private boolean refreshBuffer;
157
158    /** The scale factor used to draw the chart. */
159    protected double scaleX;
160
161    /** The scale factor used to draw the chart. */
162    protected double scaleY;
163
164    /** The chart anchor point. */
165    private Point2D anchor;
166
167    /** The zoom rectangle (selected by the user with the mouse). */
168    private transient Rectangle2D zoomRectangle = null;
169
170    /** 
171     * The zoom rectangle starting point (selected by the user with a mouse 
172     * click).  This is a point on the screen, not the chart (which may have
173     * been scaled up or down to fit the panel).  
174     */
175    private Point zoomPoint = null;
176
177    /** The minimum distance required to drag the mouse to trigger a zoom. */
178    private int zoomTriggerDistance;
179    
180    /** Controls if the zoom rectangle is drawn as an outline or filled. */
181    private boolean fillZoomRectangle = false;
182
183    /**
184     * Default constructor.
185     */
186    public AbstractChart() {
187        this.info = new ChartRenderingInfo();
188        this.chart = createDefaultChart();
189        this.chart.addChangeListener(this);
190        this.chart.getTitle().setFont(new Font("Dialog", Font.BOLD, 14));
191        this.chart.setBackgroundPaint(Color.white);
192        this.legend = this.chart.getLegend();
193        this.legend.setItemFont(new Font("Dialog", Font.PLAIN, 10));
194        this.legendPosition = LegendPosition.BOTTOM;
195        this.subtitle = new TextTitle("Chart Subtitle", new Font("Dialog", 
196                Font.ITALIC, 10));
197        this.chart.addSubtitle(0, this.subtitle);
198        this.sourceSubtitle = new TextTitle("http://www.jfree.org/jfreechart",
199                new Font("Dialog", Font.PLAIN, 8));
200        this.sourceSubtitle.setPosition(RectangleEdge.BOTTOM);
201        this.sourceSubtitle.setHorizontalAlignment(HorizontalAlignment.RIGHT);
202        this.chart.addSubtitle(0, this.sourceSubtitle);
203        addMouseListener(this);
204        addMouseMotionListener(this);
205        setToolTipsEnabled(true);
206        setPreferredSize(new Dimension(360, 230));
207        this.listeners = new EventListenerList();
208    }
209
210    /**
211     * Creates the default chart for initial display to the user.  Subclasses
212     * implement this as appropriate for the chart type.
213     * 
214     * @return The default chart.
215     */
216    protected abstract JFreeChart createDefaultChart();
217    
218    /**
219     * Returns the flag that controls whether or not the chart is drawn
220     * with antialiasing.
221     * 
222     * @return The antialiasing flag.
223     * 
224     * @see #setAntiAlias(boolean)
225     */
226    public boolean getAntiAlias() {
227        return this.chart.getAntiAlias();
228    }
229    
230    /**
231     * Sets the flag that controls whether or not the chart is drawn with 
232     * antialiasing, and fires a {@link PropertyChangeEvent} for the 
233     * <code>antiAlias</code> property.
234     * 
235     * @param flag  the new flag value.
236     * 
237     * @see #getAntiAlias()
238     */
239    public void setAntiAlias(boolean flag) {
240        boolean old = this.chart.getAntiAlias();
241        this.chart.setAntiAlias(flag);
242        firePropertyChange("antiAlias", old, flag);
243    }
244    
245    /**
246     * Returns a flag that controls whether or not the chart border is visible.
247     * In general, it makes more sense to use a Swing border around the 
248     * component, but when saving a chart to an image, it is sometimes useful
249     * to display an outline border.
250     * 
251     * @return A flag that controls whether or not the chart border is visible.
252     * 
253     * @see #setChartBorderVisible(boolean)
254     */
255    public boolean isChartBorderVisible() {
256        return this.chart.isBorderVisible();
257    }
258    
259    /**
260     * Sets the flag that controls whether or not a border is drawn around
261     * the chart, and fires a {@link PropertyChangeEvent} for the
262     * <code>chartBorderVisible</code> property.
263     * 
264     * @param visible  the new value for the flag.
265     * 
266     * @see #isChartBorderVisible()
267     */
268    public void setChartBorderVisible(boolean visible) {
269        boolean old = this.chart.isBorderVisible();
270        this.chart.setBorderVisible(visible);
271        firePropertyChange("chartBorderVisible", old, visible);
272    }
273    
274    /**
275     * Returns the stroke used to draw the outline for the chart.
276     * 
277     * @return The stroke.
278     * 
279     * @see #setChartBorderStroke(Stroke)
280     */
281    public Stroke getChartBorderStroke() {
282        return this.chart.getBorderStroke();
283    }
284    
285    /**
286     * Sets the stroke used to draw the outline for the chart and 
287     * sends a {@link PropertyChangeEvent} to all registered listeners for the 
288     * <code>chartBorderPaint</code> property.
289     * 
290     * @param stroke  the stroke (<code>null</code> not permitted).
291     * 
292     * @see #getChartBorderStroke()
293     */
294    public void setChartBorderStroke(Stroke stroke) {
295        Stroke old = this.chart.getBorderStroke();
296        this.chart.setBorderStroke(stroke);
297        firePropertyChange("chartBorderStroke", old, stroke);
298    }
299
300    /**
301     * Returns the paint used to draw the chart border, if it is visible.
302     * 
303     * @return The paint used to draw the chart border (never 
304     *         <code>null</code>).
305     *         
306     * @see #setChartBorderPaint(Paint)
307     */
308    public Paint getChartBorderPaint() {
309        return this.chart.getBorderPaint();
310    }
311    
312    /**
313     * Sets the paint used to draw the chart border, if it is visible, and 
314     * sends a {@link PropertyChangeEvent} to all registered listeners for the 
315     * <code>chartBorderPaint</code> property.
316     * 
317     * @param paint  the paint (<code>null</code> not permitted).
318     * 
319     * @see #getChartBorderPaint()
320     */
321    public void setChartBorderPaint(Paint paint) {
322        Paint old = getChartBorderPaint();
323        this.chart.setBorderPaint(paint);
324        firePropertyChange("chartBorderPaint", old, paint);
325    }
326    
327    /**
328     * Returns the background paint for the chart.
329     * 
330     * @return The background paint for the chart (possibly <code>null</code>).
331     * 
332     * @see #setChartBackgroundPaint(Paint)
333     */
334    public Paint getChartBackgroundPaint() {
335        return this.chart.getBackgroundPaint();
336    }
337    
338    /**
339     * Sets the background paint for the chart and sends a 
340     * {@link PropertyChangeEvent} to all registered listeners for the 
341     * <code>chartBackgroundPaint</code> property.
342     * 
343     * @param paint  the paint (<code>null</code> permitted).
344     * 
345     * @see #getChartBackgroundPaint()
346     */
347    public void setChartBackgroundPaint(Paint paint) {
348        Paint old = this.chart.getBackgroundPaint();
349        this.chart.setBackgroundPaint(paint);
350        firePropertyChange("chartBackgroundPaint", old, paint);
351    }
352
353    // FIXME: the chartBackgroundImage isn't yet covered in the BeanInfo
354    
355    /**
356     * Returns the background image for the chart.
357     * 
358     * @return The image (possibly <code>null</code>).
359     * 
360     * @see #setChartBackgroundImage(Image)
361     */
362    public Image getChartBackgroundImage() {
363        return this.chart.getBackgroundImage();
364    }
365    
366    /**
367     * Sets the background image for the chart and sends a 
368     * {@link PropertyChangeEvent} to all registered listeners for the 
369     * <code>chartBackgroundImage</code> property.
370     * 
371     * @param image  the image (<code>null</code> permitted).
372     * 
373     * @see #getChartBackgroundImage()
374     */
375    public void setChartBackgroundImage(Image image) {
376        Image old = this.chart.getBackgroundImage();
377        this.chart.setBackgroundImage(image);
378        firePropertyChange("chartBackgroundImage", old, image);
379    }
380    
381    // FIXME: chartBackgroundImageAlpha is not yet covered in BeanInfo
382    
383    /**
384     * Returns the alpha-transparency for the background image.
385     * 
386     * @return The alpha value.
387     * 
388     * @see #setChartBackgroundImageAlpha(float)
389     */
390    public float getChartBackgroundImageAlpha() {
391        return this.chart.getBackgroundImageAlpha();
392    }
393    
394    /**
395     * Sets the alpha transparency for the background image.
396     * 
397     * @param alpha  the new value.
398     * 
399     * @see #getChartBackgroundImageAlpha()
400     */
401    public void setChartBackgroundImageAlpha(float alpha) {
402        float old = this.chart.getBackgroundImageAlpha();
403        this.chart.setBackgroundImageAlpha(alpha);
404        firePropertyChange("chartBackgroundImageAlpha", old, alpha);
405    }
406    
407    // FIXME:  the chartPadding is not yet covered in the BeanInfo.
408    
409    /**
410     * Returns the chart padding.
411     * 
412     * @return The chart padding.
413     */
414    public RectangleInsets getChartPadding() {
415        return this.chart.getPadding();
416    }
417    
418    /**
419     * Sets the chart padding.
420     * 
421     * @param padding  the padding.
422     */
423    public void setChartPadding(RectangleInsets padding) {
424        this.chart.setPadding(padding);
425    }
426    
427    /**
428     * Returns the text for the chart title.
429     * 
430     * @return The text for the chart title.
431     * 
432     * @see #setTitle(String)
433     */
434    public String getTitle() {
435        String result = null;
436        TextTitle title = this.chart.getTitle();
437        if (title != null) {
438            result = title.getText();
439        }
440        return result;
441    }
442    
443    /**
444     * Sets the text for the chart title and sends a 
445     * {@link PropertyChangeEvent} to all registered listeners for the 
446     * <code>title</code> property.
447     * 
448     * @param title  the title (<code>null</code> not permitted).
449     * 
450     * @see #getTitle()
451     */
452    public void setTitle(String title) {
453        if (title == null) {
454            throw new IllegalArgumentException("Null 'title' argument.");
455        }
456        TextTitle t = this.chart.getTitle();
457        if (t != null) {
458            String old = getTitle();
459            t.setText(title);
460            firePropertyChange("title", old, title);
461        }
462    }
463    
464    /**
465     * Returns the font for the chart title.
466     * 
467     * @return The font for the chart title.
468     * 
469     * @see #setTitleFont(Font)
470     */
471    public Font getTitleFont() {
472        Font result= null;
473        TextTitle title = this.chart.getTitle();
474        if (title != null) {
475            result = title.getFont();
476        }
477        return result;
478    }
479    
480    /**
481     * Sets the font for the chart title and sends a 
482     * {@link PropertyChangeEvent} to all registered listeners for the 
483     * <code>titleFont</code> property.
484     * 
485     * @param font  the font.
486     * 
487     * @see #getTitleFont()
488     */
489    public void setTitleFont(Font font) {
490        TextTitle t = this.chart.getTitle();
491        if (t != null) {
492            Font old = t.getFont();
493            t.setFont(font);
494            firePropertyChange("titleFont", old, font);
495        }
496    }
497    
498    /**
499     * Returns the paint used to draw the chart title.
500     * 
501     * @return The paint used to draw the chart title.
502     * 
503     * @see #getTitlePaint()
504     */
505    public Paint getTitlePaint() {
506        Paint result = null;
507        TextTitle title = this.chart.getTitle();
508        if (title != null) {
509            result = title.getPaint();
510        }
511        return result;
512    }
513    
514    /**
515     * Sets the paint for the chart title and sends a 
516     * {@link PropertyChangeEvent} to all registered listeners for the 
517     * <code>titlePaint</code> property.
518     * 
519     * @param paint  the paint.
520     * 
521     * @see #getTitlePaint()
522     */
523    public void setTitlePaint(Paint paint) {
524        TextTitle t = this.chart.getTitle();
525        if (t != null) {
526            Paint old = t.getPaint();
527            t.setPaint(paint);
528            firePropertyChange("titlePaint", old, paint);
529        }
530    }    
531    
532    /**
533     * Returns the text for the chart's subtitle.
534     * 
535     * @return The text for the chart's subtitle.
536     * 
537     * @see #setSubtitle(String)
538     */
539    public String getSubtitle() {
540        return this.subtitle.getText();
541    }
542    
543    /**
544     * Sets the text for the chart's subtitle and sends a 
545     * {@link PropertyChangeEvent} to all registered listeners for the 
546     * <code>subtitle</code> property.
547     * 
548     * @param title  the title.
549     * 
550     * @see #getSubtitle()
551     */
552    public void setSubtitle(String title) {
553        String old = this.subtitle.getText();
554        this.subtitle.setText(title);
555        firePropertyChange("subtitle", old, title);        
556    }
557    
558    /**
559     * Returns the font for the chart's subtitle.
560     * 
561     * @return The font for the chart's subtitle.
562     * 
563     * @see #setSubtitleFont(Font)
564     */
565    public Font getSubtitleFont() {
566        return this.subtitle.getFont();
567    }
568    
569    /**
570     * Sets the font for the chart's subtitle and sends a 
571     * {@link PropertyChangeEvent} to all registered listeners for the 
572     * <code>subtitleFont</code> property.
573     * 
574     * @param font  the font (<code>null</code> not permitted).
575     * 
576     * @see #getSubtitleFont()
577     */
578    public void setSubtitleFont(Font font) {
579        if (font == null) {
580            throw new IllegalArgumentException("Null 'font' argument.");
581        }
582        Font old = this.subtitle.getFont();
583        this.subtitle.setFont(font);
584        firePropertyChange("subtitleFont", old, font);        
585    }
586
587    /**
588     * Returns the paint used to draw the chart's subtitle.
589     * 
590     * @return The paint used to draw the chart's subtitle.
591     * 
592     * @see #setSubtitlePaint(Paint)
593     */
594    public Paint getSubtitlePaint() {
595        return this.subtitle.getPaint();
596    }
597    
598    /**
599     * Sets the paint for the chart's subtitle and sends a 
600     * {@link PropertyChangeEvent} to all registered listeners for the 
601     * <code>subtitlePaint</code> property.
602     * 
603     * @param paint  the paint (<code>null</code> not permitted).
604     * 
605     * @see #getSubtitlePaint()
606     */
607    public void setSubtitlePaint(Paint paint) {
608        if (paint == null) {
609            throw new IllegalArgumentException("Null 'paint' argument");
610        }
611        Paint old = this.subtitle.getPaint();
612        this.subtitle.setPaint(paint);
613        firePropertyChange("subtitlePaint", old, paint);        
614    }    
615    
616    /**
617     * Returns the text for the chart's source subtitle.
618     * 
619     * @return The text for the chart's sourcesubtitle.
620     * 
621     * @see #setSource(String)
622     */
623    public String getSource() {
624        return this.sourceSubtitle.getText();
625    }
626    
627    /**
628     * Sets the text for the chart's source subtitle and sends a 
629     * {@link PropertyChangeEvent} to all registered listeners for the 
630     * <code>source</code> property.
631     * 
632     * @param title  the title.
633     * 
634     * @see #getSource()
635     */
636    public void setSource(String title) {
637        String old = this.sourceSubtitle.getText();
638        this.sourceSubtitle.setText(title);
639        firePropertyChange("source", old, title);        
640    }
641    
642    /**
643     * Returns the font for the chart's source subtitle.
644     * 
645     * @return The font for the chart's source subtitle.
646     * 
647     * @see #setSourceFont(Font)
648     */
649    public Font getSourceFont() {
650        return this.sourceSubtitle.getFont();
651    }
652    
653    /**
654     * Sets the font for the chart's source subtitle and sends a 
655     * {@link PropertyChangeEvent} to all registered listeners for the 
656     * <code>sourceFont</code> property.
657     * 
658     * @param font  the font (<code>null</code> not permitted).
659     * 
660     * @see #getSourceFont()
661     */
662    public void setSourceFont(Font font) {
663        if (font == null) {
664            throw new IllegalArgumentException("Null 'font' argument.");
665        }
666        Font old = this.sourceSubtitle.getFont();
667        this.sourceSubtitle.setFont(font);
668        firePropertyChange("sourceFont", old, font);        
669    }
670
671    /**
672     * Returns the paint used to draw the chart's source subtitle.
673     * 
674     * @return The paint used to draw the chart's source subtitle.
675     * 
676     * @see #setSourcePaint(Paint)
677     */
678    public Paint getSourcePaint() {
679        return this.sourceSubtitle.getPaint();
680    }
681    
682    /**
683     * Sets the paint for the chart's source subtitle and sends a 
684     * {@link PropertyChangeEvent} to all registered listeners for the 
685     * <code>sourcePaint</code> property.
686     * 
687     * @param paint  the paint (<code>null</code> not permitted).
688     * 
689     * @see #getSourcePaint()
690     */
691    public void setSourcePaint(Paint paint) {
692        if (paint == null) {
693            throw new IllegalArgumentException("Null 'paint' argument");
694        }
695        Paint old = this.sourceSubtitle.getPaint();
696        this.sourceSubtitle.setPaint(paint);
697        firePropertyChange("sourcePaint", old, paint);        
698    }    
699    
700    /**
701     * Returns the flag that controls whether or not the plot outline is
702     * visible.
703     * 
704     * @return The flag that controls whether or not the plot outline is
705     *         visible.
706     *         
707     * @see #setPlotOutlineVisible(boolean)
708     */
709    public boolean isPlotOutlineVisible() {
710        Plot plot = this.chart.getPlot();
711        if (plot != null) {
712            return plot.isOutlineVisible();
713        }
714        return false;
715    }
716    
717    /**
718     * Sets the flag that controls whether or not the plot outline is
719     * visible and sends a {@link PropertyChangeEvent} to all
720     * registered listeners for the <code>plotOutlineVisible</code> property.
721     * 
722     * @param visible  the new flag value.
723     * 
724     * @see #isPlotOutlineVisible()
725     */
726    public void setPlotOutlineVisible(boolean visible) {
727        Plot plot = this.chart.getPlot();
728        if (plot != null) {
729            boolean old = plot.isOutlineVisible();
730            plot.setOutlineVisible(visible);
731            firePropertyChange("plotOutlineVisible", old, visible);
732        }
733    }
734    
735    /**
736     * Returns the alpha transparency used when filling the background of the
737     * plot area.
738     * 
739     * @return The alpha transparency.
740     * 
741     * @see #setPlotBackgroundAlpha(float)
742     */
743    public float getPlotBackgroundAlpha() {
744        float result = 1.0f;
745        Plot plot = this.chart.getPlot();
746        if (plot != null) {
747            result = plot.getBackgroundAlpha();
748        }
749        return result;
750    }
751    
752    /**
753     * Sets the alpha transparency used when filling the background of the
754     * plot area and sends a {@link PropertyChangeEvent} to all
755     * registered listeners for the <code>plotBackgroundAlpha</code> property.
756     * 
757     * @param alpha  the alpha transparency (in the range 0.0 to 1.0).
758     * 
759     * @see #getPlotBackgroundAlpha()
760     */
761    public void setPlotBackgroundAlpha(float alpha) {
762        Plot plot = this.chart.getPlot();
763        if (plot != null) {
764            float old = plot.getBackgroundAlpha();
765            plot.setBackgroundAlpha(alpha);
766            firePropertyChange("plotBackgroundAlpha", old, alpha);
767        }
768    }
769    
770    /**
771     * Returns the background paint for the plot, or <code>null</code>.
772     * 
773     * @return The background paint (possibly <code>null</code>).
774     * 
775     * @see #setPlotBackgroundPaint(Paint)
776     */
777    public Paint getPlotBackgroundPaint() {
778        Paint result = null;
779        Plot plot = this.chart.getPlot();
780        if (plot != null) {
781            result = plot.getBackgroundPaint();
782        }
783        return result;        
784    }
785
786    /**
787     * Sets the background paint and sends a {@link PropertyChangeEvent} to all
788     * registered listeners for the <code>plotBackgroundPaint</code> property.
789     * 
790     * @param paint  the paint (<code>null</code> permitted).
791     * 
792     * @see #getPlotBackgroundPaint()
793     */
794    public void setPlotBackgroundPaint(Paint paint) {
795        Plot plot = this.chart.getPlot();
796        if (plot != null) {
797            Paint old = plot.getBackgroundPaint();
798            plot.setBackgroundPaint(paint);
799            firePropertyChange("plotBackgroundPaint", old, paint);
800        }
801    }
802    
803    /**
804     * Returns the legend position.
805     * 
806     * @return The legend position.
807     * 
808     * @see #setLegendPosition(LegendPosition)
809     */
810    public LegendPosition getLegendPosition() {
811        return this.legendPosition;
812    }
813    
814    /**
815     * Sets the legend position and sends a {@link PropertyChangeEvent} to all
816     * registered listeners for the <code>legendPosition</code> property.
817     * 
818     * @param position  the position (<code>null</code> not permitted).
819     * 
820     * @see #getLegendPosition()
821     */
822    public void setLegendPosition(LegendPosition position) {
823        if (position == null) {
824            throw new IllegalArgumentException("Null 'position' argument.");
825        }
826        LegendPosition old = this.legendPosition;
827        if (position.equals(LegendPosition.NONE)) {
828            if (!old.equals(LegendPosition.NONE)) {
829                this.chart.removeSubtitle(this.legend);
830            }
831        }
832        else {
833            if (old.equals(LegendPosition.NONE)) {
834                this.chart.addSubtitle(1, this.legend);
835            }
836            if (position.equals(LegendPosition.TOP)) {
837                this.legend.setPosition(RectangleEdge.TOP);
838            }
839            else if (position.equals(LegendPosition.BOTTOM)) {
840                this.legend.setPosition(RectangleEdge.BOTTOM);
841            }
842            else if (position.equals(LegendPosition.LEFT)) {
843                this.legend.setPosition(RectangleEdge.LEFT);
844            }
845            else if (position.equals(LegendPosition.RIGHT)) {
846                this.legend.setPosition(RectangleEdge.RIGHT);
847            }
848        }
849        this.legendPosition = position;
850        firePropertyChange("legendPosition", old, position);
851    }
852    
853    /**
854     * Returns the font for the legend items.
855     * 
856     * @return The font.
857     * 
858     * @see #setLegendItemFont(Font)
859     */
860    public Font getLegendItemFont() {
861        return this.legend.getItemFont();
862    }
863    
864    /**
865     * Sets the font for the legend items and sends a 
866     * {@link PropertyChangeEvent} to all registered listeners for the
867     * <code>legendItemFont</code> property.
868     * 
869     * @param font  the font (<code>null</code> not permitted).
870     * 
871     * @see #getLegendItemFont()
872     */
873    public void setLegendItemFont(Font font) {
874        Font old = this.legend.getItemFont();
875        this.legend.setItemFont(font);
876        firePropertyChange("legendItemFont", old, font);
877    }
878
879    /**
880     * Returns the paint used to display the legend items.
881     * 
882     * @return The paint.
883     * 
884     * @see #setLegendItemPaint(Paint)
885     */
886    public Paint getLegendItemPaint() {
887        return this.legend.getItemPaint();
888    }
889    
890    /**
891     * Sets the paint used to display the legend items and sends a 
892     * {@link PropertyChangeEvent} to all registered listeners for the 
893     * <code>legendItemPaint</code> property.
894     * 
895     * @param paint  the paint (<code>null</code> not permitted).
896     * 
897     * @see #getLegendItemPaint()
898     */
899    public void setLegendItemPaint(Paint paint) {
900        Paint old = this.legend.getItemPaint();
901        this.legend.setItemPaint(paint);
902        firePropertyChange("legendItemPaint", old, paint);
903    }
904
905    private static final int MIN_DRAW_WIDTH = 200;
906    
907    private static final int MIN_DRAW_HEIGHT = 150;
908    
909    /**
910     * Paints this component, including the chart it contains.
911     * 
912     * @param g  the graphics target.
913     */
914    protected void paintComponent(Graphics g) {
915        
916        super.paintComponent(g);
917        if (this.chart == null) {
918            return;
919        }
920        
921        Graphics2D g2 = (Graphics2D) g.create();
922
923        // first determine the size of the chart rendering area...
924        Dimension size = getSize();
925        Insets insets = getInsets();
926        Rectangle2D available = new Rectangle2D.Double(insets.left, insets.top,
927                size.getWidth() - insets.left - insets.right,
928                size.getHeight() - insets.top - insets.bottom);
929
930        // work out if scaling is required (when the component is small, the
931        // chart will be drawn at a larger size and scaled down to fit the
932        // space...
933        boolean scale = false;
934        double drawWidth = available.getWidth();
935        double drawHeight = available.getHeight();
936        this.scaleX = 1.0;
937        this.scaleY = 1.0;
938
939        if (drawWidth < MIN_DRAW_WIDTH) {
940            this.scaleX = drawWidth / MIN_DRAW_WIDTH;
941            drawWidth = MIN_DRAW_WIDTH;
942            scale = true;
943        }
944
945        if (drawHeight < MIN_DRAW_HEIGHT) {
946            this.scaleY = drawHeight / MIN_DRAW_HEIGHT;
947            drawHeight = MIN_DRAW_HEIGHT;
948            scale = true;
949        }
950
951        Rectangle2D chartArea = new Rectangle2D.Double(0.0, 0.0, drawWidth, 
952                drawHeight);
953
954        if ((this.chartBuffer == null) 
955                || (this.chartBufferWidth != available.getWidth())
956                || (this.chartBufferHeight != available.getHeight())) {
957            this.chartBufferWidth = (int) available.getWidth();
958            this.chartBufferHeight = (int) available.getHeight();
959            GraphicsConfiguration gc = g2.getDeviceConfiguration();
960            this.chartBuffer = gc.createCompatibleImage(
961                    this.chartBufferWidth, this.chartBufferHeight, 
962                    Transparency.TRANSLUCENT);
963            this.refreshBuffer = true;
964        }
965
966        // do we need to redraw the buffer?
967        if (this.refreshBuffer) {
968
969            Rectangle2D bufferArea = new Rectangle2D.Double(0, 0, 
970                    this.chartBufferWidth, this.chartBufferHeight);
971
972            Graphics2D bufferG2 = (Graphics2D) this.chartBuffer.getGraphics();
973            if (scale) {
974                AffineTransform saved = bufferG2.getTransform();
975                AffineTransform st = AffineTransform.getScaleInstance(
976                        this.scaleX, this.scaleY);
977                bufferG2.transform(st);
978                this.chart.draw(bufferG2, chartArea, this.anchor, this.info);
979                bufferG2.setTransform(saved);
980            }
981            else {
982                this.chart.draw(bufferG2, bufferArea, this.anchor, this.info);
983            }
984
985            this.refreshBuffer = false;
986
987        }
988
989        // zap the buffer onto the panel...
990        g2.drawImage(this.chartBuffer, insets.left, insets.top, this);
991        g2.dispose();
992    }
993
994    /**
995     * If the user clicks on the chart, see if that translates into an event
996     * that we report...
997     * 
998     * @param event  the event.
999     */
1000    public void mouseClicked(MouseEvent event) {
1001        // if no-one is listening, just return...
1002        Object[] listeners = this.listeners.getListeners(
1003                LegendClickListener.class);
1004        if (listeners.length == 0) {
1005            return;
1006        }
1007
1008        Insets insets = getInsets();
1009        int x = event.getX() - insets.left;
1010        int y = event.getY() - insets.top;
1011
1012        ChartEntity entity = null;
1013        if (this.info != null) {
1014            EntityCollection entities = this.info.getEntityCollection();
1015            if (entities != null) {
1016                entity = entities.getEntity(x, y);
1017            }
1018        }
1019        if (entity instanceof LegendItemEntity) {
1020            LegendItemEntity lie = (LegendItemEntity) entity;
1021            LegendClickEvent lce = new LegendClickEvent(this, lie.getDataset(),
1022                    lie.getSeriesKey());
1023            fireLegendClickEvent(lce);
1024        }
1025    }
1026
1027    /* (non-Javadoc)
1028     * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
1029     */
1030    public void mouseEntered(MouseEvent e) {
1031        // ignore   
1032    }
1033
1034    /* (non-Javadoc)
1035     * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
1036     */
1037    public void mouseExited(MouseEvent e) {
1038        // ignore   
1039    }
1040
1041    /* (non-Javadoc)
1042     * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
1043     */
1044    public void mousePressed(MouseEvent e) {
1045        if (this.zoomRectangle == null) {
1046            Rectangle2D screenDataArea = getScreenDataArea();
1047            if (screenDataArea != null) {
1048                this.zoomPoint = getPointInRectangle(e.getX(), e.getY(), 
1049                        screenDataArea);
1050            }
1051            else {
1052                this.zoomPoint = null;
1053            }
1054        }
1055        if (e.isPopupTrigger()) {
1056            JPopupMenu popup = createPopup();
1057            popup.show(this, e.getX(), e.getY());
1058        }
1059    }
1060
1061    /* (non-Javadoc)
1062     * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
1063     */
1064    public void mouseReleased(MouseEvent e) {
1065        if (this.zoomRectangle != null) {
1066            boolean hZoom = false;
1067            boolean vZoom = false;
1068            Plot plot = this.chart.getPlot();
1069            if (plot instanceof Zoomable) {
1070                Zoomable z = (Zoomable) plot;
1071                PlotOrientation orientation = z.getOrientation();
1072                if (orientation == PlotOrientation.HORIZONTAL) {
1073                    hZoom = z.isRangeZoomable();
1074                    vZoom = z.isDomainZoomable();
1075                }
1076                else {
1077                    hZoom = z.isDomainZoomable();           
1078                    vZoom = z.isRangeZoomable();
1079                }
1080            }
1081            
1082            boolean zoomTrigger1 = hZoom && Math.abs(e.getX() 
1083                - this.zoomPoint.getX()) >= this.zoomTriggerDistance;
1084            boolean zoomTrigger2 = vZoom && Math.abs(e.getY() 
1085                - this.zoomPoint.getY()) >= this.zoomTriggerDistance;
1086            if (zoomTrigger1 || zoomTrigger2) {
1087                if ((hZoom && (e.getX() < this.zoomPoint.getX())) 
1088                    || (vZoom && (e.getY() < this.zoomPoint.getY()))) {
1089                    restoreAutoBounds();
1090                }
1091                else {
1092                    double x, y, w, h;
1093                    Rectangle2D screenDataArea = getScreenDataArea();
1094                    // for mouseReleased event, (horizontalZoom || verticalZoom)
1095                    // will be true, so we can just test for either being false;
1096                    // otherwise both are true
1097                    if (!vZoom) {
1098                        x = this.zoomPoint.getX();
1099                        y = screenDataArea.getMinY();
1100                        w = Math.min(this.zoomRectangle.getWidth(),
1101                                screenDataArea.getMaxX() 
1102                                - this.zoomPoint.getX());
1103                        h = screenDataArea.getHeight();
1104                    }
1105                    else if (!hZoom) {
1106                        x = screenDataArea.getMinX();
1107                        y = this.zoomPoint.getY();
1108                        w = screenDataArea.getWidth();
1109                        h = Math.min(this.zoomRectangle.getHeight(),
1110                                screenDataArea.getMaxY() 
1111                                - this.zoomPoint.getY());
1112                    }
1113                    else {
1114                        x = this.zoomPoint.getX();
1115                        y = this.zoomPoint.getY();
1116                        w = Math.min(this.zoomRectangle.getWidth(),
1117                                screenDataArea.getMaxX() 
1118                                - this.zoomPoint.getX());
1119                        h = Math.min(this.zoomRectangle.getHeight(),
1120                                screenDataArea.getMaxY() 
1121                                - this.zoomPoint.getY());
1122                    }
1123                    Rectangle2D zoomArea = new Rectangle2D.Double(x, y, w, h);
1124                    zoom(zoomArea);
1125                }
1126                this.zoomPoint = null;
1127                this.zoomRectangle = null;
1128            }
1129            else {
1130                // Erase the zoom rectangle
1131                Graphics2D g2 = (Graphics2D) getGraphics();
1132                drawZoomRectangle(g2);
1133                g2.dispose();
1134                this.zoomPoint = null;
1135                this.zoomRectangle = null;
1136            }
1137
1138        }
1139
1140        else if (e.isPopupTrigger()) {
1141            JPopupMenu popup = createPopup();
1142            popup.show(this, e.getX(), e.getY());
1143        }
1144    }
1145
1146    /**
1147     * Implementation of the MouseMotionListener's method.
1148     *
1149     * @param e  the event.
1150     */
1151    public void mouseMoved(MouseEvent e) {
1152        // do nothing
1153    }
1154    
1155    /**
1156     * Handles a 'mouse dragged' event.
1157     *
1158     * @param e  the mouse event.
1159     */
1160    public void mouseDragged(MouseEvent e) {
1161        // FIXME: handle popup
1162        // if the popup menu has already been triggered, then ignore dragging...
1163//        if (this.popup != null && this.popup.isShowing()) {
1164//            return;
1165//        }
1166        // if no initial zoom point was set, ignore dragging...
1167        if (this.zoomPoint == null) {
1168            return;
1169        }
1170        Graphics2D g2 = (Graphics2D) getGraphics();
1171
1172        // Erase the previous zoom rectangle (if any)...
1173        drawZoomRectangle(g2);
1174
1175        boolean hZoom = false;
1176        boolean vZoom = false;
1177        Plot plot = this.chart.getPlot();
1178        if (plot instanceof Zoomable) {
1179            Zoomable z = (Zoomable) plot;
1180            PlotOrientation orientation = z.getOrientation();
1181            if (orientation == PlotOrientation.HORIZONTAL) {
1182                hZoom = z.isRangeZoomable();
1183                vZoom = z.isDomainZoomable();
1184            }
1185            else {
1186                hZoom = z.isDomainZoomable();           
1187                vZoom = z.isRangeZoomable();
1188            }
1189        }
1190        Rectangle2D scaledDataArea = getScreenDataArea();
1191        if (hZoom && vZoom) {
1192            // selected rectangle shouldn't extend outside the data area...
1193            double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
1194            double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
1195            this.zoomRectangle = new Rectangle2D.Double(
1196                    this.zoomPoint.getX(), this.zoomPoint.getY(),
1197                    xmax - this.zoomPoint.getX(), ymax - this.zoomPoint.getY());
1198        }
1199        else if (hZoom) {
1200            double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
1201            this.zoomRectangle = new Rectangle2D.Double(
1202                    this.zoomPoint.getX(), scaledDataArea.getMinY(),
1203                    xmax - this.zoomPoint.getX(), scaledDataArea.getHeight());
1204        }
1205        else if (vZoom) {
1206            double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
1207            this.zoomRectangle = new Rectangle2D.Double(
1208                    scaledDataArea.getMinX(), this.zoomPoint.getY(),
1209                    scaledDataArea.getWidth(), ymax - this.zoomPoint.getY());
1210        }
1211
1212        // Draw the new zoom rectangle...
1213        drawZoomRectangle(g2);
1214        
1215        g2.dispose();
1216
1217    }
1218    
1219    /**
1220     * Creates a popup menu for display on the component.
1221     * 
1222     * @return A popup menu.
1223     */
1224    protected JPopupMenu createPopup() {
1225        // This is still a work-in-progress, obviously!
1226        JPopupMenu popup = new JPopupMenu();
1227        JMenuItem saveAs = new JMenuItem("Save As...");
1228        saveAs.addActionListener(this);
1229        saveAs.setActionCommand("SAVE_AS");
1230        popup.add(saveAs);
1231        return popup;
1232    }
1233    
1234    /**
1235     * Returns the data area for the chart (the area inside the axes) with the
1236     * current scaling applied (that is, the area as it appears on screen).
1237     *
1238     * @return The scaled data area.
1239     */
1240    public Rectangle2D getScreenDataArea() {
1241        Rectangle2D dataArea = this.info.getPlotInfo().getDataArea();
1242        Insets insets = getInsets();
1243        double x = dataArea.getX() * this.scaleX + insets.left;
1244        double y = dataArea.getY() * this.scaleY + insets.top;
1245        double w = dataArea.getWidth() * this.scaleX;
1246        double h = dataArea.getHeight() * this.scaleY;
1247        return new Rectangle2D.Double(x, y, w, h);
1248    }
1249
1250    /**
1251     * Returns a point based on (x, y) but constrained to be within the bounds
1252     * of the given rectangle.  This method could be moved to JCommon.
1253     * 
1254     * @param x  the x-coordinate.
1255     * @param y  the y-coordinate.
1256     * @param area  the rectangle (<code>null</code> not permitted).
1257     * 
1258     * @return A point within the rectangle.
1259     */
1260    private Point getPointInRectangle(int x, int y, Rectangle2D area) {
1261        x = (int) Math.max(Math.ceil(area.getMinX()), Math.min(x, 
1262                Math.floor(area.getMaxX())));   
1263        y = (int) Math.max(Math.ceil(area.getMinY()), Math.min(y, 
1264                Math.floor(area.getMaxY())));
1265        return new Point(x, y);
1266    }
1267
1268    /**
1269     * Restores the auto-range calculation on both axes.
1270     */
1271    public void restoreAutoBounds() {
1272        restoreAutoDomainBounds();
1273        restoreAutoRangeBounds();
1274    }
1275
1276    /**
1277     * Restores the auto-range calculation on the domain axis.
1278     */
1279    public void restoreAutoDomainBounds() {
1280        Plot p = this.chart.getPlot();
1281        if (p instanceof Zoomable) {
1282            Zoomable z = (Zoomable) p;
1283            // we need to guard against this.zoomPoint being null
1284            Point zp = (this.zoomPoint != null ? this.zoomPoint : new Point());
1285            z.zoomDomainAxes(0.0, this.info.getPlotInfo(), zp);
1286        }
1287    }
1288
1289    /**
1290     * Restores the auto-range calculation on the range axis.
1291     */
1292    public void restoreAutoRangeBounds() {
1293        Plot p = this.chart.getPlot();
1294        if (p instanceof Zoomable) {
1295            Zoomable z = (Zoomable) p;
1296            // we need to guard against this.zoomPoint being null
1297            Point zp = (this.zoomPoint != null ? this.zoomPoint : new Point());
1298            z.zoomRangeAxes(0.0, this.info.getPlotInfo(), zp);
1299        }
1300    }
1301
1302    /**
1303     * Translates a Java2D point on the chart to a screen location.
1304     *
1305     * @param java2DPoint  the Java2D point.
1306     *
1307     * @return The screen location.
1308     */
1309    public Point translateJava2DToScreen(Point2D java2DPoint) {
1310        Insets insets = getInsets();
1311        int x = (int) (java2DPoint.getX() * this.scaleX + insets.left);
1312        int y = (int) (java2DPoint.getY() * this.scaleY + insets.top);
1313        return new Point(x, y);
1314    }
1315
1316    /**
1317     * Translates a screen location to a Java2D point.
1318     *
1319     * @param screenPoint  the screen location.
1320     *
1321     * @return The Java2D coordinates.
1322     */
1323    public Point2D translateScreenToJava2D(Point screenPoint) {
1324        Insets insets = getInsets();
1325        double x = (screenPoint.getX() - insets.left) / this.scaleX;
1326        double y = (screenPoint.getY() - insets.top) / this.scaleY;
1327        return new Point2D.Double(x, y);
1328    }
1329
1330    /**
1331     * Zooms in on a selected region.
1332     *
1333     * @param selection  the selected region.
1334     */
1335    public void zoom(Rectangle2D selection) {
1336
1337        // get the origin of the zoom selection in the Java2D space used for
1338        // drawing the chart (that is, before any scaling to fit the panel)
1339        Point2D selectOrigin = translateScreenToJava2D(new Point(
1340                (int) Math.ceil(selection.getX()), 
1341                (int) Math.ceil(selection.getY())));
1342        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
1343        Rectangle2D scaledDataArea = getScreenDataArea();
1344        if ((selection.getHeight() > 0) && (selection.getWidth() > 0)) {
1345
1346            double hLower = (selection.getMinX() - scaledDataArea.getMinX()) 
1347                / scaledDataArea.getWidth();
1348            double hUpper = (selection.getMaxX() - scaledDataArea.getMinX()) 
1349                / scaledDataArea.getWidth();
1350            double vLower = (scaledDataArea.getMaxY() - selection.getMaxY()) 
1351                / scaledDataArea.getHeight();
1352            double vUpper = (scaledDataArea.getMaxY() - selection.getMinY()) 
1353                / scaledDataArea.getHeight();
1354
1355            Plot p = this.chart.getPlot();
1356            if (p instanceof Zoomable) {
1357                Zoomable z = (Zoomable) p;
1358                if (z.getOrientation() == PlotOrientation.HORIZONTAL) {
1359                    z.zoomDomainAxes(vLower, vUpper, plotInfo, selectOrigin);
1360                    z.zoomRangeAxes(hLower, hUpper, plotInfo, selectOrigin);
1361                }
1362                else {
1363                    z.zoomDomainAxes(hLower, hUpper, plotInfo, selectOrigin);
1364                    z.zoomRangeAxes(vLower, vUpper, plotInfo, selectOrigin);
1365                }
1366            }
1367
1368        }
1369
1370    }
1371
1372    /**
1373     * Switches the display of tooltips for the panel on or off.  Note that 
1374     * tooltips can only be displayed if the chart has been configured to
1375     * generate tooltip items.
1376     *
1377     * @param flag  <code>true</code> to enable tooltips, <code>false</code> to
1378     *              disable tooltips.
1379     */
1380    protected void setToolTipsEnabled(boolean flag) {
1381        if (flag) {
1382            ToolTipManager.sharedInstance().registerComponent(this);
1383        }
1384        else {
1385            ToolTipManager.sharedInstance().unregisterComponent(this);
1386        }
1387    }
1388
1389    /**
1390     * Returns a string for the tooltip.
1391     *
1392     * @param e  the mouse event.
1393     *
1394     * @return A tool tip or <code>null</code> if no tooltip is available.
1395     */
1396    public String getToolTipText(MouseEvent e) {
1397        String result = null;
1398        if (this.info != null) {
1399            EntityCollection entities = this.info.getEntityCollection();
1400            if (entities != null) {
1401                Insets insets = getInsets();
1402                ChartEntity entity = entities.getEntity(e.getX() - insets.left,
1403                        e.getY() - insets.top);
1404                if (entity != null) {
1405                    result = entity.getToolTipText();
1406                }
1407            }
1408        }
1409        return result;
1410    }
1411
1412    /**
1413     * Registers a listener to receive notification of legend clicks.
1414     * 
1415     * @param listener  the listener (<code>null</code> not permitted).
1416     */
1417    public void addLegendClickListener(LegendClickListener listener) {
1418        if (listener == null) {
1419            throw new IllegalArgumentException("Null 'listener' argument.");
1420        }
1421        this.listeners.add(LegendClickListener.class, listener);
1422    }
1423    
1424    /**
1425     * Unregisters a listener so that it no longer receives notification of 
1426     * legend clicks.
1427     * 
1428     * @param listener  the listener (<code>null</code> not permitted).
1429     */
1430    public void removeLegendClickListener(LegendClickListener listener) {
1431        if (listener == null) {
1432            throw new IllegalArgumentException("Null 'listener' argument.");
1433        }
1434        this.listeners.remove(LegendClickListener.class, listener);        
1435    }
1436    
1437    /**
1438     * Fires a legend click event.
1439     * 
1440     * @param event  the event.
1441     */
1442    public void fireLegendClickEvent(LegendClickEvent event) {
1443        Object[] listeners = this.listeners.getListeners(
1444                LegendClickListener.class);
1445        for (int i = listeners.length - 1; i >= 0; i -= 1) {
1446            ((LegendClickListener) listeners[i]).onLegendClick(event);
1447        }                
1448        
1449    }
1450    
1451    /**
1452     * Draws zoom rectangle (if present).
1453     * The drawing is performed in XOR mode, therefore
1454     * when this method is called twice in a row,
1455     * the second call will completely restore the state
1456     * of the canvas.
1457     * 
1458     * @param g2 the graphics device. 
1459     */
1460    private void drawZoomRectangle(Graphics2D g2) {
1461        // Set XOR mode to draw the zoom rectangle
1462        g2.setXORMode(Color.gray);
1463        if (this.zoomRectangle != null) {
1464            if (this.fillZoomRectangle) {
1465                g2.fill(this.zoomRectangle);
1466            }
1467            else {
1468                g2.draw(this.zoomRectangle);
1469            }
1470        }
1471        // Reset to the default 'overwrite' mode
1472        g2.setPaintMode();
1473    }
1474    
1475    /**
1476     * Receives notification of changes to the chart, and redraws the chart.
1477     *
1478     * @param event  details of the chart change event.
1479     */
1480    public void chartChanged(ChartChangeEvent event) {
1481        this.refreshBuffer = true;
1482        //Plot plot = this.chart.getPlot();
1483        //if (plot instanceof Zoomable) {
1484            //Zoomable z = (Zoomable) plot;
1485            //this.orientation = z.getOrientation();
1486        //}
1487        repaint();
1488    }
1489
1490    /* (non-Javadoc)
1491     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
1492     */
1493    public void actionPerformed(ActionEvent e) {
1494        if (e.getActionCommand().equals("SAVE_AS")) {
1495            try {
1496                doSaveAs();
1497            }
1498            catch (IOException ioe) {
1499                ioe.printStackTrace();
1500            }
1501        }
1502    }
1503    
1504    protected void doSaveAs() throws IOException {
1505        JFileChooser fileChooser = new JFileChooser();
1506        ExtensionFileFilter filter = new ExtensionFileFilter("PNG Files", 
1507                ".png");
1508        fileChooser.addChoosableFileFilter(filter);
1509
1510        int option = fileChooser.showSaveDialog(this);
1511        if (option == JFileChooser.APPROVE_OPTION) {
1512            String filename = fileChooser.getSelectedFile().getPath();
1513            ChartUtilities.saveChartAsPNG(new File(filename), this.chart, 
1514                    getWidth(), getHeight());
1515        }
1516    
1517    }
1518    
1519}