2015-06-26

JavaFX: チャートのカスタマイズ (1)

JavaFX のチャートを使いこなせるようになりたいと、いくつかチャートのサンプルを紹介してきました。前回は ScatterChart を利用した散布図のサンプルを紹介しましたが、やはり、点と線をチャートでは自由に扱いたいと感じました。データ点(シンボル)と線を別々に扱いたければ、LineChart で、データ点については点と点を結ぶ線を透明にすれば良いのですが、なんだかとても無駄な気がします。

X と Y 座標でデータ表現する ScatterChart などのクラスは、抽象クラス XYChart を継承しています。JavaFX のチャートをカスタマイズしたければ、XYChart を継承して好きなようにチャートを作れば良いのですが、いきなりは敷居が高いです。そこで、線も点(シンボル)も扱える LineChart を継承したクラスで、部分的に機能をカスタマイズしてみます。

動作環境は次の通りです。

  • OS: Fedora 22 x86_64
  • jdk1.8.0_45-1.8.0_45-fcs.x86_64 (Oracle)
  • IDE: NetBeans IDE 8.0.2
  • commons-math3-3.5

今回は、回帰計算をするために、Apache Commons Math ライブラリを使用しています

さて、まず LineChart を継承するクラス MyChart1 です。ここでは、最初のデータ群 (series) をデータ点(シンボル)のみ、二番目以降のデータ群はシンボルなしで直線で結ぶだけ、という仕様にしました。

MyChart1 では、メソッド layoutPlotChildren をオーバーライドしています。参考サイト [1] にある LineChart のメソッド layoutPlotChildren のコードに手を加えました。なお、JavaFX Demos and Samples の CandleStickChart にある layoutPlotChildren メソッドを参考にしています。

リスト:MyChart1.java 
package mychartsample;

import java.util.Iterator;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

/**
 * MyChart1
 *
 * @author bitwalk
 * @param <X>
 * @param <Y>
 */
public class MyChart1<X, Y> extends LineChart<X, Y> {

    // -------------- CONSTRUCTORS ----------------------------------------------
    /**
     * Construct a new MyChart with the given axis.
     *
     * @param xAxis The x axis to use
     * @param yAxis The y axis to use
     */
    public MyChart1(Axis<X> xAxis, Axis<Y> yAxis) {
        super(xAxis, yAxis);
    }

    // -------------- METHODS ------------------------------------------------------------------------------------------
    /**
     *
     */
    @Override
    protected void layoutPlotChildren() {
        for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
            Series<X, Y> series = getData().get(seriesIndex);
            Iterator<Data<X, Y>> iter = getDisplayedDataIterator(series);
            boolean isFirst = true;

            if (series.getNode() instanceof Path) {
                Path seriesLine = (Path) series.getNode();
                seriesLine.getElements().clear();

                while (iter.hasNext()) {
                    Data<X, Y> item = iter.next();
                    double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
                    double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));

                    if (seriesIndex == 0) {
                        // 最初のデータ群はシンボルのみ
                        Node symbol = item.getNode();
                        if (symbol != null) {
                            final double w = symbol.prefWidth(-1);
                            final double h = symbol.prefHeight(-1);
                            symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h);
                        }
                    } else {
                        // 最初のデータ群でない場合は線のみ
                        if (isFirst) {
                            isFirst = false;
                            seriesLine.getElements().add(new MoveTo(x, y));
                        } else {
                            seriesLine.getElements().add(new LineTo(x, y));
                        }
                    }
                }
            }
        }
    }
}

MyChart1 を使ってチャートを表示するサンプル MyChart1Sample.java では、[2] で紹介したサンプルをベースにして、同じデータ点に、Commons Math ライブラリで計算した回帰直線と三次の重回帰式を加えてみました。

リスト:MyChart1Sample.java 
package mychartsample;

import java.util.ArrayList;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;
import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression;
import org.apache.commons.math3.stat.regression.SimpleRegression;

/**
 * MyChart1Sample - for sample of MyChart1, customized LineChart
 *
 * @author bitwalk
 */
public class MyChart1Sample extends Application {

    @Override
    public void start(Stage stage) {
        stage.setTitle("MyChart Sample");

        final NumberAxis xAxis = new NumberAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("X軸ラベル(時間)");
        yAxis.setLabel("Y軸ラベル(数値)");

        final MyChart1<Number, Number> myChart = new MyChart1<>(xAxis, yAxis);
        myChart.setTitle("最小二乗法のサンプル");

        myChart.setAnimated(false);
        myChart.setCreateSymbols(true);

        myChart.setData(getChartData());

        Scene scene = new Scene(myChart, 600, 400);
        scene.getStylesheets().add(getClass().getResource("MyChart1.css").toExternalForm());

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

    /**
     * getChartData
     *
     * @return ObservableList<XYChart.Series<String, Double>>
     */
    private ObservableList<XYChart.Series<Number, Number>> getChartData() {
        Series<Number, Number> series1 = new Series<>();
        Series<Number, Number> series2 = new Series<>();
        Series<Number, Number> series3 = new Series<>();

        // raw data
        ArrayList<PairData> dataArr = new ArrayList<>();
        dataArr.add(setPair(1d, 3d));
        dataArr.add(setPair(2d, 7d));
        dataArr.add(setPair(3d, 14d));
        dataArr.add(setPair(4d, 10d));
        dataArr.add(setPair(5d, 17d));

        // data range used for line/curve
        ArrayList<Double> xRange = new ArrayList<>();
        xRange.add(0.5);
        xRange.add(5.5);

        // for simple regression
        SimpleRegression regression = new SimpleRegression();

        // for multiple regression
        OLSMultipleLinearRegression mulreg = new OLSMultipleLinearRegression();
        double[] y_mulreg = new double[dataArr.size()];
        double[][] x_mulreg = new double[dataArr.size()][];

        // raw data setting and regression
        series1.setName("観測データ");
        for (int i = 0; i < dataArr.size(); i++) {
            double x = dataArr.get(i).X;
            double y = dataArr.get(i).Y;
            series1.getData().add(new XYChart.Data(x, y));

            // simple regression
            regression.addData(x, y);

            // muliple regression for cubic
            y_mulreg[i] = y;
            x_mulreg[i] = new double[]{x, x * x, x * x * x};
        }

        // regression
        series2.setName("単回帰(直線)");
        xRange.stream().forEach((xr) -> {
            series2.getData().add(new XYChart.Data(xr, regression.predict(xr)));
        });

        // multiple requression
        series3.setName("重回帰(三次式)");
        mulreg.newSampleData(y_mulreg, x_mulreg);
        double[] beta = mulreg.estimateRegressionParameters();

        double x0 = xRange.get(0);
        while (x0 <= xRange.get(1)) {
            series3.getData().add(new XYChart.Data(x0, getPolynomialValue(beta, x0)));
            x0 = x0 + 0.01;
        }

        // set series data to collection
        ObservableList<XYChart.Series<Number, Number>> seriesList = FXCollections.observableArrayList();
        seriesList.addAll(series1, series2, series3);

        return seriesList;
    }

    /**
     * getPolynomialValue
     *
     * @param coef coefficient
     * @param x x value
     * @return calculated polynomial
     */
    private double getPolynomialValue(double[] coef, double x) {
        double y = 0d;
        for (int i = 0; i < coef.length; i++) {
            y = y + coef[i] * Math.pow(x, i);
        }
        return y;
    }

    /**
     * setPair - set pair of data to the PairData structure
     *
     * @param x
     * @param y
     * @return PairData
     */
    private PairData setPair(double x, double y) {
        PairData data = new PairData();
        data.X = x;
        data.Y = y;

        return data;
    }

    /**
     * PairData - structure for handling (x, y) data
     */
    private class PairData {

        double X;
        double Y;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

CSS は、チャートの主要部分は今まで紹介したサンプルで使用した CSS をベースとしていますが、シンボルとラインの修飾については、今回の仕様に合うように書き直しています。

リスト:MyChart.css 
.chart {
    -fx-padding: 10px;
    -fx-background-color: white;
}

.chart-content {
    -fx-padding: 10px;    
}

.chart-title {
    -fx-font-size: 18pt;    
}

.chart-vertical-grid-lines {
    -fx-stroke: #c1e0fd;
}
.chart-horizontal-grid-lines {
    -fx-stroke: #c1e0fd;
}

.chart-legend {
    -fx-font-size: 12pt;    
    -fx-background-color:  transparent;
    -fx-padding: 10px;
}

.axis-label {
    -fx-font-size: 14pt;
}

.axis {
    -fx-tick-label-font: 12pt system;
}

.chart-series-line {
    -fx-stroke-width: 1px;
    -fx-effect: null;
}

.default-color0.chart-series-line { -fx-stroke: transparent; }
.default-color1.chart-series-line { -fx-stroke: blue; }

.default-color0.chart-line-symbol { 
    -fx-background-color: red;
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,
        10 L0,10 L0,8 L4,5 L0,2 L0,0 Z";
}

.default-color1.chart-line-symbol { 
    -fx-background-color: transparent, transparent; 
}

.default-color1.chart-legend-item-symbol{
    -fx-background-color: blue;
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M0,5 L0,7 L12,7 L12,5 Z";
    -fx-scale-shape: false;
}

実行例を以下に示します。

今回はほんの僅かですが、すこしずつカスタマイズできる範囲を広げて行く予定です。

参考サイト

  1. openjfx/8/master/rt: 88d3bef80ffc modules/controls/src/main/java/javafx/scene/chart/LineChart.java
  2. bitWalk's: JavaFX: LineChart を使いこなそう (3)
  3. JavaFX CSS Reference Guide
  4. Using JavaFX Charts: Styling Charts with CSS | JavaFX 2 Tutorials and Documentation

0 件のコメント: