Chart.js で折れ線グラフの交点単純な X軸:double 値、Y軸:double 値 のパターンを先日書いたので、
今回は、X軸:時刻、Y軸:double 値 の線グラフの交点である。
プロットするデータの型は、先日書いた中の Ploter クラスである。x軸である x は、double であるが
LocalDateTime としての getter を持ちコンストラクトとして LocalDateTime での指定を可能としている。
oboe2uran.hatenablog.com
この Ploter を使うのだが、JSONとしてクライアント側に、double の文字列を渡してもいいのだが、
あえて、日時フォーマットで渡したい。デバッグしやすくるためであるし副作用も将来可能だからだ。
そこで、Ploter データのJSONを作るためにGSON のアダプタを用意する。ついでだから
、シリアライズ、デシリアライズ両方を作る。
import java.lang.reflect.Type; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Map.Entry; import java.util.Optional; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; /** * PloterTimeAdapter. { x: yyyy/MM/dd HH:mm:ss.SSS , y: double } */ public class PloterTimeAdapter implements JsonSerializer<Ploter>, JsonDeserializer<Ploter>{ @Override public Ploter deserialize(JsonElement jsonElement, Type typeOfT , JsonDeserializationContext context) throws JsonParseException{ if (!jsonElement.isJsonObject()){ return null; } JsonObject jsonObject = jsonElement.getAsJsonObject(); try{ Optional<String> x = Optional.empty(); Optional<Double> y = Optional.empty(); for(Entry<String, JsonElement> entry : jsonObject.entrySet()){ if (entry.getKey().equals("x")){ x = Optional.ofNullable(entry.getValue().getAsString()); }else if(entry.getKey().equals("y")){ y = Optional.ofNullable(entry.getValue().getAsDouble()); }else{ return null; } } return Ploter.of( LocalDateTime.parse(x.get(), DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")), y.get() ); }catch(Exception e){ return null; } } @Override public JsonElement serialize(Ploter src, Type typeOfSrc, JsonSerializationContext context){ final JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("x", src.getXtime() .format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"))); jsonObject.addProperty("y", src.y ); return jsonObject; } }
書式、 yyyy/MM/dd HH:mm:ss.SSS 限定である!
これを、Wicket の WebPageで。
/** * TimeCrossBasicJsons. mountPage("/tmcrossbasic", TimeCrossBasicJsons.class); */ public class TimeCrossBasicJsons extends WebPage{ public TimeCrossBasicJsons(){ Map<String, Object> map = new HashMap<String,Object>(); List<Ploter> alpha = new ArrayList<>(); alpha.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(7, 23, 54)), 24.345)); alpha.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(9, 03, 14)), 18.5)); alpha.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(12, 42, 8)), 97)); alpha.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(16, 2, 22)), 80)); List<Ploter> beta = new ArrayList<>(); beta.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(6, 3, 54)), 54.345)); beta.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(8, 18, 9)), 80)); beta.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(11, 2, 18)), 17)); beta.add(Ploter.of(LocalDateTime.of(LocalDate.now(), LocalTime.of(15, 12, 42)), 10)); // 描画する 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); map.put("target", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))); Gson gson = new GsonBuilder() .registerTypeAdapter(new TypeToken<Ploter>(){}.getType(), new PloterTimeAdapter()) .create(); try(StringResourceStream s = new StringResourceStream(gson.toJson(map))){ getRequestCycle().scheduleRequestHandlerAfterCurrent( new ResourceStreamRequestHandler(s) ); }catch(IOException e){ e.printStackTrace(); } } @Override protected void configureResponse(WebResponse response){ response.setContentType("application/json"); super.configureResponse(response); } }
を用意して、WebApplication でマウントPageする。
交点の求め方は、
Chart.js で折れ線グラフの交点(intersect)をToolTip 表示(double値編) - Oboe吹きプログラマの黙示録
の時と同じで、片方の線の Ploter の順序組み合わせリストを作成して
もう片方の線の Ploter の順序組み合わせリストを総なめでチェックして交点を LineUtil で
見つけ出すのである。
Ploter というものを定義してストリームで LineUtil の交点取得メソッドを実行する。
mountPage("/tmcrossbasic", TimeCrossBasicJsons.class);
グラフの描画 JavaScript
var graph = { xLabels: [], datasets: [{ label: "alpha", type: "line", showLine: true, lineTension: 0, backgroundColor: "rgba(255, 140, 0, 0.6)", borderColor: "rgba(255, 140, 0, 0.6)", borderWidth: 2, borderCapStyle: 'round', borderDash: [], borderDashOffset: 0.0, borderJoinStyle: "round", pointBorderColor: "rgba(255, 140, 0, 0.6)", pointBackgroundColor: "rgba(255, 140, 0, 0.6)", pointBorderWidth: 1, pointHoverRadius: 10, pointHoverBackgroundColor: "rgba(255, 140, 0, 0.6)", pointHoverBorderColor: "rgba(255, 140, 0, 0.6)", pointHoverBorderWidth: 10, pointRadius: 1, pointHitRadius: 10, fill: false, data: [] },{ label: "beta", type: "line", showLine: true, lineTension: 0, backgroundColor: "rgba(0, 100, 0, 0.6)", borderColor: "rgba(0, 100, 0, 0.6)", borderWidth: 2, borderCapStyle: 'round', borderDash: [], borderDashOffset: 0.0, borderJoinStyle: "round", pointBorderColor: "rgba(0, 100, 0, 0.6)", pointBackgroundColor: "rgba(0, 100, 0, 0.6)", pointBorderWidth: 1, pointHoverRadius: 10, pointHoverBackgroundColor: "rgba(0, 100, 0, 0.6)", pointHoverBorderColor: "rgba(0, 100, 0, 0.6)", pointHoverBorderWidth: 6, pointRadius: 1, pointHitRadius: 20, fill: false, data: [] }] }; var graphOptions = { responsive: true, title:{ display:true, text:'Time-line sample' }, chartArea: { backgroundColor: 'rgba(255, 255, 255, 1)' }, scales: { xAxes: [{ display: true, scaleLabel: { display: true, labelString: 'Today Time' }, type: "time", time: { displayFormats: { hour: 'H' }, min: new moment().hour(0).minute(0).second(0).millisecond(0), max: new moment().add(1, "day").hour(0).minute(0).second(0).millisecond(0), stepSize: 3 }, }], yAxes: [{ display: true, scaleLabel: { display: true, fontSize: 22, labelString: 'Value' }, ticks: { fontSize: 26, min: 0, max: 120, stepSize: 40 } }] }, tooltips: { titleFontSize: 22, bodyFontSize: 22, callbacks: { title: function (tooltipItem, data){ return data.datasets[tooltipItem[0].datasetIndex].label; }, label: function (tooltipItem, data){ return moment(tooltipItem.xLabel).format("YYYY/MM/DD(ddd) HH:mm:ss.SSS") + " value:" + tooltipItem.yLabel.toFixed(2); } } }, chartArea: { backgroundColor: 'rgba(255, 255, 255, 1)' }, }; var multiChart; var setChart = function(){ var init_url = "/chartsample/tmcrossbasic" $.ajax({ type: 'POST', timeout: 10000, url: init_url, data: { "kind":"init" }, dataType: 'json', cache: false, }).done(function(e){ multiChart.options.scales.xAxes[0].time.min = moment(e.target, "YYYY/MM/DD").hour(0).minute(0).second(0).millisecond(0); multiChart.options.scales.xAxes[0].time.max = moment(e.target, "YYYY/MM/DD").add(1, "day") .hour(0).minute(0).second(0).millisecond(0); $.each(e.alpha, function(i, v){ graph.datasets[0].data.push({ t:moment(v.x, "YYYY/MM/DD HH:mm:ss.SSS"), y:v.y }); }); $.each(e.beta, function(i, v){ graph.datasets[1].data.push({ t:moment(v.x, "YYYY/MM/DD HH:mm:ss.SSS"), y:v.y }); }); multiChart.update(); }).fail(function(e){ console.log(e); }).always(function(e){ }); }; $(function(){ moment.updateLocale('ja', { weekdays: ["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"], weekdaysShort: ["日","月","火","水","木","金","土"], }); Chart.pluginService.register({ beforeDraw: function(c){ if (c.config.options.chartArea && c.config.options.chartArea.backgroundColor) { var ctx = c.chart.ctx; var chartArea = c.chartArea; ctx.fillStyle = "rgba(240, 249, 255, 1)"; ctx.fillRect(0, 0, c.chart.width, c.chart.height); ctx.save(); ctx.fillStyle = c.config.options.chartArea.backgroundColor; ctx.fillRect(chartArea.left, chartArea.top , chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); ctx.restore(); } } }); Chart.defaults.global.defaultFontSize = 26; multiChart = new Chart(document.getElementById("myChart").getContext('2d'), { type: 'line', data: graph, options: graphOptions }); });
WebPage表示したた時に、onAfterRender() でこの中の setChart() を実行すれば
交点でツールTipするグラフができる。
交点のツールチップが両方出るように、
oboe2uran.hatenablog.com
も参照!!