7

Peace be upon you!

How to draw multiple axis using JavaFX charts API?

Following is an image of a trend in a huge software where some real time as well as history data is being plotted. There are two pens registered and separate axis for each pen is defined on the same trend.

Trends with multiple axis in IntelliMax

I have been trying to do exactly this in JavaFX 2.0 charts. I have been able to plot a real time chart which is as follows:

JavaFX 2.0 Line Chart;two NumberSeries with random points plotted

I have been researching about multiple axis using JavaFX and could find the this link but I think this is an older version of JavaFX using FXML. However, I am using JavaFX regular classes to accomplish this.

HELP!!!

Joe Doyle
  • 6,363
  • 3
  • 42
  • 45
Zaki Imtiaz
  • 183
  • 1
  • 2
  • 16

4 Answers4

10

Here you can find my solution - MultipleAxesLineChart. It is not generic and just fits my needs, but I think it can give a good sight of how it can be done by using StackPane.

MultipleAxesLineChart

Maciej Dobrowolski
  • 11,561
  • 5
  • 45
  • 67
6

Place two charts in a StackPane.

Use css lookups on the top chart to translate (using translate-x and translate-y) it's axes and labels so that they can be read independently of the bottom chart. Leave the top chart's data plot so that it overlays the bottom chart. Modify colors and legends (or mix chart styles, e.g. line and bar) so that it is obvious which data plot belongs to which series.

Some of the techniques above are demonstrated:

