javajavafx

Changing styles of individual lines drawn in JavaFX


I need to change the styles of the graph lines. At the moment, I have connected the CSS file and found how to change styles I need. But these parameters only apply to all lines. In .java, I found the ability to change these parameters, but I can't change multiple parameters at the same time. I can change the line type of the graph and the point color, but I can't change the point color of the legend. Here is the error that the IDE gives me:

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:465)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:364)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1082)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:901)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:196)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.NullPointerException: Cannot invoke "javafx.scene.Node.setStyle(String)" because the return value of "javafx.scene.Node.lookup(String)" is null
    at com.example.lab2_int/com.example.lab2_int.HelloApplication.start(HelloApplication.java:290)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:847)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:484)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
    ... 1 more
Exception running application com.example.lab2_int.HelloApplication

My code(I give only the necessary parts, if you need more code, you can say about it):

       package com.example.lab3_int;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.scene.Node;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        NumberAxis xAxis0 = new NumberAxis();
        NumberAxis yAxis0 = new NumberAxis();

        LineChart<Number, Number> lineChart0 = new  LineChart<>(xAxis0, yAxis0);

        XYChart.Series<Number, Number> exception_g = new XYChart.Series<>();
        exception_g.setName("Exceptions");
        XYChart.Series<Number, Number> w0_g = new XYChart.Series<>();
        w0_g.setName("w0");
        XYChart.Series<Number, Number> w1_g = new XYChart.Series<>();
        w1_g.setName("w1");
        XYChart.Series<Number, Number> w2_g = new XYChart.Series<>();
        w2_g.setName("w2");
        XYChart.Series<Number, Number> w3_g = new XYChart.Series<>();
        w3_g.setName("w3");

        exception_g.getData().add(new XYChart.Data<>(0,7));
        exception_g.getData().add(new XYChart.Data<>(1,17));
        exception_g.getData().add(new XYChart.Data<>(2,-2));

        w0_g.getData().add(new XYChart.Data<>(0,11));
        w0_g.getData().add(new XYChart.Data<>(1,6));
        w0_g.getData().add(new XYChart.Data<>(2,0));

        w1_g.getData().add(new XYChart.Data<>(0,-7));
        w1_g.getData().add(new XYChart.Data<>(1,9));
        w1_g.getData().add(new XYChart.Data<>(2,13));

        w2_g.getData().add(new XYChart.Data<>(0,9));
        w2_g.getData().add(new XYChart.Data<>(1,2));
        w2_g.getData().add(new XYChart.Data<>(2,5));

        w3_g.getData().add(new XYChart.Data<>(0,14));
        w3_g.getData().add(new XYChart.Data<>(1,-10));
        w3_g.getData().add(new XYChart.Data<>(2,5));


        lineChart0.getStylesheets().add("/stylesheet.css");


        lineChart0.getData().add(exception_g);
        lineChart0.getData().add(w0_g);
        lineChart0.getData().add(w1_g);
        lineChart0.getData().add(w2_g);
        lineChart0.getData().add(w3_g);


        exception_g.getNode().lookup(".chart-series-line").setStyle("-fx-stroke: black;-fx-stroke-dash-array: 5 5;");
        for (XYChart.Data<Number, Number> data : exception_g.getData()) {
            Node symbol = data.getNode().lookup(".chart-line-symbol");
            symbol.setStyle("-fx-background-color: black, white;");
        }
        exception_g.getNode().lookup(".chart-legend").setStyle("-fx-background-color: black, white;"); //It is this line that the ide refers to.

        GridPane pane = new GridPane();
        pane.add(lineChart0, 0, 0);

        Scene scene = new Scene(pane, 1920, 1000);
        stage.setScene(scene);
        stage.show();
    }

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

I decided to leave the selectors empty in the css file:

.chart-series-line:{
}

.chart-line-symbol{
}

.chart-legend{
}

