プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JavaFXのChartでMinorTickでも線を描く

 JavaFXのChartって使いにくいなこんちきしょう。グラフ内部のマウス座標とか、全然取れないし 。(グラフ内部というのは、本当にグラフの表示領域内部だけでの座標ね。あと、根性で取れるようにしたよ、ちきしょう。Javaのバージョンアップとかで、もしかしたら駄目になるかも。)


 そんななか、私を丸1日悩ませたのは、MinorTickにおける背景の線を描けないこと。ちょっと特殊な目的でMinorTickでも線が欲しい。(下図参照。通常では線が描画されるのはMajorTickだけ。しかし、MajorTickの数は増やしたくない。)
 

 


 まず、前提条件として、AxisはValueAxisを拡張したクラス(NumberAxis)を利用するとします。また、たぶんどのChartでも同じように出来ると思いますが、今回はLineChartを対象とします。

 最初に、MinorTickの線を引くには、MinorTickがどこに表示されているのか知る必要がある。それはValueAxisのcalculateMinorTickMarksで取得が可能だ。
 しかし、calculateMinorTickMarksは可視性がprotectedで見えない。さらに困ったことに、NumberAxisはfinal宣言されてて、可視性の上書きも出来ない。
 というわけで、こうしました↓。

package javafx.scene.chart;//←これ
import java.util.List;

public class CallProtectedMethod{
  
  public static <T extends Number> List<T> calculateMinorTickMarks(ValueAxis<T> axis)
  {
    return axis.calculateMinorTickMarks();
  }
}

 javafx.scene.chartパッケージ下で新たにクラスを作成して、それを介して無理矢理呼び出します。
 今回はわかりやすい名前にしたけど、絶対に衝突しないような名前にしとくと良いでしょう。



 で、次に背景に線を引く処理ですが、困ったことにXYChartで実際に線を引いている背景の領域というのは取得できない。一応、内部の構造を理解した上で、子ノードを辿り、取得は出来るが、その座標系は表内部だけの座標系にされていないので、細かい調整がよく分からん。ていうか、本来ブラックボックスのはずの内部処理を理解した上での実装だから、バージョンアップでちょっと処理を書き換えられたらすぐに使えなくなる。それはしたくない。
 そこで、妥協としてグラフの値を描画する領域(Group)において、グラフの線を描く前に描画をします。線の更新処理はlayoutPlotChildrenの処理に追加することで実装します。layoutChildrenだと、Axisの大きさがまだ設定されていないので上手くいかない。いや〜、layoutPlotChildrenがfinal宣言されてなくて良かった。
 グラフの表示領域の大きさと、Axisの長さが同じに設定されているので、線の長さはAxisの長さと同じにすれば良い。次に、線の位置はAxisのgetDisplayPositionから取得できる。以上をまとめると次のようになる。

package nodamushi.jfx.chart;

import java.util.*;

import javafx.collections.ObservableList;
import javafx.scene.chart.*;
import javafx.scene.shape.*;

public class LineChart2<X extends Number,Y extends Number> extends LineChart<X, Y>{
  private Path xMinorPath,yMinorPath;
  
  public LineChart2(ValueAxis<X> xAxis,ValueAxis<Y> yAxis){
    super(xAxis,yAxis);
    init();
  }
  
  public LineChart2(ValueAxis<X> xAxis, ValueAxis<Y> yAxis,
      ObservableList<javafx.scene.chart.XYChart.Series<X, Y>> data){
    super(xAxis, yAxis, data);
    init();
  }
  
  private void init(){
    Path xMinorPath = new Path();
    Path yMinorPath = new Path();
    this.xMinorPath=xMinorPath;
    this.yMinorPath=yMinorPath;
    //グラフよりも先に描画されるように
    //0に挿入します
    getPlotChildren().addAll(0, Arrays.asList(xMinorPath,yMinorPath));
    //CSSの設定
    xMinorPath.getStyleClass().add("chart-vertical-grid-lines");
    yMinorPath.getStyleClass().add("chart-horizontal-grid-lines");

  }
  
  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  protected void layoutPlotChildren(){
    super.layoutPlotChildren();
    final ValueAxis 
    xa = (ValueAxis)getXAxis(),
    ya = (ValueAxis)getYAxis();
    final Path 
    xpath = this.xMinorPath,
    ypath = this.yMinorPath;
    final List<Number> 
    xminor = 
    CallProtectedMethod.calculateMinorTickMarks(xa),
    yminor = 
    CallProtectedMethod.calculateMinorTickMarks(ya);
    
    xpath.getElements().clear();
    double h = ya.getHeight();
    for(Number o :xminor){
      double display = xa.getDisplayPosition(o);
      xpath.getElements().addAll(
          new MoveTo(display,0),
          new LineTo(display,h)
          );
    }
    
    ypath.getElements().clear();
    double w = xa.getWidth();
    for(Number o :yminor){
      double display = ya.getDisplayPosition(o);
      ypath.getElements().addAll(
          new MoveTo(0,display),
          new LineTo(w,display)
          );
    }
  }
}



 で、実際に使ってみると以下のように描画されました。問題なさそうですね。

 もっと良い方法があるという場合は是非教えてください。