Chart.js Time グラフの為のデータJSONを作成するクラス

Chart.js https://www.chartjs.org/ X軸=時刻 の線グラフを描画するためのJSONデータ作成する
Java クラスを作成して、AJAX通信によるJSONデータ → Chart.js グラフ描画をデザインを除いた
可変の JSONデータ生成だけでも汎用的にならないかと考えました。

グラフ描画の点となるデータの定義の Javaオブジェクト
整数(int) と 小数点 (double) 各々のケースを考慮して用意します。

import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * LocalDateTime - 整数値 
 */
public class IntPoint implements Serializable{
   /** グラフX軸の時刻. */
   public LocalDateTime time;
   /** グラフY軸の値. */
   public Integer value;
   public IntPoint(){}
   public IntPoint(LocalDateTime time, int value){
      this.time = time;
      this.value = value;
   }
   public void setTime(LocalDateTime time){
      this.time = time;
   }
   public void setValue(int value){
      this.value = value;
   }
   @Override
   public String toString() {
      return "Pointer(" + time + ", " + value + ")";
   }
}
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * LocalDateTime - 小数点
 */
public class DoublePoint implements Serializable{
   /** グラフX軸の時刻. */
   public LocalDateTime time;
   /** グラフY軸の値. */
   public Double value;
   public DoublePoint(){}
   public DoublePoint(LocalDateTime time, double value){
      this.time = time;
      this.value = value;
   }
   public void setTime(LocalDateTime time) {
      this.time = time;
   }
   public void setValue(Double value) {
      this.value = value;
   }
}

JSON生成するクラス、Google gson を使います。

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.yipuran.gsonhelper.LocalDateTimeAdapter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
 * Time Chart Json Builder.
 * <PRE>
 *
 * String json = new TimeChartJsonBuilder()
 *              .addIntegers(list)
 *              .addYAxesMin(10)
 *              .addYAxesMax(500)
 *              .addStepSize(20)
 *              .addFontSize(12)
 *              .build();
 *
 * String json = new TimeChartJsonBuilder()
 *              .addDoubles(list)
 *              .addMaxTicksLimit(5)
 *              .build();
 * </PRE>
 */
public final class TimeChartJsonBuilder{
   private Gson gson;
   private Map<String, Object> map;
   private int min = 0;
   private int max = 100;
   private Integer yAxesMax = null;
   private int yAxesStepSize = 10;
   private Double doubleStepsize = null;
   private int fontSize = 12;
   private Integer maxTicksLimit = null;
   private LocalDateTime xAxesMin = null;
   private LocalDateTime xAxesMax = null;
   private List<Map<String, Object>> mlist = new ArrayList<>();

   public TimeChartJsonBuilder(){
      map = new HashMap<>();
      gson = new GsonBuilder().registerTypeHierarchyAdapter(LocalDateTime.class, LocalDateTimeAdapter.of("yyyy/MM/dd HH:mm:ss.SSS")).create();
   }
   public TimeChartJsonBuilder addIntegers(List<IntPoint> list){
      if (list.size() > 0){
         max = list.stream().max((a, b)->a.value.compareTo(b.value)).map(e->e.value.intValue()).orElse(100);
         LocalDateTime tmin = list.stream().map(e->e.time).min((a, b)->a.compareTo(b)).orElse(null);
         if (tmin != null){
            xAxesMin = xAxesMin==null ? tmin : xAxesMin.compareTo(tmin) < 0 ? xAxesMin : tmin;
         }
         LocalDateTime tmax = list.stream().map(e->e.time).max((a, b)->a.compareTo(b)).orElse(null);
         if (tmax != null){
            xAxesMax = xAxesMax==null ? tmax : xAxesMax.compareTo(tmax) < 0 ? tmax : xAxesMax;
         }
         Map<String, Object> map = new HashMap<>();
         map.put("plist", list);
         mlist.add(map);
      }
      return this;
   }
   public TimeChartJsonBuilder addDoubles(List<DoublePoint> list){
      if (list.size() > 0){
         max = list.stream().max((a, b)->a.value.compareTo(b.value)).map(e->e.value.intValue()).orElse(100);
         LocalDateTime tmin = list.stream().map(e->e.time).min((a, b)->a.compareTo(b)).orElse(null);
         if (tmin != null){
            xAxesMin = xAxesMin==null ? tmin : xAxesMin.compareTo(tmin) < 0 ? xAxesMin : tmin;
         }
         LocalDateTime tmax = list.stream().map(e->e.time).max((a, b)->a.compareTo(b)).orElse(null);
         if (tmax != null){
            xAxesMax = xAxesMax==null ? tmax : xAxesMax.compareTo(tmax) < 0 ? tmax : xAxesMax;
         }
         Map<String, Object> map = new HashMap<>();
         map.put("plist", list);
         mlist.add(map);
      }
      return this;
   }

