2016-03-19

JavaFX: LineChart を使いこなそう (5) - 縦線・横線

JavaFX のラインチャートを使っていると、チャート内に縦線あるいは横線の固定線を描画したい場合があります。例えば、目標値を強調する場合や、管理範囲を表示したい場合などです。参考サイト [1] にぴったりの例がありましたので紹介します。

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

  • OS: Fedora 23 x86_64
  • jdk1.8.0_74-1.8.0_74-fcs.x86_64 (Oracle)
  • IDE: NetBeans IDE 8.1

まず、LineChartWithMarkers.java ですが、これは参考サイト [1] で紹介されている LineChart を継承した内部クラス LineChartWithMarkers を、通常のクラスにして、少しだけ書き換えたものです。

リスト:LineChartWithMarkers.java 
package linechartsample;

import java.util.Objects;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.shape.Line;

public class LineChartWithMarkers<X, Y> extends LineChart<X, Y> {

    private final ObservableList<Data<X, Y>> horizontalMarkers;
    private final ObservableList<Data<X, Y>> verticalMarkers;

    public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
        super(xAxis, yAxis);
        horizontalMarkers = FXCollections.observableArrayList(data -> new Observable[]{data.YValueProperty()});
        horizontalMarkers.addListener((InvalidationListener) observable -> layoutPlotChildren());
        verticalMarkers = FXCollections.observableArrayList(data -> new Observable[]{data.XValueProperty()});
        verticalMarkers.addListener((InvalidationListener) observable -> layoutPlotChildren());
    }

    public void addHorizontalValueMarker(Data<X, Y> marker) {
        Objects.requireNonNull(marker, "the marker must not be null");
        if (horizontalMarkers.contains(marker)) {
            return;
        }
        Line line = new Line();
        line.setStyle("-fx-stroke:green;-fx-stroke-width:1px;");
        marker.setNode(line);
        getPlotChildren().add(line);
        horizontalMarkers.add(marker);
    }

    public void removeHorizontalValueMarker(Data<X, Y> marker) {
        Objects.requireNonNull(marker, "the marker must not be null");
        if (marker.getNode() != null) {
            getPlotChildren().remove(marker.getNode());
            marker.setNode(null);
        }
        horizontalMarkers.remove(marker);
    }

    public void addVerticalValueMarker(Data<X, Y> marker) {
        Objects.requireNonNull(marker, "the marker must not be null");
        if (verticalMarkers.contains(marker)) {
            return;
        }
        Line line = new Line();
        line.setStyle("-fx-stroke:orange;-fx-stroke-width:1px;");
        marker.setNode(line);
        getPlotChildren().add(line);
        verticalMarkers.add(marker);
    }

    public void removeVerticalValueMarker(Data<X, Y> marker) {
        Objects.requireNonNull(marker, "the marker must not be null");
        if (marker.getNode() != null) {
            getPlotChildren().remove(marker.getNode());
            marker.setNode(null);
        }
        verticalMarkers.remove(marker);
    }

    @Override
    protected void layoutPlotChildren() {
        horizontalMarkers.stream().forEach((horizontalMarker) -> {
            drawHorizontalMarker(horizontalMarker);
        });
        verticalMarkers.stream().forEach((verticalMarker) -> {
            drawVerticalMarker(verticalMarker);
        });
        super.layoutPlotChildren();
    }

    private void drawHorizontalMarker(Data<X, Y> horizontalMarker) {
        Line line = (Line) horizontalMarker.getNode();
        line.setStartX(0);
        line.setEndX(getBoundsInLocal().getWidth());
        line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
        line.setEndY(line.getStartY());
        line.toFront();
    }

    private void drawVerticalMarker(Data<X, Y> verticalMarker) {
        Line line = (Line) verticalMarker.getNode();
        line.setStartX(getXAxis().getDisplayPosition(verticalMarker.getXValue()) + 0.5);  // 0.5 for crispness
        line.setEndX(line.getStartX());
        line.setStartY(0d);
        line.setEndY(getBoundsInLocal().getHeight());
        line.toFront();
    }

}

次に、サンプルチャートを描画する LineChartSample.java ですが、これは過去の記事 [2] をベースに、横線を前述の LineChartWithMarkers クラスの addHorizontalValueMarker メソッドで描画する機能を追加しています。なお、縦線を描画するには addVertialValueMarker メソッドを用います。

リスト:LineChartSample.java 
package linechartsample;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;

public class LineChartSample extends Application {

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

        //defining the axes
        final CategoryAxis xAxis = new CategoryAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("X軸ラベル(年)");
        yAxis.setLabel("Y軸ラベル(数値)");

        //creating the chart
        LineChartWithMarkers<String, Number> lineChart = new LineChartWithMarkers<>(xAxis, yAxis);
        lineChart.setData(getChartData());
        lineChart.setTitle("折れ線グラフのサンプル");
        lineChart.addHorizontalValueMarker(new XYChart.Data<>("", 1));
        lineChart.addHorizontalValueMarker(new XYChart.Data<>("", -1));

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

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

    /**
     *
     * @return ObservableList<XYChart.Series<String, Double>>
     */
    private ObservableList<XYChart.Series<String, Number>> getChartData() {
        double y1 = 0.5;
        double y2 = -0.5;

        Series<String, Number> series1 = new Series<>();
        Series<String, Number> series2 = new Series<>();

        series1.setName("系列1");
        series2.setName("系列2");

        for (int i = 2011; i < 2021; i++) {
            series1.getData().add(new XYChart.Data<>(Integer.toString(i), y1));
            y1 = y1 + Math.random() - .5;

            series2.getData().add(new XYChart.Data<>(Integer.toString(i), y2));
            y2 = y2 + Math.random() - .5;
        }

        ObservableList<XYChart.Series<String, Number>> seriesList = FXCollections.observableArrayList();
        seriesList.add(series1);
        seriesList.add(series2);

        return seriesList;
    }

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

LineChart.css は過去の記事 [2] と同じです。

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

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

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


.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: 2px;
    -fx-effect: null;
}

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

.default-color0.chart-line-symbol {
    -fx-background-color: red, #f88;
    -fx-background-radius: 6px;
    -fx-padding: 6px;
}
.default-color1.chart-line-symbol {
    -fx-background-color: blue, #88f;
    -fx-background-radius: 0;
    -fx-padding: 6px;
}

実行例を以下に示します。データは乱数でプロットされていますので、場合によっては横線 y = ± 1 が表示エリアに含まれない場合があります。

参考サイト

  1. java - How to add two vertical lines with JavaFX LineChart - Stack Overflow
  2. bitWalk's: JavaFX: LineChart を使いこなそう (2)

 

0 件のコメント: