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

时间:2015-03-09 21:31:58

标签: java javafx

我的桌面应用程序有一个启动和停止测试的计时器。在图表上,我想创建两条垂直线来指示开始和停止时间。 “使用JavaFX向StackPane添加垂直线”对我的情况不起作用,因为我不希望线保持在相同的位置,并且这些线应该在绘图中而不是布局中绘制。当用户放大图表时,那些垂直线应该对应于用户放大的位置移动。谢谢你的任何提示。

以下是我创建图表的代码:

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

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

3 个答案:

答案 0 :(得分:12)

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

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

    }
}

enter image description here

要添加更多标记线,请使用:

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

使它看起来像一个范围:

enter image description here

答案 1 :(得分:3)

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

然后你只需要让绑定观察它需要注意变化的所有东西:这些将是轴的(数字)边界,图表的(图形)边界,如果线要去move,表示其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);
    }
}

答案 2 :(得分:0)

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

#include <iostream>
#include <tuple>
#include <vector>

template<size_t i, size_t size, typename F, typename... T>
void tuple_foreach_constexpr(const std::tuple<T...>& tuple, F func)
{
    if constexpr(i<size)
    {
        func(std::get<i>(tuple));
        tuple_foreach_constexpr<i+1, size, F, T...>(tuple, func);
    }
}
template<typename F, typename... T>
void tuple_foreach_constexpr(const std::tuple<T...>& tuple, F func)
{
    tuple_foreach_constexpr<0, std::tuple_size<std::tuple<T...>>::value, F, T...>(tuple, func);
}

template<typename A, typename... B>
void abra
(
    const std::vector<A>& a_vector,
    const std::tuple<B...>& b_tuple
)
{
    for(const auto& a : a_vector)
    {
        tuple_foreach_constexpr(b_tuple, [&a](const auto &x)
        {
            if constexpr(std::is_pointer<typename std::remove_reference<decltype(a.*x)>::type>::value)
            {
                std::cout << *(a.*x) << std::endl;
            }
            else
            {
                std::cout << a.*x << std::endl;
            } // this does NOT work

            //std::cout << a.*x << std::endl; // this does work
        });
    }
}

struct Harry
{
    int a;
    int* b;
};    

int main()
{
    int m = 20;
    std::vector<Harry> h_vector = {Harry{10, &m}};
    std::tuple t_tuple = std::make_tuple(&Harry::a, &Harry::b);

    abra(h_vector, t_tuple);
}