   public TimeChartJsonBuilder addYAxesMin(int min){
      this.min = min;
      return this;
   }
   public TimeChartJsonBuilder addYAxesMax(int yAxesMax){
      this.yAxesMax = yAxesMax;
      return this;
   }

   public TimeChartJsonBuilder addFontSize(int fontSize){
      this.fontSize = fontSize;
      return this;
   }
   public TimeChartJsonBuilder addStepSize(int yAxesStepSize){
      this.yAxesStepSize = yAxesStepSize;
      return this;
   }
   public TimeChartJsonBuilder addStepSize(double yAxesStepSize){
      this.doubleStepsize = yAxesStepSize;
      return this;
   }
   public TimeChartJsonBuilder addMaxTicksLimit(int maxTicksLimit){
      if (maxTicksLimit < 1) throw new IllegalArgumentException("maxTicksLimit must be over 1");
      this.maxTicksLimit = maxTicksLimit;
      return this;
   }
   public TimeChartJsonBuilder addXAxesMin(LocalDateTime xAxesMin){
      this.xAxesMin = xAxesMin;
      return this;
   }
   public TimeChartJsonBuilder addXAxesMax(LocalDateTime xAxesMax){
      this.xAxesMax = xAxesMax;
      return this;
   }

   public String build(){
      map.put("list", mlist);
      map.put("yAxesMin", min);
      map.put("yAxesStepSize", yAxesStepSize <= 0 ? max : yAxesStepSize);
      max = (max / yAxesStepSize + 1) * yAxesStepSize;
      map.put("yAxesMax", yAxesMax==null ? max : yAxesMax);
      if (doubleStepsize != null) map.put("yAxesStepSize", doubleStepsize);
      if (maxTicksLimit != null) map.put("maxTicksLimit", maxTicksLimit);
      map.put("yAxesFontSize", fontSize);
      map.put("xAxesMin", xAxesMin);
      map.put("xAxesMax", xAxesMax);
      return gson.toJson(map);
   }
}

Wicket Pageでの使用

List<IntPoint> list ;
// list に描画するデータを格納
TimeChartJsonBuilder builder = new TimeChartJsonBuilder().addIntegers(list)
.addYAxesMax(200)
.addStepSize(20);
String json = builder.build();
try(StringResourceStream s = new StringResourceStream(json)){
    getRequestCycle().scheduleRequestHandlerAfterCurrent(
         new ResourceStreamRequestHandler(s)
    );
}catch(IOException e){
    logger.warn(e.getMessage(), e);
}

注意しなければならないのは、TimeChartJsonBuilder の addIntegers で渡すリストは
グラフ描画の為の時系列に並んでなければならない。必要であればメソッドの実行前にソート
する必要がある。

HTML Chart.js と moment-with-locale.js を使用できるように ヘッダは記述しておく。
https://momentjs.com/

<div class="chart">
   <canvas id="myChart"></canvas>
</div>

JSソース

