FX Experience Has Gone Read-Only

I've been maintaining FX Experience for a really long time now, and I love hearing from people who enjoy my weekly links roundup. One thing I've noticed recently is that maintaining two sites (FX Experience and JonathanGiles.net) takes more time than ideal, and splits the audience up. Therefore, FX Experience will become read-only for new blog posts, but weekly posts will continue to be published on JonathanGiles.net. If you follow @FXExperience on Twitter, I suggest you also follow @JonathanGiles. This is not the end - just a consolidation of my online presence to make my life a little easier!

tl;dr: Follow me on Twitter and check for the latest news on JonathanGiles.net.

I was experimenting today with extending AreaChart to do curve fitting for some example code I was hacking on. It is also a example of what can be done with styling JavaFX charts with CSS. Here is the result:



Curve Fitting

I have to admit I did not work out all the Math myself for the curve fitting I found a great article on CodeProject “Draw a Smooth Curve through a Set of 2D Points with Bezier Primitives”. Then ported the code to Java from C# and wired it up to our AreaChart. My version is not the most efficient as we do not yet have the correct API hooks to make it easy but we are thinking about how to add them. The current implementation extends AreaChart and overrides the layoutPlotChildren() method. That method creates all the nodes need for the chart and populates the node properties of the Series and Data items. So I can call super to let the standard code do its job then look at the Path elements for the stroked Path and filled Path that make up the AreaChart. I can then replace the LineTo elements with CubicCurveTo with the new control points calculated with the maths from the article. This way everything like auto ranging and animation still works as expected. The only waste is we are creating all the LineTo object and then throwing them away to replace them with CubicCurveTo objects. So here is the code that does this:

 
    /** @inheritDoc */
    @Override protected void layoutPlotChildren() {
        super.layoutPlotChildren();
        for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
            final XYChart.Series<Number, Number> series = getData().get(seriesIndex);
            final Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1);
            final Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0);
            smooth(seriesLine.getElements(), fillPath.getElements());
        }
    }

    private int getDataSize() {
        final ObservableList<XYChart.Series<Number, Number>> data = getData();
        return (data!=null) ? data.size() : 0;
    }

    private static void smooth(ObservableList<PathElement> strokeElements, ObservableList<PathElement> fillElements) {
        // as we do not have direct access to the data, first recreate the list of all the data points we have
        final Point2D[] dataPoints = new Point2D[strokeElements.size()];
        for (int i = 0; i < strokeElements.size(); i++) {
            final PathElement element = strokeElements.get(i);
            if (element instanceof MoveTo) {
                final MoveTo move = (MoveTo)element;
                dataPoints[i] = new Point2D(move.getX(), move.getY());
            } else if (element instanceof LineTo) {
                final LineTo line = (LineTo)element;
                final double x = line.getX(), y = line.getY();
                dataPoints[i] = new Point2D(x, y);
            }
        }
        // next we need to know the zero Y value
        final double zeroY = ((MoveTo) fillElements.get(0)).getY();

        // now clear and rebuild elements
        strokeElements.clear();
        fillElements.clear();
        Pair<Point2D[], Point2D[]> result = calcCurveControlPoints(dataPoints);
        Point2D[] firstControlPoints = result.getKey();
        Point2D[] secondControlPoints = result.getValue();
        // start both paths
        strokeElements.add(new MoveTo(dataPoints[0].getX(),dataPoints[0].getY()));
        fillElements.add(new MoveTo(dataPoints[0].getX(),zeroY));
        fillElements.add(new LineTo(dataPoints[0].getX(),dataPoints[0].getY()));
        // add curves
        for (int i = 1; i < dataPoints.length; i++) {
            final int ci = i-1;
            strokeElements.add(new CubicCurveTo(
                    firstControlPoints[ci].getX(),firstControlPoints[ci].getY(), 
                    secondControlPoints[ci].getX(),secondControlPoints[ci].getY(), 
                    dataPoints[i].getX(),dataPoints[i].getY()));
            fillElements.add(new CubicCurveTo(
                    firstControlPoints[ci].getX(),firstControlPoints[ci].getY(), 
                    secondControlPoints[ci].getX(),secondControlPoints[ci].getY(), 
                    dataPoints[i].getX(),dataPoints[i].getY()));
        }
        // end the paths
        fillElements.add(new LineTo(dataPoints[dataPoints.length-1].getX(),zeroY));
        fillElements.add(new ClosePath());
    }

CSS Styling

This is just the standard AreaChart in terms of the nodes its drawing with. The first part of the CSS is styling the main chart node and its background. We are using a dark gray texture as the background, adding a 1px black border and a drop shadow. Also adjusting the padding a little.

.chart {
    -fx-background-image: url("background.png");
    -fx-padding: 15 25 15 15;    
    -fx-border-color: black;
    -fx-effect: dropshadow( gaussian , rgba(0,0,0,0.8) , 10, 0.0 , 0 , 2 );
}

Next we are looking at the background of the plot area, this is the rectangular area bordered by the two axis. We are using two background images here one a colored glow and the second is transparent white gridlines. The first image is sized to “cover” the whole area of the plot background and the second is tiled to fill. We also add black borders to the top and right to complete a box around the plot area including the two axis lines.

.chart-plot-background {
    -fx-background-image: url("chart-background.png"), url("graph-gridlines.png");
    -fx-background-size: cover, auto;
    -fx-background-repeat: no-repeat, repeat;
    -fx-background-position: 0% 0%, 0% 100%;
    -fx-border-color: black black transparent transparent;
}

The line that connects the data points we are styling to be 2px wide and white.

.chart-series-area-line { 
    -fx-stroke: white; 
    -fx-stroke-width: 2px; 
}

The filled area under the line we are styling with a linear gradient from white on the left to 100% transparent white on the right. We also use a blend mode to get the desired effect when blending with the colored background of the plot area.

.chart-series-area-fill { 
    -fx-fill: linear-gradient(to right, white, rgba(255,255,255,0)); 
    -fx-blend-mode: OVERLAY;
}

Making the plot symbols for each data point white.

.chart-area-symbol {
    -fx-background-color: white;
}

Hiding the tick marks and changing the color of the axis lines to black

.axis {
    -fx-tick-mark-visible: false;
    -fx-minor-tick-visible: false;
    -fx-tick-length: 3;
    -fx-minor-tick-length: 0;
    -fx-border-color: transparent;
}
.axis:bottom {
    -fx-border-color: black transparent transparent transparent;
}
.axis:left {
    -fx-border-color: transparent black transparent transparent;
}

Styling the text labels on the axis so they are white with black shadow and a little larger than default.

.axis Text{
    -fx-fill: white;
}
.tick-mark {
    -fx-font-size: 10px;
    -fx-effect: dropshadow( one-pass-box , rgba(0,0,0,0.8) , 0, 0.0 , 0 , 1 );
}

There you go that is all you need to take one of our standard charts and make it look completely different. Hopefully this will inspire some of you to try some exciting designs with JavaFX charts.

Source Code

The full Netbeans project and code including the maths is available here (CurveFittedChart.zip).