Without this line( exception_g.getNode().lookup(".chart-legend").setStyle("-fx-background-color: black, white;"); ), everything works fine except for the legend, like this:enter image description here

Perhaps there is some way to change the parameters of the lines directly in css. I tried use selector .chart-series-line:series(0) {...} in CSS with the name of my axis, and with the line id, but it didn't help. Or else everything is not so simple here and it is necessary to prescribe in .java file all styles. Both solutions to this problem will work.


Solution

  • Example for generating a chart with 17 different colors for the series.

    The example generates a stylesheet in code, then applies that, which has some flexibility because you can change the generation function to create whatever style you want dynamically at runtime. But you could use a static stylesheet instead if that fits your purpose.

    After I wrote this, I noted Slaw posted a similar idea in comments:

    Just do .chart-series-line.series<n> to target the line and .chart-line-symbol.series<n> to target the line/legend symbol.

    That works for an arbitrarily large value of n, at least in JavaFX 20.

    Note the line is a Path by default, so set -fx-stroke, and the symbol is a Region by default, so set -fx-background-color.

    The insight (for me) on finding the correct CSS selectors to use was by looking at how the selectors from modena.css are applied in the LineChart code.

    linechart

    Example Many Colored Chart

    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.stage.Stage;
    
    public class LineChartSample extends Application {
    
        private final String[] CHART_COLOR_NAMES = {
                "blue",
                "blueviolet",
                "burlywood",
                "cadetblue",
                "chartreuse",
                "chocolate",
                "cornflowerblue",
                "red",
                "cyan",
                "darkgreen",
                "darkmagenta",
                "darksalmon",
                "darkseagreen",
                "deeppink",
                "gold",
                "grey",
                "indigo"
        };
    
        private static final String CSS_DATA_URL_TYPE = "data:text/css,";
        private static final String LINE_CHART_SERIES_CSS_TEMPLATE =
               """
               .series%d.chart-line-symbol { -fx-background-color: %s, white; }
               .series%d.chart-series-line { -fx-stroke: %s; }
               """;
    
        @Override public void start(Stage stage) {
            final NumberAxis xAxis = new NumberAxis();
            final NumberAxis yAxis = new NumberAxis();
    
            xAxis.setLabel("Number of Month");
            final LineChart<Number,Number> lineChart = new LineChart<>(xAxis, yAxis);
            lineChart.getStylesheets().add(
                    createLineChartStyleSheet()
            );
    
            for (int i = 0; i < CHART_COLOR_NAMES.length; i++) {
                XYChart.Series<Number, Number> series = new XYChart.Series<>();
                series.setName("Series " + i + " " + CHART_COLOR_NAMES[i]);
    
                genChartData(i * 5, series);
    
                lineChart.getData().add(series);
            }
    
            Scene scene = new Scene(lineChart,800,600);
            stage.setScene(scene);
            stage.show();
        }
    
        private static void genChartData(int offset, XYChart.Series<Number, Number> series) {
            series.getData().add(new XYChart.Data<>(1, 23 + offset));
            series.getData().add(new XYChart.Data<>(2, 14 + offset));
            series.getData().add(new XYChart.Data<>(4, 24 + offset));
            series.getData().add(new XYChart.Data<>(5, 34 + offset));
            series.getData().add(new XYChart.Data<>(6, 36 + offset));
            series.getData().add(new XYChart.Data<>(7, 22 + offset));
            series.getData().add(new XYChart.Data<>(8, 45 + offset));
            series.getData().add(new XYChart.Data<>(9, 43 + offset));
            series.getData().add(new XYChart.Data<>(10, 17 + offset));
            series.getData().add(new XYChart.Data<>(11, 29 + offset));
            series.getData().add(new XYChart.Data<>(12, 25 + offset));
        }
    
        private String createLineChartStyleSheet() {
            StringBuilder cssBuf = new StringBuilder(CSS_DATA_URL_TYPE);
    
            for (int i = 0; i < CHART_COLOR_NAMES.length; i++) {
                cssBuf.append(
                        LINE_CHART_SERIES_CSS_TEMPLATE
                                .formatted(
                                        i, CHART_COLOR_NAMES[i],
                                        i, CHART_COLOR_NAMES[i]
                                )
                );
            }
    
            return cssBuf.toString();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Example Static CSS for Many Colored Chart

    If you wanted to do this statically rather than generating the CSS, the CSS would be:

    .series0.chart-line-symbol { -fx-background-color: blue, white; }
    .series0.chart-series-line { -fx-stroke: blue; }
    .series1.chart-line-symbol { -fx-background-color: blueviolet, white; }
    .series1.chart-series-line { -fx-stroke: blueviolet; }
    .series2.chart-line-symbol { -fx-background-color: burlywood, white; }
    .series2.chart-series-line { -fx-stroke: burlywood; }
    .series3.chart-line-symbol { -fx-background-color: cadetblue, white; }
    .series3.chart-series-line { -fx-stroke: cadetblue; }
    .series4.chart-line-symbol { -fx-background-color: chartreuse, white; }
    .series4.chart-series-line { -fx-stroke: chartreuse; }
    .series5.chart-line-symbol { -fx-background-color: chocolate, white; }
    .series5.chart-series-line { -fx-stroke: chocolate; }
    .series6.chart-line-symbol { -fx-background-color: cornflowerblue, white; }
    .series6.chart-series-line { -fx-stroke: cornflowerblue; }
    .series7.chart-line-symbol { -fx-background-color: red, white; }
    .series7.chart-series-line { -fx-stroke: red; }
    .series8.chart-line-symbol { -fx-background-color: cyan, white; }
    .series8.chart-series-line { -fx-stroke: cyan; }
    .series9.chart-line-symbol { -fx-background-color: darkgreen, white; }
    .series9.chart-series-line { -fx-stroke: darkgreen; }
    .series10.chart-line-symbol { -fx-background-color: darkmagenta, white; }
    .series10.chart-series-line { -fx-stroke: darkmagenta; }
    .series11.chart-line-symbol { -fx-background-color: darksalmon, white; }
    .series11.chart-series-line { -fx-stroke: darksalmon; }
    .series12.chart-line-symbol { -fx-background-color: darkseagreen, white; }
    .series12.chart-series-line { -fx-stroke: darkseagreen; }
    .series13.chart-line-symbol { -fx-background-color: deeppink, white; }
    .series13.chart-series-line { -fx-stroke: deeppink; }
    .series14.chart-line-symbol { -fx-background-color: gold, white; }
    .series14.chart-series-line { -fx-stroke: gold; }
    .series15.chart-line-symbol { -fx-background-color: grey, white; }
    .series15.chart-series-line { -fx-stroke: grey; }
    .series16.chart-line-symbol { -fx-background-color: indigo, white; }
    .series16.chart-series-line { -fx-stroke: indigo; }
    

    Example with Interactivity

    If you wish to change the styles at runtime, you can dynamically generate a new style sheet with different values and replace the existing stylesheet on the line chart with your new one. Or, you can define different style classes and change the associated style classes for the series when the series node is interacted with.

    The example below demonstrates the changing style class technique.

    If you hover over a line in a series, it will be highlighted. If you click on it, the style class to make the line dashed will be added or removed to indicate that the series has been selected. For simplification, the interactivity is only placed on the line, but you could also do the same for the symbols if you wished (to do that you would need to get the symbol nodes from the data and modify the style class). Adding similar interactivity to the legend would be a bit trickier as I am not sure there is enough public API to access it.

    import javafx.application.Application;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.chart.LineChart;
    import javafx.scene.chart.NumberAxis;
    import javafx.scene.chart.XYChart;
    import javafx.stage.Stage;
    
    public class LineChartHighlightSample extends Application {
    
        private static final String[] CHART_COLOR_NAMES = {
                "blue",
                "blueviolet",
                "burlywood",
                "cadetblue",
                "chartreuse",
                "chocolate",
                "cornflowerblue",
                "red",
                "cyan",
                "darkgreen",
                "darkmagenta",
                "darksalmon",
                "darkseagreen",
                "deeppink",
                "gold",
                "grey",
                "indigo"
        };
    
        private static final String CSS_DATA_URL_TYPE = "data:text/css,";
        private static final String LINE_CHART_SERIES_CSS_TEMPLATE =
               """
               .series%d.chart-line-symbol { -fx-background-color: %s, white; }
               .series%d.chart-series-line { -fx-stroke: %s; }
               .series%d.chart-series-line:hover { -fx-effect: dropshadow(gaussian, gold, 10, 0, 0, 0); }
               .series%d.chart-series-line.selected { -fx-stroke: %s; -fx-stroke-dash-array: 12 8; }
               """;
    
        private static final String SELECTED_SERIES_STYLE_CLASS = "selected";
    
        @Override public void start(Stage stage) {
            final NumberAxis xAxis = new NumberAxis();
            final NumberAxis yAxis = new NumberAxis();
    
            xAxis.setLabel("Number of Month");
            final LineChart<Number,Number> lineChart = new LineChart<>(xAxis, yAxis);
            lineChart.getStylesheets().add(
                    createLineChartStyleSheet()
            );
    
            for (int i = 0; i < CHART_COLOR_NAMES.length; i++) {
                XYChart.Series<Number, Number> series = new XYChart.Series<>();
                series.setName("Series " + i + " " + CHART_COLOR_NAMES[i]);
    
                genChartData(i * 5, series);
    
                lineChart.getData().add(series);
    
                final int finalIdx = i;
                series.getNode().setOnMouseClicked(e -> handleSeriesClicked(lineChart, finalIdx));
            }
    
            Scene scene = new Scene(lineChart,800,600);
            stage.setScene(scene);
            stage.show();
        }
    
        private void handleSeriesClicked(LineChart<Number, Number> lineChart, int seriesIdx) {
            ObservableList<String> styleClasses =
                    lineChart
                            .getData()
                            .get(seriesIdx)
                            .getNode()
                            .getStyleClass();
    
            if (styleClasses.contains(SELECTED_SERIES_STYLE_CLASS)) {
                styleClasses.remove(SELECTED_SERIES_STYLE_CLASS);
            } else {
                styleClasses.add(SELECTED_SERIES_STYLE_CLASS);
            }
        }
    
        private static void genChartData(int offset, XYChart.Series<Number, Number> series) {
            series.getData().add(new XYChart.Data<>(1, 23 + offset));
            series.getData().add(new XYChart.Data<>(2, 14 + offset));
            series.getData().add(new XYChart.Data<>(4, 24 + offset));
            series.getData().add(new XYChart.Data<>(5, 34 + offset));
            series.getData().add(new XYChart.Data<>(6, 36 + offset));
            series.getData().add(new XYChart.Data<>(7, 22 + offset));
            series.getData().add(new XYChart.Data<>(8, 45 + offset));
            series.getData().add(new XYChart.Data<>(9, 43 + offset));
            series.getData().add(new XYChart.Data<>(10, 17 + offset));
            series.getData().add(new XYChart.Data<>(11, 29 + offset));
            series.getData().add(new XYChart.Data<>(12, 25 + offset));
        }
    
        private String createLineChartStyleSheet() {
            StringBuilder cssBuf = new StringBuilder(CSS_DATA_URL_TYPE);
    
            for (int i = 0; i < CHART_COLOR_NAMES.length; i++) {
                cssBuf.append(
                        LINE_CHART_SERIES_CSS_TEMPLATE
                                .formatted(
                                        i, CHART_COLOR_NAMES[i],
                                        i, CHART_COLOR_NAMES[i],
                                        i, i, CHART_COLOR_NAMES[i]
                                )
                );
            }
    
            System.out.println(cssBuf);
    
            return cssBuf.toString();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }