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