Chart.js で折れ線グラフの交点(intersect)をToolTip 表示(double値編)

標準でありそうで存在しない。だから線を描画する2本の線、プロットする2点(X,Y座標値)を
2本線が交差するしないを Chart.js で描画するプロットリスト
~2つのリストの特殊な順列組合せで交点を見つけて描画する。

対象グラフは前回投稿のようなグラフ(今回は少しデータ値が異なる)
Chart.js の 線グラフ scatter で線グラフにする - Oboe吹きプログラマの黙示録


線を構成するための2点の組み合わせから、順に重複なしの組み合わせで隣接するプロット同志だけの
チェックになる。
oboe2uran.hatenablog.com

このロジックを使うのだが、最小限の2つのプロット同志を比較するのは、
以下を参考にさせて貰った。
GitHub - shogonir/JavaSample

まず、描画の為の点を表すXY座標は、double 型で以下のクラスを用意する。

import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

/**
 * Ploter.
 */
public class Ploter{
   public double x;
   public double y;

   public Ploter(double x, double y){
      this.x = x;
      this.y = y;
   }
   public Ploter(LocalDateTime time, double y){
      this.x = time.atZone(ZoneId.of("Asia/Tokyo")).toInstant().toEpochMilli();
      this.y = y;
   }
   public static Ploter of(double x, double y){
      return new Ploter(x, y);
   }
   public static Ploter of(LocalDateTime time, double y){
      return new Ploter(time.atZone(ZoneId.of("Asia/Tokyo")).toInstant().toEpochMilli(), y);
   }
   /** @param x セット x */
   public void setX(double x){
      this.x = x;
   }
   /** @param y セット y */
   public void setY(double y){
      this.y = y;
   }
   public LocalDateTime getXtime(){
      return    Instant.ofEpochMilli(BigDecimal.valueOf(x).setScale(0, BigDecimal.ROUND_HALF_UP).longValue())
            .atZone(ZoneId.systemDefault()).toLocalDateTime();
   }
   public double getYvalue(){
      return (double)y;
   }
}

これは、時刻スケールをX軸にした時に対応させる為で、交点を求める為に この Ploter の xは、double のままで
変換メソッドを用意する。
どうしても

return new Ploter(time.atZone(ZoneId.of("Asia/Tokyo")).toInstant().toEpochMilli(), y);

この部分がよろしくないが、自分が管理して使う分にはいいだろう。

やはりこうすべきか。。。

return new Ploter(time.atZone(ZoneId.of(System.getProperty("user.timezone"))).toInstant().toEpochMilli(), y);

そして交点を求めるユーティリティもどきのクラス

import java.util.Optional;
import java.util.function.Function;
/**
 * LineUtil.
 */
public final class LineUtil{
	private Function<Double, Double> yFunction;

	public LineUtil(){}

	public LineUtil(Function<Double, Double> yFunction){
		this.yFunction = yFunction;
	}

	public Optional<Ploter> intersection(Ploter p1s, Ploter p1e, Ploter p2s, Ploter p2e){
		double ax = p1s.x;
		double ay = p1s.getYvalue();
		double bx = p1e.x;
		double by = p1e.getYvalue();
		double cx = p2s.x;
		double cy = p2s.getYvalue();
		double dx = p2e.x;
		double dy = p2e.getYvalue();
		double ta = (cx - dx) * (ay - cy) + (cy - dy) * (cx - ax);
		double tb = (cx - dx) * (by - cy) + (cy - dy) * (cx - bx);
		double tc = (ax - bx) * (cy - ay) + (ay - by) * (ax - cx);
		double td = (ax - bx) * (dy - ay) + (ay - by) * (ax - dx);
		if (tc * td <= 0 && ta * tb <= 0){
			double d = (bx - ax) * (dy - cy) - (by - ay) * (dx - cx);
			double u = ((cx - ax) * (dy - cy) - (cy - ay) * (dx - cx)) / d;
			double v = ((cx - ax) * (by - ay) - (cy - ay) * (bx - ax)) / d;
			if (u < 0 || u > 1 || v < 0 || v > 1){
	           return Optional.empty();
			}
			double x = ax + u * (bx - ax);
			double y = ay + u * (by - ay);
			return Optional.of(Ploter.of(x, yFunction==null ? y : yFunction.apply(y)));
		}
		return Optional.empty();
	}
}

