简体   繁体   中英

JavaFX custom chart class - how to bind a node's layoutX and layoutY properties to the display positions of a NumberAxis?

I'm writing a rudimentary Candlestick chart class in which the candlesticks are created as Regions and are plotted by setting their layoutX and layoutY values to the getDisplayPosition() of the relevant axis.

For example, to plot a candlestick at value 3 on an X axis, I do this:

candlestick.setLayoutX(xAxis.getDisplayPosition(3));

When the stage resizes or when the axes are zoomed in or out, the candlesticks' layout values have to be reset so that the chart renders correctly. I'm currently handling this via ChangeListener s for resize events and Button.setOnAction() s for zooming.

However, I'd rather bind the candlesticks' layout properties to the axes' display positions than set/reset the layout values, but can't find a "displayPositionProperty" (or similar) for a NumberAxis .

Is it possible to do this? Which NumberAxis property would I bind to? ie.

candlestick.layoutXProperty().bind(xAxis.WHICH_PROPERTY?);

Also, would binding the properties be more efficient than resetting layout positions? Some of the charts could potentially have thousands of candlesticks but I can't test resource usage until I figure out how to code the bind.

I've experimented with scaling the candlesticks to the axes' scale but can't use that approach because scaling a Region affects its border width. For certain types of candlesticks, that can change its meaning.

I've also played with the Ensemble candlestick demo chart. It was useful in giving me a start but is too simplistic for my needs.

Here's a MVCE that demonstrates my approach. Any guidance re binding would be very much appreciated.

I'm using OpenJFX 17.

package test023;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Test023 extends Application {

    @Override
    public void start(Stage stage) {    

        NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
        Pane pChart = new Pane();
        Pane pAxis = new Pane();
        VBox vb = new VBox();
        BorderPane bp = new BorderPane();

        pChart.setPrefHeight(100D);
        pAxis.getChildren().add(xAxis);
        xAxis.prefWidthProperty().bind(pAxis.widthProperty());
        xAxis.setAnimated(false);

        vb.setPadding(new Insets(10D));
        vb.getChildren().addAll(pChart, pAxis);

        Region point = new Region();
        point.setPrefSize(5D, 5D);
        point.setStyle("-fx-background-color: black;");

        pChart.getChildren().add(point);

        //Plot the point in its initial position (value 3 on the axis)
        double pointXValue = 3D;
        plotPoint(point, pointXValue, xAxis);

        //*****************************************************************
        //Can the listeners and button.setOnActions be replaced by binding
        //the point's layout value to the axis display position?
        //*****************************************************************

        //Handle resize events
        pChart.widthProperty().addListener((obs, oldVal, newVal) -> {
            plotPoint(point, pointXValue, xAxis);
        });

        stage.maximizedProperty().addListener((obs, oldVal, newVal) -> {
            plotPoint(point, pointXValue, xAxis);
        });        

        //Handle zooming (hard-coded upper and lower bounds for the 
        //sake of simplicity)
        Button btnZoomIn = new Button("Zoom in");
        btnZoomIn.setOnAction((event) -> {
            xAxis.setLowerBound(2D);
            xAxis.setUpperBound(8D);
            xAxis.layout();
            plotPoint(point, pointXValue, xAxis);
        });

        Button btnZoomOut = new Button("Zoom out");
        btnZoomOut.setOnAction((event) -> {
            xAxis.setLowerBound(0D);
            xAxis.setUpperBound(10D);
            xAxis.layout();
            plotPoint(point, pointXValue, xAxis);
        });

        bp.setCenter(vb);
        bp.setTop(new HBox(btnZoomIn, btnZoomOut));

        stage.setScene(new Scene(bp));
        stage.setTitle("Test bind layoutX");
        stage.setWidth(400D);
        stage.setHeight(200D);
        stage.show();

    }

    private void plotPoint(Region region, double axisPos, NumberAxis axis) {

        Platform.runLater(() -> {
            double posX = axis.getDisplayPosition(axisPos);
            region.setLayoutX(posX);
            region.setLayoutY(80D);
        });

    }

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

}

Something like this would work

