001/* ================================================
002 * JFreeChart-FX : JavaFX extensions for JFreeChart
003 * ================================================
004 *
005 * (C) Copyright 2017-2021 by Object Refinery Limited 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-2021, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
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) -> {
116            contextMenu.show(ChartViewer.this.getScene().getWindow(),
117                    event.getScreenX(), event.getScreenY());
118        });
119        this.contextMenu.setOnShowing(
120                e -> ChartViewer.this.getCanvas().setTooltipEnabled(false));
121        this.contextMenu.setOnHiding(
122                e -> ChartViewer.this.getCanvas().setTooltipEnabled(true));
123    }
124
125    /**
126     * Returns the chart that is being displayed by this viewer.
127     * 
128     * @return The chart (possibly {@code null}). 
129     */
130    public JFreeChart getChart() {
131        return this.canvas.getChart();
132    }
133    
134    /**
135     * Sets the chart to be displayed by this viewer.
136     * 
137     * @param chart  the chart ({@code null} not permitted). 
138     */
139    public void setChart(JFreeChart chart) {
140        Args.nullNotPermitted(chart, "chart");
141        this.canvas.setChart(chart);
142    }
143
144    /**
145     * Returns the {@link ChartCanvas} embedded in this component.
146     * 
147     * @return The {@code ChartCanvas} (never {@code null}).
148     */
149    public ChartCanvas getCanvas() {
150        return this.canvas;
151    }
152 
153    /**
154     * Returns the context menu for this component.
155     * 
156     * @return The context menu for this component. 
157     */
158    public ContextMenu getContextMenu() {
159        return this.contextMenu;
160    }
161    
162    /**
163     * Returns the rendering info from the most recent drawing of the chart.
164     * 
165     * @return The rendering info (possibly {@code null}).
166     */
167    public ChartRenderingInfo getRenderingInfo() {
168        return getCanvas().getRenderingInfo();
169    }
170    
171    /**
172     * Returns the current fill paint for the zoom rectangle.
173     * 
174     * @return The fill paint.
175     */
176    public Paint getZoomFillPaint() {
177        return this.zoomRectangle.getFill();
178    }
179    
180    /**
181     * Sets the fill paint for the zoom rectangle.
182     * 
183     * @param paint  the new paint.
184     */
185    public void setZoomFillPaint(Paint paint) {
186        this.zoomRectangle.setFill(paint);
187    }
188    
189    @Override
190    protected void layoutChildren() {
191        super.layoutChildren();
192        this.canvas.setLayoutX(0);
193        this.canvas.setLayoutY(0);
194        this.canvas.setWidth(getWidth());
195        this.canvas.setHeight(getHeight());
196    }
197    
198    /**
199     * Registers a listener to receive {@link ChartMouseEvent} notifications
200     * from the canvas embedded in this viewer.
201     *
202     * @param listener  the listener ({@code null} not permitted).
203     */
204    public void addChartMouseListener(ChartMouseListenerFX listener) {
205        Args.nullNotPermitted(listener, "listener");
206        this.canvas.addChartMouseListener(listener);
207    }
208
209    /**
210     * Removes a listener from the list of objects listening for chart mouse
211     * events.
212     *
213     * @param listener  the listener.
214     */
215    public void removeChartMouseListener(ChartMouseListenerFX listener) {
216        Args.nullNotPermitted(listener, "listener");
217        this.canvas.removeChartMouseListener(listener);
218    }
219    
220    /**
221     * Creates the context menu.
222     * 
223     * @return The context menu.
224     */
225    private ContextMenu createContextMenu() {
226        final ContextMenu menu = new ContextMenu();
227        menu.setAutoHide(true);
228        Menu export = new Menu("Export As");
229        
230        MenuItem pngItem = new MenuItem("PNG...");
231        pngItem.setOnAction(e -> handleExportToPNG());        
232        export.getItems().add(pngItem);
233        
234        MenuItem jpegItem = new MenuItem("JPEG...");
235        jpegItem.setOnAction(e -> handleExportToJPEG());        
236        export.getItems().add(jpegItem);
237        
238        if (ExportUtils.isOrsonPDFAvailable()) {
239            MenuItem pdfItem = new MenuItem("PDF...");
240            pdfItem.setOnAction(e -> handleExportToPDF());
241            export.getItems().add(pdfItem);
242        }
243        if (ExportUtils.isJFreeSVGAvailable()) {
244            MenuItem svgItem = new MenuItem("SVG...");
245            svgItem.setOnAction(e -> handleExportToSVG());
246            export.getItems().add(svgItem);        
247        }
248        menu.getItems().add(export);
249        return menu;
250    }
251    
252    /**
253     * A handler for the export to PDF option in the context menu.
254     */
255    private void handleExportToPDF() {
256        FileChooser chooser = new FileChooser();
257        chooser.setTitle("Export to PDF");
258        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
259                "Portable Document Format (PDF)", "*.pdf");
260        chooser.getExtensionFilters().add(filter);
261        File file = chooser.showSaveDialog(getScene().getWindow());
262        if (file != null) {
263            ExportUtils.writeAsPDF(this.canvas.getChart(), (int) getWidth(), 
264                    (int) getHeight(), file);
265        } 
266    }
267    
268    /**
269     * A handler for the export to SVG option in the context menu.
270     */
271    private void handleExportToSVG() {
272        FileChooser chooser = new FileChooser();
273        chooser.setTitle("Export to SVG");
274        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
275                "Scalable Vector Graphics (SVG)", "*.svg");
276        chooser.getExtensionFilters().add(filter);
277        File file = chooser.showSaveDialog(getScene().getWindow());
278        if (file != null) {
279            ExportUtils.writeAsSVG(this.canvas.getChart(), (int) getWidth(), 
280                    (int) getHeight(), file);
281        }
282    }
283    
284    /**
285     * A handler for the export to PNG option in the context menu.
286     */
287    private void handleExportToPNG() {
288        FileChooser chooser = new FileChooser();
289        chooser.setTitle("Export to PNG");
290        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter(
291                "Portable Network Graphics (PNG)", "*.png");
292        chooser.getExtensionFilters().add(filter);
293        File file = chooser.showSaveDialog(getScene().getWindow());
294        if (file != null) {
295            try {
296                ExportUtils.writeAsPNG(this.canvas.getChart(), (int) getWidth(),
297                        (int) getHeight(), file);
298            } catch (IOException ex) {
299                // FIXME: show a dialog with the error
300                throw new RuntimeException(ex);
301            }
302        }        
303    }
304
305    /**
306     * A handler for the export to JPEG option in the context menu.
307     */
308    private void handleExportToJPEG() {
309        FileChooser chooser = new FileChooser();
310        chooser.setTitle("Export to JPEG");
311        FileChooser.ExtensionFilter filter = new FileChooser.ExtensionFilter("JPEG", "*.jpg");
312        chooser.getExtensionFilters().add(filter);
313        File file = chooser.showSaveDialog(getScene().getWindow());
314        if (file != null) {
315            try {
316                ExportUtils.writeAsJPEG(this.canvas.getChart(), (int) getWidth(),
317                        (int) getHeight(), file);
318            } catch (IOException ex) {
319                // FIXME: show a dialog with the error
320                throw new RuntimeException(ex);
321            }
322        }        
323    }
324
325    /**
326     * Sets the size and location of the zoom rectangle and makes it visible
327     * if it wasn't already visible..  This method is provided for the use of 
328     * the {@link ZoomHandlerFX} class, you won't normally need to call it 
329     * directly.
330     * 
331     * @param x  the x-location.
332     * @param y  the y-location.
333     * @param w  the width.
334     * @param h  the height.
335     */
336    public void showZoomRectangle(double x, double y, double w, double h) {
337        this.zoomRectangle.setX(x);
338        this.zoomRectangle.setY(y);
339        this.zoomRectangle.setWidth(w);
340        this.zoomRectangle.setHeight(h);
341        this.zoomRectangle.setVisible(true);
342    }
343    
344    /**
345     * Hides the zoom rectangle.  This method is provided for the use of the
346     * {@link ZoomHandlerFX} class, you won't normally need to call it directly.
347     */
348    public void hideZoomRectangle() {
349        this.zoomRectangle.setVisible(false);
350    }
351
352}
353