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