How to draw multiple axes in a chart using JAVAFX charts

Peace to you!

How to draw multiple axes using JavaFX APIs?

Below is an image of the trend in huge software, where real-time data is built, as well as historical data. There are two registered pens, and a separate axis for each pen is defined in one trend.

Trends with multiple axis in IntelliMax

I am trying to do just that in JavaFX 2.0 diagrams. I was able to build a real time graph that looks like this:

JavaFX 2.0 Line Chart; two NumberSeries with random points plotted

I am researching several axes using JavaFX and can find the link, but I think it is an older version of JavaFX using FXML. However, for this I use the usual JavaFX classes.

HELP !!!

+7
source share
4 answers

Here you can find my solution - MultipleAxesLineChart . It is not generic and just fits my needs, but I think that it can well understand how this can be done using StackPane .

MultipleAxesLineChart

+9
source

Place the two charts in the StackPane .

Use the css lookups at the top of the chart to translate (using translate-x and translate-y) its axes and labels so that they can be read regardless of the bottom chart. Leave the chart graph on top so that it overlays the bottom graph. Change the colors and legends (or mix chart styles such as rows and rows) to make it clear which data graph belongs to that series.

Some of the above methods are shown:

layered

+5
source
 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 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); } } 
0
source

made a small change about the UI, resuting left and right to have the same amount of YAxis, based on Maciej's answer. Preview

0
source

Source: https://habr.com/ru/post/910498/


All Articles