001/* ================================================
002 * JFreeChart-FX : JavaFX extensions for JFreeChart
003 * ================================================
004 *
005 * (C) Copyright 2017-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  https://github.com/jfree/jfreechart-fx
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 * ChartViewer.java
029 * ----------------
030 * (C) Copyright 2014-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.chart.fx;
038
039import java.io.File;
040import java.io.IOException;
041import javafx.scene.control.ContextMenu;
042import javafx.scene.control.Menu;
043import javafx.scene.control.MenuItem;
044import javafx.scene.input.ContextMenuEvent;
045import javafx.scene.layout.Region;
046import javafx.scene.paint.Color;
047import javafx.scene.paint.Paint;
048import javafx.scene.shape.Rectangle;
049import javafx.stage.FileChooser;
050import org.jfree.chart.ChartMouseEvent;
051import org.jfree.chart.ChartRenderingInfo;
052import org.jfree.chart.JFreeChart;
053import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
054import org.jfree.chart.fx.interaction.ZoomHandlerFX;
055import org.jfree.chart.util.ExportUtils;
056import org.jfree.chart.util.Args;
057
058/**
059 * A control for displaying a {@link JFreeChart} in JavaFX (embeds a 
060 * {@link ChartCanvas}, adds drag zooming and provides a popup menu for export
061 * to PNG/JPG/SVG and PDF formats).  Many behaviours (tooltips, zooming etc) are 
062 * provided directly by the canvas.
063 */
064public class ChartViewer extends Region {
065
066    private ChartCanvas canvas;
067    
068    /** 
069     * The zoom rectangle is used to display the zooming region when
070     * doing a drag-zoom with the mouse.  Most of the time this rectangle
071     * is not visible.
072     */
073    private Rectangle zoomRectangle;
074
075    /** The context menu for the chart viewer. */
076    private ContextMenu contextMenu;
077    
078    /**
079     * Creates a new instance, initially with no chart to display.  This 
080     * constructor is required so that this control can be used within
081     * FXML.
082     */
083    public ChartViewer() {
084        this(null);
085    }
086
087    /**
088     * Creates a new viewer to display the supplied chart in JavaFX.
089     * 
090     * @param chart  the chart ({@code null} permitted). 
091     */
092    public ChartViewer(JFreeChart chart) {
093        this(chart, true);
094    }
095    
096    /**
097     * Creates a new viewer instance.
098     * 
099     * @param chart  the chart ({@code null} permitted).
100     * @param contextMenuEnabled  enable the context menu?
101     */
102    public ChartViewer(JFreeChart chart, boolean contextMenuEnabled) {
103        this.canvas = new ChartCanvas(chart);
104        this.canvas.setTooltipEnabled(true);
105        this.canvas.addMouseHandler(new ZoomHandlerFX("zoom", this));
106        setFocusTraversable(true);
107        getChildren().add(this.canvas);
108        
109        this.zoomRectangle = new Rectangle(0, 0, new Color(0, 0, 1, 0.25));
110        this.zoomRectangle.setManaged(false);
111        this.zoomRectangle.setVisible(false);
112        getChildren().add(this.zoomRectangle);
113        
114        this.contextMenu = createContextMenu();
115        setOnContextMenuRequested((ContextMenuEvent event) -> contextMenu.show(ChartViewer.this.getScene().getWindow(),
116                event.getScreenX(), event.getScreenY()));
117        this.contextMenu.setOnShowing(
118                e -> ChartViewer.this.getCanvas().setTooltipEnabled(false));
119        this.contextMenu.setOnHiding(
120                e -> ChartViewer.this.getCanvas().setTooltipEnabled(true));
121    }
122
123    /**
124     * Returns the chart that is being displayed by this viewer.
125     * 
126     * @return The chart (possibly {@code null}). 
127     */
128    public JFreeChart getChart() {
129        return this.canvas.getChart();
130    }
131    
132    /**
133     * Sets the chart to be displayed by this viewer.
134     * 
135     * @param chart  the chart ({@code null} not permitted). 
136     */
137    public void setChart(JFreeChart chart) {
138        Args.nullNotPermitted(chart, "chart");
139        this.canvas.setChart(chart);
140    }
141
142    /**
143     * Returns the {@link ChartCanvas} embedded in this component.
144     * 
145     * @return The {@code ChartCanvas} (never {@code null}).
146     */
147    public ChartCanvas getCanvas() {
148        return this.canvas;
149    }
150 
151    /**
152     * Returns the context menu for this component.
153     * 
154     * @return The context menu for this component. 
155     */
156    public ContextMenu getContextMenu() {
157        return this.contextMenu;
158    }
159    
160    /**
161     * Returns the rendering info from the most recent drawing of the chart.
162     * 
163     * @return The rendering info (possibly {@code null}).
164     */
165    public ChartRenderingInfo getRenderingInfo() {
166        return getCanvas().getRenderingInfo();
167    }
168    
169    /**
170     * Returns the current fill paint for the zoom rectangle.
171     * 
172     * @return The fill paint.
173     */
174    public Paint getZoomFillPaint() {
175        return this.zoomRectangle.getFill();
176    }
177    
178    /**
179     * Sets the fill paint for the zoom rectangle.
180     * 
181     * @param paint  the new paint.
182     */
183    public void setZoomFillPaint(Paint paint) {
184        this.zoomRectangle.setFill(paint);
185    }
186    
187    @Override
188    protected void layoutChildren() {
189        super.layoutChildren();
190        this.canvas.setLayoutX(0);
191        this.canvas.setLayoutY(0);
192        this.canvas.setWidth(getWidth());
193        this.canvas.setHeight(getHeight());
194    }
195    
196    /**
197     * Registers a listener to receive {@link ChartMouseEvent} notifications
198     * from the canvas embedded in this viewer.
199     *
200     * @param listener  the listener ({@code null} not permitted).
201     */
202    public void addChartMouseListener(ChartMouseListenerFX listener) {
203        Args.nullNotPermitted(listener, "listener");
204        this.canvas.addChartMouseListener(listener);
205    }
206
207    /**
208     * Removes a listener from the list of objects listening for chart mouse
209     * events.
210     *
211     * @param listener  the listener.
212     */
213    public void removeChartMouseListener(ChartMouseListenerFX listener) {
214        Args.nullNotPermitted(listener, "listener");
215        this.canvas.removeChartMouseListener(listener);
216    }
217    
218    /**
219     * Creates the context menu.
220     * 
221     * @return The context menu.
222     */
223    private ContextMenu createContextMenu() {
224        final ContextMenu menu = new ContextMenu();
225        menu.setAutoHide(true);
226        Menu export = new Menu("Export As");
227        
228        MenuItem pngItem = new MenuItem("PNG...");
229        pngItem.setOnAction(e -> handleExportToPNG());        
230        export.getItems().add(pngItem);
231        
232        MenuItem jpegItem = new MenuItem("JPEG...");
233        jpegItem.setOnAction(e -> handleExportToJPEG());        
234        export.getItems().add(jpegItem);
235        
236        if (ExportUtils.isOrsonPDFAvailable()) {
237            MenuItem pdfItem = new MenuItem("PDF...");
238            pdfItem.setOnAction(e -> handleExportToPDF());
239            export.getItems().add(pdfItem);
240        }
241        if (ExportUtils.isJFreeSVGAvailable()) {
242            MenuItem svgItem = new MenuItem("SVG...");
243            svgItem.setOnAction(e -> handleExportToSVG());
244            export.getItems().add(svgItem);        
245        }
246        menu.getItems().add(export);
247        return menu;
248    }
249    
250    /**
251     * A handler for the export to PDF option in the context menu.
252     */
253    private void handleExportToPDF() {
254        FileChooser chooser = new FileChooser();
255        chooser.setTitle("Export to PDF");
256        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
257                "Portable Document Format (PDF)", "*.pdf");
258        chooser.getExtensionFilters().add(filter);
259        File file = chooser.showSaveDialog(getScene().getWindow());
260        if (file != null) {
261            ExportUtils.writeAsPDF(this.canvas.getChart(), (int) getWidth(), 
262                    (int) getHeight(), file);
263        } 
264    }
265    
266    /**
267     * A handler for the export to SVG option in the context menu.
268     */
269    private void handleExportToSVG() {
270        FileChooser chooser = new FileChooser();
271        chooser.setTitle("Export to SVG");
272        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
273                "Scalable Vector Graphics (SVG)", "*.svg");
274        chooser.getExtensionFilters().add(filter);
275        File file = chooser.showSaveDialog(getScene().getWindow());
276        if (file != null) {
277            ExportUtils.writeAsSVG(this.canvas.getChart(), (int) getWidth(), 
278                    (int) getHeight(), file);
279        }
280    }
281    
282    /**
283     * A handler for the export to PNG option in the context menu.
284     */
285    private void handleExportToPNG() {
286        FileChooser chooser = new FileChooser();
287        chooser.setTitle("Export to PNG");
288        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
289                "Portable Network Graphics (PNG)", "*.png");
290        chooser.getExtensionFilters().add(filter);
291        File file = chooser.showSaveDialog(getScene().getWindow());
292        if (file != null) {
293            try {
294                ExportUtils.writeAsPNG(this.canvas.getChart(), (int) getWidth(),
295                        (int) getHeight(), file);
296            } catch (IOException ex) {
297                // FIXME: show a dialog with the error
298                throw new RuntimeException(ex);
299            }
300        }        
301    }
302
303    /**
304     * A handler for the export to JPEG option in the context menu.
305     */
306    private void handleExportToJPEG() {
307        FileChooser chooser = new FileChooser();
308        chooser.setTitle("Export to JPEG");
309        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("JPEG", "*.jpg");
310        chooser.getExtensionFilters().add(filter);
311        File file = chooser.showSaveDialog(getScene().getWindow());
312        if (file != null) {
313            try {
314                ExportUtils.writeAsJPEG(this.canvas.getChart(), (int) getWidth(),
315                        (int) getHeight(), file);
316            } catch (IOException ex) {
317                // FIXME: show a dialog with the error
318                throw new RuntimeException(ex);
319            }
320        }        
321    }
322
323    /**
324     * Sets the size and location of the zoom rectangle and makes it visible
325     * if it wasn't already visible..  This method is provided for the use of 
326     * the {@link ZoomHandlerFX} class, you won't normally need to call it 
327     * directly.
328     * 
329     * @param x  the x-location.
330     * @param y  the y-location.
331     * @param w  the width.
332     * @param h  the height.
333     */
334    public void showZoomRectangle(double x, double y, double w, double h) {
335        this.zoomRectangle.setX(x);
336        this.zoomRectangle.setY(y);
337        this.zoomRectangle.setWidth(w);
338        this.zoomRectangle.setHeight(h);
339        this.zoomRectangle.setVisible(true);
340    }
341    
342    /**
343     * Hides the zoom rectangle.  This method is provided for the use of the
344     * {@link ZoomHandlerFX} class, you won't normally need to call it directly.
345     */
346    public void hideZoomRectangle() {
347        this.zoomRectangle.setVisible(false);
348    }
349
350}
351