JavaFX8 - LineChartの使い方と、マウス位置のデータ値を取得するまで

最近もっぱらJava8 & JavaFX8を勉強中。

JavaFXには色々と標準コントロールがついているけど、
その使い方について、日本語情報があんまり見つかりませんね。

折角調べたのでメモメモ。

# LineChartを置く

とりあえずLineChartを置く。
題材はてきとーに、月別のチーム人数を出すグラフと言うことで。

```xml
<LineChart fx:id="testLineChart" layoutX="-11.0" layoutY="21.0" prefHeight="464.0" prefWidth="578.0" title="月別チーム人数">
                    <xAxis>
                      <CategoryAxis label="Month" side="BOTTOM" />
                    </xAxis>
                    <yAxis>
                      <NumberAxis side="LEFT" />
                    </yAxis>
                  </LineChart>
```

Paneやら何やらの設定は略
大事なのはxAxisとyAxisで、それぞれ横軸と縦軸の事。以下の二つが設定できる。

* NumberAxis → 名前の通り数値軸(0, 10, 20...のような軸)
* CategoryAxis → 文字列軸(1月、2月...のような軸)

今回は縦軸が人数、横軸が月でどっちもNumberAxisで良いが、
面白くないので横軸は「x月」としてCategoryAxisにした。

# データを設定する

このまま画面表示すると空っぽのグラフが出ますが、空っぽの時点でグラフでもないのでデータを設定。

データもべたで設定は面白くないので、以下のてきとーなカンマ区切りテキストファイル(chartData.txt)から読み込み。

```vim
01月,10
02月,20
03月,5
04月,9
05月,45
06月,21
07月,9
08月,65
09月,0
10月,11
11月,2
12月,8
```

今回はControllerクラスのInitializeで設定。
LineChartの折れ線1本分はXYChart.Seriesクラスで表すので作成。

```java
public void initialize(URL location, ResourceBundle resources) {
        XYChart.Series<String, Number> series = new XYChart.Series<String, Number>();
        series.setName("人数");     // 凡例の名前
```
凡例はExcelでお馴染みのあれ。

で、ファイルからデータを取って設定。
データを取る部分は本題ではないので、forEachとsplitでざくっと。

```java
        try (Stream<String> chartStream = Files.lines(Paths.get("chartData.txt"), StandardCharsets.UTF_8)) {

            // 各要素をカンマで区切り、XYChartに設定。設定したXYChartをseriesに追加する。
            chartStream.forEach(s -> {
                String[] splitResult = s.split(",");

                Data<String, Number> xyChart
                    = new XYChart.Data<String, Number>(splitResult[0], Integer.parseInt(splitResult[1]));

                series.getData().add(xyChart);
            });

        } catch (IOException e) {
            e.printStackTrace();
        }

        // Data設定されたSeriesをLineChartに追加する。
        testLineChart.getData().add(series);
```

本題が分かりづらいですが、大事なのは以下だけ。
折れ線中のデータ部分はXYChartで表す。

```java
Data<String, Number> xyChart = new XYChart.Data<String, Number>(splitResult[0], Integer.parseInt(splitResult[1]));

series.getData().add(xyChart);
testLineChart.getData().add(series);
```

SeriesのgetData()で取れるObservableListにXYChartを設定してあげれば良い。
最後にLineChartに同じようにSeriesを設定する。

↓こんな感じのグラフに。0のデータの○がはみ出るのはどうにかできるのかな。
また特に指定してないけど、縦軸・横軸の最大は勝手に設定される。
![2015-03-07_154112.png](https://qiita-image-store.s3.amazonaws.com/0/71591/1a3e26b5-8564-cb1a-a28b-061707615137.png)


# マウス位置のデータ値を表示する

もうちょっと機能が欲しいので、
マウスをあわせたら、その位置のデータ値が出るようにしてみる。

調べた感じ、以下のように単純にマウスイベント設定では駄目

```java
// LineChartそのものにMouseMoved設定
testLineChart.setOnMouseMoved(ev ->{/* ～～～ */});
```
LineChartは軸の部分や凡例の部分も含まれるので、
チャート部分のマウス位置がちゃんと取れないらしい。

なので以下のようにlookupでチャート背景部分のNodeを取れば良い。

```java
Node chartBackground = testLineChart.lookup(".chart-plot-background");
```

マウス位置のデータ値は、各AxisのgetValueForDisplayメソッドで取得できるので、イベントはこんな感じ。
(dataLabelはただのLabelです)

```java
        chartBackground.setOnMouseMoved(ev -> {
            String pointXAxis = testLineChart.getXAxis().getValueForDisplay(ev.getX());
            Number pointYAxis = testLineChart.getYAxis().getValueForDisplay(ev.getY());
            String dataLabelText = String.format("xAxis:%s, yAxis:%.2f", pointXAxis, pointYAxis);

            dataLabel.setText(dataLabelText);
        });
```

ついでにチャート部分でのマウスカーソルが矢印だとかっこ悪いので、
クロスヘアに変えてみた。

```java
        chartBackground.setCursor(Cursor.CROSSHAIR);
```

![2015-03-07_160356.png](https://qiita-image-store.s3.amazonaws.com/0/71591/30c88000-a410-88c1-e966-b0787d80c84a.png)

やったぜ。

# setMouseTransparent

実はこのままだと、折れ線上や○の部分でデータ値が出なかった。マウスカーソルも戻っちゃう。
それもそのはずで、折れ線の部分は.chart-plot-backgroundでは無いのでイベントが適用されない。

全部のスタイルに適用するのも大変なので、以下のようにして
LineChart上の.chart-plot-background以外の要素にsetMouseTransparent(true)してあげると良いみたい？

```java
chartBackground.getParent().getChildrenUnmodifiable().stream().filter(
        n -> n != chartBackground).forEach(
                n -> n.setMouseTransparent(true));
```
testLineChart.getChildren～でも良さそうだけど。
この設定をしてやると、そのNodeのマウスイベントは無効になって、その下のNodeのイベントが呼ばれるようになる。

とりあえずやりたいことは出来たので満足。