var graph = {
   xLabels: [],
   yLabels: [],
   datasets: [{
      label: "Value",
      lineTension: 0,
      backgroundColor: "rgba(185, 64, 71, 0.6)",
      borderColor: "rgba(185, 64, 71, 0.6)",
      borderWidth: 1,
      borderCapStyle: 'round',
      borderDash: [],
      borderDashOffset: 0.0,
      borderJoinStyle: "round",
      pointBorderColor: "rgba(185, 64, 71, 0.6)",
      pointBackgroundColor: "rgba(185, 64, 71, 0.6)",
      pointBorderWidth: 0,
      pointHoverRadius: 4,
      pointHoverBackgroundColor: "rgba(185, 64, 71, 0.6)",
      pointHoverBorderColor: "rgba(185, 64, 71, 0.6)",
      pointHoverBorderWidth: 4,
      pointRadius: 0,
      pointHitRadius: 10,
      fill: false,
      spanGaps: false,
      data: [],
      yAxisID: "y-axis-1",
   }]
};
var options = {
   responsive: true,
   title:{ display:true,
      text:'Sample'
   },
   scales: {
      xAxes: [{ display: true,
         scaleLabel: { display: true, labelString: 'Time' },
         type: "time",
         time: {
            displayFormat: true,
            displayFormats: { minute: "HH:mm" },
            stepSize: 2
         },
      }],
      yAxes: [{ display: true,
         id: "y-axis-1",
         offset: true,
         scaleLabel: { display: true, labelString: 'Value',  fontSize: 22 },
         ticks: {}
      }]
   },
   chartArea: {
      /* グラフ領域の背景色 */
      backgroundColor: 'rgba(240, 248, 255, 1)'
   },
   tooltips: {
      callbacks: {
         title: function(tooltipItem, data){
            return moment(tooltipItem[0].xLabel._d).format('YYYY年M月D日(ddd)A hh:mm:ss');
         },
         label: function(tooltipItem, data){
            return  "value:" + tooltipItem.yLabel;
         }
      }
   },
};

$(function(){
   /* moment.js 設定 */
   moment.locale('ja');
   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(255, 255, 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();
         }
      }
   });
   var ctx = document.getElementById("myChart").getContext("2d");
   var myChart = new Chart(ctx, {
       type: 'line',
       data: graph,
       options: options
   });

   var drawGraph = function(e){
      graph.datasets[0].data = [];
      myChart.options.scales.yAxes[0].ticks['min'] = parseInt(e.yAxesMin, 10);
      myChart.options.scales.yAxes[0].ticks['max'] = parseInt(e.yAxesMax, 10);
      myChart.options.scales.yAxes[0].ticks['stepSize'] = parseInt(e.yAxesStepSize, 10);
      myChart.options.scales.yAxes[0].ticks['fontSize'] = parseInt(e.yAxesFontSize, 10);
      if (e.xAxesMin != null){
         myChart.options.scales.xAxes[0].time['min']
          = moment(e.xAxesMin, "YYYY/MM/DD HH:mm:ss.SSS");
      }
      if (e.xAxesMax != null){
         myChart.options.scales.xAxes[0].time['max']
          = moment(e.xAxesMax, "YYYY/MM/DD HH:mm:ss.SSS");
      }
      if (e.maxTicksLimit != undefined){
         myChart.options.scales.yAxes[0].ticks['maxTicksLimit'] = e.maxTicksLimit;
      }
      $.each(e.list, function(i, data){
         $.each(data.plist, function(n, v){
            graph.datasets[i].data.push(
               { t:moment(v.time, "YYYY-MM-DD HH:mm:ss.SSS"), y:v.value }
            );
         });
      });
      myChart.update();
   };

   var init_url = "/chartsample/line"
   $.ajax({
      type: 'POST',
      timeout: 10000,
      url: init_url,
      data: { },
      dataType: 'json',
      cache: false,
   }).done(function(e){
      drawGraph(e);
   }).fail(function(e){
      // 通信エラー
   }).always(function(e){
   });
});

グラフの線、太さ、線の種類、色、背景色、スケールの文字フォントサイズ、
タイトルの文字フォントサイズ、ツールチップの表示コンテンツ、、、
沢山、JSで書かなくてはならないのだが、
1本の線だけではなく複数の線を描画するケースを考慮して、
TimeChartJsonBuilder は、複数の線のJSONデータを生成するために、
List → 複数の線、
Map → 各線のJSON 配列

private List<Map<String, Object>> mlist = new ArrayList<>();

で構成して、AJAX通信で受信したJSプログラム側が、Chart.js グラフオブジェクトに展開する時に、
1本ずつ展開する。
時刻フォーマットは、TimeChartJsonBuilder で決められており、
時刻のデータ moment-with-locale.js で生成する。

      $.each(e.list, function(i, data){
         $.each(data.plist, function(n, v){
            graph.datasets[i].data.push(
               { t:moment(v.time, "YYYY-MM-DD HH:mm:ss.SSS"), y:v.value }
            );
         });
      });

サンプルグラフ描画
f:id:posturan:20190401095816j:plain