如何使用 JavaFX LineChart 添加两条垂直线

How to add two vertical lines with JavaFX LineChart

我的桌面应用程序有一个用于开始和停止测试的计时器。在图表上,我想创建两条垂直线来指示开始和停止时间。 "Adding vertical lines to StackPane with JavaFX" 不适用于我的情况,因为我不希望线条保持在同一位置并且这些线条应该绘制在绘图而不是布局中。当用户在图表上缩放时,这些垂直线应相应于用户缩放的位置移动。感谢您的提示。

这是我创建图表的代码:

LineChart<Number, Number> chart = new LineChart<Number, Number>(xAxis, yAxis, dataset);

xAxis.setLabel("time(s)");
yAxis.setLabel("deg/s");

我不确定你指的是哪个问题。您基本上可以使用一些绑定魔法来完成所有这些操作:诀窍是使用 xAxis.getDisplayPosition(...) 将线的 x 值映射到相对于 xAxis 的坐标。然后您需要将该坐标转换为相对于包含图表和线条的容器的坐标:最简单的方法是首先使用 xAxis.localToScene(...) 转换为 Scene 坐标,然后转换为容器,使用 container.sceneToLocal(...).

然后你只需要让绑定观察它需要观察变化的一切:这些将是轴的(数字)边界,图表的(图形)边界,如果线是要移动,一个 属性 表示它的 x 值。

这是一个 SSCCE。在此示例中,我使用 Slider 来移动线条。我还使线条仅在范围内可见,并绑定 y 坐标,使其跨越 yAxis.

import java.util.Random;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;


public class LineChartWithVerticalLine extends Application {

