JavaFX Chart Coloring

Working on a Java(FX) port of my hoplite simulator Myriarch, I ran into an unexpected problem with the history chart. This part of the “Simulation Report” dialog shows how each faction’s unit count changes over the course of the simulation. You can see a screenshot from the current build below.

The control is a standard JavaFX LineChart, a subclass of the versatile Chart API available since JavaFX 2. The original .NET version of Myriarch had used a custom chart. The good news is that LineChart does exactly what I want, so I could save quite a bit of work.

Colored Chart

CSS Chart Styling

The bad news is that the JavaFX Chart API lacks one crucial feature, namely customizing the colors of the lines and symbols (circles) for each data series. I wanted them to match the faction colors defined by the simulation which are known only at runtime.

But Chart only supports styling through the JavaFX CSS mechanism, as described in Styling Charts with CSS. All examples in that article use a static CSS stylesheet to define custom colors and shapes. Useless to me, as I would have to build an actual disk file at runtime and load it as a resource.

JavaFX display nodes do offer a style property to set CSS styles at runtime – but only for the specific node the method is called on. This property does not accept CSS class selectors and is not inherited by sub-nodes, so attempting to set line or symbol colors on the LineChart itself does nothing.

Coloring Lines and Symbols

That is because lines and symbols are represented by sub-nodes nested deeply within the display structure of the LineChart. So in order to change their colors at runtime, we must first build the LineChart and then use the lookup(All) inspection method to find the actual display nodes for lines and symbols.

The official Oracle documentation is unfortunately silent on this, but John Smith (Jewelsea) has posted a number of replies with sample code on Stack Overflow which helped me along. You can find the complete method from the current Myriarch build below. Here are the necessary steps:

  1. Create a LineChart and fetch each data Series, here prebuilt for each simulation faction.
  2. Add the Series to the LineChart. That is important, as this adding triggers the creation of display nodes within the Series.
  3. Get the desired color for the Series and convert it to a CSS rgba string.
  4. Look up the (single) node with a CSS class named .chart-series-line within the Series node. This is the display node for the line. Set its color via -fx-stroke.
  5. Iterate through all Data items within the Series. Each symbol (here the default circle) painted over the Series line corresponds to one Data item.
  6. Look up the (single) node with a CSS class named .chart-line-symbol within each Data node. This is the display node for the symbol. Set its color via -fx-background-color (!). The secondary color (here whitesmoke) is the interior of the symbol.

Coloring the Chart Legend

So far for the chart’s data display. However, we also want to show a legend that explains what the colors represent. And unlike with the CSS stylesheet procedure, the symbols in the legend are not affected by the above changes. Once again they use separate display nodes that must be tracked down individually.

Here I found an additional problem. While the display nodes for Series and Data nodes are created as soon as a Series is added to the LineChart, the legend is not actually created until the chart is added to a Scene, i.e. the dialog’s top layout node.

You could simply add the requisite code after the chart has been added to the Scene in the dialog’s constructor, but I prefer to have the entire chart creation encapsulated in one method. So instead I used Platform.runLater to ensure that legend nodes are looked up only after the dialog has been built. Here’s what happens then:

  1. Look up all nodes with a CSS class named .chart-legend-item-symbol within the LineChart. These are the display nodes for symbols within the legend.
  2. But which legend symbol corresponds to which Series? To find out, iterate through all CSS style classes defined on the display node.
  3. One of those classes is of course .chart-legend-item-symbol but we are interested in classes of the form seriesN where N is an integer number. These numbers count up from zero, indicating the order in which each Series has been added.
  4. Importantly, this number may differ from any external identifiers used in constructing the Series (here faction keys). So the Series insertion order is tracked by a separate symbolStyles array while building the chart.
  5. Split off and parse the N part of the seriesN class name. Set the current node’s color to the corresponding symbolStyles element.
/**
 * Creates a {@link Unit} chart for the specified {@link Faction} map.
 * @param units a {@link Map} that maps {@link Faction} keys to the corresponding
 *              {@link XYChart.Series} of present or steady {@link Unit} objects
 * @return the {@link LineChart} for the specified {@code units}
 * @throws NullPointerException if {@code units} is {@code null}
 */
private LineChart<Number, Number> createChart(Map<Integer, XYChart.Series<Number, Number>> units) {
    final LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());
    chart.setCreateSymbols(true);

    final Collection<Faction> factions = _simulation.factions.collection().values();
    final String[] symbolStyles = new String[factions.size()];
    int index = 0;

    for (Faction faction: factions) {
        final XYChart.Series<Number, Number> series = units.get(faction.key());
        chart.getData().add(series);

        // convert Faction color to CSS format
        final String color = String.format("rgba(%d, %d, %d, 1.0)",
                faction.colorRed(), faction.colorGreen(), faction.colorBlue());

        // set line color on Series node
        final String lineStyle = String.format("-fx-stroke: %s;", color);
        series.getNode().lookup(".chart-series-line").setStyle(lineStyle);

        // set symbol color on Data nodes, remember for legend nodes
        final String symbolStyle = String.format("-fx-background-color: %s, whitesmoke;", color);
        symbolStyles[index++] = symbolStyle;
        for (XYChart.Data<Number, Number> data: series.getData())
            data.getNode().lookup(".chart-line-symbol").setStyle(symbolStyle);
    }

    /*
     * Set remembered symbol colors (in Chart insertion order) on legend nodes.
     * We must use runLater here because the legend is only created on display.
     */
    Platform.runLater(() -> {
        for (Node node: chart.lookupAll(".chart-legend-item-symbol"))
            for (String styleClass: node.getStyleClass())
                if (styleClass.startsWith("series")) {
                    final int i = Integer.parseInt(styleClass.substring(6));
                    node.setStyle(symbolStyles[i]);
                    break;
                }
    });

    return chart;
}

One Final Problem

The dialog requires charts for two different data sets, using identical axes. Initially I tried to reproduce the .NET implementation where only one chart element existed, and radio buttons selected which data to show. In response I cleared the chart’s data and re-added the requisite Series set.

That did not work out too well. The LineChart would correctly display the first data set, then the second data set after toggling. Any further switching between data sets would result in the loss of all symbols overlaid on the data lines. I reckon this is because Series holds internal information related to its LineGraph and gets confused when it is removed and later re-added.

Perhaps a workable solution would be to clear each Series and manually reinsert Data items instead. For now, though, I simply switched my implementation to creating two LineGraph objects, one for each data set, and leaving them alone after construction.

2 thoughts on “JavaFX Chart Coloring

  1. Pingback: Java desktop links of the week, May 15 – Jonathan Giles

  2. Pingback: 5月15日,本周 JavaFX 链接 | JavaFX中文、OSGi、Eclipse开源资料

Leave a Reply