torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXで折れ線グラフを出そうとしてみた

はじめに(お題)

電気信号を時間領域から周波数領域に変換し、周波数に対するエネルギー量を解析できる測定器にスペクトラム・アナライザがあります。今回、このスペクトラム・アナライザで解析した結果をファイルに保存し、別な場所にあるPC上でその結果を表示、分析したいとします。

計測器にCSVファイルで結果を保存する機能があればこれをExcelで読み込ませてグラフ化することは容易ですが、今回の計測器はバイナリデータのファイルに保存するものでした。そこで、JavaFXで保存したバイナリデータのファイルを読み込み折れ線グラフ(LineChart)で表示するプログラムを作成してみます。

LineChart

JavaFXのプロジェクトを作成し、折れ線グラフ(LineChart)を表示する画面をScene Builderで作成しました。 ファイル構成は次です。

ファイル名 内容
SimpleSpectrumApp.java Application派生クラス、プログラムのエントリ(main)
SimpleSpectrumView.fxml 画面(ビュー)のレイアウトを定義するFXMLファイル
SimpleSpectrumView.css 画面(ビュー)の見栄えを定義するCSSファイル
SimpleSpectrumViewController.java 画面(ビュー)に対応するコントローラークラス
SimpleSpectrumViewModel.java GUI内のモデルクラス

デフォルトのLineChart

Scene BuilderでLineChartを画面にポトペタすると次の表示となります。 f:id:torutk:20190706211210p:plain

縦軸は数値メモリとなっていますが、横軸は表示されていません。Scene Builderの左下ペインを見ると、LineChartの子ノードとしてNumberAxisとCategoryAxisが1つずつ置かれています。縦軸がNumberAxisで、横軸がCategoryAxisです。NumberAxisは名前の通り数値の軸を表し、CategoryAxisは何らかの分類(県名、年別、チーム別、など)の軸を表します。

Scene Builder上では、LineChartの子ノードの軸を変更する方法が見つからなかったので、FXMLファイルを直接エディタで開き修正しました。

      <LineChart BorderPane.alignment="CENTER">
        <xAxis>
-         <CategoryAxis side="BOTTOM" />
+         <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>

横軸がNumberAxisに変更されました。 f:id:torutk:20190706212808p:plain

最初のコード

SimpleSpectrumApp.java
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class SimpleSpectrumApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("SimpleSpectrumView.fxml"));
        primaryStage.setTitle("Simple Spectrum");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

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

JavaFXの典型的なApplication派生クラスの実装です。mainメソッドでプロセスのエントリを用意し、startメソッドでFXMLファイルを読み込みシーングラフを作成、Stageを表示させています。

SimpleSpectrumView.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.SimpleSpectrumViewController">
   <center>
      <LineChart fx:id="chart" BorderPane.alignment="CENTER">
        <xAxis>
          <NumberAxis side="BOTTOM" fx:id="xAxis" />
        </xAxis>
        <yAxis>
          <NumberAxis fx:id="yAxis" side="LEFT" />
        </yAxis>
      </LineChart>
   </center>
</BorderPane>

Scene Builderでポトペタ作成したFXMLファイルで、ビュー(画面)を定義しています。 コントローラークラスを指定しているので、このFXMLがロードされると自動でコントローラークラスのインスタンスが生成されます。

SimpleSpectrumViewController.java
package sample;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;

import java.net.URL;
import java.util.ResourceBundle;

public class SimpleSpectrumViewController implements Initializable {
    @FXML
    LineChart chart;
    @FXML
    NumberAxis xAxis;
    @FXML
    NumberAxis yAxis;

    SimpleSpectrumViewModel model = SimpleSpectrumViewModel.ofRandom();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        chart.getData().setAll(model.getSeriesList());
    }
}

FXMLに対応するコントローラークラスです。 コントローラーから操作するビュー部品(コントロール)は、@FXMLアノテーションを指定し、FXML側で指定したfx:idの識別子と同じ名前の変数名とすることで、FXMLロード時にインスタンスがインジェクションされます。

データについては、今回はデータファイルの読み込み部分が未実装なので、ランダムな値を表示するモデルを使います。

SimpleSpectrumViewModel.java
package sample;

import javafx.scene.chart.XYChart;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SimpleSpectrumViewModel {

    private List<XYChart.Series<Double, Double>> seriesList = new ArrayList<>();

    public List<XYChart.Series<Double, Double>> getSeriesList() {
        return Collections.unmodifiableList(seriesList);
    }

    public static SimpleSpectrumViewModel ofRandom() {
        var model = new SimpleSpectrumViewModel();
        model.seriesList.add(createRandomSeries("Random"));
        return model;
    }

    private static XYChart.Series<Double, Double> createRandomSeries(String name) {
        XYChart.Series<Double, Double> series = new XYChart.Series<>();
        series.setName(name);
        for (double x = 950d; x <= 1450d; x++) {
            series.getData().add(new XYChart.Data<Double, Double>(
                    x, ThreadLocalRandom.current().nextGaussian() * 5.0 - 60.0));
        }
        return series;
    }
    
}

JavaFXではモデルクラスについては特にフレームワークが用意されていませんが、コントローラーを軽くするために、ちょっとしたロジックであってもモデルクラスを設けるようにしています。