    @Override
    public void start(Stage primaryStage) {
        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
        chart.getData().add(createSeries());

        Pane chartHolder = new Pane();
        chartHolder.getChildren().add(chart);


        DoubleProperty lineX = new SimpleDoubleProperty();
        Slider slider = new Slider();
        slider.minProperty().bind(xAxis.lowerBoundProperty());
        slider.maxProperty().bind(xAxis.upperBoundProperty());

        slider.setPadding(new Insets(20));

        lineX.bind(slider.valueProperty());

        chartHolder.getChildren().add(createVerticalLine(chart, xAxis, yAxis, chartHolder, lineX));

        BorderPane root = new BorderPane(chartHolder, null, null, slider, null);

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Line createVerticalLine(XYChart<Number, Number> chart, NumberAxis xAxis, NumberAxis yAxis, Pane container, ObservableDoubleValue x) {
        Line line = new Line();
        line.startXProperty().bind(Bindings.createDoubleBinding(() -> {
                double xInAxis = xAxis.getDisplayPosition(x.get());
                Point2D pointInScene = xAxis.localToScene(xInAxis, 0);
                double xInContainer = container.sceneToLocal(pointInScene).getX();
                return xInContainer ;
            }, 
            x, 
            chart.boundsInParentProperty(), 
            xAxis.lowerBoundProperty(),
            xAxis.upperBoundProperty()));
        line.endXProperty().bind(line.startXProperty());
        line.startYProperty().bind(Bindings.createDoubleBinding(() -> {
                double lowerY = yAxis.getDisplayPosition(yAxis.getLowerBound());
                Point2D pointInScene = yAxis.localToScene(0, lowerY);
                double yInContainer = container.sceneToLocal(pointInScene).getY();
                return yInContainer ;
            }, 
            chart.boundsInParentProperty(), 
            yAxis.lowerBoundProperty()));
        line.endYProperty().bind(Bindings.createDoubleBinding(() -> {
                double upperY = yAxis.getDisplayPosition(yAxis.getUpperBound());
                Point2D pointInScene = yAxis.localToScene(0, upperY);
                double yInContainer = container.sceneToLocal(pointInScene).getY();
                return yInContainer ;
            }, 
            chart.boundsInParentProperty(), 
            yAxis.lowerBoundProperty()));

        line.visibleProperty().bind(
                Bindings.lessThan(x, xAxis.lowerBoundProperty())
                .and(Bindings.greaterThan(x, xAxis.upperBoundProperty())).not());

        return line ;
    }

    private Series<Number, Number> createSeries() {
        Series<Number, Number> series = new Series<>();
        series.setName("Data");
        Random rng = new Random();
        for (int i=0; i<=20; i++) {
            series.getData().add(new Data<>(i, rng.nextInt(101)));
        }
        return series ;
    }

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

您需要扩展 LineChart class 并覆盖 layoutPlotChildren 方法以显示您的标记。

克利奥帕特拉做了 very good example for a Scatter chart。下面的代码是折线图的修改版本,同时具有垂直和水平标记:

public class LineChartSample extends Application {

    @Override public void start(Stage stage) {

        final NumberAxis xAxis = new NumberAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("Number of Month");

        final LineChartWithMarkers<Number,Number> lineChart = new LineChartWithMarkers<Number,Number>(xAxis,yAxis);

        XYChart.Series series = new XYChart.Series();
        series.setName("My portfolio");

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

        lineChart.getData().add(series);

        Data<Number, Number> horizontalMarker = new Data<>(0, 25);
        lineChart.addHorizontalValueMarker(horizontalMarker);

        Data<Number, Number> verticalMarker = new Data<>(10, 0);
        lineChart.addVerticalValueMarker(verticalMarker);

        Slider horizontalMarkerSlider = new Slider(yAxis.getLowerBound(), yAxis.getUpperBound(), 0);
        horizontalMarkerSlider.setOrientation(Orientation.VERTICAL);
        horizontalMarkerSlider.setShowTickLabels(true);
        horizontalMarkerSlider.valueProperty().bindBidirectional(horizontalMarker.YValueProperty());
        horizontalMarkerSlider.minProperty().bind(yAxis.lowerBoundProperty());
        horizontalMarkerSlider.maxProperty().bind(yAxis.upperBoundProperty());

        Slider verticalMarkerSlider = new Slider(xAxis.getLowerBound(), xAxis.getUpperBound(), 0);
        verticalMarkerSlider.setOrientation(Orientation.HORIZONTAL);
        verticalMarkerSlider.setShowTickLabels(true);
        verticalMarkerSlider.valueProperty().bindBidirectional(verticalMarker.XValueProperty());
        verticalMarkerSlider.minProperty().bind(xAxis.lowerBoundProperty());
        verticalMarkerSlider.maxProperty().bind(xAxis.upperBoundProperty());

        BorderPane borderPane = new BorderPane();
        borderPane.setCenter( lineChart);
        borderPane.setTop(verticalMarkerSlider);
        borderPane.setRight(horizontalMarkerSlider);

        Scene scene  = new Scene(borderPane,800,600);

        stage.setScene(scene);
        stage.show();
    }

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

    private class LineChartWithMarkers<X,Y> extends LineChart {

        private ObservableList<Data<X, Y>> horizontalMarkers;
        private ObservableList<Data<X, Y>> verticalMarkers;

        public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
            super(xAxis, yAxis);
            horizontalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()});
            horizontalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
            verticalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
            verticalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
        }

        public void addHorizontalValueMarker(Data<X, Y> marker) {
            Objects.requireNonNull(marker, "the marker must not be null");
            if (horizontalMarkers.contains(marker)) return;
            Line line = new Line();
            marker.setNode(line );
            getPlotChildren().add(line);
            horizontalMarkers.add(marker);
        }

        public void removeHorizontalValueMarker(Data<X, Y> marker) {
            Objects.requireNonNull(marker, "the marker must not be null");
            if (marker.getNode() != null) {
                getPlotChildren().remove(marker.getNode());
                marker.setNode(null);
            }
            horizontalMarkers.remove(marker);
        }

        public void addVerticalValueMarker(Data<X, Y> marker) {
            Objects.requireNonNull(marker, "the marker must not be null");
            if (verticalMarkers.contains(marker)) return;
            Line line = new Line();
            marker.setNode(line );
            getPlotChildren().add(line);
            verticalMarkers.add(marker);
        }

        public void removeVerticalValueMarker(Data<X, Y> marker) {
            Objects.requireNonNull(marker, "the marker must not be null");
            if (marker.getNode() != null) {
                getPlotChildren().remove(marker.getNode());
                marker.setNode(null);
            }
            verticalMarkers.remove(marker);
        }


        @Override
        protected void layoutPlotChildren() {
            super.layoutPlotChildren();
            for (Data<X, Y> horizontalMarker : horizontalMarkers) {
                Line line = (Line) horizontalMarker.getNode();
                line.setStartX(0);
                line.setEndX(getBoundsInLocal().getWidth());
                line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
                line.setEndY(line.getStartY());
                line.toFront();
            }
            for (Data<X, Y> verticalMarker : verticalMarkers) {
                Line line = (Line) verticalMarker.getNode();
                line.setStartX(getXAxis().getDisplayPosition(verticalMarker.getXValue()) + 0.5);  // 0.5 for crispness
                line.setEndX(line.getStartX());
                line.setStartY(0d);
                line.setEndY(getBoundsInLocal().getHeight());
                line.toFront();
            }      
        }

    }
}

为了添加更多的标记线,只需使用这个:

Data<Number, Number> verticalMarker = new Data<>(10, 0);
lineChart.addVerticalValueMarker(verticalMarker);

当然你也可以使用矩形而不是像这样的直线:

private ObservableList<Data<X, X>> verticalRangeMarkers;

public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
    ...            
    verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
    verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()}); // 2nd type of the range is X type as well
    verticalRangeMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
}        


public void addVerticalRangeMarker(Data<X, X> marker) {
    Objects.requireNonNull(marker, "the marker must not be null");
    if (verticalRangeMarkers.contains(marker)) return;

    Rectangle rectangle = new Rectangle(0,0,0,0);
    rectangle.setStroke(Color.TRANSPARENT);
    rectangle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.2));

    marker.setNode( rectangle);

    getPlotChildren().add(rectangle);
    verticalRangeMarkers.add(marker);
}

public void removeVerticalRangeMarker(Data<X, X> marker) {
    Objects.requireNonNull(marker, "the marker must not be null");
    if (marker.getNode() != null) {
        getPlotChildren().remove(marker.getNode());
        marker.setNode(null);
    }
    verticalRangeMarkers.remove(marker);
}

protected void layoutPlotChildren() {

    ...

    for (Data<X, X> verticalRangeMarker : verticalRangeMarkers) {

        Rectangle rectangle = (Rectangle) verticalRangeMarker.getNode();
        rectangle.setX( getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()) + 0.5);  // 0.5 for crispness
        rectangle.setWidth( getXAxis().getDisplayPosition(verticalRangeMarker.getYValue()) - getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()));
        rectangle.setY(0d);
        rectangle.setHeight(getBoundsInLocal().getHeight());
        rectangle.toBack();

    }
} 

这样使用:

Data<Number, Number> verticalRangeMarker = new Data<>(4, 10);
lineChart.addVerticalRangeMarker(verticalRangeMarker);

让它看起来像一个范围:

我能够使用此处提到的折线图示例创建拖动和放大功能。该代码侦听鼠标事件并添加到垂直范围,使其看起来像是在拖动。 JavaFX Drag and Zoom Line Chart Example

/**
 * The ChartView.
 */
public class ChartController {

  private ChartViewModel chartViewModel;
  private CustomLineChart<Number, Number> lineChart;
  private NumberAxis xAxis;
  private NumberAxis yAxis;
  private XYChart.Series<Number, Number> series;
  private List<Integer> data;
  private boolean mouseDragged;
  private double initialNumberStart;
  private double initialNumberEnd;

  @FXML
  private VBox mainContainer;

  @FXML
  private HBox chartContainer;

