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