表示確認のために、データファイルを読み込む前にランダムなスペクトルを表示するモデルを作りました。

JavaFXの折れ線グラフ(LineChart)に表示する1本の連続する線は、データ系列(XYChart.Series)で定義し、複数のデータ系列を表示することができます。 データ系列は、複数の点データ(XYChart.Data)のリストとして作られています。 今回は、1つのデータ系列を用意しています。 用意するデータは、周波数軸(X軸)の950~1450までの間を1刻みで作成し、縦軸の電力は正規分布乱数で平均-60.0、分散5.0としています。

表示

プログラムを実行すると、次の画面が表示されました。

f:id:torutk:20190707043824p:plain

いろいろ改善したい点があります。

  • 縦軸、横軸に名称・単位を表示したい
  • 〇の表示(シンボル)は不要としたい
  • 折れ線を細くしたい
  • 縦軸、横軸の表示範囲を指定したい

LineChartの設定

縦軸、横軸に名称を表示

これは、Scene Builderで、軸のLabelプロパティに文字列を設定すれば表示できます。 f:id:torutk:20190707050535p:plain

縦軸、横軸の表示範囲を指定

縦軸は、-30.0~-80.0の範囲を表示し、横軸は950~1450の範囲を表示します。 表示すべきデータを読み込んだ際にデータに応じた値を設定するので、コードで制御します。

前回からのコード変更部分を次に記載します。

    @Override
    public void initialize(URL location, ResourceBundle resources) {
+       xAxis.setLowerBound(950d);
+       xAxis.setUpperBound(1450d);
+       yAxis.setLowerBound(-80d);
+       yAxis.setUpperBound(-30d);
        chart.getData().setAll(model.getSeriesList());
    }

NumberAxisには、表示する軸の値の上限値・下限値を指定するlowerBound、upperBoundがあります。 ここに表示する軸の数値範囲を指定します。この上下限値設定は、LineChartにデータ(XYChart.Series)を登録する前に実行する必要があります。あとから実行したら表示には反映されません。

ただし、これだけでは意図したとおりの動作をしません。NumberAxisのAutoRangingプロパティが有効だと、登録したデータの値に応じた軸の数値範囲に自動調整が働いてしまいます。今回はScene Builder上でAutoRangingプロパティを無効にします(次の図参照)。

f:id:torutk:20190707101033p:plain

プログラムを実行すると、次の様に意図した数値範囲でグラフが表示されました。

f:id:torutk:20190707101215p:plain

  • グラフの罫線間隔については、NumberAxisのTickUnitプロパティで制御可能です。
〇の表示(シンボル)をなくす

デフォルトでは、折れ線グラフの点データに丸いシンボルが表示されます。 データ数が少ないときは、データとデータを補完する線とを明確に表現するこの表示が適していますが、データ数が多いときは上図のようにシンボルだらけで肝心の折れ線が見えなくなってしまいます。

シンボルをなくすのであれば、LineChartのCreateSymbolプロパティを無効にします(次の図参照)。

f:id:torutk:20190707103825p:plain

プログラムを実行すると、次のとおり丸いシンボルが表示されなくなりました。

  • シンボルを消すのではなく、シンボルの形状を変更したいときは、CSSセレクターに.chart-line-symbolを指定し見栄えを記述します。

f:id:torutk:20190707104015p:plain

折れ線を細く

デフォルトでは、折れ線の幅がちょっと太いので、これを細くします。形状の変更はCSSで記述します。 セレクターに.chart-series-lineを指定し見栄えを記述します。

.chart-series-line {
    -fx-stroke-width: 0.75px;
}

CSSファイルは、アプリケーション派生クラスでSceneに設定するか、FXMLで最上位要素に指定するかの方法があります。今回は、FXML(Scene Builder)上で最上位要素のBorderPaneにCSSファイルを設定します。CSSファイルはFXMLファイルと同じ場所に置き、BorderPaneのStylesheetsプロパティに指定します(次の図参照)。

f:id:torutk:20190707105138p:plain

プログラムを実行すると、次のとおり折れ線が細くなりました。

f:id:torutk:20190707105538p:plain

凡例の記号を四角ではなく線にする

CSSファイルで凡例の記号を定義している@.chart-legend-item-symbol@の定義を変更します。

.chart-legend-item-symbol {
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M0,5 L0,7 L12,7 L12,5 Z";
    -fx-scale-shape: false;
}
.default-color0.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_1;
}
.default-color1.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_2;
}
.default-color2.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_3;
}
.default-color3.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_4;
}
.default-color4.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_5;
}
.default-color5.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_6;
}
.default-color6.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_7;
}
.default-color7.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_8;
}
  • デフォルトの凡例シンボルは枠が系列色、中が白色塗りつぶしの円です。@-fx-background-radius@と@-fx-background-insets@はデフォルトの定義を無効化するのに指定する模様です(細部よくわからない)
  • グラフはデフォルトで8つまで色分けできるようにCHART_COLOR_1からCHART_COLOR_8まで色が定義されています。svg-pathで定義した枠の中をグラフの対応する色で塗りつぶすために、@.default-colorX.chart-legend-item-symbol@を定義しています。