Tooltip on Live LineChart

时光总嘲笑我的痴心妄想 提交于 2019-11-29 12:53:05
James_D

First, use a regular AreaChart instead of the anonymous subclass you are using (i.e. don't override the dataAddedItem(...) method). That method creates a default node to display for the data point if none already exists (this is a huge violation of separating data from presentation, imho, but there's nothing we can do about that...); you obviously need a graphic there to attach the tooltip to.

Once the data point has a node, you don't need to listen to the changes, so in your addDataToSeries() method, remove the listener and just replace it with

        Tooltip t = new Tooltip(data.getYValue().toString() + '\n' + data.getXValue());
        Tooltip.install(data.getNode(), t);

Or, just create your own graphic, attach a tooltip to it, and pass it to data.setNode(...);.

You will still have a general usability problem; I don't see how the user is going to hover over a data point in the chart when everything is flying by at 5 units per second. And even if they could, by the time the tooltip appeared the points would have moved, so the values would be incorrect...

Update:

Just for fun, I tried this:

    ObjectProperty<Point2D> mouseLocationInScene = new SimpleObjectProperty<>();

    Tooltip tooltip = new Tooltip();

    sc.addEventHandler(MouseEvent.MOUSE_MOVED, evt -> {
        if (! tooltip.isShowing()) {
            mouseLocationInScene.set(new Point2D(evt.getSceneX(), evt.getSceneY()));
        }
    });

    tooltip.textProperty().bind(Bindings.createStringBinding(() -> {
        if (mouseLocationInScene.isNull().get()) {
            return "" ;
        }
        double xInXAxis = xAxis.sceneToLocal(mouseLocationInScene.get()).getX() ;
        double x = xAxis.getValueForDisplay(xInXAxis).doubleValue();
        double yInYAxis = yAxis.sceneToLocal(mouseLocationInScene.get()).getY() ;
        double y = yAxis.getValueForDisplay(yInYAxis).doubleValue() ;
        return String.format("[%.3f, %.3f]", x, y);
    }, mouseLocationInScene, xAxis.lowerBoundProperty(), xAxis.upperBoundProperty(),
    yAxis.lowerBoundProperty(), yAxis.upperBoundProperty()));

    Tooltip.install(sc, tooltip);

This sets a tooltip on the chart that updates both as you move the mouse and as the chart scrolls below. This combines ideas from this question and this one.

kleopatra

Answering an implicit part of the question: how to install a tooltip that's showing the current x/y values if there are no symbols, that is the data has no node? And only for a static chart (or one that's reasonable slow changing such that the location/value of the tooltip wouldn't make sense while showing)

There are several problems to solve

  • the tooltip has to be installed on the node of the series: same trick as with installing on the node of the data - done in a listener to its nodeProperty, only then can we be certain that the node is created
  • it's text has to be updated when showing: done onShowing (note a bug in the setter, so we need to use the property directly)
  • it's text should be related to the mouse location of its triggering event: as there doesn't seem to be any api that allows access to the triggering event we have to keep track of that location ourselves, below in a mouse-moved that stores the current mouse screen location in the properties of the tooltip
  • coordinate transformation from mouse-coordinates to "world" chart-coordinates

An example:

public class ToolTipOnChartSeries extends Application {

    private static final Object MOUSE_TRIGGER_LOCATION = "tooltip-last-location";

    private ObservableList<XYChart.Series<String, Double>> getChartData() {
        double javaValue = 17.56;
        ObservableList<XYChart.Series<String, Double>> answer = FXCollections.observableArrayList();
        Series<String, Double> java = new Series<String, Double>();
        java.setName("java");
        Tooltip t = new Tooltip();
        t.setOnShowing(e -> {
            Point2D screen = (Point2D) t.getProperties().get(MOUSE_TRIGGER_LOCATION);
            if (screen == null) return;
            XYChart chart = java.getChart();
            double localX = chart.getXAxis().screenToLocal(screen).getX();
            double localY = chart.getYAxis().screenToLocal(screen).getY();
            Object xValue = chart.getXAxis().getValueForDisplay(localX);
            Object yValue = chart.getYAxis().getValueForDisplay(localY);
            t.textProperty().set("x/y: " + t.getX() + " / " + t.getY() 
                    + "\n localX " + localX + "/" + xValue 
                    + "\n localY " + localY + "/" + yValue 

                    );
        });
        java.nodeProperty().addListener(new ChangeListener<Node>()
        { 
            @Override
            public void changed(ObservableValue<? extends Node> arg0, Node arg1,
                Node node)
            {
                Tooltip.install(node, t);
                node.setOnMouseMoved(e -> {
                    Point2D screen = new Point2D(e.getScreenX(), e.getScreenY());
                    t.getProperties().put(MOUSE_TRIGGER_LOCATION, screen);
                });
                java.nodeProperty().removeListener(this);
            }
        });
        for (int i = 2011; i < 2021; i++) {
            // adding a tooltip to the data node
            final XYChart.Data data = new XYChart.Data(Integer.toString(i), javaValue);
            java.getData().add(data);
            javaValue = javaValue + Math.random() - .5;
        }
        answer.addAll(java); //, c, cpp);
        return answer;
    }

    @Override
    public void start(Stage primaryStage) {
        CategoryAxis xAxis = new CategoryAxis();
        NumberAxis yAxis = new NumberAxis();
        LineChart lineChart = new LineChart(xAxis, yAxis);
        lineChart.setCreateSymbols(false);
        lineChart.setData(getChartData());
        lineChart.setTitle("speculations");
        primaryStage.setTitle("LineChart example");

        StackPane root = new StackPane();
        root.getChildren().add(lineChart);
        primaryStage.setScene(new Scene(root)); //, 400, 250));
        primaryStage.setTitle(FXUtils.version());
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(ToolTipOnChartSeries.class
            .getName());
} 

BTW: fully agree with James on the useability issue: data racing across the chart can't really be handled by a tooltip on the data/series. If you really need that, you'll have to implement some custom marker (like f.i. a vertical line) that's added on a mouse gesture, keep that line sticky (aka: sync'ed to the moving x-values) to the data, and attach the tooltip to that line.

I didn't really got the problem you want to solve (simply adding a tooltip should be exactly the same) but if you want to "update" your tooltip with the livedata you could simply make a bind between the data and if the tooltip shouldn't update itself change the data of the tooltip in a Platform.runLater().

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!