实时折线图上的工具提示

Tooltip on Live LineChart

我找到了很多如何在折线图上添加工具提示的示例,但没有关于如何在实时折线图上添加工具提示的信息或示例。

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.stage.Stage;

public class MainApp extends Application
{
    private static final int MAX_DATA_POINTS = 50;

    private Series series;
    private Series series2;
    private int xSeriesData = 0;
    private ConcurrentLinkedQueue<Number> dataQ = new ConcurrentLinkedQueue<Number>();
    private ConcurrentLinkedQueue<Number> dataQ2 = new ConcurrentLinkedQueue<Number>();
    private ExecutorService executor;
    private AddToQueue addToQueue;
    private NumberAxis xAxis;

    private void init(Stage primaryStage)
    {
        xAxis = new NumberAxis(0, MAX_DATA_POINTS, MAX_DATA_POINTS / 10);
        xAxis.setForceZeroInRange(false);
        xAxis.setAutoRanging(false);

        NumberAxis yAxis = new NumberAxis();
        yAxis.setAutoRanging(true);

        //-- Chart
        final AreaChart<Number, Number> sc = new AreaChart<Number, Number>(xAxis, yAxis)
        {
            // Override to remove symbols on each data point
            @Override
            protected void dataItemAdded(Series<Number, Number> series, int itemIndex, Data<Number, Number> item)
            {
            }
        };

        sc.setAnimated(false);
        sc.setId("liveAreaChart");
        sc.setTitle("Animated Area Chart");

        //-- Chart Series
        series = new AreaChart.Series<Number, Number>();
        series.setName("Area Chart Series");
        series2 = new AreaChart.Series<Number, Number>();
        series2.setName("Area Chart Series");
        sc.getData().addAll(series, series2);

        xAxis.setTickLabelsVisible(false);
        xAxis.setTickMarkVisible(false);
        xAxis.setMinorTickVisible(false);

        primaryStage.setScene(new Scene(sc));
    }

    @Override
    public void start(Stage primaryStage) throws Exception
    {
        init(primaryStage);
        primaryStage.show();

        //-- Prepare Executor Services
        executor = Executors.newCachedThreadPool(new ThreadFactory()
        {
            @Override
            public Thread newThread(Runnable r)
            {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                return thread;
            }
        });
        addToQueue = new AddToQueue();
        executor.execute(addToQueue);
        //-- Prepare Timeline
        prepareTimeline();
    }

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

    private class AddToQueue implements Runnable
    {
        @Override
        public void run()
        {
            try
            {
                // add a item of random data to queue
                dataQ.add(Math.random());
                dataQ2.add(Math.random());
                Thread.sleep(200);
                executor.execute(this);
            }
            catch (InterruptedException ex)
            {
                Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    //-- Timeline gets called in the JavaFX Main thread
    private void prepareTimeline()
    {
        // Every frame to take any data from queue and add to chart
        new AnimationTimer()
        {
            @Override
            public void handle(long now)
            {
                addDataToSeries();
            }
        }.start();
    }

    private void addDataToSeries()
    {
        for (int i = 0; i < 20; i++)
        { //-- add 20 numbers to the plot+
            if (dataQ.isEmpty())
                break;

            Data data = new AreaChart.Data(xSeriesData++, dataQ.remove());

            series.getData().add(data);

            data.nodeProperty().addListener(new ChangeListener<Node>()
            {
                @Override
                public void changed(ObservableValue<? extends Node> arg0, Node arg1,
                    Node arg2)
                {
                    Tooltip t = new Tooltip(data.getYValue().toString() + '\n' + data.getXValue());
                    Tooltip.install(arg2, t);
                    data.nodeProperty().removeListener(this);
                }
            });

            if (dataQ2.isEmpty())
                break;
            series2.getData().add(new AreaChart.Data(xSeriesData, dataQ2.remove()));
        }
        // remove points to keep us at no more than MAX_DATA_POINTS
        if (series.getData().size() > MAX_DATA_POINTS)
        {
            series.getData().remove(0, series.getData().size() - MAX_DATA_POINTS);
        }

        // remove points to keep us at no more than MAX_DATA_POINTS
        if (series2.getData().size() > MAX_DATA_POINTS)
        {
            series2.getData().remove(0, series2.getData().size() - MAX_DATA_POINTS);
        }

        // update
        xAxis.setLowerBound(xSeriesData - MAX_DATA_POINTS);
        xAxis.setUpperBound(xSeriesData - 1);
    }
}

这是我想要创建的结果:

如果可能我设置 setCreateSymbols(false);

我真的没有得到你想要解决的问题(简单地添加一个工具提示应该是完全一样的)但是如果你想"update"你的工具提示和实时数据你可以简单地在两者之间进行绑定数据以及工具提示是否不应自行更新更改 Platform.runLater().

中的工具提示数据

首先,使用常规 AreaChart 而不是您正在使用的匿名子类(即不要覆盖 dataAddedItem(...) 方法)。如果 none 已经存在,该方法会创建一个默认节点来显示数据点(恕我直言,这严重违反了将数据与表示分离的规定,但我们对此无能为力...);你显然需要一个图形来附加工具提示。

一旦数据点有一个节点,你就不需要监听变化了,所以在你的addDataToSeries()方法中,删除监听器并用

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

或者,创建您自己的图形,在其上附加工具提示,然后将其传递给 data.setNode(...);

您仍然会遇到一般的可用性问题;当一切都以每秒 5 个单位飞过时,我看不到用户将如何将鼠标悬停在图表中的数据点上。即使他们可以,当工具提示出现时,点也会移动,因此值将不正确...

更新:

只是为了好玩,我尝试了这个:

    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);

这会在图表上设置一个工具提示,它会在您移动鼠标和图表在下方滚动时更新。这结合了 and .

的想法

回答问题的隐含部分:如果没有符号,即数据没有节点,如何安装显示当前 x/y 值的工具提示?并且仅适用于静态图表(或合理缓慢变化的图表,因此工具提示的 location/value 在显示时没有意义)

有几个问题需要解决

  • 工具提示必须安装在系列的节点上:与在数据节点上安装相同的技巧 - 在其 nodeProperty 的侦听器中完成,只有这样我们才能确定节点已创建
  • 它的文本必须在显示时更新:done onShowing(注意一个bug in the setter,所以我们需要直接使用属性)
  • 它的文本应该与其触发事件的鼠标位置相关:因为触发事件似乎没有 我们必须自己跟踪该位置,在鼠标下方-moved 在工具提示的属性中存储当前鼠标屏幕位置
  • 从鼠标坐标到"world"图表坐标的坐标转换

一个例子:

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());
} 

顺便说一句:完全同意 James 在可用性问题上的观点:图表上的数据竞速无法通过 data/series 上的工具提示真正处理。如果你真的需要它,你必须实现一些自定义标记(比如 f.i. 一条垂直线),它被添加到鼠标手势上,保持该线粘性(又名:同步到移动的 x 值) 到数据,并将工具提示附加到该行。