  /**
   * The constructor.
   */
  public ChartController() {
    chartViewModel = new ChartViewModel();
    mouseDragged = false;
  }

  /**
   * The initialize method.
   */
  public void initialize() {
    createChart();
    handleEvents();
  }

  /**
   * Handles the events.
   */
  private void handleEvents() {
    lineChart.setOnMousePressed(pressed -> {
      int minSize = 1;
      // Get coordinate from the scene and transform to coordinates from the chart axis
      Point2D firstSceneCoordinate = new Point2D(pressed.getSceneX(), pressed.getSceneY());
      double firstX = xAxis.sceneToLocal(firstSceneCoordinate).getX();

      lineChart.setOnMouseDragged(dragged -> {
        mouseDragged = true;

        Point2D draggedSceneCoordinate = new Point2D(dragged.getSceneX(), dragged.getSceneY());
        double draggedX = xAxis.sceneToLocal(draggedSceneCoordinate).getX();

        List<Double> numbers = filterSeries(firstX, draggedX);
        int size = numbers.size();
        double numberStart = size > minSize ? numbers.get(0) : initialNumberStart;
        double numberEnd = numbers.size() > minSize ? numbers.get(size - 1) : initialNumberEnd;

        if (size > minSize) {
          lineChart.addVerticalRangeLines(new Data<>(numberStart, numberEnd));
        }

        lineChart.setOnMouseReleased(released -> {
          if (mouseDragged) {
            initialNumberStart = numberStart;
            initialNumberEnd = numberEnd;
            mouseDragged = false;

            redrawChart();
          }
        });
      });
    });
  }

  /**
   * Creates the charts.
   */
  private void createChart() {
    xAxis = new NumberAxis();
    yAxis = new NumberAxis();

    lineChart = new CustomLineChart<>(xAxis, yAxis);

    data = chartViewModel.getData();

    createSeries(data);
    lineChart.getData().add(series);

    initialNumberStart = 1;
    initialNumberEnd = data.size() - 1;

    chartContainer.getChildren().add(lineChart);

    HBox.setHgrow(lineChart, Priority.ALWAYS);
  }

  /**
   * Creates the series for the line chart.
   * 
   * @param numbers The list of numbers for the series
   */
  private void createSeries(List<Integer> numbers) {
    int size = numbers.size();
    series = new XYChart.Series<>();
    series.setName("Example");

    for (int i = 0; i < size; i++) {
      series.getData().add(new XYChart.Data<Number, Number>(i, numbers.get(i)));
    }
  }

  /**
   * Filters the nodes and returns the node x positions within the firstX and lastX positions.
   * 
   * @param firstX The first x position
   * @param lastX The last x position
   * @return The x positions for the nodes within the firstX and lastX
   */
  private List<Double> filterSeries(double firstX, double lastX) {
    List<Double> nodeXPositions = new ArrayList<>();

    lineChart.getData().get(0).getData().forEach(node -> {

      double nodeXPosition = lineChart.getXAxis().getDisplayPosition(node.getXValue());

      if (nodeXPosition >= firstX && nodeXPosition <= lastX) {
        nodeXPositions.add(Double.parseDouble(node.getXValue().toString()));
      }
    });

    return nodeXPositions;
  }

  /**
   * Updates the series for the chart.
   */
  private void updateSeries() {
    lineChart.getData().remove(0);
    lineChart.getData().add(series);
  }

  /**
   * Redraws the chart.
   */
  private void redrawChart() {
    List<Integer> filteredSeries = new ArrayList<>();

    data.forEach(number -> {
      if (number >= initialNumberStart && number <= initialNumberEnd) {
        filteredSeries.add(number);
      }
    });

    if (!filteredSeries.isEmpty()) {
      createSeries(filteredSeries);
      updateSeries();
      lineChart.removeVerticalRangeLines();
    }
  }

  /**
   * Resets the series for the chart.
   * 
   * @param event The event
   */
  @FXML
  void resetChart(ActionEvent event) {
    createSeries(data);
    updateSeries();
  }

}