layered

Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • 1
    hmm...it is going to be a difficult task but probably a solution in our case. Thanks jewelsea =) – Zaki Imtiaz Mar 14 '12 at 08:09
  • I know this is old but is there anyway to make the stacked charts listen for mouseevents. As of now only the last chart added will receive events, seemingly due to some kind of clip even though the background is transparent. – BAR Jun 02 '13 at 01:21
  • @user417896 somebody can answer your additional question at: [JavaFX Pass MouseEvents through Transparent Node to Children](http://stackoverflow.com/questions/16876083/javafx-pass-mouseevents-through-transparent-node-to-children) – jewelsea Jun 02 '13 at 04:06
  • 1
    @jewelsea I am currently trying to code multiple-axis linechart, but I don't think it is that easy :) The biggest difficulty is with setting one width for all charts plot areas - http://stackoverflow.com/questions/26538542/setting-linechart-plot-area-width – Maciej Dobrowolski Oct 23 '14 at 22:36
0
public class MultipleAxesLineChart extends StackPane {

private final LineChart baseChart;
private final ObservableList<LineChart> backgroundCharts = FXCollections.observableArrayList();
private final Map<LineChart, Color> chartColorMap = new HashMap<>();

private final double yAxisWidth = 60;
private final AnchorPane detailsWindow;

private final double yAxisSeparation = 20;
private double strokeWidth = 0.3;

public MultipleAxesLineChart(LineChart baseChart, Color lineColor) {
    this(baseChart, lineColor, null);
}

public MultipleAxesLineChart(LineChart baseChart, Color lineColor, Double strokeWidth) {
    if (strokeWidth != null) {
        this.strokeWidth = strokeWidth;
    }
    this.baseChart = baseChart;

    chartColorMap.put(baseChart, lineColor);

    styleBaseChart(baseChart);
    styleChartLine(baseChart, lineColor);
    setFixedAxisWidth(baseChart);

    setAlignment(Pos.CENTER_LEFT);

    backgroundCharts.addListener((Observable observable) -> rebuildChart());

    detailsWindow = new AnchorPane();
    bindMouseEvents(baseChart, this.strokeWidth);

    rebuildChart();
}

private void bindMouseEvents(LineChart baseChart, Double strokeWidth) {
    final DetailsPopup detailsPopup = new DetailsPopup();
    getChildren().add(detailsWindow);
    detailsWindow.getChildren().add(detailsPopup);
    detailsWindow.prefHeightProperty().bind(heightProperty());
    detailsWindow.prefWidthProperty().bind(widthProperty());
    detailsWindow.setMouseTransparent(true);

    setOnMouseMoved(null);
    setMouseTransparent(false);

    final Axis xAxis = baseChart.getXAxis();
    final Axis yAxis = baseChart.getYAxis();

    final Line xLine = new Line();
    final Line yLine = new Line();
    yLine.setFill(Color.GRAY);
    xLine.setFill(Color.GRAY);
    yLine.setStrokeWidth(strokeWidth/2);
    xLine.setStrokeWidth(strokeWidth/2);
    xLine.setVisible(false);
    yLine.setVisible(false);

    final Node chartBackground = baseChart.lookup(".chart-plot-background");
    for (Node n: chartBackground.getParent().getChildrenUnmodifiable()) {
        if (n != chartBackground && n != xAxis && n != yAxis) {
            n.setMouseTransparent(true);
        }
    }
    chartBackground.setCursor(Cursor.CROSSHAIR);
    chartBackground.setOnMouseEntered((event) -> {
        chartBackground.getOnMouseMoved().handle(event);
        detailsPopup.setVisible(true);
        xLine.setVisible(true);
        yLine.setVisible(true);
        detailsWindow.getChildren().addAll(xLine, yLine);
    });
    chartBackground.setOnMouseExited((event) -> {
        detailsPopup.setVisible(false);
        xLine.setVisible(false);
        yLine.setVisible(false);
        detailsWindow.getChildren().removeAll(xLine, yLine);
    });
    chartBackground.setOnMouseMoved(event -> {
        double x = event.getX() + chartBackground.getLayoutX();
        double y = event.getY() + chartBackground.getLayoutY();

        xLine.setStartX(10);
        xLine.setEndX(detailsWindow.getWidth()-10);
        xLine.setStartY(y+5);
        xLine.setEndY(y+5);

        yLine.setStartX(x+5);
        yLine.setEndX(x+5);
        yLine.setStartY(10);
        yLine.setEndY(detailsWindow.getHeight()-10);

        detailsPopup.showChartDescrpition(event);

        if (y + detailsPopup.getHeight() + 10 < getHeight()) {
            AnchorPane.setTopAnchor(detailsPopup, y+10);
        } else {
            AnchorPane.setTopAnchor(detailsPopup, y-10-detailsPopup.getHeight());
        }

        if (x + detailsPopup.getWidth() + 10 < getWidth()) {
            AnchorPane.setLeftAnchor(detailsPopup, x+10);
        } else {
            AnchorPane.setLeftAnchor(detailsPopup, x-10-detailsPopup.getWidth());
        }
    });
}

private void styleBaseChart(LineChart baseChart) {
    baseChart.setCreateSymbols(false);
    baseChart.setLegendVisible(false);
    baseChart.getXAxis().setAutoRanging(false);
    baseChart.getXAxis().setAnimated(false);
    baseChart.getYAxis().setAnimated(false);
}

private void setFixedAxisWidth(LineChart chart) {
    chart.getYAxis().setPrefWidth(yAxisWidth);
    chart.getYAxis().setMaxWidth(yAxisWidth);
}

private void rebuildChart() {
    getChildren().clear();

    getChildren().add(resizeBaseChart(baseChart));
    for (LineChart lineChart : backgroundCharts) {
        getChildren().add(resizeBackgroundChart(lineChart));
    }
    getChildren().add(detailsWindow);
}

private Node resizeBaseChart(LineChart lineChart) {
    HBox hBox = new HBox(lineChart);
    hBox.setAlignment(Pos.CENTER_LEFT);
    hBox.prefHeightProperty().bind(heightProperty());
    hBox.prefWidthProperty().bind(widthProperty());

    lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));
    lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));
    lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth+yAxisSeparation)*backgroundCharts.size()));

    return lineChart;
}

private Node resizeBackgroundChart(LineChart lineChart) {
    HBox hBox = new HBox(lineChart);
    hBox.setAlignment(Pos.CENTER_LEFT);
    hBox.prefHeightProperty().bind(heightProperty());
    hBox.prefWidthProperty().bind(widthProperty());
    hBox.setMouseTransparent(true);

    lineChart.minWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));
    lineChart.prefWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));
    lineChart.maxWidthProperty().bind(widthProperty().subtract((yAxisWidth + yAxisSeparation) * backgroundCharts.size()));

    lineChart.translateXProperty().bind(baseChart.getYAxis().widthProperty());
    lineChart.getYAxis().setTranslateX((yAxisWidth + yAxisSeparation) * backgroundCharts.indexOf(lineChart));

    return hBox;
}