@Override
public void start(Stage stage) {    

    NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
    Pane pChart = new Pane();
    Pane pAxis = new Pane();
    VBox vb = new VBox();
    BorderPane bp = new BorderPane();

    pChart.setPrefHeight(100D);
    pAxis.getChildren().add(xAxis);
    xAxis.prefWidthProperty().bind(pAxis.widthProperty());
    xAxis.setAnimated(false);

    vb.setPadding(new Insets(10D));
    vb.getChildren().addAll(pChart, pAxis);

    Region point = new Region();
    point.setPrefSize(5D, 5D);
    point.setStyle("-fx-background-color: black;");

    pChart.getChildren().add(point);

    //Plot the point in its initial position (value 3 on the axis)
    double pointXValue = 3D;

    point.setLayoutY(80D);
    
    point.layoutXProperty().bind(Bindings.createDoubleBinding(()-> {
        return xAxis.getDisplayPosition(pointXValue);
    }, xAxis.lowerBoundProperty(), xAxis.upperBoundProperty(), pChart.widthProperty()));
        
    //Handle zooming (hard-coded upper and lower bounds for the 
    //sake of simplicity)
    Button btnZoomIn = new Button("Zoom in");
    btnZoomIn.setOnAction((event) -> {
        xAxis.setLowerBound(2D);
        xAxis.setUpperBound(8D);
        xAxis.layout();
    });

    Button btnZoomOut = new Button("Zoom out");
    btnZoomOut.setOnAction((event) -> {
        xAxis.setLowerBound(0D);
        xAxis.setUpperBound(10D);
        xAxis.layout();
    });

    bp.setCenter(vb);
    bp.setTop(new HBox(btnZoomIn, btnZoomOut));

    stage.setScene(new Scene(bp));
    stage.setTitle("Test bind layoutX");
    stage.setWidth(400D);
    stage.setHeight(200D);
    stage.show();

}

This creates a custom double binding with a function that calculates the value of the binding every time the dependencies are changed, see createDoubleBinding​ for more info.

+1 for @LukasOwen answer which actually you actual question related to bindings.

But as you are aware that every problem has more than one approach, I am suggesting mine, considering the scalability (adding many points) and too many bindings (for every point).

The key things in this approach are:

  • You add all your points numbers and its node to a map.
  • Every time the xAxis is rendered, you update the all the points position. So this will be implicitly done if you resize, change range, or maximize the window.

Below is the example of the approach:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;

public class Test023 extends Application {

    Map<Double, Region> plotPoints = new HashMap<>();
    double yOffset = 80D;
    Pane pChart;

    @Override
    public void start(Stage stage) {
        NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
        xAxis.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
            if(!needsLayout) {
                plotPoints.forEach((num, point) -> {
                    double posX = xAxis.getDisplayPosition(num);
                    point.setLayoutX(posX);
                    point.setLayoutY(yOffset);
                });
            }
        });
        pChart = new Pane();
        Pane pAxis = new Pane();
        VBox vb = new VBox();
        BorderPane root = new BorderPane();

        pChart.setPrefHeight(100D);
        pAxis.getChildren().add(xAxis);
        xAxis.prefWidthProperty().bind(pAxis.widthProperty());
        xAxis.setAnimated(false);

        vb.setPadding(new Insets(10D));
        vb.getChildren().addAll(pChart, pAxis);

        addPoint(3D, "black");
        addPoint(4D, "red");
        addPoint(5D, "blue");

        //Handle zooming (hard-coded upper and lower bounds for the sake of simplicity)
        Button btnZoomIn = new Button("Zoom in");
        btnZoomIn.setOnAction((event) -> {
            xAxis.setLowerBound(2D);
            xAxis.setUpperBound(8D);
        });

        Button btnZoomOut = new Button("Zoom out");
        btnZoomOut.setOnAction((event) -> {
            xAxis.setLowerBound(0D);
            xAxis.setUpperBound(10D);
        });

        root.setCenter(vb);
        root.setTop(new HBox(btnZoomIn, btnZoomOut));

        stage.setScene(new Scene(root));
        stage.setTitle("Test bind layoutX");
        stage.setWidth(400D);
        stage.setHeight(200D);
        stage.show();
    }

    private void addPoint(double num, String color) {
        Region point = new Region();
        point.setPrefSize(5D, 5D);
        point.setStyle("-fx-background-color: " + color);
        plotPoints.put(num, point);
        pChart.getChildren().add(point);
    }

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

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM