0

I'm trying to create vertical and horizontal lines on ScatterChart together with points. I cannot find a way to mix them up.

enter image description here

This is the code I generate points on chart.

vbox {
    add(ScatterChart(NumberAxis(), NumberAxis()).apply {
        val seriesMap: HashMap<String, XYChart.Series<Number, Number>> = HashMap()

        pointsList
                .map { it.decisionClass }
                .distinct()
                .forEach {
                    seriesMap.put(it, XYChart.Series())
                }

        for (point in pointsList) {
            seriesMap.get(point.decisionClass)?.data(point.axisesValues[0], point.axisesValues[1])
        }

        seriesMap
                .toSortedMap()
                .forEach { key, value ->
                    value.name = key
                    data.add(value)
                }
        (xAxis as NumberAxis).setForceZeroInRange(false)
        (yAxis as NumberAxis).setForceZeroInRange(false)
    })
}
Humberd
  • 2,794
  • 3
  • 20
  • 37
  • See if you can use the techniques described in https://stackoverflow.com/questions/38871202/how-to-add-shapes-on-javafx-linechart (If not, I can post a way to do this in Java, but I don't know Kotlin...) – James_D Jan 15 '18 at 21:36
  • I'm afraid I need to ask you for help. A simple example in Java would be great. – Humberd Jan 15 '18 at 22:11

1 Answers1

2

I don't know Kotlin, so this answer is in Java. I think you can probably translate it to Kotlin (and feel free to post another answer if so).

To add additional nodes to a chart, there are three things you need:

  1. Call getPlotChildren() and add the new nodes to the chart's "plot children"
  2. Override the layoutPlotChildren() method to update the positions of your nodes when the chart is laid out
  3. Use getDisplayPosition(...), defined in Axis, to get the location in the coordinate system of the plot area of the chart from a value on the axis.

The following SSCCE creates a scatter chart somewhat similar to the one you posted in the screen shot, and adds lines gating a specified series (i.e. the line on the left extends the height of the chart and passes through the minimum x-value of the series; the line at the top extends the width of the chart, and passes through the maximum y-value of the series, etc). I added radio buttons so you can choose which series is "bounded" by the lines.

import java.util.Random;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

public class ScatterChartWithLines extends Application {

    private final class ScatterChartWithBoundary extends ScatterChart<Number, Number> {

        private Series<Number, Number> boundedSeries ;

        private final NumberAxis yAxis;
        private final NumberAxis xAxis;
        private final Line leftLine = new Line();
        private final Line rightLine = new Line();
        private final Line topLine = new Line();
        private final Line bottomLine = new Line();
        {
            getPlotChildren().addAll(leftLine, rightLine, topLine, bottomLine);
        }

        private ScatterChartWithBoundary(NumberAxis xAxis, NumberAxis yAxis) {
            super(xAxis, yAxis);
            this.yAxis = yAxis;
            this.xAxis = xAxis;
        }

        @Override
        protected void layoutPlotChildren() {
            super.layoutPlotChildren();
            getPlotChildren().removeAll(leftLine, rightLine, topLine, bottomLine);
            if (boundedSeries != null) {
                getPlotChildren().addAll(leftLine, rightLine, topLine, bottomLine);
                double minX = Double.MAX_VALUE ;
                double minY = Double.MAX_VALUE ;
                double maxX = Double.MIN_VALUE ;
                double maxY = Double.MIN_VALUE ;
                for (Data<Number, Number> d : boundedSeries.getData()) {
                    if (d.getXValue().doubleValue() < minX) minX = d.getXValue().doubleValue() ;
                    if (d.getXValue().doubleValue() > maxX) maxX = d.getXValue().doubleValue() ;
                    if (d.getYValue().doubleValue() < minY) minY = d.getYValue().doubleValue() ;
                    if (d.getYValue().doubleValue() > maxY) maxY = d.getYValue().doubleValue() ;
                }
                positionLineInAxisCoordinates(leftLine, minX, yAxis.getLowerBound(), minX, yAxis.getUpperBound());
                positionLineInAxisCoordinates(rightLine, maxX, yAxis.getLowerBound(), maxX, yAxis.getUpperBound());
                positionLineInAxisCoordinates(bottomLine, xAxis.getLowerBound(), minY, xAxis.getUpperBound(), minY);
                positionLineInAxisCoordinates(topLine, xAxis.getLowerBound(), maxY, xAxis.getUpperBound(), maxY);
            }
        }

        private void positionLineInAxisCoordinates(Line line, double startX, double startY, double endX, double endY) {
            double x0 = xAxis.getDisplayPosition(startX);
            double x1 = xAxis.getDisplayPosition(endX);
            double y0 = yAxis.getDisplayPosition(startY);
            double y1 = yAxis.getDisplayPosition(endY);


            line.setStartX(x0);
            line.setStartY(y0);
            line.setEndX(x1);
            line.setEndY(y1);
        }

        public void setBoundedSeries(Series<Number, Number> boundedSeries) {
            if (! getData().contains(boundedSeries)) {
                throw new IllegalArgumentException("Specified series is not displayed in this chart");
            }
            this.boundedSeries = boundedSeries ;
            requestChartLayout();
        }
    }

    private final Random rng = new Random();

    @Override
    public void start(Stage primaryStage) {
        Series<Number, Number> series1 = new Series<>("Series 1", FXCollections.observableArrayList());
        Series<Number, Number> series2 = new Series<>("Series 2", FXCollections.observableArrayList());
        Series<Number, Number> series3 = new Series<>("Series 3", FXCollections.observableArrayList());

        for (int i = 0 ; i < 40 ; i++) {
            series1.getData().add(new Data<>(rng.nextDouble()*2 + 4, rng.nextDouble()*3 + 2));
            series2.getData().add(new Data<>(rng.nextDouble()*2.5 + 4.75, rng.nextDouble()*1.5 + 2));
            series3.getData().add(new Data<>(rng.nextDouble()*3 + 5, rng.nextDouble()*1.5 + 2.75));         
        }

        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        xAxis.setForceZeroInRange(false);
        yAxis.setForceZeroInRange(false);

        ScatterChartWithBoundary chart = new ScatterChartWithBoundary(xAxis, yAxis);

        chart.getData().addAll(series1, series2, series3);

        VBox buttons = new VBox(2);
        ToggleGroup toggleGroup = new ToggleGroup();
        for (Series<Number, Number> series : chart.getData()) { 
            RadioButton rb = new RadioButton(series.getName());
            rb.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
                if (isNowSelected) {
                    chart.setBoundedSeries(series);
                }
            });
            rb.setToggleGroup(toggleGroup);
            buttons.getChildren().add(rb);
        }


        BorderPane root = new BorderPane(chart);
        root.setTop(buttons);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Here is a typical result:

enter image description here

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks, that worked. But do you know a way to refresh the lines? Let's say I add more lines asynchronously and after each addition I want them to be updated on the chart, but unfortunately `layoutPlotChildren` is protected. – Humberd Jan 15 '18 at 23:34
  • 1
    @Humberd You would need to create a named class for the chart subclass, and provide methods with the functionality to change the appropriate parameters. Call `requestChartLayout()` to layout the chart on the next rendering pulse: `layoutPlotChildren()` will be called during the layout. I modified the answer so that is it structured like this. – James_D Jan 16 '18 at 00:02