Adding a line in a JavaFX chart

2019-01-06 22:31发布

I have problems with adding a line at a defined position in JavaFX. The line has to be line a constant line as shown here: How to add a value marker to JavaFX chart?

My problem is, that my layout definition is a bit more complicated. Have a look:

chart

The important part is the one on the top. I want to have the line on the y=60 line. The left part with the RadioBoxes is a VBox. The part with the (Scatter-)Chart is a StackPane (because I want it to fill the rest of the width). Inside this StackPane is the chart and a Group. The only child of the Group is the line.

I think the problem is, that the StackPane centers the Group with the line above the chart. But I can't get the combination of layouts which 1. stretches the chart 2. sets the Line above the chart 3. doesn't center the line

I tried lots of combinations but I just can't get it the way I want. Does anybody have an idea?!

2条回答
劳资没心,怎么记你
2楼-- · 2019-01-06 23:15

Unfortunately, ValueMarkers are not supported in XYCharts (that's probably the place in the hierarchy were it should be done) (Mis-)using data with constant value is a hack which might (or not) be acceptable/possible in some contexts.

A cleaner way out is a custom Chart that supports such markers. F.i. a custom ScatterChart like:

public class ScatterXChart<X, Y> extends ScatterChart<X, Y> {

    // data defining horizontal markers, xValues are ignored
    private ObservableList<Data<X, Y>> horizontalMarkers;

    public ScatterXChart(Axis<X> xAxis, Axis<Y> yAxis) {
        super(xAxis, yAxis);
        // a list that notifies on change of the yValue property
        horizontalMarkers = FXCollections.observableArrayList(d -> new Observable[] {d.YValueProperty()});
        // listen to list changes and re-plot
        horizontalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
    }

    /**
     * Add horizontal value marker. The marker's Y value is used to plot a
     * horizontal line across the plot area, its X value is ignored.
     * 
     * @param marker must not be null.
     */
    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);
    }

    /**
     * Remove horizontal value marker.
     * 
     * @param horizontalMarker must not be null
     */
    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);
    }

    /**
     * Overridden to layout the value markers.
     */
    @Override
    protected void layoutPlotChildren() {
        super.layoutPlotChildren();
        for (Data<X, Y> horizontalMarker : horizontalMarkers) {
            double lower = ((ValueAxis) getXAxis()).getLowerBound();
            X lowerX = getXAxis().toRealValue(lower);
            double upper = ((ValueAxis) getXAxis()).getUpperBound();
            X upperX = getXAxis().toRealValue(upper);
            Line line = (Line) horizontalMarker.getNode();
            line.setStartX(getXAxis().getDisplayPosition(lowerX));
            line.setEndX(getXAxis().getDisplayPosition(upperX));
            line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()));
            line.setEndY(line.getStartY());

        }
    }
}

A snippet testing the chart (f.i. insert into the online example in oracle's tutorial):

// instantiate chart
NumberAxis xAxis = new NumberAxis(0, 10, 1);
NumberAxis yAxis = new NumberAxis(-100, 500, 100);        
ScatterXChart<Number,Number> sc = new ScatterXChart<>(xAxis,yAxis);
// .. fill with some data
...
// ui to add/change/remove a value marker
Data<Number, Number> horizontalMarker = new Data<>(0, 110);
Button add = new Button("Add Marker");  
add.setOnAction(e -> sc.addHorizontalValueMarker(horizontalMarker));
Slider move = new Slider(yAxis.getLowerBound(), yAxis.getUpperBound(), 0);
move.setShowTickLabels(true);
move.valueProperty().bindBidirectional(horizontalMarker.YValueProperty());
Button remove = new Button("Remove Marker");
remove.setOnAction(e -> sc.removeHorizontalValueMarker(horizontalMarker));

Addendum:

While I wouldn't recommend the approach in the related question (adding the marker line to the chart's parent and managing its position/length externally) it is possible to use it in resizable containers. The crucial thingies to make it work:

  • listen to chart's size/location changes and update the line appropriately
  • set the marker's managed property to false

in code (updateShift is the part in the original that calculates the yShift/lineX):

Pane pane = new StackPane(chart);
chart.widthProperty().addListener(o -> updateShift(chart));
chart.heightProperty().addListener(o -> updateShift(chart));
valueMarker.setManaged(false);
pane.getChildren().add(valueMarker);
查看更多
Juvenile、少年°
3楼-- · 2019-01-06 23:36

Why don't you add that line into your chart? I have charts where I display the 100, 99, 95 and 50 percentiles, and the way I solve this is to add a line at the correct y-value for each percentile.

To do this, just add a line with two points, one at y=60 x=70 (the left-most x-axis value) and the other one at y=60 and x=120 (the right-most x-axis value).

The upside of this is that you do not have to align the horizontal line yourself manually, the downside being that this horizontal line will also be part of the legend. However, seeing as you do not have a legend that should be OK.

If you decide to add a legend, be sure to name the horizontal line appropriately, l

查看更多
登录 后发表回答