An example is presented here, it won't do exactly what you want, but it should be adaptable to your case, as I understand it.
As you move the mouse around the chart, the crosshairs track the current mouse position, and the closest data node is highlighted.

The app places the lines for a crosshair in a Pane above the chart. When the mouse moves, a comparator is invoked which determines the nearest node to the current crosshair position, which is then highlighted. When the mouse leaves the chart area, the crosshairs are removed and the highlight is removed from the highlighted node.
To calculate distances accurately, all coordinates need to be translated to a common co-ordinate system. In the example, I translate to scene coordinates and use those.
NearestPointApp.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class NearestPointApp extends Application {
@Override public void start(Stage stage) {
final InteractiveChartPane chartPane = new InteractiveChartPane(
createSeries()
);
Scene scene = new Scene(chartPane);
stage.setScene(scene);
stage.show();
chartPane.enableInteractivityInScene(scene);
}
private static XYChart.Series<Number, Number> createSeries() {
XYChart.Series<Number, Number> series = new XYChart.Series<>();
series.getData().add(new XYChart.Data<>(1, 23));
series.getData().add(new XYChart.Data<>(2, 14));
series.getData().add(new XYChart.Data<>(3, 15));
series.getData().add(new XYChart.Data<>(4, 24));
series.getData().add(new XYChart.Data<>(5, 34));
series.getData().add(new XYChart.Data<>(6, 36));
series.getData().add(new XYChart.Data<>(7, 22));
series.getData().add(new XYChart.Data<>(8, 45));
series.getData().add(new XYChart.Data<>(9, 43));
series.getData().add(new XYChart.Data<>(10, 17));
series.getData().add(new XYChart.Data<>(11, 29));
series.getData().add(new XYChart.Data<>(12, 25));
return series;
}
public static void main(String[] args) {
launch(args);
}
}
InteractiveChartPane.java
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.robot.Robot;
import javafx.scene.shape.Line;
public class InteractiveChartPane extends Pane {
// crosshair display in chart.
private final Line horizontal = createLine();
private final Line vertical = createLine();
private final HighlightManager highlightManager = new HighlightManager();
private final LineChart<Number, Number> chart = new LineChart<>(
makeSimpleAxis(), makeSimpleAxis()
);
public InteractiveChartPane(XYChart.Series<Number, Number> series) {
chart.getData().add(series);
chart.setLegendVisible(false);
getChildren().setAll(
chart,
horizontal,
vertical
);
}
public void enableInteractivityInScene(Scene scene) {
// size chart to scene,
// not sure why this doesn't happen automatically,
// but probably the charts have some
// default preferred size they like to stick to.
chart.prefWidthProperty().bind(scene.widthProperty());
chart.prefHeightProperty().bind(scene.heightProperty());
// hide the cursor when in the chart
// as the crosshair overlay will indicate mouse position.
Node chartArea = chart.lookup(".chart-plot-background");
chartArea.setCursor(Cursor.NONE);
// show crosshair, highlight chart and closest node
// when the mouse is in the chart.
chart.setOnMouseMoved(e ->
handleMovementInChart(
new Point2D(e.getSceneX(), e.getSceneY())
)
);
// find the initial mouse position and handle chart highlighting for it.
Point2D mousePositionInScene = findCurrentMousePositionInScene(chart);
if (mousePositionInScene != null) {
handleMovementInChart(mousePositionInScene);
}
}
private void handleMovementInChart(
Point2D mousePositionInScene
) {
Scene scene = getScene();
XYChart.Series<Number, Number> series = chart.getData().get(0);
Node chartArea = lookup(".chart-plot-background");
Bounds chartBoundsInScene = chartArea.localToScene(chartArea.getBoundsInLocal());
if (chartBoundsInScene.contains(mousePositionInScene)) {
scene.setCursor(Cursor.NONE);
chartArea.setStyle("-fx-background-color: azure;");
horizontal.setVisible(true);
vertical.setVisible(true);
horizontal.setStartX(chartBoundsInScene.getMinX());
horizontal.setEndX(chartBoundsInScene.getMaxX());
horizontal.setStartY(mousePositionInScene.getY());
horizontal.setEndY(mousePositionInScene.getY());
vertical.setStartX(mousePositionInScene.getX());
vertical.setEndX(mousePositionInScene.getX());
vertical.setStartY(chartBoundsInScene.getMinY());
vertical.setEndY(chartBoundsInScene.getMaxY());
// highlight the closest data node to the mouse.
series.getData().stream()
.min(new DistanceComparator(mousePositionInScene))
.ifPresent(closestData ->
highlightManager.setHighlightedNode(
closestData.getNode()
)
);
} else { // mouse is not in the chart area, remove interactive guides.
scene.setCursor(Cursor.DEFAULT);
chartArea.setStyle(null);
horizontal.setVisible(false);
vertical.setVisible(false);
highlightManager.setHighlightedNode(null);
}
}
private static Point2D findCurrentMousePositionInScene(LineChart<Number, Number> chart) {
Robot robot = new Robot();
Point2D mousePositionInScreen = robot.getMousePosition();
Point2D mousePositionInLocal = chart.screenToLocal(mousePositionInScreen);
if (mousePositionInLocal == null) {
return null;
}
return chart.localToScene(mousePositionInLocal);
}
private static Line createLine() {
Line horizontal = new Line();
horizontal.setStroke(Color.PLUM);
horizontal.setMouseTransparent(true);
return horizontal;
}
private static NumberAxis makeSimpleAxis() {
final NumberAxis xAxis = new NumberAxis();
xAxis.setTickMarkVisible(false);
xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickCount(0);
return xAxis;
}
}
DistanceComparator.java
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.chart.XYChart;
import java.util.Comparator;
// compares data nodes based on the distances of their center from a pivot point in the scene.
public record DistanceComparator(Point2D pivotInScene) implements Comparator<XYChart.Data<Number, Number>> {
@Override
public int compare(XYChart.Data<Number, Number> o1, XYChart.Data<Number, Number> o2) {
Node o1Node = o1.getNode();
Node o2Node = o2.getNode();
Bounds o1Bounds = o1Node.localToScene(o1Node.getBoundsInLocal());
Bounds o2Bounds = o2Node.localToScene(o2Node.getBoundsInLocal());
Point2D o1Center = new Point2D(o1Bounds.getCenterX(), o1Bounds.getCenterY());
Point2D o2Center = new Point2D(o2Bounds.getCenterX(), o2Bounds.getCenterY());
double o1Dist = pivotInScene.distance(o1Center);
double o2Dist = pivotInScene.distance(o2Center);
return Double.compare(o1Dist, o2Dist);
}
}
HighlightManager.java
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.effect.Glow;
import javafx.scene.paint.Color;
// utility class for highlighting a node in a chart.
class HighlightManager {
// highlighted data node in chart.
private final ObjectProperty<Node> highlightedNode = new SimpleObjectProperty<>();
private final Effect highlightEffect = createHighlightEffect();
public HighlightManager() {
highlightedNode.addListener((observable, oldNode, newNode) -> {
removeHighlight(oldNode);
applyHighlight(newNode);
});
}
public void setHighlightedNode(Node highlightedNode) {
this.highlightedNode.set(highlightedNode);
}
private static Effect createHighlightEffect() {
DropShadow outerShadow = new DropShadow(15, Color.GOLD);
Glow glow = new Glow();
glow.setInput(outerShadow);
return glow;
}
private void applyHighlight(Node node) {
if (node == null) {
return;
}
node.setStyle("-fx-background-color: CHART_COLOR_1, yellow;");
node.setEffect(highlightEffect);
}
private void removeHighlight(Node node) {
if (node == null) {
return;
}
node.setStyle(null);
node.setEffect(null);
}
}
Related Questions