標準でありそうで存在しない。だから線を描画する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) ; } } },
交点のツールチップが両方出るように、
oboe2uran.hatenablog.com
も参照!!