public void addSeries(XYChart.Series series, Color lineColor) {
    NumberAxis yAxis = new NumberAxis();
    NumberAxis xAxis = new NumberAxis();

    // style x-axis
    xAxis.setAutoRanging(false);
    xAxis.setVisible(false);
    xAxis.setOpacity(0.0); // somehow the upper setVisible does not work
    xAxis.lowerBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).lowerBoundProperty());
    xAxis.upperBoundProperty().bind(((NumberAxis) baseChart.getXAxis()).upperBoundProperty());
    xAxis.tickUnitProperty().bind(((NumberAxis) baseChart.getXAxis()).tickUnitProperty());

    // style y-axis
    yAxis.setSide(Side.RIGHT);
    yAxis.setLabel(series.getName());

    // create chart
    LineChart lineChart = new LineChart(xAxis, yAxis);
    lineChart.setAnimated(false);
    lineChart.setLegendVisible(false);
    lineChart.getData().add(series);

    styleBackgroundChart(lineChart, lineColor);
    setFixedAxisWidth(lineChart);

    chartColorMap.put(lineChart, lineColor);
    backgroundCharts.add(lineChart);
}

private void styleBackgroundChart(LineChart lineChart, Color lineColor) {
    styleChartLine(lineChart, lineColor);

    Node contentBackground = lineChart.lookup(".chart-content").lookup(".chart-plot-background");
    contentBackground.setStyle("-fx-background-color: transparent;");

    lineChart.setVerticalZeroLineVisible(false);
    lineChart.setHorizontalZeroLineVisible(false);
    lineChart.setVerticalGridLinesVisible(false);
    lineChart.setHorizontalGridLinesVisible(false);
    lineChart.setCreateSymbols(false);
}

private String toRGBCode(Color color) {
    return String.format("#%02X%02X%02X",
            (int) (color.getRed() * 255),
            (int) (color.getGreen() * 255),
            (int) (color.getBlue() * 255));
}

private void styleChartLine(LineChart chart, Color lineColor) {
    chart.getYAxis().lookup(".axis-label").setStyle("-fx-text-fill: " + toRGBCode(lineColor) + "; -fx-font-weight: bold;");
    Node seriesLine = chart.lookup(".chart-series-line");
    seriesLine.setStyle("-fx-stroke: " + toRGBCode(lineColor) + "; -fx-stroke-width: " + strokeWidth + ";");
}

public Node getLegend() {
    HBox hBox = new HBox();

    final CheckBox baseChartCheckBox = new CheckBox(baseChart.getYAxis().getLabel());
    baseChartCheckBox.setSelected(true);
    baseChartCheckBox.setStyle("-fx-text-fill: " + toRGBCode(chartColorMap.get(baseChart)) + "; -fx-font-weight: bold;");
    baseChartCheckBox.setDisable(true);
    baseChartCheckBox.getStyleClass().add("readonly-checkbox");
    baseChartCheckBox.setOnAction(event -> baseChartCheckBox.setSelected(true));
    hBox.getChildren().add(baseChartCheckBox);

    for (final LineChart lineChart : backgroundCharts) {
        CheckBox checkBox = new CheckBox(lineChart.getYAxis().getLabel());
        checkBox.setStyle("-fx-text-fill: " + toRGBCode(chartColorMap.get(lineChart)) + "; -fx-font-weight: bold");
        checkBox.setSelected(true);
        checkBox.setOnAction(event -> {
            if (backgroundCharts.contains(lineChart)) {
                backgroundCharts.remove(lineChart);
            } else {
                backgroundCharts.add(lineChart);
            }
        });
        hBox.getChildren().add(checkBox);
    }

    hBox.setAlignment(Pos.CENTER);
    hBox.setSpacing(20);
    hBox.setStyle("-fx-padding: 0 10 20 10");

    return hBox;
}

private class DetailsPopup extends VBox {

    private DetailsPopup() {
        setStyle("-fx-border-width: 1px; -fx-padding: 5 5 5 5px; -fx-border-color: gray; -fx-background-color: whitesmoke;");
        setVisible(false);
    }

