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