D3.js + django -- 目標に費やした時間(累積)を折れ線グラフで描画する(2)

概要

累積データを作成する

データが 1 件しかない場合でも線を描画したいので、開始日のデータをあらかじめ追加しておく必要がある ( = グラフの左下に相当する点 ) 。

/* 累積データを取得する */
function accum(recordsTarget, start) {

  /* 開始日を追加する */
  let records = [{
    id: 0,    // レコードの id
    cost: 0,    // 努力した時間 (分)
    at: start,    // 開始日
    memo: "開始日",    // 一口メモ
  }];

  /* 累積の合計コスト */
  let accumCost = 0;
  recordsTarget.forEach((record) => {

    /* cost を累積する */
    accumCost += record.cost;

    records.push({
      id: record.id,    // レコードの id
      cost: accumCost,    // 累積コスト (分)
      at: new Date(record.at),    // 日付時刻
      memo: record.memo,    // 一口メモ
    });
  });

  return records;
}

折れ線を描画する

/* 折れ線を描画する */
function drawLine(accumData, targetClassSuffix,
  svg, xScale, yScale, color="lightgray") {

  /* 折れ線描画用の関数を作成する */
  let valueLineFunc = d3.line()
    .x((d) => xScale(d.at))
    .y((d) => yScale(d.cost));

  /* 折れ線を描画する */
  let valueLine = svg.append("path")
    .data([accumData])
    .attr("class", `path-${targetClassSuffix}`)
    .attr("d", valueLineFunc)
    .style("fill", "none")
    .style("stroke", color)
    .style("stroke-width", "1.5px");

  return valueLine;
}

折れ線に対応する点 (circle要素) を描画する

/* 折れ線に対応する点 ( Circle 要素) を描画する */
function drawCircles(accumData, targetClassSuffix,
  svg, xScale, yScale, color="lightgray", circleR=4) {

  /* circle 要素(とその親となる g 要素)を追加 */
  let circles = svg.append("g")
    .attr("class", `circle-${targetClassSuffix}`)
    .selectAll("a")
    .data(accumData)
    .enter()
    .append("circle")
    .attr("class", `circle-${targetClassSuffix}`)
    .attr("cx", (d, i) => xScale(d.at))
    .attr("cy", (d, i) => yScale(d.cost))
    .attr("fill", color)
    .attr("r", circleR);

  /* ツールチップを追加する */
  circles.append("title")
    .text((d) => d.memo);

  /* マウスオーバー時の処理 */
  circles.on("mouseover", function () {
    d3.select(this)
      .transition()
      .duration(300)
      .attr("r", 8);
  });

  /* マウスアウト時の処理 */
  circles.on("mouseout", function () {
    d3.select(this)
      .transition()
      .duration(300)
      .attr("r", circleR);
  });

}

まとめて描画する

/* 目標に対する努力実績の折れ線グラフを描画する */
function draw(targetId, targetDescription, 
  lineColor="lightgray", maxFlexible=false) {

  /* 描画領域のマージン */
  let margin = {
    left: 50,
    right: 30,
    top: 30,
    bottom: 30,
  };

  /* SVG 要素を作成する */
  let destId = "graph-area";
  let padding = {
    right: 50,
    bottom: 120,
  };
  let width = window.innerWidth - padding.right;
  let height = window.innerHeight - padding.bottom;
  let svg = Graph.createSvg(destId, width, height);

  /* 開始日 & 終了日 */
  let start = new Date(2018, 1, 18);    // 2 月 18 日
  let finish = WK.weekend();    // 今週の土曜日
  finish.setHours(23);
  finish.setMinutes(23);
  finish.setSeconds(23);

  /* JSON データを取得する */
  let data;
  let requestURL = `/events/effort-list-json/${targetId}/`;
  d3.json(requestURL, (err, data) => {
    if (err) {
      console.log(err);
      return;
    }

    /* 累積データを取得 */
    let accumData = accum(data[targetDescription], start);

    /* スケールを作成する */
    let x = d3.scaleTime()
      .domain([start, finish])
      .range([margin.left, width - margin.right]);
    let yMax = accumData[accumData.length - 1].cost * 1.2;
    let y = d3.scaleLinear()
      .domain([0, yMax])
      .range([height - margin.bottom, margin.top]);

    /* x 軸を作成する */
    let nTicksX = 14;
    let gAxisX = Graph.createAxisBottomAndGridLine(
      svg, x, nTicksX, height, margin);

    /* y 軸を作成する */
    let nTicksY = 12;
    let gAxisY = Graph.createAxisLeftAndGridLine(
      svg, y, nTicksY, width, margin);

    /* カテゴリー名を取得 */
    let targetReg = /\[(.+)\]/;
    let category = targetDescription.match(targetReg)[1];

    /* カテゴリー名 → 折れ線と点の色 */
    let colors = {
      "django": "green",
      "機械学習": "pink",
      "Python": "deeppink",
      "JavaScript": "springgreen",
    };
    let color = colors[category];

    /* 折れ線を描画する */
    drawLine(accumData, "keras_1", svg, x, y, color);

    /* 折れ線に対応した点 (Circle要素) を描画する */
    drawCircles(accumData, "keras_1", svg, x, y, color);

  });    // end of d3.json() { ... }

}    // end of function draw() { ... }

コード全体と動作イメージ

/* target-line.js を読み込む必要がある */
if (!TargetLine) {
  let emsg = "object Targetline not found";
  throw new Error(emsg);
}

/* カテゴリー名を取得する */
let catReg = /\[(.+)\]/;
let catName = targetDescription.match(catReg)[1];

/* カテゴリー名 → 折れ線グラフの色 */
let colors = {
  "django": "green",
  "機械学習": "pink",
  "Python": "deeppink",
  "JavaScript": "springgreen",
};
let lineColor = colors[catName];

/* 目標に対する努力実績のグラフを描画する */
/* targetId は HTML埋め込み済み */
let maxFlexible = true;
TargetLine.draw(targetId, targetDescription, lineColor, maxFlexible);

f:id:zdassen:20180407223528j:plain

考察と感想

  • 自分の中で盛り上がった部分 ( = 集中的に時間をかけた ) 部分が分かる
  • サボっていると折れ線の間隔が広がるので直観的
  • ツールチップも動いた