    public void showChartDescrpition(MouseEvent event) {
        getChildren().clear();

        Long xValueLong = Math.round((double)baseChart.getXAxis().getValueForDisplay(event.getX()));

        HBox baseChartPopupRow = buildPopupRow(event, xValueLong, baseChart);
        if (baseChartPopupRow != null) {
            getChildren().add(baseChartPopupRow);
        }

        for (LineChart lineChart : backgroundCharts) {
            HBox popupRow = buildPopupRow(event, xValueLong, lineChart);
            if (popupRow == null) continue;

            getChildren().add(popupRow);
        }
    }

    private HBox buildPopupRow(MouseEvent event, Long xValueLong, LineChart lineChart) {
        Label seriesName = new Label(lineChart.getYAxis().getLabel());
        seriesName.setTextFill(chartColorMap.get(lineChart));

        Number yValueForChart = getYValueForX(lineChart, xValueLong.intValue());
        if (yValueForChart == null) {
            return null;
        }
        Number yValueLower = Math.round(normalizeYValue(lineChart, event.getY() - 10));
        Number yValueUpper = Math.round(normalizeYValue(lineChart, event.getY() + 10));
        Number yValueUnderMouse = Math.round((double) lineChart.getYAxis().getValueForDisplay(event.getY()));

        // make series name bold when mouse is near given chart's line
        if (isMouseNearLine(yValueForChart, yValueUnderMouse, Math.abs(yValueLower.doubleValue()-yValueUpper.doubleValue()))) {
            seriesName.setStyle("-fx-font-weight: bold");
        }

        HBox popupRow = new HBox(10, seriesName, new Label("["+yValueForChart+"]"));
        return popupRow;
    }

    private double normalizeYValue(LineChart lineChart, double value) {
        Double val = (Double) lineChart.getYAxis().getValueForDisplay(value);
        if (val == null) {
            return 0;
        } else {
            return val;
        }
    }

    private boolean isMouseNearLine(Number realYValue, Number yValueUnderMouse, Double tolerance) {
        return (Math.abs(yValueUnderMouse.doubleValue() - realYValue.doubleValue()) < tolerance);
    }

    public Number getYValueForX(LineChart chart, Number xValue) {
        List<XYChart.Data> dataList = ((List<XYChart.Data>)((XYChart.Series)chart.getData().get(0)).getData());
        for (XYChart.Data data : dataList) {
            if (data.getXValue().equals(xValue)) {
                return (Number)data.getYValue();
            }
        }
        return null;
    }
}
}

MultipleAxisMainChart:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.function.Function;

public class MultipleAxesLineChartMain extends Application {

    public static final int X_DATA_COUNT = 3600;

    @Override
    public void start(Stage primaryStage) throws Exception{
        NumberAxis xAxis = new NumberAxis(0, X_DATA_COUNT, 200);
        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("Series 1");

        LineChart baseChart = new LineChart(xAxis, yAxis);
        baseChart.getData().add(prepareSeries("Series 1", (x) -> (double)x));

        MultipleAxesLineChart chart = new MultipleAxesLineChart(baseChart, Color.RED);
        chart.addSeries(prepareSeries("Series 2", (x) -> (double)x*x),Color.BLUE);
        chart.addSeries(prepareSeries("Series 3", (x) -> (double)-x*x),Color.GREEN);
        chart.addSeries(prepareSeries("Series 4", (x) -> ((double) (x-250))*x),Color.DARKCYAN);
        chart.addSeries(prepareSeries("Series 5", (x) -> ((double)(x+100)*(x-200))),Color.BROWN);

        primaryStage.setTitle("MultipleAxesLineChart");

        BorderPane borderPane = new BorderPane();
        borderPane.setCenter(chart);
        borderPane.setBottom(chart.getLegend());

        Scene scene = new Scene(borderPane, 1024, 600);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());

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

    private XYChart.Series<Number, Number> prepareSeries(String name, Function<Integer, Double> function) {
        XYChart.Series<Number, Number> series = new XYChart.Series<>();
        series.setName(name);
        for (int i = 0; i < X_DATA_COUNT; i++) {
            series.getData().add(new XYChart.Data<>(i, function.apply(i)));
        }
        return series;
    }

    public static void main(String[] args) {
        launch(args);
    }
}
jediz
  • 4,459
  • 5
  • 36
  • 41
0

made a little change about the UI, resuting left and right to have same number of yaxis, based on Maciej‘s answer。 preview