/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2014, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* ----------------
* ChartCanvas.java
* ----------------
* (C) Copyright 2014, by Object Refinery Limited and Contributors.
*
* Original Author: David Gilbert (for Object Refinery Limited);
* Contributor(s): -;
*
* Changes:
* --------
* 25-Jun-2014 : Version 1 (DG);
* 19-Jul-2014 : Add clearRect() call for each draw (DG);
*
*/
package org.jfree.chart.fx;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartRenderingInfo;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.fx.interaction.AnchorHandlerFX;
import org.jfree.chart.fx.interaction.DispatchHandlerFX;
import org.jfree.chart.fx.interaction.ChartMouseEventFX;
import org.jfree.chart.fx.interaction.ChartMouseListenerFX;
import org.jfree.chart.fx.interaction.TooltipHandlerFX;
import org.jfree.chart.fx.interaction.ScrollHandlerFX;
import org.jfree.chart.fx.interaction.PanHandlerFX;
import org.jfree.chart.fx.interaction.MouseHandlerFX;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.util.ParamChecks;
/**
* A canvas for displaying a {@link JFreeChart} in JavaFX. You can use the
* canvas directly to display charts, but usually the {@link ChartViewer}
* class (which embeds a canvas) is a better option.
* <p>
* The canvas installs several default mouse handlers, if you don't like the
* behaviour provided by these you can retrieve the handler by ID and
* disable or remove it (the IDs are "tooltip", "scroll", "anchor", "pan" and
* "dispatch").
*
* <p>THE API FOR THIS CLASS IS SUBJECT TO CHANGE IN FUTURE RELEASES. This is
* so that we can incorporate feedback on the (new) JavaFX support in
* JFreeChart.</p>
*
* @since 1.0.18
*/
public class ChartCanvas extends Canvas implements ChartChangeListener {
/** The chart being displayed in the canvas (never null). */
private JFreeChart chart;
/**
* The graphics drawing context (will be an instance of FXGraphics2D).
*/
private Graphics2D g2;
/**
* The anchor point (can be null) is usually updated to reflect the most
* recent mouse click and is used during chart rendering to update
* crosshairs belonging to the chart.
*/
private Point2D anchor;
/** The chart rendering info from the most recent drawing of the chart. */
private ChartRenderingInfo info;
/** The tooltip object for the canvas (can be null). */
private Tooltip tooltip;
/**
* A flag that controls whether or not tooltips will be generated from the
* chart as the mouse pointer moves over it.
*/
private boolean tooltipEnabled;
/** Storage for registered chart mouse listeners. */
private transient List<ChartMouseListenerFX> chartMouseListeners;
/** The current live handler (can be null). */
private MouseHandlerFX liveHandler;
/**
* The list of available live mouse handlers (can be empty but not null).
*/
private List<MouseHandlerFX> availableMouseHandlers;
/** The auxiliary mouse handlers (can be empty but not null). */
private List<MouseHandlerFX> auxiliaryMouseHandlers;
/**
* Creates a new canvas to display the supplied chart in JavaFX.
*
* @param chart the chart ({@code null} not permitted).
*/
public ChartCanvas(JFreeChart chart) {
ParamChecks.nullNotPermitted(chart, "chart");
this.chart = chart;
this.chart.addChangeListener(this);
this.tooltip = null;
this.tooltipEnabled = true;
this.chartMouseListeners = new ArrayList<ChartMouseListenerFX>();
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
this.g2 = new FXGraphics2D(getGraphicsContext2D());
this.liveHandler = null;
this.availableMouseHandlers = new ArrayList<MouseHandlerFX>();
this.availableMouseHandlers.add(new PanHandlerFX("pan", true, false,
false, false));
this.auxiliaryMouseHandlers = new ArrayList<MouseHandlerFX>();
this.auxiliaryMouseHandlers.add(new TooltipHandlerFX("tooltip"));
this.auxiliaryMouseHandlers.add(new ScrollHandlerFX("scroll"));
this.auxiliaryMouseHandlers.add(new AnchorHandlerFX("anchor"));
this.auxiliaryMouseHandlers.add(new DispatchHandlerFX("dispatch"));
setOnMouseMoved((MouseEvent e) -> { handleMouseMoved(e); });
setOnMouseClicked((MouseEvent e) -> { handleMouseClicked(e); });
setOnMousePressed((MouseEvent e) -> { handleMousePressed(e); });
setOnMouseDragged((MouseEvent e) -> { handleMouseDragged(e); });
setOnMouseReleased((MouseEvent e) -> { handleMouseReleased(e); });
setOnScroll((ScrollEvent event) -> { handleScroll(event); });
}
/**
* Returns the chart that is being displayed by this node.
*
* @return The chart (never {@code null}).
*/
public JFreeChart getChart() {
return this.chart;
}
/**
* Sets the chart to be displayed by this node.
*
* @param chart the chart ({@code null} not permitted).
*/
public void setChart(JFreeChart chart) {
ParamChecks.nullNotPermitted(chart, "chart");
this.chart.removeChangeListener(this);
this.chart = chart;
this.chart.addChangeListener(this);
draw();
}
/**
* Returns the rendering info from the most recent drawing of the chart.
*
* @return The rendering info (possibly {@code null}).
*/
public ChartRenderingInfo getRenderingInfo() {
return this.info;
}
/**
* Returns the flag that controls whether or not tooltips are enabled.
* The default value is {@code true}. The {@link TooltipHandlerFX}
* class will only update the tooltip if this flag is set to
* {@code true}.
*
* @return The flag.
*/
public boolean isTooltipEnabled() {
return this.tooltipEnabled;
}
/**
* Sets the flag that controls whether or not tooltips are enabled.
*
* @param tooltipEnabled the new flag value.
*/
public void setTooltipEnabled(boolean tooltipEnabled) {
this.tooltipEnabled = tooltipEnabled;
}
/**
* Set the anchor point and forces a redraw of the chart (the anchor point
* is used to determine the position of the crosshairs on the chart, if
* they are visible).
*
* @param anchor the anchor ({@code null} permitted).
*/
public void setAnchor(Point2D anchor) {
this.anchor = anchor;
this.chart.setNotify(true); // force a redraw
}
/**
* Registers a listener to receive {@link ChartMouseEvent} notifications.
*
* @param listener the listener ({@code null} not permitted).
*/
public void addChartMouseListener(ChartMouseListenerFX listener) {
ParamChecks.nullNotPermitted(listener, "listener");
this.chartMouseListeners.add(listener);
}
/**
* Removes a listener from the list of objects listening for chart mouse
* events.
*
* @param listener the listener.
*/
public void removeChartMouseListener(ChartMouseListenerFX listener) {
this.chartMouseListeners.remove(listener);
}
/**
* Returns the mouse handler with the specified ID, or {@code null} if
* there is no handler with that ID. This method will look for handlers
* in both the regular and auxiliary handler lists.
*
* @param id the ID ({@code null} not permitted).
*
* @return The handler with the specified ID
*/
public MouseHandlerFX getMouseHandler(String id) {
for (MouseHandlerFX h: this.availableMouseHandlers) {
if (h.getID().equals(id)) {
return h;
}
}
for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
if (h.getID().equals(id)) {
return h;
}
}
return null;
}
/**
* Adds a mouse handler to the list of available handlers (handlers that
* are candidates to take the position of live handler). The handler must
* have an ID that uniquely identifies it amongst the handlers registered
* with this canvas.
*
* @param handler the handler ({@code null} not permitted).
*/
public void addMouseHandler(MouseHandlerFX handler) {
if (!this.hasUniqueID(handler)) {
throw new IllegalArgumentException(
"There is already a handler with that ID ("
+ handler.getID() + ").");
}
this.availableMouseHandlers.add(handler);
}
/**
* Removes a handler from the list of available handlers.
*
* @param handler the handler ({@code null} not permitted).
*/
public void removeMouseHandler(MouseHandlerFX handler) {
this.availableMouseHandlers.remove(handler);
}
/**
* Validates that the specified handler has an ID that uniquely identifies
* it amongst the existing handlers for this canvas.
*
* @param handler the handler ({@code null} not permitted).
*
* @return A boolean.
*/
private boolean hasUniqueID(MouseHandlerFX handler) {
for (MouseHandlerFX h: this.availableMouseHandlers) {
if (handler.getID().equals(h.getID())) {
return false;
}
}
for (MouseHandlerFX h: this.auxiliaryMouseHandlers) {
if (handler.getID().equals(h.getID())) {
return false;
}
}
return true;
}
/**
* Clears the current live handler. This method is intended for use by the
* handlers themselves, you should not call it directly.
*/
public void clearLiveHandler() {
this.liveHandler = null;
}
/**
* Draws the content of the canvas and updates the
* {@code renderingInfo} attribute with the latest rendering
* information.
*/
public final void draw() {
GraphicsContext ctx = getGraphicsContext2D();
ctx.save();
double width = getWidth();
double height = getHeight();
if (width > 0 && height > 0) {
ctx.clearRect(0, 0, width, height);
this.info = new ChartRenderingInfo();
this.chart.draw(this.g2, new Rectangle((int) width, (int) height),
this.anchor, this.info);
}
ctx.restore();
this.anchor = null;
}
/**
* Returns the data area (the area inside the axes) for the plot or subplot.
*
* @param point the selection point (for subplot selection).
*
* @return The data area.
*/
public Rectangle2D findDataArea(Point2D point) {
PlotRenderingInfo plotInfo = this.info.getPlotInfo();
Rectangle2D result;
if (plotInfo.getSubplotCount() == 0) {
result = plotInfo.getDataArea();
}
else {
int subplotIndex = plotInfo.getSubplotIndex(point);
if (subplotIndex == -1) {
return null;
}
result = plotInfo.getSubplotInfo(subplotIndex).getDataArea();
}
return result;
}
/**
* Return {@code true} to indicate the canvas is resizable.
*
* @return {@code true}.
*/
@Override
public boolean isResizable() {
return true;
}
/**
* Sets the tooltip text, with the (x, y) location being used for the
* anchor. If the text is {@code null}, no tooltip will be displayed.
* This method is intended for calling by the {@link TooltipHandlerFX}
* class, you won't normally call it directly.
*
* @param text the text ({@code null} permitted).
* @param x the x-coordinate of the mouse pointer.
* @param y the y-coordinate of the mouse pointer.
*/
public void setTooltip(String text, double x, double y) {
if (text != null) {
if (this.tooltip == null) {
this.tooltip = new Tooltip(text);
Tooltip.install(this, this.tooltip);
} else {
this.tooltip.setText(text);
this.tooltip.setAnchorX(x);
this.tooltip.setAnchorY(y);
}
} else {
Tooltip.uninstall(this, this.tooltip);
this.tooltip = null;
}
}
/**
* Handles a mouse pressed event by (1) selecting a live handler if one
* is not already selected, (2) passing the event to the live handler if
* there is one, and (3) passing the event to all enabled auxiliary
* handlers.
*
* @param e the mouse event.
*/
private void handleMousePressed(MouseEvent e) {
if (this.liveHandler == null) {
for (MouseHandlerFX handler: this.availableMouseHandlers) {
if (handler.isEnabled() && handler.hasMatchingModifiers(e)) {
this.liveHandler = handler;
}
}
}
if (this.liveHandler != null) {
this.liveHandler.handleMousePressed(this, e);
}
// pass on the event to the auxiliary handlers
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleMousePressed(this, e);
}
}
}
/**
* Handles a mouse moved event by passing it on to the registered handlers.
*
* @param e the mouse event.
*/
private void handleMouseMoved(MouseEvent e) {
if (this.liveHandler != null && this.liveHandler.isEnabled()) {
this.liveHandler.handleMouseMoved(this, e);
}
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleMouseMoved(this, e);
}
}
}
/**
* Handles a mouse dragged event by passing it on to the registered
* handlers.
*
* @param e the mouse event.
*/
private void handleMouseDragged(MouseEvent e) {
if (this.liveHandler != null && this.liveHandler.isEnabled()) {
this.liveHandler.handleMouseDragged(this, e);
}
// pass on the event to the auxiliary handlers
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleMouseDragged(this, e);
}
}
}
/**
* Handles a mouse released event by passing it on to the registered
* handlers.
*
* @param e the mouse event.
*/
private void handleMouseReleased(MouseEvent e) {
if (this.liveHandler != null && this.liveHandler.isEnabled()) {
this.liveHandler.handleMouseReleased(this, e);
}
// pass on the event to the auxiliary handlers
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleMouseReleased(this, e);
}
}
}
/**
* Handles a mouse released event by passing it on to the registered
* handlers.
*
* @param e the mouse event.
*/
private void handleMouseClicked(MouseEvent e) {
if (this.liveHandler != null && this.liveHandler.isEnabled()) {
this.liveHandler.handleMouseClicked(this, e);
}
// pass on the event to the auxiliary handlers
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleMouseClicked(this, e);
}
}
}
/**
* Handles a scroll event by passing it on to the registered handlers.
*
* @param e the scroll event.
*/
protected void handleScroll(ScrollEvent e) {
if (this.liveHandler != null && this.liveHandler.isEnabled()) {
this.liveHandler.handleScroll(this, e);
}
for (MouseHandlerFX handler: this.auxiliaryMouseHandlers) {
if (handler.isEnabled()) {
handler.handleScroll(this, e);
}
}
}
/**
* Receives a notification from the chart that it has been changed and
* responds by redrawing the chart entirely.
*
* @param event event information.
*/
@Override
public void chartChanged(ChartChangeEvent event) {
draw();
}
public void dispatchMouseMovedEvent(Point2D point, MouseEvent e) {
double x = point.getX();
double y = point.getY();
ChartEntity entity = this.info.getEntityCollection().getEntity(x, y);
ChartMouseEventFX event = new ChartMouseEventFX(this.chart, e, entity);
for (ChartMouseListenerFX listener : this.chartMouseListeners) {
listener.chartMouseMoved(event);
}
}
public void dispatchMouseClickedEvent(Point2D point, MouseEvent e) {
double x = point.getX();
double y = point.getY();
ChartEntity entity = this.info.getEntityCollection().getEntity(x, y);
ChartMouseEventFX event = new ChartMouseEventFX(this.chart, e, entity);
for (ChartMouseListenerFX listener : this.chartMouseListeners) {
listener.chartMouseClicked(event);
}
}
}