交点ある時だけ処理したいから、 Optional で結果を受け取る。
この結果を含めた線のデータをJSONで、AJAX で描画データの問い合わせがきたときに返してあげれば良い
Wicket のレスポンスに書くのに以下のようにしてあげる。。

Map<String, Object> map = new HashMap<String,Object>();
List<Ploter> alpha = new ArrayList<>();
alpha.add(Ploter.of(12, 8));
alpha.add(Ploter.of(18, 19));
alpha.add(Ploter.of(34, 34));
alpha.add(Ploter.of(40, 34));
alpha.add(Ploter.of(60, 81));

List<Ploter> beta = new ArrayList<>();
beta.add(Ploter.of(10, 50));
beta.add(Ploter.of(26, 9));
beta.add(Ploter.of(30, 47));
beta.add(Ploter.of(70, 63));
// 描画する alpha 線グラフと  beta 線グラフ の交点を求めて追加する。
// X軸スケールで昇順ソートをしてグラフデータにする。
List<SimpleEntry<Ploter, Ploter>> alist = new ArrayList<>();
for(ListIterator<Ploter> it=alpha.listIterator(1);it.hasNext();){
   alist.add(new SimpleEntry<>(alpha.get(it.nextIndex()-1), it.next()));
}

LineUtil lineUtil = new LineUtil(d->d);

List<Ploter> plus = new ArrayList<>();

for(ListIterator<Ploter> it=beta.listIterator(1);it.hasNext();){
   SimpleEntry<Ploter, Ploter> other
    = new SimpleEntry<>(beta.get(it.nextIndex()-1), it.next());
   alist.stream().forEach(e->{
      Optional<Ploter> op
       = lineUtil.intersection(other.getKey(), other.getValue(), e.getKey(), e.getValue());
      op.ifPresent(p->{
         plus.add(p);
      });
   });
}
if (plus.size() > 0){
   alpha.addAll(plus);
   beta.addAll(plus);
   alpha = alpha.stream()
   .sorted((a, b)->Double.valueOf(a.x).compareTo(Double.valueOf(b.x)))
   .collect(Collectors.toList());
   beta = beta.stream()
   .sorted((a, b)->Double.valueOf(a.x).compareTo(Double.valueOf(b.x)))
   .collect(Collectors.toList());
}

map.put("alpha", alpha);
map.put("beta", beta);

Gson gson = new GsonBuilder().create();
try(StringResourceStream s = new StringResourceStream(gson.toJson(map))){
   getRequestCycle().scheduleRequestHandlerAfterCurrent(
      new ResourceStreamRequestHandler(s)
   );
}catch(IOException e){
   e.printStackTrace();
}

AJAXグラフ描画データ受信→描画

$.ajax({
	type: 'POST',
	timeout: 10000,
	url: init_url,
	dataType: 'json',
	cache: false,
}).done(function(e){

	$.each(e.alpha, function(i, v){
		graph.datasets[0].data.push({ x:v.x, y:v.y });
	});
	$.each(e.beta, function(i, v){
		graph.datasets[1].data.push({ x:v.x, y:v.y });
	});
	multiChart.update();

}).fail(function(e){
	console.log(e);
}).always(function(e){
});

交点の小数点の精度は、AJAX送信側で丸めてしまうと描画にズレが起きるので
ツールチップの表示で値を表示する時に、丸めを実行する
toFixed() で充分であろう。

tooltips: {
   titleFontSize: 36,
   bodyFontSize: 36,
   callbacks: {
      title: function (tooltipItem, data){
         return data.datasets[tooltipItem[0].datasetIndex].label;
      },
      label: function (tooltipItem, data){
         return "scale:" + tooltipItem.xLabel.toFixed(2) + "  value:" + tooltipItem.yLabel.toFixed(2) ;
      }
   }
},

f:id:posturan:20180415230538j:plain

交点のツールチップが両方出るように、
oboe2uran.hatenablog.com
も参照!!