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:  not-yet-released
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    
029    package org.jfree.beans;
030    
031    import java.awt.Color;
032    import java.awt.Dimension;
033    import java.awt.Font;
034    import java.awt.Graphics;
035    import java.awt.Graphics2D;
036    import java.awt.GraphicsConfiguration;
037    import java.awt.Image;
038    import java.awt.Insets;
039    import java.awt.Paint;
040    import java.awt.Point;
041    import java.awt.Stroke;
042    import java.awt.Transparency;
043    import java.awt.event.ActionEvent;
044    import java.awt.event.ActionListener;
045    import java.awt.event.MouseEvent;
046    import java.awt.event.MouseListener;
047    import java.awt.event.MouseMotionListener;
048    import java.awt.geom.AffineTransform;
049    import java.awt.geom.Point2D;
050    import java.awt.geom.Rectangle2D;
051    import java.beans.PropertyChangeEvent;
052    import java.beans.PropertyEditorManager;
053    import java.io.File;
054    import java.io.IOException;
055    
056    import javax.swing.JComponent;
057    import javax.swing.JFileChooser;
058    import javax.swing.JMenuItem;
059    import javax.swing.JPopupMenu;
060    import javax.swing.ToolTipManager;
061    import javax.swing.event.EventListenerList;
062    
063    import org.jfree.beans.editors.AxisLocationEditor;
064    import org.jfree.beans.editors.LegendPositionEditor;
065    import org.jfree.beans.editors.PaintEditor;
066    import org.jfree.beans.editors.PlotOrientationEditor;
067    import org.jfree.beans.editors.RectangleEdgeEditor;
068    import org.jfree.beans.editors.StrokeEditor;
069    import org.jfree.beans.events.LegendClickEvent;
070    import org.jfree.beans.events.LegendClickListener;
071    import org.jfree.chart.ChartRenderingInfo;
072    import org.jfree.chart.ChartUtilities;
073    import org.jfree.chart.JFreeChart;
074    import org.jfree.chart.axis.AxisLocation;
075    import org.jfree.chart.entity.ChartEntity;
076    import org.jfree.chart.entity.EntityCollection;
077    import org.jfree.chart.entity.LegendItemEntity;
078    import org.jfree.chart.event.ChartChangeEvent;
079    import org.jfree.chart.event.ChartChangeListener;
080    import org.jfree.chart.plot.Plot;
081    import org.jfree.chart.plot.PlotOrientation;
082    import org.jfree.chart.plot.PlotRenderingInfo;
083    import org.jfree.chart.plot.Zoomable;
084    import org.jfree.chart.title.LegendTitle;
085    import org.jfree.chart.title.TextTitle;
086    import org.jfree.ui.ExtensionFileFilter;
087    import org.jfree.ui.HorizontalAlignment;
088    import org.jfree.ui.RectangleEdge;
089    import org.jfree.ui.RectangleInsets;
090    
091    /**
092     * A base class for creating chart beans.
093     */
094    public 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    }