001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------
028 * ChartPanel.java
029 * ---------------
030 * (C) Copyright 2000-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Andrzej Porebski;
034 *                   Soren Caspersen;
035 *                   Jonathan Nash;
036 *                   Hans-Jurgen Greiner;
037 *                   Andreas Schneider;
038 *                   Daniel van Enckevort;
039 *                   David M O'Donnell;
040 *                   Arnaud Lelievre;
041 *                   Matthias Rose;
042 *                   Onno vd Akker;
043 *                   Sergei Ivanov;
044 *                   Ulrich Voigt - patch 2686040;
045 *                   Alessandro Borges - patch 1460845;
046 *                   Martin Hoeller;
047 *                   Simon Legner - patch from bug 1129;
048 *                   Yuri Blankenstein;
049 */
050
051package org.jfree.chart;
052
053import java.awt.AWTEvent;
054import java.awt.AlphaComposite;
055import java.awt.Color;
056import java.awt.Composite;
057import java.awt.Cursor;
058import java.awt.Dimension;
059import java.awt.Graphics;
060import java.awt.Graphics2D;
061import java.awt.GraphicsConfiguration;
062import java.awt.Insets;
063import java.awt.Paint;
064import java.awt.Point;
065import java.awt.Rectangle;
066import java.awt.Toolkit;
067import java.awt.Transparency;
068import java.awt.datatransfer.Clipboard;
069import java.awt.event.ActionEvent;
070import java.awt.event.ActionListener;
071import java.awt.event.InputEvent;
072import java.awt.event.MouseEvent;
073import java.awt.event.MouseListener;
074import java.awt.event.MouseMotionListener;
075import java.awt.geom.AffineTransform;
076import java.awt.geom.Line2D;
077import java.awt.geom.Point2D;
078import java.awt.geom.Rectangle2D;
079import java.awt.image.BufferedImage;
080import java.awt.print.PageFormat;
081import java.awt.print.Printable;
082import java.awt.print.PrinterException;
083import java.awt.print.PrinterJob;
084import java.io.BufferedWriter;
085import java.io.File;
086import java.io.FileWriter;
087import java.io.IOException;
088import java.io.ObjectInputStream;
089import java.io.ObjectOutputStream;
090import java.io.Serializable;
091import java.lang.reflect.Constructor;
092import java.lang.reflect.InvocationTargetException;
093import java.lang.reflect.Method;
094import java.util.ArrayList;
095import java.util.EventListener;
096import java.util.List;
097import java.util.ResourceBundle;
098
099import javax.swing.JFileChooser;
100import javax.swing.JMenu;
101import javax.swing.JMenuItem;
102import javax.swing.JOptionPane;
103import javax.swing.JPanel;
104import javax.swing.JPopupMenu;
105import javax.swing.SwingUtilities;
106import javax.swing.ToolTipManager;
107import javax.swing.event.EventListenerList;
108import javax.swing.filechooser.FileNameExtensionFilter;
109
110import org.jfree.chart.editor.ChartEditor;
111import org.jfree.chart.editor.ChartEditorManager;
112import org.jfree.chart.entity.ChartEntity;
113import org.jfree.chart.entity.EntityCollection;
114import org.jfree.chart.event.ChartChangeEvent;
115import org.jfree.chart.event.ChartChangeListener;
116import org.jfree.chart.event.ChartProgressEvent;
117import org.jfree.chart.event.ChartProgressListener;
118import org.jfree.chart.event.OverlayChangeEvent;
119import org.jfree.chart.event.OverlayChangeListener;
120import org.jfree.chart.panel.Overlay;
121import org.jfree.chart.plot.Pannable;
122import org.jfree.chart.plot.Plot;
123import org.jfree.chart.plot.PlotOrientation;
124import org.jfree.chart.plot.PlotRenderingInfo;
125import org.jfree.chart.plot.Zoomable;
126import org.jfree.chart.util.Args;
127import org.jfree.chart.util.ResourceBundleWrapper;
128import org.jfree.chart.util.SerialUtils;
129
130/**
131 * A Swing GUI component for displaying a {@link JFreeChart} object.
132 * <P>
133 * The panel registers with the chart to receive notification of changes to any
134 * component of the chart.  The chart is redrawn automatically whenever this
135 * notification is received.
136 */
137public class ChartPanel extends JPanel implements ChartChangeListener,
138        ChartProgressListener, ActionListener, MouseListener,
139        MouseMotionListener, OverlayChangeListener, Printable, Serializable {
140
141    /** For serialization. */
142    private static final long serialVersionUID = 6046366297214274674L;
143
144    /**
145     * Default setting for buffer usage.  The default has been changed to
146     * {@code true} from version 1.0.13 onwards, because of a severe
147     * performance problem with drawing the zoom rectangle using XOR (which
148     * now happens only when the buffer is NOT used).
149     */
150    public static final boolean DEFAULT_BUFFER_USED = true;
151
152    /** The default panel width. */
153    public static final int DEFAULT_WIDTH = 1024;
154
155    /** The default panel height. */
156    public static final int DEFAULT_HEIGHT = 768;
157
158    /** The default limit below which chart scaling kicks in. */
159    public static final int DEFAULT_MINIMUM_DRAW_WIDTH = 300;
160
161    /** The default limit below which chart scaling kicks in. */
162    public static final int DEFAULT_MINIMUM_DRAW_HEIGHT = 200;
163
164    /** The default limit above which chart scaling kicks in. */
165    public static final int DEFAULT_MAXIMUM_DRAW_WIDTH = 2048;
166
167    /** The default limit above which chart scaling kicks in. */
168    public static final int DEFAULT_MAXIMUM_DRAW_HEIGHT = 1536;
169
170    /** The minimum size required to perform a zoom on a rectangle */
171    public static final int DEFAULT_ZOOM_TRIGGER_DISTANCE = 10;
172
173    /** Properties action command. */
174    public static final String PROPERTIES_COMMAND = "PROPERTIES";
175
176    /**
177     * Copy action command.
178     */
179    public static final String COPY_COMMAND = "COPY";
180
181    /** Save action command. */
182    public static final String SAVE_COMMAND = "SAVE";
183
184    /** Action command to save as PNG. */
185    private static final String SAVE_AS_PNG_COMMAND = "SAVE_AS_PNG";
186    
187    /** Action command to save as SVG. */
188    private static final String SAVE_AS_SVG_COMMAND = "SAVE_AS_SVG";
189    
190    /** Action command to save as PDF. */
191    private static final String SAVE_AS_PDF_COMMAND = "SAVE_AS_PDF";
192    
193    /** Print action command. */
194    public static final String PRINT_COMMAND = "PRINT";
195
196    /** Zoom in (both axes) action command. */
197    public static final String ZOOM_IN_BOTH_COMMAND = "ZOOM_IN_BOTH";
198
199    /** Zoom in (domain axis only) action command. */
200    public static final String ZOOM_IN_DOMAIN_COMMAND = "ZOOM_IN_DOMAIN";
201
202    /** Zoom in (range axis only) action command. */
203    public static final String ZOOM_IN_RANGE_COMMAND = "ZOOM_IN_RANGE";
204
205    /** Zoom out (both axes) action command. */
206    public static final String ZOOM_OUT_BOTH_COMMAND = "ZOOM_OUT_BOTH";
207
208    /** Zoom out (domain axis only) action command. */
209    public static final String ZOOM_OUT_DOMAIN_COMMAND = "ZOOM_DOMAIN_BOTH";
210
211    /** Zoom out (range axis only) action command. */
212    public static final String ZOOM_OUT_RANGE_COMMAND = "ZOOM_RANGE_BOTH";
213
214    /** Zoom reset (both axes) action command. */
215    public static final String ZOOM_RESET_BOTH_COMMAND = "ZOOM_RESET_BOTH";
216
217    /** Zoom reset (domain axis only) action command. */
218    public static final String ZOOM_RESET_DOMAIN_COMMAND = "ZOOM_RESET_DOMAIN";
219
220    /** Zoom reset (range axis only) action command. */
221    public static final String ZOOM_RESET_RANGE_COMMAND = "ZOOM_RESET_RANGE";
222
223    /** The chart that is displayed in the panel. */
224    private JFreeChart chart;
225
226    /** Storage for registered (chart) mouse listeners. */
227    private transient EventListenerList chartMouseListeners;
228
229    /** A flag that controls whether the off-screen buffer is used. */
230    private final boolean useBuffer;
231
232    /** A flag that indicates that the buffer should be refreshed. */
233    private boolean refreshBuffer;
234
235    /** A buffer for the rendered chart. */
236    private transient BufferedImage chartBuffer;
237
238    /**
239     * The minimum width for drawing a chart (uses scaling for smaller widths).
240     */
241    private int minimumDrawWidth;
242
243    /**
244     * The minimum height for drawing a chart (uses scaling for smaller
245     * heights).
246     */
247    private int minimumDrawHeight;
248
249    /**
250     * The maximum width for drawing a chart (uses scaling for bigger
251     * widths).
252     */
253    private int maximumDrawWidth;
254
255    /**
256     * The maximum height for drawing a chart (uses scaling for bigger
257     * heights).
258     */
259    private int maximumDrawHeight;
260
261    /** The popup menu for the frame. */
262    private JPopupMenu popup;
263
264    /** The drawing info collected the last time the chart was drawn. */
265    private final ChartRenderingInfo info;
266
267    /** The chart anchor point. */
268    private Point2D anchor;
269
270    /** The scale factor used to draw the chart. */
271    private double scaleX;
272
273    /** The scale factor used to draw the chart. */
274    private double scaleY;
275
276    /** The plot orientation. */
277    private PlotOrientation orientation = PlotOrientation.VERTICAL;
278
279    /** A flag that controls whether or not domain zooming is enabled. */
280    private boolean domainZoomable = false;
281
282    /** A flag that controls whether or not range zooming is enabled. */
283    private boolean rangeZoomable = false;
284
285    /**
286     * The zoom rectangle starting point (selected by the user with a mouse
287     * click).  This is a point on the screen, not the chart (which may have
288     * been scaled up or down to fit the panel).
289     */
290    private Point2D zoomPoint = null;
291
292    /** The zoom rectangle (selected by the user with the mouse). */
293    private transient Rectangle2D zoomRectangle = null;
294
295    /** Controls if the zoom rectangle is drawn as an outline or filled. */
296    private boolean fillZoomRectangle = true;
297
298    /** The minimum distance required to drag the mouse to trigger a zoom. */
299    private int zoomTriggerDistance;
300
301    /** A flag that controls whether or not horizontal tracing is enabled. */
302    private boolean horizontalAxisTrace = false;
303
304    /** A flag that controls whether or not vertical tracing is enabled. */
305    private boolean verticalAxisTrace = false;
306
307    /** A vertical trace line. */
308    private transient Line2D verticalTraceLine;
309
310    /** A horizontal trace line. */
311    private transient Line2D horizontalTraceLine;
312
313    /** Menu item for zooming in on a chart (both axes). */
314    private JMenuItem zoomInBothMenuItem;
315
316    /** Menu item for zooming in on a chart (domain axis). */
317    private JMenuItem zoomInDomainMenuItem;
318
319    /** Menu item for zooming in on a chart (range axis). */
320    private JMenuItem zoomInRangeMenuItem;
321
322    /** Menu item for zooming out on a chart. */
323    private JMenuItem zoomOutBothMenuItem;
324
325    /** Menu item for zooming out on a chart (domain axis). */
326    private JMenuItem zoomOutDomainMenuItem;
327
328    /** Menu item for zooming out on a chart (range axis). */
329    private JMenuItem zoomOutRangeMenuItem;
330
331    /** Menu item for resetting the zoom (both axes). */
332    private JMenuItem zoomResetBothMenuItem;
333
334    /** Menu item for resetting the zoom (domain axis only). */
335    private JMenuItem zoomResetDomainMenuItem;
336
337    /** Menu item for resetting the zoom (range axis only). */
338    private JMenuItem zoomResetRangeMenuItem;
339
340    /**
341     * The default directory for saving charts to file.
342     */
343    private File defaultDirectoryForSaveAs;
344
345    /** A flag that controls whether or not file extensions are enforced. */
346    private boolean enforceFileExtensions;
347
348    /** A flag that indicates if original tooltip delays are changed. */
349    private boolean ownToolTipDelaysActive;
350
351    /** Original initial tooltip delay of ToolTipManager.sharedInstance(). */
352    private int originalToolTipInitialDelay;
353
354    /** Original reshow tooltip delay of ToolTipManager.sharedInstance(). */
355    private int originalToolTipReshowDelay;
356
357    /** Original dismiss tooltip delay of ToolTipManager.sharedInstance(). */
358    private int originalToolTipDismissDelay;
359
360    /** Own initial tooltip delay to be used in this chart panel. */
361    private int ownToolTipInitialDelay;
362
363    /** Own reshow tooltip delay to be used in this chart panel. */
364    private int ownToolTipReshowDelay;
365
366    /** Own dismiss tooltip delay to be used in this chart panel. */
367    private int ownToolTipDismissDelay;
368
369    /** The factor used to zoom in on an axis range. */
370    private double zoomInFactor = 0.5;
371
372    /** The factor used to zoom out on an axis range. */
373    private double zoomOutFactor = 2.0;
374
375    /**
376     * A flag that controls whether zoom operations are centred on the
377     * current anchor point, or the centre point of the relevant axis.
378     */
379    private boolean zoomAroundAnchor;
380
381    /**
382     * The paint used to draw the zoom rectangle outline.
383     */
384    private transient Paint zoomOutlinePaint;
385
386    /**
387     * The zoom fill paint (should use transparency).
388     */
389    private transient Paint zoomFillPaint;
390
391    /** The resourceBundle for the localization. */
392    protected static ResourceBundle localizationResources
393            = ResourceBundleWrapper.getBundle(
394                    "org.jfree.chart.LocalizationBundle");
395
396    /** 
397     * Temporary storage for the width and height of the chart 
398     * drawing area during panning.
399     */
400    private double panW, panH;
401
402    /** The last mouse position during panning. */
403    private Point panLast;
404
405    /**
406     * The mask for mouse events to trigger panning.
407     */
408    private int panMask = InputEvent.CTRL_MASK;
409
410    /**
411     * A list of overlays for the panel.
412     */
413    private final List<Overlay> overlays;
414    
415    /**
416     * Constructs a panel that displays the specified chart.
417     *
418     * @param chart  the chart.
419     */
420    public ChartPanel(JFreeChart chart) {
421        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT,
422            DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT,
423            DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT,
424            DEFAULT_BUFFER_USED,
425            true,  // properties
426            true,  // save
427            true,  // print
428            true,  // zoom
429            true   // tooltips
430        );
431
432    }
433
434    /**
435     * Constructs a panel containing a chart.  The {@code useBuffer} flag
436     * controls whether or not an offscreen {@code BufferedImage} is
437     * maintained for the chart.  If the buffer is used, more memory is
438     * consumed, but panel repaints will be a lot quicker in cases where the
439     * chart itself hasn't changed (for example, when another frame is moved
440     * to reveal the panel).  WARNING: If you set the {@code useBuffer}
441     * flag to false, note that the mouse zooming rectangle will (in that case)
442     * be drawn using XOR, and there is a SEVERE performance problem with that
443     * on JRE6 on Windows.
444     *
445     * @param chart  the chart.
446     * @param useBuffer  a flag controlling whether or not an off-screen buffer
447     *                   is used (read the warning above before setting this
448     *                   to {@code false}).
449     */
450    public ChartPanel(JFreeChart chart, boolean useBuffer) {
451
452        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH,
453                DEFAULT_MINIMUM_DRAW_HEIGHT, DEFAULT_MAXIMUM_DRAW_WIDTH,
454                DEFAULT_MAXIMUM_DRAW_HEIGHT, useBuffer,
455                true,  // properties
456                true,  // save
457                true,  // print
458                true,  // zoom
459                true   // tooltips
460                );
461
462    }
463
464    /**
465     * Constructs a JFreeChart panel.
466     *
467     * @param chart  the chart.
468     * @param properties  a flag indicating whether or not the chart property
469     *                    editor should be available via the popup menu.
470     * @param save  a flag indicating whether or not save options should be
471     *              available via the popup menu.
472     * @param print  a flag indicating whether or not the print option
473     *               should be available via the popup menu.
474     * @param zoom  a flag indicating whether or not zoom options should
475     *              be added to the popup menu.
476     * @param tooltips  a flag indicating whether or not tooltips should be
477     *                  enabled for the chart.
478     */
479    public ChartPanel(JFreeChart chart, boolean properties, boolean save,
480            boolean print, boolean zoom, boolean tooltips) {
481
482        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT,
483             DEFAULT_MINIMUM_DRAW_WIDTH, DEFAULT_MINIMUM_DRAW_HEIGHT,
484             DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT,
485             DEFAULT_BUFFER_USED, properties, save, print, zoom, tooltips);
486
487    }
488
489    /**
490     * Constructs a JFreeChart panel.
491     *
492     * @param chart  the chart.
493     * @param width  the preferred width of the panel.
494     * @param height  the preferred height of the panel.
495     * @param minimumDrawWidth  the minimum drawing width.
496     * @param minimumDrawHeight  the minimum drawing height.
497     * @param maximumDrawWidth  the maximum drawing width.
498     * @param maximumDrawHeight  the maximum drawing height.
499     * @param useBuffer  a flag that indicates whether to use the off-screen
500     *                   buffer to improve performance (at the expense of
501     *                   memory).
502     * @param properties  a flag indicating whether or not the chart property
503     *                    editor should be available via the popup menu.
504     * @param save  a flag indicating whether or not save options should be
505     *              available via the popup menu.
506     * @param print  a flag indicating whether or not the print option
507     *               should be available via the popup menu.
508     * @param zoom  a flag indicating whether or not zoom options should be
509     *              added to the popup menu.
510     * @param tooltips  a flag indicating whether or not tooltips should be
511     *                  enabled for the chart.
512     */
513    public ChartPanel(JFreeChart chart, int width, int height,
514            int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
515            int maximumDrawHeight, boolean useBuffer, boolean properties,
516            boolean save, boolean print, boolean zoom, boolean tooltips) {
517
518        this(chart, width, height, minimumDrawWidth, minimumDrawHeight,
519                maximumDrawWidth, maximumDrawHeight, useBuffer, properties,
520                true, save, print, zoom, tooltips);
521    }
522
523    /**
524     * Constructs a JFreeChart panel.
525     *
526     * @param chart  the chart.
527     * @param width  the preferred width of the panel.
528     * @param height  the preferred height of the panel.
529     * @param minimumDrawWidth  the minimum drawing width.
530     * @param minimumDrawHeight  the minimum drawing height.
531     * @param maximumDrawWidth  the maximum drawing width.
532     * @param maximumDrawHeight  the maximum drawing height.
533     * @param useBuffer  a flag that indicates whether to use the off-screen
534     *                   buffer to improve performance (at the expense of
535     *                   memory).
536     * @param properties  a flag indicating whether or not the chart property
537     *                    editor should be available via the popup menu.
538     * @param copy  a flag indicating whether or not a copy option should be
539     *              available via the popup menu.
540     * @param save  a flag indicating whether or not save options should be
541     *              available via the popup menu.
542     * @param print  a flag indicating whether or not the print option
543     *               should be available via the popup menu.
544     * @param zoom  a flag indicating whether or not zoom options should be
545     *              added to the popup menu.
546     * @param tooltips  a flag indicating whether or not tooltips should be
547     *                  enabled for the chart.
548     */
549    public ChartPanel(JFreeChart chart, int width, int height,
550           int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
551           int maximumDrawHeight, boolean useBuffer, boolean properties,
552           boolean copy, boolean save, boolean print, boolean zoom,
553           boolean tooltips) {
554
555        setChart(chart);
556        this.chartMouseListeners = new EventListenerList();
557        this.info = new ChartRenderingInfo();
558        setPreferredSize(new Dimension(width, height));
559        this.useBuffer = useBuffer;
560        this.refreshBuffer = false;
561        this.minimumDrawWidth = minimumDrawWidth;
562        this.minimumDrawHeight = minimumDrawHeight;
563        this.maximumDrawWidth = maximumDrawWidth;
564        this.maximumDrawHeight = maximumDrawHeight;
565        this.zoomTriggerDistance = DEFAULT_ZOOM_TRIGGER_DISTANCE;
566
567        // set up popup menu...
568        this.popup = null;
569        if (properties || copy || save || print || zoom) {
570            this.popup = createPopupMenu(properties, copy, save, print, zoom);
571        }
572
573        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
574        enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
575        setDisplayToolTips(tooltips);
576        addMouseListener(this);
577        addMouseMotionListener(this);
578
579        this.defaultDirectoryForSaveAs = null;
580        this.enforceFileExtensions = true;
581
582        // initialize ChartPanel-specific tool tip delays with
583        // values the from ToolTipManager.sharedInstance()
584        ToolTipManager ttm = ToolTipManager.sharedInstance();
585        this.ownToolTipInitialDelay = ttm.getInitialDelay();
586        this.ownToolTipDismissDelay = ttm.getDismissDelay();
587        this.ownToolTipReshowDelay = ttm.getReshowDelay();
588
589        this.zoomAroundAnchor = false;
590        this.zoomOutlinePaint = Color.BLUE;
591        this.zoomFillPaint = new Color(0, 0, 255, 63);
592
593        this.panMask = InputEvent.CTRL_MASK;
594        // for MacOSX we can't use the CTRL key for mouse drags, see:
595        // http://developer.apple.com/qa/qa2004/qa1362.html
596        String osName = System.getProperty("os.name").toLowerCase();
597        if (osName.startsWith("mac os x")) {
598            this.panMask = InputEvent.ALT_MASK;
599        }
600
601        this.overlays = new ArrayList<>();
602    }
603
604    /**
605     * Returns the chart contained in the panel.
606     *
607     * @return The chart (possibly {@code null}).
608     */
609    public JFreeChart getChart() {
610        return this.chart;
611    }
612
613    /**
614     * Sets the chart that is displayed in the panel.
615     *
616     * @param chart  the chart ({@code null} permitted).
617     */
618    public void setChart(JFreeChart chart) {
619
620        // stop listening for changes to the existing chart
621        if (this.chart != null) {
622            this.chart.removeChangeListener(this);
623            this.chart.removeProgressListener(this);
624        }
625
626        // add the new chart
627        this.chart = chart;
628        if (chart != null) {
629            this.chart.addChangeListener(this);
630            this.chart.addProgressListener(this);
631            Plot plot = chart.getPlot();
632            this.domainZoomable = false;
633            this.rangeZoomable = false;
634            if (plot instanceof Zoomable) {
635                Zoomable z = (Zoomable) plot;
636                this.domainZoomable = z.isDomainZoomable();
637                this.rangeZoomable = z.isRangeZoomable();
638                this.orientation = z.getOrientation();
639            }
640        }
641        else {
642            this.domainZoomable = false;
643            this.rangeZoomable = false;
644        }
645        if (this.useBuffer) {
646            this.refreshBuffer = true;
647        }
648        repaint();
649
650    }
651
652    /**
653     * Returns the minimum drawing width for charts.
654     * <P>
655     * If the width available on the panel is less than this, then the chart is
656     * drawn at the minimum width then scaled down to fit.
657     *
658     * @return The minimum drawing width.
659     */
660    public int getMinimumDrawWidth() {
661        return this.minimumDrawWidth;
662    }
663
664    /**
665     * Sets the minimum drawing width for the chart on this panel.
666     * <P>
667     * At the time the chart is drawn on the panel, if the available width is
668     * less than this amount, the chart will be drawn using the minimum width
669     * then scaled down to fit the available space.
670     *
671     * @param width  The width.
672     */
673    public void setMinimumDrawWidth(int width) {
674        this.minimumDrawWidth = width;
675    }
676
677    /**
678     * Returns the maximum drawing width for charts.
679     * <P>
680     * If the width available on the panel is greater than this, then the chart
681     * is drawn at the maximum width then scaled up to fit.
682     *
683     * @return The maximum drawing width.
684     */
685    public int getMaximumDrawWidth() {
686        return this.maximumDrawWidth;
687    }
688
689    /**
690     * Sets the maximum drawing width for the chart on this panel.
691     * <P>
692     * At the time the chart is drawn on the panel, if the available width is
693     * greater than this amount, the chart will be drawn using the maximum
694     * width then scaled up to fit the available space.
695     *
696     * @param width  The width.
697     */
698    public void setMaximumDrawWidth(int width) {
699        this.maximumDrawWidth = width;
700    }
701
702    /**
703     * Returns the minimum drawing height for charts.
704     * <P>
705     * If the height available on the panel is less than this, then the chart
706     * is drawn at the minimum height then scaled down to fit.
707     *
708     * @return The minimum drawing height.
709     */
710    public int getMinimumDrawHeight() {
711        return this.minimumDrawHeight;
712    }
713
714    /**
715     * Sets the minimum drawing height for the chart on this panel.
716     * <P>
717     * At the time the chart is drawn on the panel, if the available height is
718     * less than this amount, the chart will be drawn using the minimum height
719     * then scaled down to fit the available space.
720     *
721     * @param height  The height.
722     */
723    public void setMinimumDrawHeight(int height) {
724        this.minimumDrawHeight = height;
725    }
726
727    /**
728     * Returns the maximum drawing height for charts.
729     * <P>
730     * If the height available on the panel is greater than this, then the
731     * chart is drawn at the maximum height then scaled up to fit.
732     *
733     * @return The maximum drawing height.
734     */
735    public int getMaximumDrawHeight() {
736        return this.maximumDrawHeight;
737    }
738
739    /**
740     * Sets the maximum drawing height for the chart on this panel.
741     * <P>
742     * At the time the chart is drawn on the panel, if the available height is
743     * greater than this amount, the chart will be drawn using the maximum
744     * height then scaled up to fit the available space.
745     *
746     * @param height  The height.
747     */
748    public void setMaximumDrawHeight(int height) {
749        this.maximumDrawHeight = height;
750    }
751
752    /**
753     * Returns the X scale factor for the chart.  This will be 1.0 if no
754     * scaling has been used.
755     *
756     * @return The scale factor.
757     */
758    public double getScaleX() {
759        return this.scaleX;
760    }
761
762    /**
763     * Returns the Y scale factory for the chart.  This will be 1.0 if no
764     * scaling has been used.
765     *
766     * @return The scale factor.
767     */
768    public double getScaleY() {
769        return this.scaleY;
770    }
771
772    /**
773     * Returns the anchor point.
774     *
775     * @return The anchor point (possibly {@code null}).
776     */
777    public Point2D getAnchor() {
778        return this.anchor;
779    }
780
781    /**
782     * Sets the anchor point.  This method is provided for the use of
783     * subclasses, not end users.
784     *
785     * @param anchor  the anchor point ({@code null} permitted).
786     */
787    protected void setAnchor(Point2D anchor) {
788        this.anchor = anchor;
789    }
790
791    /**
792     * Returns the popup menu.
793     *
794     * @return The popup menu.
795     */
796    public JPopupMenu getPopupMenu() {
797        return this.popup;
798    }
799
800    /**
801     * Sets the popup menu for the panel.
802     *
803     * @param popup  the popup menu ({@code null} permitted).
804     */
805    public void setPopupMenu(JPopupMenu popup) {
806        this.popup = popup;
807    }
808
809    /**
810     * Returns the chart rendering info from the most recent chart redraw.
811     *
812     * @return The chart rendering info.
813     */
814    public ChartRenderingInfo getChartRenderingInfo() {
815        return this.info;
816    }
817
818    /**
819     * A convenience method that switches on mouse-based zooming.
820     *
821     * @param flag  {@code true} enables zooming and rectangle fill on
822     *              zoom.
823     */
824    public void setMouseZoomable(boolean flag) {
825        setMouseZoomable(flag, true);
826    }
827
828    /**
829     * A convenience method that switches on mouse-based zooming.
830     *
831     * @param flag  {@code true} if zooming enabled
832     * @param fillRectangle  {@code true} if zoom rectangle is filled,
833     *                       false if rectangle is shown as outline only.
834     */
835    public void setMouseZoomable(boolean flag, boolean fillRectangle) {
836        setDomainZoomable(flag);
837        setRangeZoomable(flag);
838        setFillZoomRectangle(fillRectangle);
839    }
840
841    /**
842     * Returns the flag that determines whether or not zooming is enabled for
843     * the domain axis.
844     *
845     * @return A boolean.
846     */
847    public boolean isDomainZoomable() {
848        return this.domainZoomable;
849    }
850
851    /**
852     * Sets the flag that controls whether or not zooming is enabled for the
853     * domain axis.  A check is made to ensure that the current plot supports
854     * zooming for the domain values.
855     *
856     * @param flag  {@code true} enables zooming if possible.
857     */
858    public void setDomainZoomable(boolean flag) {
859        if (flag) {
860            Plot plot = this.chart.getPlot();
861            if (plot instanceof Zoomable) {
862                Zoomable z = (Zoomable) plot;
863                this.domainZoomable = flag && (z.isDomainZoomable());
864            }
865        }
866        else {
867            this.domainZoomable = false;
868        }
869    }
870
871    /**
872     * Returns the flag that determines whether or not zooming is enabled for
873     * the range axis.
874     *
875     * @return A boolean.
876     */
877    public boolean isRangeZoomable() {
878        return this.rangeZoomable;
879    }
880
881    /**
882     * A flag that controls mouse-based zooming on the vertical axis.
883     *
884     * @param flag  {@code true} enables zooming.
885     */
886    public void setRangeZoomable(boolean flag) {
887        if (flag) {
888            Plot plot = this.chart.getPlot();
889            if (plot instanceof Zoomable) {
890                Zoomable z = (Zoomable) plot;
891                this.rangeZoomable = flag && (z.isRangeZoomable());
892            }
893        }
894        else {
895            this.rangeZoomable = false;
896        }
897    }
898
899    /**
900     * Returns the flag that controls whether or not the zoom rectangle is
901     * filled when drawn.
902     *
903     * @return A boolean.
904     */
905    public boolean getFillZoomRectangle() {
906        return this.fillZoomRectangle;
907    }
908
909    /**
910     * A flag that controls how the zoom rectangle is drawn.
911     *
912     * @param flag  {@code true} instructs to fill the rectangle on
913     *              zoom, otherwise it will be outlined.
914     */
915    public void setFillZoomRectangle(boolean flag) {
916        this.fillZoomRectangle = flag;
917    }
918
919    /**
920     * Returns the zoom trigger distance.  This controls how far the mouse must
921     * move before a zoom action is triggered.
922     *
923     * @return The distance (in Java2D units).
924     */
925    public int getZoomTriggerDistance() {
926        return this.zoomTriggerDistance;
927    }
928
929    /**
930     * Sets the zoom trigger distance.  This controls how far the mouse must
931     * move before a zoom action is triggered.
932     *
933     * @param distance  the distance (in Java2D units).
934     */
935    public void setZoomTriggerDistance(int distance) {
936        this.zoomTriggerDistance = distance;
937    }
938
939    /**
940     * Returns the flag that controls whether or not a horizontal axis trace
941     * line is drawn over the plot area at the current mouse location.
942     *
943     * @return A boolean.
944     */
945    public boolean getHorizontalAxisTrace() {
946        return this.horizontalAxisTrace;
947    }
948
949    /**
950     * A flag that controls trace lines on the horizontal axis.
951     *
952     * @param flag  {@code true} enables trace lines for the mouse
953     *      pointer on the horizontal axis.
954     */
955    public void setHorizontalAxisTrace(boolean flag) {
956        this.horizontalAxisTrace = flag;
957    }
958
959    /**
960     * Returns the horizontal trace line.
961     *
962     * @return The horizontal trace line (possibly {@code null}).
963     */
964    protected Line2D getHorizontalTraceLine() {
965        return this.horizontalTraceLine;
966    }
967
968    /**
969     * Sets the horizontal trace line.
970     *
971     * @param line  the line ({@code null} permitted).
972     */
973    protected void setHorizontalTraceLine(Line2D line) {
974        this.horizontalTraceLine = line;
975    }
976
977    /**
978     * Returns the flag that controls whether or not a vertical axis trace
979     * line is drawn over the plot area at the current mouse location.
980     *
981     * @return A boolean.
982     */
983    public boolean getVerticalAxisTrace() {
984        return this.verticalAxisTrace;
985    }
986
987    /**
988     * A flag that controls trace lines on the vertical axis.
989     *
990     * @param flag  {@code true} enables trace lines for the mouse
991     *              pointer on the vertical axis.
992     */
993    public void setVerticalAxisTrace(boolean flag) {
994        this.verticalAxisTrace = flag;
995    }
996
997    /**
998     * Returns the vertical trace line.
999     *
1000     * @return The vertical trace line (possibly {@code null}).
1001     */
1002    protected Line2D getVerticalTraceLine() {
1003        return this.verticalTraceLine;
1004    }
1005
1006    /**
1007     * Sets the vertical trace line.
1008     *
1009     * @param line  the line ({@code null} permitted).
1010     */
1011    protected void setVerticalTraceLine(Line2D line) {
1012        this.verticalTraceLine = line;
1013    }
1014
1015    /**
1016     * Returns the default directory for the "save as" option.
1017     *
1018     * @return The default directory (possibly {@code null}).
1019     */
1020    public File getDefaultDirectoryForSaveAs() {
1021        return this.defaultDirectoryForSaveAs;
1022    }
1023
1024    /**
1025     * Sets the default directory for the "save as" option.  If you set this
1026     * to {@code null}, the user's default directory will be used.
1027     *
1028     * @param directory  the directory ({@code null} permitted).
1029     */
1030    public void setDefaultDirectoryForSaveAs(File directory) {
1031        if (directory != null) {
1032            if (!directory.isDirectory()) {
1033                throw new IllegalArgumentException(
1034                        "The 'directory' argument is not a directory.");
1035            }
1036        }
1037        this.defaultDirectoryForSaveAs = directory;
1038    }
1039
1040    /**
1041     * Returns {@code true} if file extensions should be enforced, and
1042     * {@code false} otherwise.
1043     *
1044     * @return The flag.
1045     *
1046     * @see #setEnforceFileExtensions(boolean)
1047     */
1048    public boolean isEnforceFileExtensions() {
1049        return this.enforceFileExtensions;
1050    }
1051
1052    /**
1053     * Sets a flag that controls whether or not file extensions are enforced.
1054     *
1055     * @param enforce  the new flag value.
1056     *
1057     * @see #isEnforceFileExtensions()
1058     */
1059    public void setEnforceFileExtensions(boolean enforce) {
1060        this.enforceFileExtensions = enforce;
1061    }
1062
1063    /**
1064     * Returns the flag that controls whether or not zoom operations are
1065     * centered around the current anchor point.
1066     *
1067     * @return A boolean.
1068     *
1069     * @see #setZoomAroundAnchor(boolean)
1070     */
1071    public boolean getZoomAroundAnchor() {
1072        return this.zoomAroundAnchor;
1073    }
1074
1075    /**
1076     * Sets the flag that controls whether or not zoom operations are
1077     * centered around the current anchor point.
1078     *
1079     * @param zoomAroundAnchor  the new flag value.
1080     *
1081     * @see #getZoomAroundAnchor()
1082     */
1083    public void setZoomAroundAnchor(boolean zoomAroundAnchor) {
1084        this.zoomAroundAnchor = zoomAroundAnchor;
1085    }
1086
1087    /**
1088     * Returns the zoom rectangle fill paint.
1089     *
1090     * @return The zoom rectangle fill paint (never {@code null}).
1091     *
1092     * @see #setZoomFillPaint(java.awt.Paint)
1093     * @see #setFillZoomRectangle(boolean)
1094     */
1095    public Paint getZoomFillPaint() {
1096        return this.zoomFillPaint;
1097    }
1098
1099    /**
1100     * Sets the zoom rectangle fill paint.
1101     *
1102     * @param paint  the paint ({@code null} not permitted).
1103     *
1104     * @see #getZoomFillPaint()
1105     * @see #getFillZoomRectangle()
1106     */
1107    public void setZoomFillPaint(Paint paint) {
1108        Args.nullNotPermitted(paint, "paint");
1109        this.zoomFillPaint = paint;
1110    }
1111
1112    /**
1113     * Returns the zoom rectangle outline paint.
1114     *
1115     * @return The zoom rectangle outline paint (never {@code null}).
1116     *
1117     * @see #setZoomOutlinePaint(java.awt.Paint)
1118     * @see #setFillZoomRectangle(boolean)
1119     */
1120    public Paint getZoomOutlinePaint() {
1121        return this.zoomOutlinePaint;
1122    }
1123
1124    /**
1125     * Sets the zoom rectangle outline paint.
1126     *
1127     * @param paint  the paint ({@code null} not permitted).
1128     *
1129     * @see #getZoomOutlinePaint()
1130     * @see #getFillZoomRectangle()
1131     */
1132    public void setZoomOutlinePaint(Paint paint) {
1133        this.zoomOutlinePaint = paint;
1134    }
1135
1136    /**
1137     * The mouse wheel handler.
1138     */
1139    private MouseWheelHandler mouseWheelHandler;
1140
1141    /**
1142     * Returns {@code true} if the mouse wheel handler is enabled, and
1143     * {@code false} otherwise.
1144     *
1145     * @return A boolean.
1146     */
1147    public boolean isMouseWheelEnabled() {
1148        return this.mouseWheelHandler != null;
1149    }
1150
1151    /**
1152     * Enables or disables mouse wheel support for the panel.
1153     *
1154     * @param flag  a boolean.
1155     */
1156    public void setMouseWheelEnabled(boolean flag) {
1157        if (flag && this.mouseWheelHandler == null) {
1158            this.mouseWheelHandler = new MouseWheelHandler(this);
1159        }
1160        else if (!flag && this.mouseWheelHandler != null) {
1161            this.removeMouseWheelListener(this.mouseWheelHandler);
1162            this.mouseWheelHandler = null;
1163        } 
1164    }
1165
1166    /**
1167     * Add an overlay to the panel.
1168     *
1169     * @param overlay  the overlay ({@code null} not permitted).
1170     */
1171    public void addOverlay(Overlay overlay) {
1172        Args.nullNotPermitted(overlay, "overlay");
1173        this.overlays.add(overlay);
1174        overlay.addChangeListener(this);
1175        repaint();
1176    }
1177
1178    /**
1179     * Removes an overlay from the panel.
1180     *
1181     * @param overlay  the overlay to remove ({@code null} not permitted).
1182     */
1183    public void removeOverlay(Overlay overlay) {
1184        Args.nullNotPermitted(overlay, "overlay");
1185        boolean removed = this.overlays.remove(overlay);
1186        if (removed) {
1187            overlay.removeChangeListener(this);
1188            repaint();
1189        }
1190    }
1191
1192    /**
1193     * Handles a change to an overlay by repainting the panel.
1194     *
1195     * @param event  the event.
1196     */
1197    @Override
1198    public void overlayChanged(OverlayChangeEvent event) {
1199        repaint();
1200    }
1201
1202    /**
1203     * Switches the display of tooltips for the panel on or off.  Note that
1204     * tooltips can only be displayed if the chart has been configured to
1205     * generate tooltip items.
1206     *
1207     * @param flag  {@code true} to enable tooltips, {@code false} to
1208     *              disable tooltips.
1209     */
1210    public void setDisplayToolTips(boolean flag) {
1211        if (flag) {
1212            ToolTipManager.sharedInstance().registerComponent(this);
1213        }
1214        else {
1215            ToolTipManager.sharedInstance().unregisterComponent(this);
1216        }
1217    }
1218
1219    /**
1220     * Returns a string for the tooltip.
1221     *
1222     * @param e  the mouse event.
1223     *
1224     * @return A tool tip or {@code null} if no tooltip is available.
1225     */
1226    @Override
1227    public String getToolTipText(MouseEvent e) {
1228        String result = null;
1229        if (this.info != null) {
1230            EntityCollection entities = this.info.getEntityCollection();
1231            if (entities != null) {
1232                Insets insets = getInsets();
1233                ChartEntity entity = entities.getEntity(
1234                        (int) ((e.getX() - insets.left) / this.scaleX),
1235                        (int) ((e.getY() - insets.top) / this.scaleY));
1236                if (entity != null) {
1237                    result = entity.getToolTipText();
1238                }
1239            }
1240        }
1241        return result;
1242    }
1243
1244    /**
1245     * Translates a Java2D point on the chart to a screen location.
1246     *
1247     * @param java2DPoint  the Java2D point.
1248     *
1249     * @return The screen location.
1250     */
1251    public Point translateJava2DToScreen(Point2D java2DPoint) {
1252        Insets insets = getInsets();
1253        int x = (int) (java2DPoint.getX() * this.scaleX + insets.left);
1254        int y = (int) (java2DPoint.getY() * this.scaleY + insets.top);
1255        return new Point(x, y);
1256    }
1257
1258    /**
1259     * Translates a panel (component) location to a Java2D point.
1260     *
1261     * @param screenPoint  the screen location ({@code null} not
1262     *                     permitted).
1263     *
1264     * @return The Java2D coordinates.
1265     */
1266    public Point2D translateScreenToJava2D(Point screenPoint) {
1267        Insets insets = getInsets();
1268        double x = (screenPoint.getX() - insets.left) / this.scaleX;
1269        double y = (screenPoint.getY() - insets.top) / this.scaleY;
1270        return new Point2D.Double(x, y);
1271    }
1272
1273    /**
1274     * Applies any scaling that is in effect for the chart drawing to the
1275     * given rectangle.
1276     *
1277     * @param rect  the rectangle ({@code null} not permitted).
1278     *
1279     * @return A new scaled rectangle.
1280     */
1281    public Rectangle2D scale(Rectangle2D rect) {
1282        Insets insets = getInsets();
1283        double x = rect.getX() * getScaleX() + insets.left;
1284        double y = rect.getY() * getScaleY() + insets.top;
1285        double w = rect.getWidth() * getScaleX();
1286        double h = rect.getHeight() * getScaleY();
1287        return new Rectangle2D.Double(x, y, w, h);
1288    }
1289
1290    /**
1291     * Returns the chart entity at a given point.
1292     * <P>
1293     * This method will return null if there is (a) no entity at the given
1294     * point, or (b) no entity collection has been generated.
1295     *
1296     * @param viewX  the x-coordinate.
1297     * @param viewY  the y-coordinate.
1298     *
1299     * @return The chart entity (possibly {@code null}).
1300     */
1301    public ChartEntity getEntityForPoint(int viewX, int viewY) {
1302
1303        ChartEntity result = null;
1304        if (this.info != null) {
1305            Insets insets = getInsets();
1306            double x = (viewX - insets.left) / this.scaleX;
1307            double y = (viewY - insets.top) / this.scaleY;
1308            EntityCollection entities = this.info.getEntityCollection();
1309            result = entities != null ? entities.getEntity(x, y) : null;
1310        }
1311        return result;
1312
1313    }
1314
1315    /**
1316     * Returns the flag that controls whether or not the offscreen buffer
1317     * needs to be refreshed.
1318     *
1319     * @return A boolean.
1320     */
1321    public boolean getRefreshBuffer() {
1322        return this.refreshBuffer;
1323    }
1324
1325    /**
1326     * Sets the refresh buffer flag.  This flag is used to avoid unnecessary
1327     * redrawing of the chart when the offscreen image buffer is used.
1328     *
1329     * @param flag  {@code true} indicates that the buffer should be
1330     *              refreshed.
1331     */
1332    public void setRefreshBuffer(boolean flag) {
1333        this.refreshBuffer = flag;
1334    }
1335
1336    /**
1337     * Paints the component by drawing the chart to fill the entire component,
1338     * but allowing for the insets (which will be non-zero if a border has been
1339     * set for this component).  To increase performance (at the expense of
1340     * memory), an off-screen buffer image can be used.
1341     *
1342     * @param g  the graphics device for drawing on.
1343     */
1344    @Override
1345    public void paintComponent(Graphics g) {
1346        super.paintComponent(g);
1347        if (this.chart == null) {
1348            return;
1349        }
1350        Graphics2D g2 = (Graphics2D) g.create();
1351        
1352        // first determine the size of the chart rendering area...
1353        Dimension size = getSize();
1354        Insets insets = getInsets();
1355        final int availableWidth = size.width - insets.left - insets.right; 
1356        final int availableHeight = size.height - insets.top - insets.bottom;
1357
1358        // work out if scaling is required...
1359        boolean scale = false;
1360        int drawWidth = availableWidth;
1361        int drawHeight = availableHeight;
1362        this.scaleX = 1.0;
1363        this.scaleY = 1.0;
1364
1365        if (drawWidth < this.minimumDrawWidth) {
1366            this.scaleX = (double) drawWidth / (double) this.minimumDrawWidth;
1367            drawWidth = this.minimumDrawWidth;
1368            scale = true;
1369        }
1370        else if (drawWidth > this.maximumDrawWidth) {
1371            this.scaleX = (double) drawWidth / (double) this.maximumDrawWidth;
1372            drawWidth = this.maximumDrawWidth;
1373            scale = true;
1374        }
1375
1376        if (drawHeight < this.minimumDrawHeight) {
1377            this.scaleY = (double) drawHeight / (double) this.minimumDrawHeight;
1378            drawHeight = this.minimumDrawHeight;
1379            scale = true;
1380        }
1381        else if (drawHeight > this.maximumDrawHeight) {
1382            this.scaleY = (double) drawHeight / (double) this.maximumDrawHeight;
1383            drawHeight = this.maximumDrawHeight;
1384            scale = true;
1385        }
1386
1387        Dimension chartSize = new Dimension(drawWidth, drawHeight);
1388
1389        // are we using the chart buffer?
1390        if (this.useBuffer) {
1391
1392            // for better rendering on the HiDPI monitors upscaling the buffer to the "native" resolution
1393            // instead of using logical one provided by Swing
1394            final AffineTransform globalTransform = ((Graphics2D) g).getTransform();
1395            final double globalScaleX = globalTransform.getScaleX();
1396            final double globalScaleY = globalTransform.getScaleY();
1397
1398            final Dimension bufferSize = new Dimension(
1399                    (int) Math.ceil(availableWidth * globalScaleX),
1400                    (int) Math.ceil(availableHeight * globalScaleY));
1401
1402            this.chartBuffer = paintChartToBuffer(g2, bufferSize, chartSize, anchor, info);
1403
1404            // zap the buffer onto the panel...
1405            g2.drawImage(this.chartBuffer, insets.left, insets.top, availableWidth, availableHeight, this);
1406            g2.addRenderingHints(this.chart.getRenderingHints()); // bug#187
1407
1408        } else { // redrawing the chart every time...
1409            AffineTransform saved = g2.getTransform();
1410            g2.translate(insets.left, insets.top);
1411            if (scale) {
1412                AffineTransform st = AffineTransform.getScaleInstance(
1413                        this.scaleX, this.scaleY);
1414                g2.transform(st);
1415            }
1416            this.chart.draw(g2, new Rectangle(chartSize), this.anchor, this.info);
1417            g2.setTransform(saved);
1418
1419        }
1420
1421        for (Overlay overlay : this.overlays) {
1422            overlay.paintOverlay(g2, this);
1423        }
1424
1425        // redraw the zoom rectangle (if present) - if useBuffer is false,
1426        // we use XOR so we can XOR the rectangle away again without redrawing
1427        // the chart
1428        drawZoomRectangle(g2, !this.useBuffer);
1429
1430        g2.dispose();
1431
1432        this.anchor = null;
1433        this.verticalTraceLine = null;
1434        this.horizontalTraceLine = null;
1435    }
1436
1437    /**
1438     * Paints the chart to fill the entire off-screen buffer image.
1439     * 
1440     * @param g2         the graphics context to create an off-screen buffer
1441     *                   image.
1442     * @param bufferSize the required off-screen buffer image size.
1443     * @param chartSize  the size with which the chart should be drawn (apply
1444     *                   scaling if not equal to {@code bufferSize}).
1445     * @param anchor     the anchor point (in Java2D space) for the chart
1446     *                   ({@code null} permitted).
1447     * @param info       records info about the drawing ({@code null} means
1448     *                   collect no info).
1449     * @return the off-screen buffer image to draw onto the panel.
1450     */
1451    protected BufferedImage paintChartToBuffer(Graphics2D g2, Dimension bufferSize,
1452            Dimension chartSize, Point2D anchor, ChartRenderingInfo info) {
1453        final BufferedImage buffer;
1454        if ((this.chartBuffer == null)
1455                || (this.chartBuffer.getWidth() != bufferSize.width)
1456                || (this.chartBuffer.getHeight() != bufferSize.height)) {
1457            GraphicsConfiguration gc = g2.getDeviceConfiguration();
1458
1459            buffer = gc.createCompatibleImage(bufferSize.width,
1460                    bufferSize.height, Transparency.TRANSLUCENT);
1461
1462            this.refreshBuffer = true;
1463        } else {
1464            buffer = this.chartBuffer;
1465        }
1466        
1467        
1468        // do we need to redraw the buffer?
1469        if (this.refreshBuffer) {
1470
1471            this.refreshBuffer = false; // clear the flag
1472
1473            Graphics2D bufferG2 = buffer.createGraphics();
1474            if (!bufferSize.equals(chartSize)) {
1475                // Scale the chart to fit the buffer
1476                bufferG2.scale(
1477                        bufferSize.getWidth() / chartSize.getWidth(),
1478                        bufferSize.getHeight() / chartSize.getHeight());
1479            }
1480            Rectangle chartArea = new Rectangle(chartSize);
1481
1482            // make the background of the buffer clear and transparent
1483            Composite savedComposite = bufferG2.getComposite();
1484            bufferG2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
1485            bufferG2.fill(chartArea);
1486            bufferG2.setComposite(savedComposite);
1487            
1488            this.chart.draw(bufferG2, chartArea, this.anchor, this.info);
1489            bufferG2.dispose();
1490        }
1491        
1492        return buffer;
1493    }
1494
1495    /**
1496     * Receives notification of changes to the chart, and redraws the chart.
1497     *
1498     * @param event  details of the chart change event.
1499     */
1500    @Override
1501    public void chartChanged(ChartChangeEvent event) {
1502        this.refreshBuffer = true;
1503        Plot plot = this.chart.getPlot();
1504        if (plot instanceof Zoomable) {
1505            Zoomable z = (Zoomable) plot;
1506            this.orientation = z.getOrientation();
1507        }
1508        repaint();
1509    }
1510
1511    /**
1512     * Receives notification of a chart progress event.
1513     *
1514     * @param event  the event.
1515     */
1516    @Override
1517    public void chartProgress(ChartProgressEvent event) {
1518        // does nothing - override if necessary
1519    }
1520
1521    /**
1522     * Handles action events generated by the popup menu.
1523     *
1524     * @param event  the event.
1525     */
1526    @Override
1527    public void actionPerformed(ActionEvent event) {
1528
1529        String command = event.getActionCommand();
1530
1531        // many of the zoom methods need a screen location - all we have is
1532        // the zoomPoint, but it might be null.  Here we grab the x and y
1533        // coordinates, or use defaults...
1534        double screenX = -1.0;
1535        double screenY = -1.0;
1536        if (this.zoomPoint != null) {
1537            screenX = this.zoomPoint.getX();
1538            screenY = this.zoomPoint.getY();
1539        }
1540
1541        if (command.equals(PROPERTIES_COMMAND)) {
1542            doEditChartProperties();
1543        }
1544        else if (command.equals(COPY_COMMAND)) {
1545            doCopy();
1546        }
1547        else if (command.equals(SAVE_AS_PNG_COMMAND)) {
1548            try {
1549                doSaveAs();
1550            }
1551            catch (IOException e) {
1552                JOptionPane.showMessageDialog(this, "I/O error occurred.",
1553                        localizationResources.getString("Save_as_PNG"),
1554                        JOptionPane.WARNING_MESSAGE);
1555            }
1556        }
1557        else if (command.equals(SAVE_AS_SVG_COMMAND)) {
1558            try {
1559                saveAsSVG(null);
1560            } catch (IOException e) {
1561                JOptionPane.showMessageDialog(this, "I/O error occurred.",
1562                        localizationResources.getString("Save_as_SVG"),
1563                        JOptionPane.WARNING_MESSAGE);
1564            }
1565        }
1566        else if (command.equals(SAVE_AS_PDF_COMMAND)) {
1567            saveAsPDF(null);
1568        }
1569        else if (command.equals(PRINT_COMMAND)) {
1570            createChartPrintJob();
1571        }
1572        else if (command.equals(ZOOM_IN_BOTH_COMMAND)) {
1573            zoomInBoth(screenX, screenY);
1574        }
1575        else if (command.equals(ZOOM_IN_DOMAIN_COMMAND)) {
1576            zoomInDomain(screenX, screenY);
1577        }
1578        else if (command.equals(ZOOM_IN_RANGE_COMMAND)) {
1579            zoomInRange(screenX, screenY);
1580        }
1581        else if (command.equals(ZOOM_OUT_BOTH_COMMAND)) {
1582            zoomOutBoth(screenX, screenY);
1583        }
1584        else if (command.equals(ZOOM_OUT_DOMAIN_COMMAND)) {
1585            zoomOutDomain(screenX, screenY);
1586        }
1587        else if (command.equals(ZOOM_OUT_RANGE_COMMAND)) {
1588            zoomOutRange(screenX, screenY);
1589        }
1590        else if (command.equals(ZOOM_RESET_BOTH_COMMAND)) {
1591            restoreAutoBounds();
1592        }
1593        else if (command.equals(ZOOM_RESET_DOMAIN_COMMAND)) {
1594            restoreAutoDomainBounds();
1595        }
1596        else if (command.equals(ZOOM_RESET_RANGE_COMMAND)) {
1597            restoreAutoRangeBounds();
1598        }
1599
1600    }
1601
1602    /**
1603     * Handles a 'mouse entered' event. This method changes the tooltip delays
1604     * of ToolTipManager.sharedInstance() to the possibly different values set
1605     * for this chart panel.
1606     *
1607     * @param e  the mouse event.
1608     */
1609    @Override
1610    public void mouseEntered(MouseEvent e) {
1611        if (!this.ownToolTipDelaysActive) {
1612            ToolTipManager ttm = ToolTipManager.sharedInstance();
1613
1614            this.originalToolTipInitialDelay = ttm.getInitialDelay();
1615            ttm.setInitialDelay(this.ownToolTipInitialDelay);
1616
1617            this.originalToolTipReshowDelay = ttm.getReshowDelay();
1618            ttm.setReshowDelay(this.ownToolTipReshowDelay);
1619
1620            this.originalToolTipDismissDelay = ttm.getDismissDelay();
1621            ttm.setDismissDelay(this.ownToolTipDismissDelay);
1622
1623            this.ownToolTipDelaysActive = true;
1624        }
1625    }
1626
1627    /**
1628     * Handles a 'mouse exited' event. This method resets the tooltip delays of
1629     * ToolTipManager.sharedInstance() to their
1630     * original values in effect before mouseEntered()
1631     *
1632     * @param e  the mouse event.
1633     */
1634    @Override
1635    public void mouseExited(MouseEvent e) {
1636        if (this.ownToolTipDelaysActive) {
1637            // restore original tooltip dealys
1638            ToolTipManager ttm = ToolTipManager.sharedInstance();
1639            ttm.setInitialDelay(this.originalToolTipInitialDelay);
1640            ttm.setReshowDelay(this.originalToolTipReshowDelay);
1641            ttm.setDismissDelay(this.originalToolTipDismissDelay);
1642            this.ownToolTipDelaysActive = false;
1643        }
1644    }
1645
1646    /**
1647     * Handles a 'mouse pressed' event.
1648     * <P>
1649     * This event is the popup trigger on Unix/Linux.  For Windows, the popup
1650     * trigger is the 'mouse released' event.
1651     *
1652     * @param e  The mouse event.
1653     */
1654    @Override
1655    public void mousePressed(MouseEvent e) {
1656        if (this.chart == null) {
1657            return;
1658        }
1659        Plot plot = this.chart.getPlot();
1660        int mods = e.getModifiers();
1661        if ((mods & this.panMask) == this.panMask) {
1662            // can we pan this plot?
1663            if (plot instanceof Pannable) {
1664                Pannable pannable = (Pannable) plot;
1665                if (pannable.isDomainPannable() || pannable.isRangePannable()) {
1666                    Rectangle2D screenDataArea = getScreenDataArea(e.getX(),
1667                            e.getY());
1668                    if (screenDataArea != null && screenDataArea.contains(
1669                            e.getPoint())) {
1670                        this.panW = screenDataArea.getWidth();
1671                        this.panH = screenDataArea.getHeight();
1672                        this.panLast = e.getPoint();
1673                        setCursor(Cursor.getPredefinedCursor(
1674                                Cursor.MOVE_CURSOR));
1675                    }
1676                }
1677                // the actual panning occurs later in the mouseDragged() 
1678                // method
1679            }
1680        }
1681        else if (this.zoomRectangle == null) {
1682            Rectangle2D screenDataArea = getScreenDataArea(e.getX(), e.getY());
1683            if (screenDataArea != null) {
1684                this.zoomPoint = getPointInRectangle(e.getX(), e.getY(),
1685                        screenDataArea);
1686            }
1687            else {
1688                this.zoomPoint = null;
1689            }
1690            if (e.isPopupTrigger()) {
1691                if (this.popup != null) {
1692                    displayPopupMenu(e.getX(), e.getY());
1693                }
1694            }
1695        }
1696    }
1697
1698    /**
1699     * Returns a point based on (x, y) but constrained to be within the bounds
1700     * of the given rectangle.  This method could be moved to JCommon.
1701     *
1702     * @param x  the x-coordinate.
1703     * @param y  the y-coordinate.
1704     * @param area  the rectangle ({@code null} not permitted).
1705     *
1706     * @return A point within the rectangle.
1707     */
1708    private Point2D getPointInRectangle(int x, int y, Rectangle2D area) {
1709        double xx = Math.max(area.getMinX(), Math.min(x, area.getMaxX()));
1710        double yy = Math.max(area.getMinY(), Math.min(y, area.getMaxY()));
1711        return new Point2D.Double(xx, yy);
1712    }
1713
1714    /**
1715     * Handles a 'mouse dragged' event.
1716     *
1717     * @param e  the mouse event.
1718     */
1719    @Override
1720    public void mouseDragged(MouseEvent e) {
1721
1722        // if the popup menu has already been triggered, then ignore dragging...
1723        if (this.popup != null && this.popup.isShowing()) {
1724            return;
1725        }
1726
1727        // handle panning if we have a start point
1728        if (this.panLast != null) {
1729            double dx = e.getX() - this.panLast.getX();
1730            double dy = e.getY() - this.panLast.getY();
1731            if (dx == 0.0 && dy == 0.0) {
1732                return;
1733            }
1734            double wPercent = -dx / this.panW;
1735            double hPercent = dy / this.panH;
1736            boolean old = this.chart.getPlot().isNotify();
1737            this.chart.getPlot().setNotify(false);
1738            Pannable p = (Pannable) this.chart.getPlot();
1739            if (p.getOrientation() == PlotOrientation.VERTICAL) {
1740                p.panDomainAxes(wPercent, this.info.getPlotInfo(),
1741                        this.panLast);
1742                p.panRangeAxes(hPercent, this.info.getPlotInfo(),
1743                        this.panLast);
1744            }
1745            else {
1746                p.panDomainAxes(hPercent, this.info.getPlotInfo(),
1747                        this.panLast);
1748                p.panRangeAxes(wPercent, this.info.getPlotInfo(),
1749                        this.panLast);
1750            }
1751            this.panLast = e.getPoint();
1752            this.chart.getPlot().setNotify(old);
1753            return;
1754        }
1755
1756        // if no initial zoom point was set, ignore dragging...
1757        if (this.zoomPoint == null) {
1758            return;
1759        }
1760        Graphics2D g2 = (Graphics2D) getGraphics();
1761
1762        // erase the previous zoom rectangle (if any).  We only need to do
1763        // this is we are using XOR mode, which we do when we're not using
1764        // the buffer (if there is a buffer, then at the end of this method we
1765        // just trigger a repaint)
1766        if (!this.useBuffer) {
1767            drawZoomRectangle(g2, true);
1768        }
1769
1770        boolean hZoom, vZoom;
1771        if (this.orientation == PlotOrientation.HORIZONTAL) {
1772            hZoom = this.rangeZoomable;
1773            vZoom = this.domainZoomable;
1774        }
1775        else {
1776            hZoom = this.domainZoomable;
1777            vZoom = this.rangeZoomable;
1778        }
1779        Rectangle2D scaledDataArea = getScreenDataArea(
1780                (int) this.zoomPoint.getX(), (int) this.zoomPoint.getY());
1781        if (hZoom && vZoom) {
1782            // selected rectangle shouldn't extend outside the data area...
1783            double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
1784            double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
1785            this.zoomRectangle = new Rectangle2D.Double(
1786                    this.zoomPoint.getX(), this.zoomPoint.getY(),
1787                    xmax - this.zoomPoint.getX(), ymax - this.zoomPoint.getY());
1788        }
1789        else if (hZoom) {
1790            double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
1791            this.zoomRectangle = new Rectangle2D.Double(
1792                    this.zoomPoint.getX(), scaledDataArea.getMinY(),
1793                    xmax - this.zoomPoint.getX(), scaledDataArea.getHeight());
1794        }
1795        else if (vZoom) {
1796            double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
1797            this.zoomRectangle = new Rectangle2D.Double(
1798                    scaledDataArea.getMinX(), this.zoomPoint.getY(),
1799                    scaledDataArea.getWidth(), ymax - this.zoomPoint.getY());
1800        }
1801
1802        // Draw the new zoom rectangle...
1803        if (this.useBuffer) {
1804            repaint();
1805        }
1806        else {
1807            // with no buffer, we use XOR to draw the rectangle "over" the
1808            // chart...
1809            drawZoomRectangle(g2, true);
1810        }
1811        g2.dispose();
1812
1813    }
1814
1815    /**
1816     * Handles a 'mouse released' event.  On Windows, we need to check if this
1817     * is a popup trigger, but only if we haven't already been tracking a zoom
1818     * rectangle.
1819     *
1820     * @param e  information about the event.
1821     */
1822    @Override
1823    public void mouseReleased(MouseEvent e) {
1824
1825        // if we've been panning, we need to reset now that the mouse is 
1826        // released...
1827        if (this.panLast != null) {
1828            this.panLast = null;
1829            setCursor(Cursor.getDefaultCursor());
1830        }
1831
1832        else if (this.zoomRectangle != null) {
1833            boolean hZoom, vZoom;
1834            if (this.orientation == PlotOrientation.HORIZONTAL) {
1835                hZoom = this.rangeZoomable;
1836                vZoom = this.domainZoomable;
1837            }
1838            else {
1839                hZoom = this.domainZoomable;
1840                vZoom = this.rangeZoomable;
1841            }
1842
1843            boolean zoomTrigger1 = hZoom && Math.abs(e.getX()
1844                - this.zoomPoint.getX()) >= this.zoomTriggerDistance;
1845            boolean zoomTrigger2 = vZoom && Math.abs(e.getY()
1846                - this.zoomPoint.getY()) >= this.zoomTriggerDistance;
1847            if (zoomTrigger1 || zoomTrigger2) {
1848                if ((hZoom && (e.getX() < this.zoomPoint.getX()))
1849                    || (vZoom && (e.getY() < this.zoomPoint.getY()))) {
1850                    restoreAutoBounds();
1851                }
1852                else {
1853                    double x, y, w, h;
1854                    Rectangle2D screenDataArea = getScreenDataArea(
1855                            (int) this.zoomPoint.getX(),
1856                            (int) this.zoomPoint.getY());
1857                    double maxX = screenDataArea.getMaxX();
1858                    double maxY = screenDataArea.getMaxY();
1859                    // for mouseReleased event, (horizontalZoom || verticalZoom)
1860                    // will be true, so we can just test for either being false;
1861                    // otherwise both are true
1862                    if (!vZoom) {
1863                        x = this.zoomPoint.getX();
1864                        y = screenDataArea.getMinY();
1865                        w = Math.min(this.zoomRectangle.getWidth(),
1866                                maxX - this.zoomPoint.getX());
1867                        h = screenDataArea.getHeight();
1868                    }
1869                    else if (!hZoom) {
1870                        x = screenDataArea.getMinX();
1871                        y = this.zoomPoint.getY();
1872                        w = screenDataArea.getWidth();
1873                        h = Math.min(this.zoomRectangle.getHeight(),
1874                                maxY - this.zoomPoint.getY());
1875                    }
1876                    else {
1877                        x = this.zoomPoint.getX();
1878                        y = this.zoomPoint.getY();
1879                        w = Math.min(this.zoomRectangle.getWidth(),
1880                                maxX - this.zoomPoint.getX());
1881                        h = Math.min(this.zoomRectangle.getHeight(),
1882                                maxY - this.zoomPoint.getY());
1883                    }
1884                    Rectangle2D zoomArea = new Rectangle2D.Double(x, y, w, h);
1885                    zoom(zoomArea);
1886                }
1887                this.zoomPoint = null;
1888                this.zoomRectangle = null;
1889            }
1890            else {
1891                // erase the zoom rectangle
1892                Graphics2D g2 = (Graphics2D) getGraphics();
1893                if (this.useBuffer) {
1894                    repaint();
1895                }
1896                else {
1897                    drawZoomRectangle(g2, true);
1898                }
1899                g2.dispose();
1900                this.zoomPoint = null;
1901                this.zoomRectangle = null;
1902            }
1903
1904        }
1905
1906        else if (e.isPopupTrigger()) {
1907            if (this.popup != null) {
1908                displayPopupMenu(e.getX(), e.getY());
1909            }
1910        }
1911
1912    }
1913
1914    /**
1915     * Receives notification of mouse clicks on the panel. These are
1916     * translated and passed on to any registered {@link ChartMouseListener}s.
1917     *
1918     * @param event  Information about the mouse event.
1919     */
1920    @Override
1921    public void mouseClicked(MouseEvent event) {
1922
1923        Insets insets = getInsets();
1924        int x = (int) ((event.getX() - insets.left) / this.scaleX);
1925        int y = (int) ((event.getY() - insets.top) / this.scaleY);
1926
1927        this.anchor = new Point2D.Double(x, y);
1928        if (this.chart == null) {
1929            return;
1930        }
1931        // new entity code...
1932        Object[] listeners = this.chartMouseListeners.getListeners(
1933                ChartMouseListener.class);
1934        if (listeners.length == 0) {
1935            return;
1936        }
1937
1938        ChartEntity entity = null;
1939        if (this.info != null) {
1940            EntityCollection entities = this.info.getEntityCollection();
1941            if (entities != null) {
1942                entity = entities.getEntity(x, y);
1943            }
1944        }
1945        ChartMouseEvent chartEvent = new ChartMouseEvent(getChart(), event,
1946                entity);
1947        for (int i = listeners.length - 1; i >= 0; i -= 1) {
1948            ((ChartMouseListener) listeners[i]).chartMouseClicked(chartEvent);
1949        }
1950
1951    }
1952
1953    /**
1954     * Implementation of the MouseMotionListener's method.
1955     *
1956     * @param e  the event.
1957     */
1958    @Override
1959    public void mouseMoved(MouseEvent e) {
1960        Graphics2D g2 = (Graphics2D) getGraphics();
1961        if (this.horizontalAxisTrace) {
1962            drawHorizontalAxisTrace(g2, e.getX());
1963        }
1964        if (this.verticalAxisTrace) {
1965            drawVerticalAxisTrace(g2, e.getY());
1966        }
1967        g2.dispose();
1968
1969        Object[] listeners = this.chartMouseListeners.getListeners(
1970                ChartMouseListener.class);
1971        if (listeners.length == 0) {
1972            return;
1973        }
1974        Insets insets = getInsets();
1975        int x = (int) ((e.getX() - insets.left) / this.scaleX);
1976        int y = (int) ((e.getY() - insets.top) / this.scaleY);
1977
1978        ChartEntity entity = null;
1979        if (this.info != null) {
1980            EntityCollection entities = this.info.getEntityCollection();
1981            if (entities != null) {
1982                entity = entities.getEntity(x, y);
1983            }
1984        }
1985
1986        // we can only generate events if the panel's chart is not null
1987        // (see bug report 1556951)
1988        if (this.chart != null) {
1989            ChartMouseEvent event = new ChartMouseEvent(getChart(), e, entity);
1990            for (int i = listeners.length - 1; i >= 0; i -= 1) {
1991                ((ChartMouseListener) listeners[i]).chartMouseMoved(event);
1992            }
1993        }
1994
1995    }
1996
1997    /**
1998     * Zooms in on an anchor point (specified in screen coordinate space).
1999     *
2000     * @param x  the x value (in screen coordinates).
2001     * @param y  the y value (in screen coordinates).
2002     */
2003    public void zoomInBoth(double x, double y) {
2004        Plot plot = this.chart.getPlot();
2005        if (plot == null) {
2006            return;
2007        }
2008        // here we tweak the notify flag on the plot so that only
2009        // one notification happens even though we update multiple
2010        // axes...
2011        boolean savedNotify = plot.isNotify();
2012        plot.setNotify(false);
2013        zoomInDomain(x, y);
2014        zoomInRange(x, y);
2015        plot.setNotify(savedNotify);
2016    }
2017
2018    /**
2019     * Decreases the length of the domain axis, centered about the given
2020     * coordinate on the screen.  The length of the domain axis is reduced
2021     * by the value of {@link #getZoomInFactor()}.
2022     *
2023     * @param x  the x coordinate (in screen coordinates).
2024     * @param y  the y-coordinate (in screen coordinates).
2025     */
2026    public void zoomInDomain(double x, double y) {
2027        Plot plot = this.chart.getPlot();
2028        if (plot instanceof Zoomable) {
2029            // here we tweak the notify flag on the plot so that only
2030            // one notification happens even though we update multiple
2031            // axes...
2032            boolean savedNotify = plot.isNotify();
2033            plot.setNotify(false);
2034            Zoomable z = (Zoomable) plot;
2035            z.zoomDomainAxes(this.zoomInFactor, this.info.getPlotInfo(),
2036                    translateScreenToJava2D(new Point((int) x, (int) y)),
2037                    this.zoomAroundAnchor);
2038            plot.setNotify(savedNotify);
2039        }
2040    }
2041
2042    /**
2043     * Decreases the length of the range axis, centered about the given
2044     * coordinate on the screen.  The length of the range axis is reduced by
2045     * the value of {@link #getZoomInFactor()}.
2046     *
2047     * @param x  the x-coordinate (in screen coordinates).
2048     * @param y  the y coordinate (in screen coordinates).
2049     */
2050    public void zoomInRange(double x, double y) {
2051        Plot plot = this.chart.getPlot();
2052        if (plot instanceof Zoomable) {
2053            // here we tweak the notify flag on the plot so that only
2054            // one notification happens even though we update multiple
2055            // axes...
2056            boolean savedNotify = plot.isNotify();
2057            plot.setNotify(false);
2058            Zoomable z = (Zoomable) plot;
2059            z.zoomRangeAxes(this.zoomInFactor, this.info.getPlotInfo(),
2060                    translateScreenToJava2D(new Point((int) x, (int) y)),
2061                    this.zoomAroundAnchor);
2062            plot.setNotify(savedNotify);
2063        }
2064    }
2065
2066    /**
2067     * Zooms out on an anchor point (specified in screen coordinate space).
2068     *
2069     * @param x  the x value (in screen coordinates).
2070     * @param y  the y value (in screen coordinates).
2071     */
2072    public void zoomOutBoth(double x, double y) {
2073        Plot plot = this.chart.getPlot();
2074        if (plot == null) {
2075            return;
2076        }
2077        // here we tweak the notify flag on the plot so that only
2078        // one notification happens even though we update multiple
2079        // axes...
2080        boolean savedNotify = plot.isNotify();
2081        plot.setNotify(false);
2082        zoomOutDomain(x, y);
2083        zoomOutRange(x, y);
2084        plot.setNotify(savedNotify);
2085    }
2086
2087    /**
2088     * Increases the length of the domain axis, centered about the given
2089     * coordinate on the screen.  The length of the domain axis is increased
2090     * by the value of {@link #getZoomOutFactor()}.
2091     *
2092     * @param x  the x coordinate (in screen coordinates).
2093     * @param y  the y-coordinate (in screen coordinates).
2094     */
2095    public void zoomOutDomain(double x, double y) {
2096        Plot plot = this.chart.getPlot();
2097        if (plot instanceof Zoomable) {
2098            // here we tweak the notify flag on the plot so that only
2099            // one notification happens even though we update multiple
2100            // axes...
2101            boolean savedNotify = plot.isNotify();
2102            plot.setNotify(false);
2103            Zoomable z = (Zoomable) plot;
2104            z.zoomDomainAxes(this.zoomOutFactor, this.info.getPlotInfo(),
2105                    translateScreenToJava2D(new Point((int) x, (int) y)),
2106                    this.zoomAroundAnchor);
2107            plot.setNotify(savedNotify);
2108        }
2109    }
2110
2111    /**
2112     * Increases the length the range axis, centered about the given
2113     * coordinate on the screen.  The length of the range axis is increased
2114     * by the value of {@link #getZoomOutFactor()}.
2115     *
2116     * @param x  the x coordinate (in screen coordinates).
2117     * @param y  the y-coordinate (in screen coordinates).
2118     */
2119    public void zoomOutRange(double x, double y) {
2120        Plot plot = this.chart.getPlot();
2121        if (plot instanceof Zoomable) {
2122            // here we tweak the notify flag on the plot so that only
2123            // one notification happens even though we update multiple
2124            // axes...
2125            boolean savedNotify = plot.isNotify();
2126            plot.setNotify(false);
2127            Zoomable z = (Zoomable) plot;
2128            z.zoomRangeAxes(this.zoomOutFactor, this.info.getPlotInfo(),
2129                    translateScreenToJava2D(new Point((int) x, (int) y)),
2130                    this.zoomAroundAnchor);
2131            plot.setNotify(savedNotify);
2132        }
2133    }
2134
2135    /**
2136     * Zooms in on a selected region.
2137     *
2138     * @param selection  the selected region.
2139     */
2140    public void zoom(Rectangle2D selection) {
2141
2142        // get the origin of the zoom selection in the Java2D space used for
2143        // drawing the chart (that is, before any scaling to fit the panel)
2144        Point2D selectOrigin = translateScreenToJava2D(new Point(
2145                (int) Math.ceil(selection.getX()),
2146                (int) Math.ceil(selection.getY())));
2147        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
2148        Rectangle2D scaledDataArea = getScreenDataArea(
2149                (int) selection.getCenterX(), (int) selection.getCenterY());
2150        if ((selection.getHeight() > 0) && (selection.getWidth() > 0)) {
2151
2152            double hLower = (selection.getMinX() - scaledDataArea.getMinX())
2153                / scaledDataArea.getWidth();
2154            double hUpper = (selection.getMaxX() - scaledDataArea.getMinX())
2155                / scaledDataArea.getWidth();
2156            double vLower = (scaledDataArea.getMaxY() - selection.getMaxY())
2157                / scaledDataArea.getHeight();
2158            double vUpper = (scaledDataArea.getMaxY() - selection.getMinY())
2159                / scaledDataArea.getHeight();
2160
2161            Plot p = this.chart.getPlot();
2162            if (p instanceof Zoomable) {
2163                // here we tweak the notify flag on the plot so that only
2164                // one notification happens even though we update multiple
2165                // axes...
2166                boolean savedNotify = p.isNotify();
2167                p.setNotify(false);
2168                Zoomable z = (Zoomable) p;
2169                if (z.getOrientation() == PlotOrientation.HORIZONTAL) {
2170                    z.zoomDomainAxes(vLower, vUpper, plotInfo, selectOrigin);
2171                    z.zoomRangeAxes(hLower, hUpper, plotInfo, selectOrigin);
2172                }
2173                else {
2174                    z.zoomDomainAxes(hLower, hUpper, plotInfo, selectOrigin);
2175                    z.zoomRangeAxes(vLower, vUpper, plotInfo, selectOrigin);
2176                }
2177                p.setNotify(savedNotify);
2178            }
2179
2180        }
2181
2182    }
2183
2184    /**
2185     * Restores the auto-range calculation on both axes.
2186     */
2187    public void restoreAutoBounds() {
2188        Plot plot = this.chart.getPlot();
2189        if (plot == null) {
2190            return;
2191        }
2192        // here we tweak the notify flag on the plot so that only
2193        // one notification happens even though we update multiple
2194        // axes...
2195        boolean savedNotify = plot.isNotify();
2196        plot.setNotify(false);
2197        restoreAutoDomainBounds();
2198        restoreAutoRangeBounds();
2199        plot.setNotify(savedNotify);
2200    }
2201
2202    /**
2203     * Restores the auto-range calculation on the domain axis.
2204     */
2205    public void restoreAutoDomainBounds() {
2206        Plot plot = this.chart.getPlot();
2207        if (plot instanceof Zoomable) {
2208            Zoomable z = (Zoomable) plot;
2209            // here we tweak the notify flag on the plot so that only
2210            // one notification happens even though we update multiple
2211            // axes...
2212            boolean savedNotify = plot.isNotify();
2213            plot.setNotify(false);
2214            // we need to guard against this.zoomPoint being null
2215            Point2D zp = (this.zoomPoint != null
2216                    ? this.zoomPoint : new Point());
2217            z.zoomDomainAxes(0.0, this.info.getPlotInfo(), zp);
2218            plot.setNotify(savedNotify);
2219        }
2220    }
2221
2222    /**
2223     * Restores the auto-range calculation on the range axis.
2224     */
2225    public void restoreAutoRangeBounds() {
2226        Plot plot = this.chart.getPlot();
2227        if (plot instanceof Zoomable) {
2228            Zoomable z = (Zoomable) plot;
2229            // here we tweak the notify flag on the plot so that only
2230            // one notification happens even though we update multiple
2231            // axes...
2232            boolean savedNotify = plot.isNotify();
2233            plot.setNotify(false);
2234            // we need to guard against this.zoomPoint being null
2235            Point2D zp = (this.zoomPoint != null
2236                    ? this.zoomPoint : new Point());
2237            z.zoomRangeAxes(0.0, this.info.getPlotInfo(), zp);
2238            plot.setNotify(savedNotify);
2239        }
2240    }
2241
2242    /**
2243     * Returns the data area for the chart (the area inside the axes) with the
2244     * current scaling applied (that is, the area as it appears on screen).
2245     *
2246     * @return The scaled data area.
2247     */
2248    public Rectangle2D getScreenDataArea() {
2249        Rectangle2D dataArea = this.info.getPlotInfo().getDataArea();
2250        Insets insets = getInsets();
2251        double x = dataArea.getX() * this.scaleX + insets.left;
2252        double y = dataArea.getY() * this.scaleY + insets.top;
2253        double w = dataArea.getWidth() * this.scaleX;
2254        double h = dataArea.getHeight() * this.scaleY;
2255        return new Rectangle2D.Double(x, y, w, h);
2256    }
2257
2258    /**
2259     * Returns the data area (the area inside the axes) for the plot or subplot,
2260     * with the current scaling applied.
2261     *
2262     * @param x  the x-coordinate (for subplot selection).
2263     * @param y  the y-coordinate (for subplot selection).
2264     *
2265     * @return The scaled data area.
2266     */
2267    public Rectangle2D getScreenDataArea(int x, int y) {
2268        PlotRenderingInfo plotInfo = this.info.getPlotInfo();
2269        Rectangle2D result;
2270        if (plotInfo.getSubplotCount() == 0) {
2271            result = getScreenDataArea();
2272        }
2273        else {
2274            // get the origin of the zoom selection in the Java2D space used for
2275            // drawing the chart (that is, before any scaling to fit the panel)
2276            Point2D selectOrigin = translateScreenToJava2D(new Point(x, y));
2277            int subplotIndex = plotInfo.getSubplotIndex(selectOrigin);
2278            if (subplotIndex == -1) {
2279                return null;
2280            }
2281            result = scale(plotInfo.getSubplotInfo(subplotIndex).getDataArea());
2282        }
2283        return result;
2284    }
2285
2286    /**
2287     * Returns the initial tooltip delay value used inside this chart panel.
2288     *
2289     * @return An integer representing the initial delay value, in milliseconds.
2290     *
2291     * @see javax.swing.ToolTipManager#getInitialDelay()
2292     */
2293    public int getInitialDelay() {
2294        return this.ownToolTipInitialDelay;
2295    }
2296
2297    /**
2298     * Returns the reshow tooltip delay value used inside this chart panel.
2299     *
2300     * @return An integer representing the reshow  delay value, in milliseconds.
2301     *
2302     * @see javax.swing.ToolTipManager#getReshowDelay()
2303     */
2304    public int getReshowDelay() {
2305        return this.ownToolTipReshowDelay;
2306    }
2307
2308    /**
2309     * Returns the dismissal tooltip delay value used inside this chart panel.
2310     *
2311     * @return An integer representing the dismissal delay value, in
2312     *         milliseconds.
2313     *
2314     * @see javax.swing.ToolTipManager#getDismissDelay()
2315     */
2316    public int getDismissDelay() {
2317        return this.ownToolTipDismissDelay;
2318    }
2319
2320    /**
2321     * Specifies the initial delay value for this chart panel.
2322     *
2323     * @param delay  the number of milliseconds to delay (after the cursor has
2324     *               paused) before displaying.
2325     *
2326     * @see javax.swing.ToolTipManager#setInitialDelay(int)
2327     */
2328    public void setInitialDelay(int delay) {
2329        this.ownToolTipInitialDelay = delay;
2330    }
2331
2332    /**
2333     * Specifies the amount of time before the user has to wait initialDelay
2334     * milliseconds before a tooltip will be shown.
2335     *
2336     * @param delay  time in milliseconds
2337     *
2338     * @see javax.swing.ToolTipManager#setReshowDelay(int)
2339     */
2340    public void setReshowDelay(int delay) {
2341        this.ownToolTipReshowDelay = delay;
2342    }
2343
2344    /**
2345     * Specifies the dismissal delay value for this chart panel.
2346     *
2347     * @param delay the number of milliseconds to delay before taking away the
2348     *              tooltip
2349     *
2350     * @see javax.swing.ToolTipManager#setDismissDelay(int)
2351     */
2352    public void setDismissDelay(int delay) {
2353        this.ownToolTipDismissDelay = delay;
2354    }
2355
2356    /**
2357     * Returns the zoom in factor.
2358     *
2359     * @return The zoom in factor.
2360     *
2361     * @see #setZoomInFactor(double)
2362     */
2363    public double getZoomInFactor() {
2364        return this.zoomInFactor;
2365    }
2366
2367    /**
2368     * Sets the zoom in factor.
2369     *
2370     * @param factor  the factor.
2371     *
2372     * @see #getZoomInFactor()
2373     */
2374    public void setZoomInFactor(double factor) {
2375        this.zoomInFactor = factor;
2376    }
2377
2378    /**
2379     * Returns the zoom out factor.
2380     *
2381     * @return The zoom out factor.
2382     *
2383     * @see #setZoomOutFactor(double)
2384     */
2385    public double getZoomOutFactor() {
2386        return this.zoomOutFactor;
2387    }
2388
2389    /**
2390     * Sets the zoom out factor.
2391     *
2392     * @param factor  the factor.
2393     *
2394     * @see #getZoomOutFactor()
2395     */
2396    public void setZoomOutFactor(double factor) {
2397        this.zoomOutFactor = factor;
2398    }
2399
2400    /**
2401     * Draws zoom rectangle (if present).
2402     * The drawing is performed in XOR mode, therefore
2403     * when this method is called twice in a row,
2404     * the second call will completely restore the state
2405     * of the canvas.
2406     *
2407     * @param g2 the graphics device.
2408     * @param xor  use XOR for drawing?
2409     */
2410    private void drawZoomRectangle(Graphics2D g2, boolean xor) {
2411        if (this.zoomRectangle != null) {
2412            if (xor) {
2413                 // Set XOR mode to draw the zoom rectangle
2414                g2.setXORMode(Color.GRAY);
2415            }
2416            if (this.fillZoomRectangle) {
2417                g2.setPaint(this.zoomFillPaint);
2418                g2.fill(this.zoomRectangle);
2419            }
2420            else {
2421                g2.setPaint(this.zoomOutlinePaint);
2422                g2.draw(this.zoomRectangle);
2423            }
2424            if (xor) {
2425                // Reset to the default 'overwrite' mode
2426                g2.setPaintMode();
2427            }
2428        }
2429    }
2430
2431    /**
2432     * Draws a vertical line used to trace the mouse position to the horizontal
2433     * axis.
2434     *
2435     * @param g2 the graphics device.
2436     * @param x  the x-coordinate of the trace line.
2437     */
2438    private void drawHorizontalAxisTrace(Graphics2D g2, int x) {
2439
2440        Rectangle2D dataArea = getScreenDataArea();
2441
2442        g2.setXORMode(Color.ORANGE);
2443        if (((int) dataArea.getMinX() < x) && (x < (int) dataArea.getMaxX())) {
2444
2445            if (this.verticalTraceLine != null) {
2446                g2.draw(this.verticalTraceLine);
2447                this.verticalTraceLine.setLine(x, (int) dataArea.getMinY(), x,
2448                        (int) dataArea.getMaxY());
2449            }
2450            else {
2451                this.verticalTraceLine = new Line2D.Float(x,
2452                        (int) dataArea.getMinY(), x, (int) dataArea.getMaxY());
2453            }
2454            g2.draw(this.verticalTraceLine);
2455        }
2456
2457        // Reset to the default 'overwrite' mode
2458        g2.setPaintMode();
2459    }
2460
2461    /**
2462     * Draws a horizontal line used to trace the mouse position to the vertical
2463     * axis.
2464     *
2465     * @param g2 the graphics device.
2466     * @param y  the y-coordinate of the trace line.
2467     */
2468    private void drawVerticalAxisTrace(Graphics2D g2, int y) {
2469
2470        Rectangle2D dataArea = getScreenDataArea();
2471
2472        g2.setXORMode(Color.ORANGE);
2473        if (((int) dataArea.getMinY() < y) && (y < (int) dataArea.getMaxY())) {
2474
2475            if (this.horizontalTraceLine != null) {
2476                g2.draw(this.horizontalTraceLine);
2477                this.horizontalTraceLine.setLine((int) dataArea.getMinX(), y,
2478                        (int) dataArea.getMaxX(), y);
2479            }
2480            else {
2481                this.horizontalTraceLine = new Line2D.Float(
2482                        (int) dataArea.getMinX(), y, (int) dataArea.getMaxX(),
2483                        y);
2484            }
2485            g2.draw(this.horizontalTraceLine);
2486        }
2487
2488        // Reset to the default 'overwrite' mode
2489        g2.setPaintMode();
2490    }
2491
2492    /**
2493     * Displays a dialog that allows the user to edit the properties for the
2494     * current chart.
2495     */
2496    public void doEditChartProperties() {
2497
2498        ChartEditor editor = ChartEditorManager.getChartEditor(this.chart);
2499        int result = JOptionPane.showConfirmDialog(this, editor,
2500                localizationResources.getString("Chart_Properties"),
2501                JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
2502        if (result == JOptionPane.OK_OPTION) {
2503            editor.updateChart(this.chart);
2504        }
2505
2506    }
2507
2508    /**
2509     * Copies the current chart to the system clipboard.
2510     */
2511    public void doCopy() {
2512        Clipboard systemClipboard
2513                = Toolkit.getDefaultToolkit().getSystemClipboard();
2514        Insets insets = getInsets();
2515        int w = getWidth() - insets.left - insets.right;
2516        int h = getHeight() - insets.top - insets.bottom;
2517        ChartTransferable selection = new ChartTransferable(this.chart, w, h,
2518                getMinimumDrawWidth(), getMinimumDrawHeight(),
2519                getMaximumDrawWidth(), getMaximumDrawHeight(), true);
2520        systemClipboard.setContents(selection, null);
2521    }
2522
2523    /**
2524     * Opens a file chooser and gives the user an opportunity to save the chart
2525     * in PNG format.
2526     *
2527     * @throws IOException if there is an I/O error.
2528     */
2529    public void doSaveAs() throws IOException {
2530        JFileChooser fileChooser = new JFileChooser();
2531        fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2532        FileNameExtensionFilter filter = new FileNameExtensionFilter(
2533                    localizationResources.getString("PNG_Image_Files"), "png");
2534        fileChooser.addChoosableFileFilter(filter);
2535        fileChooser.setFileFilter(filter);
2536
2537        int option = fileChooser.showSaveDialog(this);
2538        if (option == JFileChooser.APPROVE_OPTION) {
2539            String filename = fileChooser.getSelectedFile().getPath();
2540            if (isEnforceFileExtensions()) {
2541                if (!filename.endsWith(".png")) {
2542                    filename = filename + ".png";
2543                }
2544            }
2545            ChartUtils.saveChartAsPNG(new File(filename), this.chart,
2546                    getWidth(), getHeight());
2547        }
2548    }
2549    
2550    /**
2551     * Saves the chart in SVG format (a filechooser will be displayed so that
2552     * the user can specify the filename).  Note that this method only works
2553     * if the JFreeSVG library is on the classpath...if this library is not 
2554     * present, the method will fail.
2555     */
2556    private void saveAsSVG(File f) throws IOException {
2557        File file = f;
2558        if (file == null) {
2559            JFileChooser fileChooser = new JFileChooser();
2560            fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2561            FileNameExtensionFilter filter = new FileNameExtensionFilter(
2562                    localizationResources.getString("SVG_Files"), "svg");
2563            fileChooser.addChoosableFileFilter(filter);
2564            fileChooser.setFileFilter(filter);
2565
2566            int option = fileChooser.showSaveDialog(this);
2567            if (option == JFileChooser.APPROVE_OPTION) {
2568                String filename = fileChooser.getSelectedFile().getPath();
2569                if (isEnforceFileExtensions()) {
2570                    if (!filename.endsWith(".svg")) {
2571                        filename = filename + ".svg";
2572                    }
2573                }
2574                file = new File(filename);
2575                if (file.exists()) {
2576                    String fileExists = localizationResources.getString(
2577                            "FILE_EXISTS_CONFIRM_OVERWRITE");
2578                    int response = JOptionPane.showConfirmDialog(this, 
2579                            fileExists,
2580                            localizationResources.getString("Save_as_SVG"),
2581                            JOptionPane.OK_CANCEL_OPTION);
2582                    if (response == JOptionPane.CANCEL_OPTION) {
2583                        file = null;
2584                    }
2585                }
2586            }
2587        }
2588        
2589        if (file != null) {
2590            // use reflection to get the SVG string
2591            String svg = generateSVG(getWidth(), getHeight());
2592            BufferedWriter writer = null;
2593            try {
2594                writer = new BufferedWriter(new FileWriter(file));
2595                writer.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
2596                writer.write(svg + "\n");
2597                writer.flush();
2598            } finally {
2599                try {
2600                    if (writer != null) {
2601                        writer.close();
2602                    }
2603                } catch (IOException ex) {
2604                    throw new RuntimeException(ex);
2605                }
2606            } 
2607
2608        }
2609    }
2610    
2611    /**
2612     * Generates a string containing a rendering of the chart in SVG format.
2613     * This feature is only supported if the JFreeSVG library is included on 
2614     * the classpath.
2615     * 
2616     * @return A string containing an SVG element for the current chart, or 
2617     *     {@code null} if there is a problem with the method invocation
2618     *     by reflection.
2619     */
2620    private String generateSVG(int width, int height) {
2621        Graphics2D g2 = createSVGGraphics2D(width, height);
2622        if (g2 == null) {
2623            throw new IllegalStateException("JFreeSVG library is not present.");
2624        }
2625        // we suppress shadow generation, because SVG is a vector format and
2626        // the shadow effect is applied via bitmap effects...
2627        g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true);
2628        String svg = null;
2629        Rectangle2D drawArea = new Rectangle2D.Double(0, 0, width, height);
2630        this.chart.draw(g2, drawArea);
2631        try {
2632            Method m = g2.getClass().getMethod("getSVGElement");
2633            svg = (String) m.invoke(g2);
2634        } catch (NoSuchMethodException e) {
2635            // null will be returned
2636        } catch (SecurityException e) {
2637            // null will be returned
2638        } catch (IllegalAccessException e) {
2639            // null will be returned
2640        } catch (IllegalArgumentException e) {
2641            // null will be returned
2642        } catch (InvocationTargetException e) {
2643            // null will be returned
2644        }
2645        return svg;
2646    }
2647
2648    private Graphics2D createSVGGraphics2D(int w, int h) {
2649        try {
2650            Class svgGraphics2d = Class.forName("org.jfree.graphics2d.svg.SVGGraphics2D");
2651            Constructor ctor = svgGraphics2d.getConstructor(int.class, int.class);
2652            return (Graphics2D) ctor.newInstance(w, h);
2653        } catch (ClassNotFoundException ex) {
2654            return null;
2655        } catch (NoSuchMethodException ex) {
2656            return null;
2657        } catch (SecurityException ex) {
2658            return null;
2659        } catch (InstantiationException ex) {
2660            return null;
2661        } catch (IllegalAccessException ex) {
2662            return null;
2663        } catch (IllegalArgumentException ex) {
2664            return null;
2665        } catch (InvocationTargetException ex) {
2666            return null;
2667        }
2668    }
2669
2670    /**
2671     * Saves the chart in PDF format (a filechooser will be displayed so that
2672     * the user can specify the filename).  Note that this method only works
2673     * if the OrsonPDF library is on the classpath...if this library is not
2674     * present, the method will fail.
2675     */
2676    private void saveAsPDF(File f) {
2677        File file = f;
2678        if (file == null) {
2679            JFileChooser fileChooser = new JFileChooser();
2680            fileChooser.setCurrentDirectory(this.defaultDirectoryForSaveAs);
2681            FileNameExtensionFilter filter = new FileNameExtensionFilter(
2682                    localizationResources.getString("PDF_Files"), "pdf");
2683            fileChooser.addChoosableFileFilter(filter);
2684            fileChooser.setFileFilter(filter);
2685
2686            int option = fileChooser.showSaveDialog(this);
2687            if (option == JFileChooser.APPROVE_OPTION) {
2688                String filename = fileChooser.getSelectedFile().getPath();
2689                if (isEnforceFileExtensions()) {
2690                    if (!filename.endsWith(".pdf")) {
2691                        filename = filename + ".pdf";
2692                    }
2693                }
2694                file = new File(filename);
2695                if (file.exists()) {
2696                    String fileExists = localizationResources.getString(
2697                            "FILE_EXISTS_CONFIRM_OVERWRITE");
2698                    int response = JOptionPane.showConfirmDialog(this, 
2699                            fileExists,
2700                            localizationResources.getString("Save_as_PDF"),
2701                            JOptionPane.OK_CANCEL_OPTION);
2702                    if (response == JOptionPane.CANCEL_OPTION) {
2703                        file = null;
2704                    }
2705                }
2706            }
2707        }
2708        
2709        if (file != null) {
2710            writeAsPDF(file, getWidth(), getHeight());
2711        }
2712    }
2713
2714    /**
2715     * Returns {@code true} if OrsonPDF is on the classpath, and 
2716     * {@code false} otherwise.  The OrsonPDF library can be found at
2717     * http://www.object-refinery.com/pdf/
2718     * 
2719     * @return A boolean.
2720     */
2721    private boolean isOrsonPDFAvailable() {
2722        Class pdfDocumentClass = null;
2723        try {
2724            pdfDocumentClass = Class.forName("com.orsonpdf.PDFDocument");
2725        } catch (ClassNotFoundException e) {
2726            // pdfDocument class will be null so the function will return false
2727        }
2728        return (pdfDocumentClass != null);
2729    }
2730    
2731    /**
2732     * Writes the current chart to the specified file in PDF format.  This 
2733     * will only work when the OrsonPDF library is found on the classpath.
2734     * Reflection is used to ensure there is no compile-time dependency on
2735     * OrsonPDF (which is non-free software).
2736     * 
2737     * @param file  the output file ({@code null} not permitted).
2738     * @param w  the chart width.
2739     * @param h  the chart height.
2740     */
2741    private void writeAsPDF(File file, int w, int h) {
2742        if (!isOrsonPDFAvailable()) {
2743            throw new IllegalStateException(
2744                    "OrsonPDF is not present on the classpath.");
2745        }
2746        Args.nullNotPermitted(file, "file");
2747        try {
2748            Class pdfDocClass = Class.forName("com.orsonpdf.PDFDocument");
2749            Object pdfDoc = pdfDocClass.newInstance();
2750            Method m = pdfDocClass.getMethod("createPage", Rectangle2D.class);
2751            Rectangle2D rect = new Rectangle(w, h);
2752            Object page = m.invoke(pdfDoc, rect);
2753            Method m2 = page.getClass().getMethod("getGraphics2D");
2754            Graphics2D g2 = (Graphics2D) m2.invoke(page);
2755            // we suppress shadow generation, because PDF is a vector format and
2756            // the shadow effect is applied via bitmap effects...
2757            g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true);
2758            Rectangle2D drawArea = new Rectangle2D.Double(0, 0, w, h);
2759            this.chart.draw(g2, drawArea);
2760            Method m3 = pdfDocClass.getMethod("writeToFile", File.class);
2761            m3.invoke(pdfDoc, file);
2762        } catch (ClassNotFoundException ex) {
2763            throw new RuntimeException(ex);
2764        } catch (InstantiationException ex) {
2765            throw new RuntimeException(ex);
2766        } catch (IllegalAccessException ex) {
2767            throw new RuntimeException(ex);
2768        } catch (NoSuchMethodException ex) {
2769            throw new RuntimeException(ex);
2770        } catch (SecurityException ex) {
2771            throw new RuntimeException(ex);
2772        } catch (IllegalArgumentException ex) {
2773            throw new RuntimeException(ex);
2774        } catch (InvocationTargetException ex) {
2775            throw new RuntimeException(ex);
2776        }
2777    }
2778
2779    /**
2780     * Creates a print job for the chart.
2781     */
2782    public void createChartPrintJob() {
2783        PrinterJob job = PrinterJob.getPrinterJob();
2784        PageFormat pf = job.defaultPage();
2785        PageFormat pf2 = job.pageDialog(pf);
2786        if (pf2 != pf) {
2787            job.setPrintable(this, pf2);
2788            if (job.printDialog()) {
2789                try {
2790                    job.print();
2791                }
2792                catch (PrinterException e) {
2793                    JOptionPane.showMessageDialog(this, e);
2794                }
2795            }
2796        }
2797    }
2798
2799    /**
2800     * Prints the chart on a single page.
2801     *
2802     * @param g  the graphics context.
2803     * @param pf  the page format to use.
2804     * @param pageIndex  the index of the page. If not {@code 0}, nothing
2805     *                   gets printed.
2806     *
2807     * @return The result of printing.
2808     */
2809    @Override
2810    public int print(Graphics g, PageFormat pf, int pageIndex) {
2811
2812        if (pageIndex != 0) {
2813            return NO_SUCH_PAGE;
2814        }
2815        Graphics2D g2 = (Graphics2D) g;
2816        double x = pf.getImageableX();
2817        double y = pf.getImageableY();
2818        double w = pf.getImageableWidth();
2819        double h = pf.getImageableHeight();
2820        this.chart.draw(g2, new Rectangle2D.Double(x, y, w, h), this.anchor,
2821                null);
2822        return PAGE_EXISTS;
2823
2824    }
2825
2826    /**
2827     * Adds a listener to the list of objects listening for chart mouse events.
2828     *
2829     * @param listener  the listener ({@code null} not permitted).
2830     */
2831    public void addChartMouseListener(ChartMouseListener listener) {
2832        Args.nullNotPermitted(listener, "listener");
2833        this.chartMouseListeners.add(ChartMouseListener.class, listener);
2834    }
2835
2836    /**
2837     * Removes a listener from the list of objects listening for chart mouse
2838     * events.
2839     *
2840     * @param listener  the listener.
2841     */
2842    public void removeChartMouseListener(ChartMouseListener listener) {
2843        this.chartMouseListeners.remove(ChartMouseListener.class, listener);
2844    }
2845
2846    /**
2847     * Returns an array of the listeners of the given type registered with the
2848     * panel.
2849     *
2850     * @param listenerType  the listener type.
2851     *
2852     * @return An array of listeners.
2853     */
2854    @Override
2855    public EventListener[] getListeners(Class listenerType) {
2856        if (listenerType == ChartMouseListener.class) {
2857            // fetch listeners from local storage
2858            return this.chartMouseListeners.getListeners(listenerType);
2859        }
2860        else {
2861            return super.getListeners(listenerType);
2862        }
2863    }
2864
2865    /**
2866     * Creates a popup menu for the panel.
2867     *
2868     * @param properties  include a menu item for the chart property editor.
2869     * @param save  include a menu item for saving the chart.
2870     * @param print  include a menu item for printing the chart.
2871     * @param zoom  include menu items for zooming.
2872     *
2873     * @return The popup menu.
2874     */
2875    protected JPopupMenu createPopupMenu(boolean properties, boolean save,
2876            boolean print, boolean zoom) {
2877        return createPopupMenu(properties, false, save, print, zoom);
2878    }
2879
2880    /**
2881     * Creates a popup menu for the panel.
2882     *
2883     * @param properties  include a menu item for the chart property editor.
2884     * @param copy include a menu item for copying to the clipboard.
2885     * @param save  include a menu item for saving the chart.
2886     * @param print  include a menu item for printing the chart.
2887     * @param zoom  include menu items for zooming.
2888     *
2889     * @return The popup menu.
2890     */
2891    protected JPopupMenu createPopupMenu(boolean properties,
2892            boolean copy, boolean save, boolean print, boolean zoom) {
2893
2894        JPopupMenu result = new JPopupMenu(localizationResources.getString("Chart") + ":");
2895        boolean separator = false;
2896
2897        if (properties) {
2898            JMenuItem propertiesItem = new JMenuItem(
2899                    localizationResources.getString("Properties..."));
2900            propertiesItem.setActionCommand(PROPERTIES_COMMAND);
2901            propertiesItem.addActionListener(this);
2902            result.add(propertiesItem);
2903            separator = true;
2904        }
2905
2906        if (copy) {
2907            if (separator) {
2908                result.addSeparator();
2909            }
2910            JMenuItem copyItem = new JMenuItem(
2911                    localizationResources.getString("Copy"));
2912            copyItem.setActionCommand(COPY_COMMAND);
2913            copyItem.addActionListener(this);
2914            result.add(copyItem);
2915            separator = !save;
2916        }
2917
2918        if (save) {
2919            if (separator) {
2920                result.addSeparator();
2921            }
2922            JMenu saveSubMenu = new JMenu(localizationResources.getString(
2923                    "Save_as"));
2924            JMenuItem pngItem = new JMenuItem(localizationResources.getString(
2925                    "PNG..."));
2926            pngItem.setActionCommand("SAVE_AS_PNG");
2927            pngItem.addActionListener(this);
2928            saveSubMenu.add(pngItem);
2929            
2930            if (createSVGGraphics2D(10, 10) != null) {
2931                JMenuItem svgItem = new JMenuItem(localizationResources.getString(
2932                        "SVG..."));
2933                svgItem.setActionCommand("SAVE_AS_SVG");
2934                svgItem.addActionListener(this);
2935                saveSubMenu.add(svgItem);                
2936            }
2937            
2938            if (isOrsonPDFAvailable()) {
2939                JMenuItem pdfItem = new JMenuItem(
2940                        localizationResources.getString("PDF..."));
2941                pdfItem.setActionCommand("SAVE_AS_PDF");
2942                pdfItem.addActionListener(this);
2943                saveSubMenu.add(pdfItem);
2944            }
2945            result.add(saveSubMenu);
2946            separator = true;
2947        }
2948
2949        if (print) {
2950            if (separator) {
2951                result.addSeparator();
2952            }
2953            JMenuItem printItem = new JMenuItem(
2954                    localizationResources.getString("Print..."));
2955            printItem.setActionCommand(PRINT_COMMAND);
2956            printItem.addActionListener(this);
2957            result.add(printItem);
2958            separator = true;
2959        }
2960
2961        if (zoom) {
2962            if (separator) {
2963                result.addSeparator();
2964            }
2965
2966            JMenu zoomInMenu = new JMenu(
2967                    localizationResources.getString("Zoom_In"));
2968
2969            this.zoomInBothMenuItem = new JMenuItem(
2970                    localizationResources.getString("All_Axes"));
2971            this.zoomInBothMenuItem.setActionCommand(ZOOM_IN_BOTH_COMMAND);
2972            this.zoomInBothMenuItem.addActionListener(this);
2973            zoomInMenu.add(this.zoomInBothMenuItem);
2974
2975            zoomInMenu.addSeparator();
2976
2977            this.zoomInDomainMenuItem = new JMenuItem(
2978                    localizationResources.getString("Domain_Axis"));
2979            this.zoomInDomainMenuItem.setActionCommand(ZOOM_IN_DOMAIN_COMMAND);
2980            this.zoomInDomainMenuItem.addActionListener(this);
2981            zoomInMenu.add(this.zoomInDomainMenuItem);
2982
2983            this.zoomInRangeMenuItem = new JMenuItem(
2984                    localizationResources.getString("Range_Axis"));
2985            this.zoomInRangeMenuItem.setActionCommand(ZOOM_IN_RANGE_COMMAND);
2986            this.zoomInRangeMenuItem.addActionListener(this);
2987            zoomInMenu.add(this.zoomInRangeMenuItem);
2988
2989            result.add(zoomInMenu);
2990
2991            JMenu zoomOutMenu = new JMenu(
2992                    localizationResources.getString("Zoom_Out"));
2993
2994            this.zoomOutBothMenuItem = new JMenuItem(
2995                    localizationResources.getString("All_Axes"));
2996            this.zoomOutBothMenuItem.setActionCommand(ZOOM_OUT_BOTH_COMMAND);
2997            this.zoomOutBothMenuItem.addActionListener(this);
2998            zoomOutMenu.add(this.zoomOutBothMenuItem);
2999
3000            zoomOutMenu.addSeparator();
3001
3002            this.zoomOutDomainMenuItem = new JMenuItem(
3003                    localizationResources.getString("Domain_Axis"));
3004            this.zoomOutDomainMenuItem.setActionCommand(
3005                    ZOOM_OUT_DOMAIN_COMMAND);
3006            this.zoomOutDomainMenuItem.addActionListener(this);
3007            zoomOutMenu.add(this.zoomOutDomainMenuItem);
3008
3009            this.zoomOutRangeMenuItem = new JMenuItem(
3010                    localizationResources.getString("Range_Axis"));
3011            this.zoomOutRangeMenuItem.setActionCommand(ZOOM_OUT_RANGE_COMMAND);
3012            this.zoomOutRangeMenuItem.addActionListener(this);
3013            zoomOutMenu.add(this.zoomOutRangeMenuItem);
3014
3015            result.add(zoomOutMenu);
3016
3017            JMenu autoRangeMenu = new JMenu(
3018                    localizationResources.getString("Auto_Range"));
3019
3020            this.zoomResetBothMenuItem = new JMenuItem(
3021                    localizationResources.getString("All_Axes"));
3022            this.zoomResetBothMenuItem.setActionCommand(
3023                    ZOOM_RESET_BOTH_COMMAND);
3024            this.zoomResetBothMenuItem.addActionListener(this);
3025            autoRangeMenu.add(this.zoomResetBothMenuItem);
3026
3027            autoRangeMenu.addSeparator();
3028            this.zoomResetDomainMenuItem = new JMenuItem(
3029                    localizationResources.getString("Domain_Axis"));
3030            this.zoomResetDomainMenuItem.setActionCommand(
3031                    ZOOM_RESET_DOMAIN_COMMAND);
3032            this.zoomResetDomainMenuItem.addActionListener(this);
3033            autoRangeMenu.add(this.zoomResetDomainMenuItem);
3034
3035            this.zoomResetRangeMenuItem = new JMenuItem(
3036                    localizationResources.getString("Range_Axis"));
3037            this.zoomResetRangeMenuItem.setActionCommand(
3038                    ZOOM_RESET_RANGE_COMMAND);
3039            this.zoomResetRangeMenuItem.addActionListener(this);
3040            autoRangeMenu.add(this.zoomResetRangeMenuItem);
3041
3042            result.addSeparator();
3043            result.add(autoRangeMenu);
3044
3045        }
3046
3047        return result;
3048
3049    }
3050
3051    /**
3052     * The idea is to modify the zooming options depending on the type of chart
3053     * being displayed by the panel.
3054     *
3055     * @param x  horizontal position of the popup.
3056     * @param y  vertical position of the popup.
3057     */
3058    protected void displayPopupMenu(int x, int y) {
3059
3060        if (this.popup == null) {
3061            return;
3062        }
3063
3064        // go through each zoom menu item and decide whether or not to
3065        // enable it...
3066        boolean isDomainZoomable = false;
3067        boolean isRangeZoomable = false;
3068        Plot plot = (this.chart != null ? this.chart.getPlot() : null);
3069        if (plot instanceof Zoomable) {
3070            Zoomable z = (Zoomable) plot;
3071            isDomainZoomable = z.isDomainZoomable();
3072            isRangeZoomable = z.isRangeZoomable();
3073        }
3074
3075        if (this.zoomInDomainMenuItem != null) {
3076            this.zoomInDomainMenuItem.setEnabled(isDomainZoomable);
3077        }
3078        if (this.zoomOutDomainMenuItem != null) {
3079            this.zoomOutDomainMenuItem.setEnabled(isDomainZoomable);
3080        }
3081        if (this.zoomResetDomainMenuItem != null) {
3082            this.zoomResetDomainMenuItem.setEnabled(isDomainZoomable);
3083        }
3084
3085        if (this.zoomInRangeMenuItem != null) {
3086            this.zoomInRangeMenuItem.setEnabled(isRangeZoomable);
3087        }
3088        if (this.zoomOutRangeMenuItem != null) {
3089            this.zoomOutRangeMenuItem.setEnabled(isRangeZoomable);
3090        }
3091
3092        if (this.zoomResetRangeMenuItem != null) {
3093            this.zoomResetRangeMenuItem.setEnabled(isRangeZoomable);
3094        }
3095
3096        if (this.zoomInBothMenuItem != null) {
3097            this.zoomInBothMenuItem.setEnabled(isDomainZoomable
3098                    && isRangeZoomable);
3099        }
3100        if (this.zoomOutBothMenuItem != null) {
3101            this.zoomOutBothMenuItem.setEnabled(isDomainZoomable
3102                    && isRangeZoomable);
3103        }
3104        if (this.zoomResetBothMenuItem != null) {
3105            this.zoomResetBothMenuItem.setEnabled(isDomainZoomable
3106                    && isRangeZoomable);
3107        }
3108
3109        this.popup.show(this, x, y);
3110
3111    }
3112
3113    /**
3114     * Updates the UI for a LookAndFeel change.
3115     */
3116    @Override
3117    public void updateUI() {
3118        // here we need to update the UI for the popup menu, if the panel
3119        // has one...
3120        if (this.popup != null) {
3121            SwingUtilities.updateComponentTreeUI(this.popup);
3122        }
3123        super.updateUI();
3124    }
3125
3126    /**
3127     * Provides serialization support.
3128     *
3129     * @param stream  the output stream.
3130     *
3131     * @throws IOException  if there is an I/O error.
3132     */
3133    private void writeObject(ObjectOutputStream stream) throws IOException {
3134        stream.defaultWriteObject();
3135        SerialUtils.writePaint(this.zoomFillPaint, stream);
3136        SerialUtils.writePaint(this.zoomOutlinePaint, stream);
3137    }
3138
3139    /**
3140     * Provides serialization support.
3141     *
3142     * @param stream  the input stream.
3143     *
3144     * @throws IOException  if there is an I/O error.
3145     * @throws ClassNotFoundException  if there is a classpath problem.
3146     */
3147    private void readObject(ObjectInputStream stream)
3148        throws IOException, ClassNotFoundException {
3149        stream.defaultReadObject();
3150        this.zoomFillPaint = SerialUtils.readPaint(stream);
3151        this.zoomOutlinePaint = SerialUtils.readPaint(stream);
3152
3153        // we create a new but empty chartMouseListeners list
3154        this.chartMouseListeners = new EventListenerList();
3155
3156        // register as a listener with sub-components...
3157        if (this.chart != null) {
3158            this.chart.addChangeListener(this);
3159        }
3160
3161    }
3162
3163}