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

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するグラフができる。
f:id:posturan:20180417223959j:plain


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