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 * ChartCanvas.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.awt.Graphics2D; 040import java.awt.Rectangle; 041import java.awt.RenderingHints; 042import java.awt.geom.Point2D; 043import java.awt.geom.Rectangle2D; 044import java.util.ArrayList; 045import java.util.List; 046import javafx.collections.FXCollections; 047import javafx.collections.ObservableList; 048import javafx.scene.canvas.Canvas; 049import javafx.scene.canvas.GraphicsContext; 050import javafx.scene.control.Tooltip; 051import javafx.scene.input.MouseEvent; 052import javafx.scene.input.ScrollEvent; 053import javafx.scene.text.FontSmoothingType; 054import org.jfree.chart.ChartMouseEvent; 055import org.jfree.chart.ChartRenderingInfo; 056import org.jfree.chart.JFreeChart; 057import org.jfree.chart.event.ChartChangeEvent; 058import org.jfree.chart.event.ChartChangeListener; 059import org.jfree.chart.event.OverlayChangeEvent; 060import org.jfree.chart.event.OverlayChangeListener; 061import org.jfree.chart.fx.interaction.AnchorHandlerFX; 062import org.jfree.chart.fx.interaction.DispatchHandlerFX; 063import org.jfree.chart.fx.interaction.ChartMouseListenerFX; 064import org.jfree.chart.fx.interaction.TooltipHandlerFX; 065import org.jfree.chart.fx.interaction.ScrollHandlerFX; 066import org.jfree.chart.fx.interaction.PanHandlerFX; 067import org.jfree.chart.fx.interaction.MouseHandlerFX; 068import org.jfree.chart.fx.overlay.OverlayFX; 069import org.jfree.chart.plot.PlotRenderingInfo; 070import org.jfree.chart.util.Args; 071import org.jfree.fx.FXGraphics2D; 072import org.jfree.fx.FXHints; 073 074/** 075 * A canvas for displaying a {@link JFreeChart} in JavaFX. You can use the 076 * canvas directly to display charts, but usually the {@link ChartViewer} 077 * class (which embeds a canvas) is a better option as it provides additional 078 * features. 079 * <p> 080 * The canvas installs several default mouse handlers, if you don't like the 081 * behaviour provided by these you can retrieve the handler by ID and 082 * disable or remove it (the IDs are "tooltip", "scroll", "anchor", "pan" and 083 * "dispatch").</p> 084 * <p> 085 * The {@code FontSmoothingType} for the underlying {@code GraphicsContext} is 086 * set to {@code FontSmoothingType.LCD} as this gives better results on the 087 * systems we've tested on. You can modify this using 088 * {@code getGraphicsContext().setFontSmoothingType(yourValue)}.</p> 089 * 090 */ 091public class ChartCanvas extends Canvas implements ChartChangeListener, 092 OverlayChangeListener { 093 094 /** The chart being displayed in the canvas. */ 095 private JFreeChart chart; 096 097 /** 098 * The graphics drawing context (will be an instance of FXGraphics2D). 099 */ 100 private Graphics2D g2; 101 102 /** 103 * The anchor point (can be null) is usually updated to reflect the most 104 * recent mouse click and is used during chart rendering to update 105 * crosshairs belonging to the chart. 106 */ 107 private Point2D anchor; 108 109 /** The chart rendering info from the most recent drawing of the chart. */ 110 private ChartRenderingInfo info; 111 112 /** The tooltip object for the canvas (can be null). */ 113 private Tooltip tooltip; 114 115 /** 116 * A flag that controls whether or not tooltips will be generated from the 117 * chart as the mouse pointer moves over it. 118 */ 119 private boolean tooltipEnabled; 120 121 /** Storage for registered chart mouse listeners. */ 122 private transient List<ChartMouseListenerFX> chartMouseListeners; 123 124 /** The current live handler (can be null). */ 125 private MouseHandlerFX liveHandler; 126 127 /** 128 * The list of available live mouse handlers (can be empty but not null). 129 */ 130 private List<MouseHandlerFX> availableMouseHandlers; 131 132 /** The auxiliary mouse handlers (can be empty but not null). */ 133 private List<MouseHandlerFX> auxiliaryMouseHandlers; 134 135 private ObservableList<OverlayFX> overlays; 136 137 /** 138 * A flag that can be used to override the plot setting for domain (x) axis 139 * zooming. 140 */ 141 private boolean domainZoomable; 142 143 /** 144 * A flag that can be used to override the plot setting for range (y) axis 145 * zooming. 146 */ 147 private boolean rangeZoomable; 148 149 /** 150 * Creates a new canvas to display the supplied chart in JavaFX. If 151 * {@code chart} is {@code null}, a blank canvas will be displayed. 152 * 153 * @param chart the chart ({@code null} permitted). 154 */ 155 public ChartCanvas(JFreeChart chart) { 156 this.chart = chart; 157 if (this.chart != null) { 158 this.chart.addChangeListener(this); 159 } 160 this.tooltip = null; 161 this.tooltipEnabled = true; 162 this.chartMouseListeners = new ArrayList<>(); 163 164 widthProperty().addListener(e -> draw()); 165 heightProperty().addListener(e -> draw()); 166 // change the default font smoothing for better results 167 GraphicsContext gc = getGraphicsContext2D(); 168 gc.setFontSmoothingType(FontSmoothingType.LCD); 169 FXGraphics2D fxg2 = new FXGraphics2D(gc); 170 fxg2.setRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS, true); 171 fxg2.setZeroStrokeWidth(0.1); 172 fxg2.setRenderingHint( 173 RenderingHints.KEY_FRACTIONALMETRICS, 174 RenderingHints.VALUE_FRACTIONALMETRICS_ON); 175 this.g2 = fxg2; 176 this.liveHandler = null; 177 this.availableMouseHandlers = new ArrayList<>(); 178 179 this.availableMouseHandlers.add(new PanHandlerFX("pan", true, false, 180 false, false)); 181 182 this.auxiliaryMouseHandlers = new ArrayList<>(); 183 this.auxiliaryMouseHandlers.add(new TooltipHandlerFX("tooltip")); 184 this.auxiliaryMouseHandlers.add(new ScrollHandlerFX("scroll")); 185 this.domainZoomable = true; 186 this.rangeZoomable = true; 187 this.auxiliaryMouseHandlers.add(new AnchorHandlerFX("anchor")); 188 this.auxiliaryMouseHandlers.add(new DispatchHandlerFX("dispatch")); 189 190 this.overlays = FXCollections.observableArrayList(); 191 192 setOnMouseMoved(e -> handleMouseMoved(e)); 193 setOnMouseClicked(e -> handleMouseClicked(e)); 194 setOnMousePressed(e -> handleMousePressed(e)); 195 setOnMouseDragged(e -> handleMouseDragged(e)); 196 setOnMouseReleased(e -> handleMouseReleased(e)); 197 setOnScroll(e -> handleScroll(e)); 198 } 199 200 /** 201 * Returns the chart that is being displayed by this node. 202 * 203 * @return The chart (possibly {@code null}). 204 */ 205 public JFreeChart getChart() { 206 return this.chart; 207 } 208 209 /** 210 * Sets the chart to be displayed by this node. 211 * 212 * @param chart the chart ({@code null} permitted). 213 */ 214 public void setChart(JFreeChart chart) { 215 if (this.chart != null) { 216 this.chart.removeChangeListener(this); 217 } 218 this.chart = chart; 219 if (this.chart != null) { 220 this.chart.addChangeListener(this); 221 } 222 draw(); 223 } 224 225 /** 226 * Returns the flag that determines whether or not zooming is enabled for 227 * the domain axis. 228 * 229 * @return A boolean. 230 */ 231 public boolean isDomainZoomable() { 232 return this.domainZoomable; 233 } 234 235 /** 236 * Sets the flag that controls whether or not domain axis zooming is 237 * enabled. If the underlying plot does not support domain axis zooming, 238 * then setting this flag to {@code true} will have no effect. 239 * 240 * @param zoomable the new flag value. 241 */ 242 public void setDomainZoomable(boolean zoomable) { 243 this.domainZoomable = zoomable; 244 } 245 246 /** 247 * Returns the flag that determines whether or not zooming is enabled for 248 * the range axis. 249 * 250 * @return A boolean. 251 */ 252 public boolean isRangeZoomable() { 253 return this.rangeZoomable; 254 } 255 256 /** 257 * Sets the flag that controls whether or not range axis zooming is 258 * enabled. If the underlying plot does not support range axis zooming, 259 * then setting this flag to {@code true} will have no effect. 260 * 261 * @param zoomable the new flag value. 262 */ 263 public void setRangeZoomable(boolean zoomable) { 264 this.rangeZoomable = zoomable; 265 } 266 267 /** 268 * Returns the rendering info from the most recent drawing of the chart. 269 * 270 * @return The rendering info (possibly {@code null}). 271 */ 272 public ChartRenderingInfo getRenderingInfo() { 273 return this.info; 274 } 275 276 /** 277 * Returns the flag that controls whether or not tooltips are enabled. 278 * The default value is {@code true}. The {@link TooltipHandlerFX} 279 * class will only update the tooltip if this flag is set to 280 * {@code true}. 281 * 282 * @return The flag. 283 */ 284 public boolean isTooltipEnabled() { 285 return this.tooltipEnabled; 286 } 287 288 /** 289 * Sets the flag that controls whether or not tooltips are enabled. 290 * 291 * @param tooltipEnabled the new flag value. 292 */ 293 public void setTooltipEnabled(boolean tooltipEnabled) { 294 this.tooltipEnabled = tooltipEnabled; 295 } 296 297 /** 298 * Returns the anchor point. This is the last point on the canvas 299 * that the user clicked with the mouse, and is used during chart 300 * rendering to determine the position of crosshairs (if visible). 301 * 302 * @return The anchor point (possibly {@code null}). 303 */ 304 public Point2D getAnchor() { 305 return this.anchor; 306 } 307 308 /** 309 * Set the anchor point and forces a redraw of the chart (the anchor point 310 * is used to determine the position of the crosshairs on the chart, if 311 * they are visible). 312 * 313 * @param anchor the anchor ({@code null} permitted). 314 */ 315 public void setAnchor(Point2D anchor) { 316 this.anchor = anchor; 317 if (this.chart != null) { 318 this.chart.setNotify(true); // force a redraw 319 } 320 } 321 322 /** 323 * Add an overlay to the canvas. 324 * 325 * @param overlay the overlay ({@code null} not permitted). 326 */ 327 public void addOverlay(OverlayFX overlay) { 328 Args.nullNotPermitted(overlay, "overlay"); 329 this.overlays.add(overlay); 330 overlay.addChangeListener(this); 331 draw(); 332 } 333 334 /** 335 * Removes an overlay from the canvas. 336 * 337 * @param overlay the overlay to remove ({@code null} not permitted). 338 */ 339 public void removeOverlay(OverlayFX overlay) { 340 Args.nullNotPermitted(overlay, "overlay"); 341 boolean removed = this.overlays.remove(overlay); 342 if (removed) { 343 overlay.removeChangeListener(this); 344 draw(); 345 } 346 } 347 348 /** 349 * Handles a change to an overlay by repainting the chart canvas. 350 * 351 * @param event the event. 352 */ 353 @Override 354 public void overlayChanged(OverlayChangeEvent event) { 355 draw(); 356 } 357 358 /** 359 * Returns a (newly created) list containing the listeners currently 360 * registered with the canvas. 361 * 362 * @return A list of listeners (possibly empty but never {@code null}). 363 */ 364 public List<ChartMouseListenerFX> getChartMouseListeners() { 365 return new ArrayList<>(this.chartMouseListeners); 366 } 367 368 /** 369 * Registers a listener to receive {@link ChartMouseEvent} notifications. 370 * 371 * @param listener the listener ({@code null} not permitted). 372 */ 373 public void addChartMouseListener(ChartMouseListenerFX listener) { 374 Args.nullNotPermitted(listener, "listener"); 375 this.chartMouseListeners.add(listener); 376 } 377 378 /** 379 * Removes a listener from the list of objects listening for chart mouse 380 * events. 381 * 382 * @param listener the listener. 383 */ 384 public void removeChartMouseListener(ChartMouseListenerFX listener) { 385 this.chartMouseListeners.remove(listener); 386 } 387 388 /** 389 * Returns the mouse handler with the specified ID, or {@code null} if 390 * there is no handler with that ID. This method will look for handlers 391 * in both the regular and auxiliary handler lists. 392 * 393 * @param id the ID ({@code null} not permitted). 394 * 395 * @return The handler with the specified ID 396 */ 397 public MouseHandlerFX getMouseHandler(String id) { 398 for (MouseHandlerFX h: this.availableMouseHandlers) { 399 if (h.getID().equals(id)) { 400 return h; 401 } 402 } 403 for (MouseHandlerFX h: this.auxiliaryMouseHandlers) { 404 if (h.getID().equals(id)) { 405 return h; 406 } 407 } 408 return null; 409 } 410 411 /** 412 * Adds a mouse handler to the list of available handlers (handlers that 413 * are candidates to take the position of live handler). The handler must 414 * have an ID that uniquely identifies it amongst the handlers registered 415 * with this canvas. 416 * 417 * @param handler the handler ({@code null} not permitted). 418 */ 419 public void addMouseHandler(MouseHandlerFX handler) { 420 if (!hasUniqueID(handler)) { 421 throw new IllegalArgumentException( 422 "There is already a handler with that ID (" 423 + handler.getID() + ")."); 424 } 425 this.availableMouseHandlers.add(handler); 426 } 427 428 /** 429 * Removes a handler from the list of available handlers. 430 * 431 * @param handler the handler ({@code null} not permitted). 432 */ 433 public void removeMouseHandler(MouseHandlerFX handler) { 434 this.availableMouseHandlers.remove(handler); 435 } 436 437 /** 438 * Adds a handler to the list of auxiliary handlers. The handler must 439 * have an ID that uniquely identifies it amongst the handlers registered 440 * with this canvas. 441 * 442 * @param handler the handler ({@code null} not permitted). 443 */ 444 public void addAuxiliaryMouseHandler(MouseHandlerFX handler) { 445 if (!hasUniqueID(handler)) { 446 throw new IllegalArgumentException( 447 "There is already a handler with that ID (" 448 + handler.getID() + ")."); 449 } 450 this.auxiliaryMouseHandlers.add(handler); 451 } 452 453 /** 454 * Removes a handler from the list of auxiliary handlers. 455 * 456 * @param handler the handler ({@code null} not permitted). 457 */ 458 public void removeAuxiliaryMouseHandler(MouseHandlerFX handler) { 459 this.auxiliaryMouseHandlers.remove(handler); 460 } 461 462 /** 463 * Validates that the specified handler has an ID that uniquely identifies 464 * it amongst the existing handlers for this canvas. 465 * 466 * @param handler the handler ({@code null} not permitted). 467 * 468 * @return A boolean. 469 */ 470 private boolean hasUniqueID(MouseHandlerFX handler) { 471 for (MouseHandlerFX h: this.availableMouseHandlers) { 472 if (handler.getID().equals(h.getID())) { 473 return false; 474 } 475 } 476 for (MouseHandlerFX h: this.auxiliaryMouseHandlers) { 477 if (handler.getID().equals(h.getID())) { 478 return false; 479 } 480 } 481 return true; 482 } 483 484 /** 485 * Clears the current live handler. This method is intended for use by the 486 * handlers themselves, you should not call it directly. 487 */ 488 public void clearLiveHandler() { 489 this.liveHandler = null; 490 } 491 492 /** 493 * Draws the content of the canvas and updates the 494 * {@code renderingInfo} attribute with the latest rendering 495 * information. 496 */ 497 public final void draw() { 498 GraphicsContext ctx = getGraphicsContext2D(); 499 ctx.save(); 500 double width = getWidth(); 501 double height = getHeight(); 502 if (width > 0 && height > 0) { 503 ctx.clearRect(0, 0, width, height); 504 this.info = new ChartRenderingInfo(); 505 if (this.chart != null) { 506 this.chart.draw(this.g2, new Rectangle((int) width, 507 (int) height), this.anchor, this.info); 508 } 509 } 510 ctx.restore(); 511 for (OverlayFX overlay : this.overlays) { 512 overlay.paintOverlay(g2, this); 513 } 514 this.anchor = null; 515 } 516 517 /** 518 * Returns the data area (the area inside the axes) for the plot or subplot. 519 * 520 * @param point the selection point (for subplot selection). 521 * 522 * @return The data area. 523 */ 524 public Rectangle2D findDataArea(Point2D point) { 525 PlotRenderingInfo plotInfo = this.info.getPlotInfo(); 526 Rectangle2D result; 527 if (plotInfo.getSubplotCount() == 0) { 528 result = plotInfo.getDataArea(); 529 } else { 530 int subplotIndex = plotInfo.getSubplotIndex(point); 531 if (subplotIndex == -1) { 532 return null; 533 } 534 result = plotInfo.getSubplotInfo(subplotIndex).getDataArea(); 535 } 536 return result; 537 } 538 539 /** 540 * Return {@code true} to indicate the canvas is resizable. 541 * 542 * @return {@code true}. 543 */ 544 @Override 545 public boolean isResizable() { 546 return true; 547 } 548 549 /** 550 * Sets the tooltip text, with the (x, y) location being used for the 551 * anchor. If the text is {@code null}, no tooltip will be displayed. 552 * This method is intended for calling by the {@link TooltipHandlerFX} 553 * class, you won't normally call it directly. 554 * 555 * @param text the text ({@code null} permitted). 556 * @param x the x-coordinate of the mouse pointer. 557 * @param y the y-coordinate of the mouse pointer. 558 */ 559 public void setTooltip(String text, double x, double y) { 560 if (text != null) { 561 if (this.tooltip == null) { 562 this.tooltip = new Tooltip(text); 563 Tooltip.install(this, this.tooltip); 564 } else { 565 this.tooltip.setText(text); 566 this.tooltip.setAnchorX(x); 567 this.tooltip.setAnchorY(y); 568 } 569 } else { 570 Tooltip.uninstall(this, this.tooltip); 571 this.tooltip = null; 572 } 573 } 574 575 /** 576 * Handles a mouse pressed event by (1) selecting a live handler if one 577 * is not already selected, (2) passing the event to the live handler if 578 * there is one, and (3) passing the event to all enabled auxiliary 579 * handlers. 580 * 581 * @param e the mouse event. 582 */ 583 private void handleMousePressed(MouseEvent e) { 584 if (this.liveHandler == null) { 585 for (MouseHandlerFX handler: this.availableMouseHandlers) { 586 if (handler.isEnabled() && handler.hasMatchingModifiers(e)) { 587 this.liveHandler = handler; 588 } 589 } 590 } 591 592 if (this.liveHandler != null) { 593 this.liveHandler.handleMousePressed(this, e); 594 } 595 596 // pass on the event to the auxiliary handlers 597 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 598 if (handler.isEnabled()) { 599 handler.handleMousePressed(this, e); 600 } 601 } 602 } 603 604 /** 605 * Handles a mouse moved event by passing it on to the registered handlers. 606 * 607 * @param e the mouse event. 608 */ 609 private void handleMouseMoved(MouseEvent e) { 610 if (this.liveHandler != null && this.liveHandler.isEnabled()) { 611 this.liveHandler.handleMouseMoved(this, e); 612 } 613 614 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 615 if (handler.isEnabled()) { 616 handler.handleMouseMoved(this, e); 617 } 618 } 619 } 620 621 /** 622 * Handles a mouse dragged event by passing it on to the registered 623 * handlers. 624 * 625 * @param e the mouse event. 626 */ 627 private void handleMouseDragged(MouseEvent e) { 628 if (this.liveHandler != null && this.liveHandler.isEnabled()) { 629 this.liveHandler.handleMouseDragged(this, e); 630 } 631 632 // pass on the event to the auxiliary handlers 633 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 634 if (handler.isEnabled()) { 635 handler.handleMouseDragged(this, e); 636 } 637 } 638 } 639 640 /** 641 * Handles a mouse released event by passing it on to the registered 642 * handlers. 643 * 644 * @param e the mouse event. 645 */ 646 private void handleMouseReleased(MouseEvent e) { 647 if (this.liveHandler != null && this.liveHandler.isEnabled()) { 648 this.liveHandler.handleMouseReleased(this, e); 649 } 650 651 // pass on the event to the auxiliary handlers 652 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 653 if (handler.isEnabled()) { 654 handler.handleMouseReleased(this, e); 655 } 656 } 657 } 658 659 /** 660 * Handles a mouse released event by passing it on to the registered 661 * handlers. 662 * 663 * @param e the mouse event. 664 */ 665 private void handleMouseClicked(MouseEvent e) { 666 if (this.liveHandler != null && this.liveHandler.isEnabled()) { 667 this.liveHandler.handleMouseClicked(this, e); 668 } 669 670 // pass on the event to the auxiliary handlers 671 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 672 if (handler.isEnabled()) { 673 handler.handleMouseClicked(this, e); 674 } 675 } 676 } 677 678 /** 679 * Handles a scroll event by passing it on to the registered handlers. 680 * 681 * @param e the scroll event. 682 */ 683 protected void handleScroll(ScrollEvent e) { 684 if (this.liveHandler != null && this.liveHandler.isEnabled()) { 685 this.liveHandler.handleScroll(this, e); 686 } 687 for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) { 688 if (handler.isEnabled()) { 689 handler.handleScroll(this, e); 690 } 691 } 692 } 693 694 /** 695 * Receives a notification from the chart that it has been changed and 696 * responds by redrawing the chart entirely. 697 * 698 * @param event event information. 699 */ 700 @Override 701 public void chartChanged(ChartChangeEvent event) { 702 draw(); 703 } 704 705} 706