D3でリアルタイムチャートを描く


![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117164/33f37dd9-d87d-407a-9ece-5df05b166faa.gif)

```html
<!doctype html>
<html>
  <meta charset="utf-8" />
  <title>D3 chart</title>
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
  <style>
    svg {
      font: 10px sans-serif;
    }
    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }
    .x.axis line {
      shape-rendering: auto;
    }
    .line {
      fill: none;
      stroke: #000;
      stroke-width: 0.8px;
    }
    .bar {
      fill: steelblue;
      opacity: 0.8;
    }
    .area {
      fill: steelblue;
      opacity: 0.6;
    }
    .grid-x,
    .grid-y {
      stroke: lightgray;
      stroke-width: 0.5;
      stroke-dasharray: 3,6;
    }
  </style>
  <script type="module">
    import { LineChart, BarChart, AreaChart } from './chart.js'

    const config = {
      n: 60,
      duration: 750,
      margin: { top: 6, right: 6, bottom: 20, left: 35 },
      width: 720,
      height: 120,
      gridx: true,
      gridy: true,
    }

    // Example of updating values
    const random = d3.random.normal(0, 100)
    const getCurrent = () => {
      return random()
    }

    new LineChart('#chart1', getCurrent, config)
    new LineChart('#chart2', getCurrent, config, 'basis')
    new BarChart('#chart3', getCurrent, config)
    new AreaChart('#chart4', getCurrent, config)
    new AreaChart('#chart5', getCurrent, config, 'basis')
  </script>
  <h1>D3 Real-time Chart</h1>
  <div id="chart-container">
    <div id="chart1">Chart1 Line linear</div>
    <div id="chart2">Chart2 Line basis</div>
    <div id="chart3">Chart3 Bar</div>
    <div id="chart4">Chart4 Area linear</div>
    <div id="chart5">Chart5 Area basis</div>
  </div>
</html>
```
## chart.js
```js
// chart.js
class Chart {
  constructor(id, config, xDomain) {
    let { n, duration, margin, width, height, gridx, gridy } = config

    this.id = id
    this.data = d3.range(n).map(() => 0)
    this.xDomain = xDomain
    this.lastIndex = xDomain[1]

    this.n = n
    this.duration = duration
    this.margin = margin
    this.width = width - margin.right
    this.height = height - margin.top - margin.bottom
    this.gridx = gridx
    this.gridy = gridy

    this.initialize()
  }

  initialize() {
    const now = new Date(Date.now() - this.duration)

    this.timeseries = d3.time
      .scale()
      .domain([now - this.lastIndex * this.duration, now - this.duration])
      .range([0, this.width])

    this.x = d3.scale.linear().domain(this.xDomain).range([0, this.width])

    this.y = d3.scale
      .linear()
      .domain([d3.min(this.data), d3.max(this.data)])
      .range([this.height, 0])

    this.rootElement = d3
      .select(this.id)
      .append('p')
      .append('svg')
      .attr('width', this.width + this.margin.left + this.margin.right)
      .attr('height', this.height + this.margin.top + this.margin.bottom)
      .append('g')
      .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')')

    this.rootElement //
      .append('defs')
      .append('clipPath')
      .attr('id', 'clip')
      .append('rect')
      .attr('width', this.width)
      .attr('height', this.height)

    this.axisY = this.rootElement
      .append('g')
      .attr('class', 'y axis')
      .call((this.y.axis = d3.svg.axis().scale(this.y).ticks(5).orient('left')))

    this.axisX = this.rootElement
      .append('g')
      .attr('class', 'x axis')
      .attr('transform', 'translate(0,' + this.height + ')')
      .call((this.timeseries.axis = d3.svg.axis().scale(this.timeseries).orient('bottom')))

    this.transition = d3.select({}).transition().duration(this.duration).ease('linear')
  }

  updateAxis() {
    const now = new Date()
    this.timeseries.domain([now - this.lastIndex * this.duration, now - this.duration])
    this.axisX.call(this.timeseries.axis)

    this.y.domain([d3.min(this.data), d3.max(this.data)])
    this.axisY.call(this.y.axis)

    if (this.gridx) {
      const xTicks = this.axisX.selectAll('g.tick')
      const height = this.height
      xTicks.each(function () {
        const tick = d3.select(this)
        tick.selectAll('.grid-x').remove()
        tick.append('line').attr('class', 'grid-x').attr('y1', 0).attr('y2', -height)
      })
    }

    if (this.gridy) {
      const yTicks = this.axisY.selectAll('g.tick')
      const width = this.width
      yTicks.each(function () {
        const tick = d3.select(this)
        tick.selectAll('.grid-y').remove()
        tick.append('line').attr('class', 'grid-y').attr('x1', 0).attr('x2', width)
      })
    }
  }
}

export class LineChart extends Chart {
  constructor(id, getCurrent, config, interpolation = 'linear') {
    super(id, config, interpolation === 'linear' ? [0, config.n - 1] : [1, config.n - 2])
    this.getCurrent = getCurrent
    this.interpolation = interpolation
    this.render()
  }

  render() {
    this.line = d3.svg
      .line()
      .interpolate(this.interpolation)
      .x((d, i) => this.x(i))
      .y((d) => this.y(d))
    this.path = this.rootElement //
      .append('g')
      .attr('clip-path', 'url(#clip)')
      .append('path')
      .datum(this.data)
      .attr('class', 'line')
      .attr('d', this.line)
    this.tick()
  }

  tick() {
    const each = () => {
      this.data.push(this.getCurrent())
      this.updateAxis()
      const translate = this.interpolation === 'linear' ? this.x(-1) : this.x(0)
      this.path //
        .attr('d', this.line)
        .attr('transform', null)
        .transition()
        .attr('transform', `translate(${translate})`)
      this.data.shift()
    }
    this.transition = this.transition
      .each(each)
      .transition()
      .each('start', () => this.tick())
  }
}

export class BarChart extends Chart {
  constructor(id, getCurrent, config) {
    super(id, config, [0, config.n - 1])
    this.getCurrent = getCurrent
    this.render()
  }

  render() {
    this.bars = this.rootElement
      .append('g')
      .attr('class', 'bars')
      .attr('clip-path', 'url(#clip)')
      .selectAll('.bar')
      .data(this.data)
      .enter()
      .append('rect')
      .attr('class', 'bar')
      .attr('x', (d, i) => this.x(i))
      .attr('y', (d) => this.y(d))
      .attr('width', this.width / this.n - 1)
      .attr('height', (d) => this.height - this.y(d))
    this.tick()
  }

  tick() {
    const each = () => {
      this.data.push(this.getCurrent())
      this.updateAxis()
      this.x.domain([this.x.domain()[0] + 1, this.x.domain()[1] + 1])
      this.bars = this.bars.data(this.data)
      this.bars
        .enter()
        .append('rect')
        .attr('class', 'bar')
        .attr('x', (d, i) => this.x(i))
        .attr('y', (d) => this.y(d))
        .attr('width', this.width / this.n - 1)
        .attr('height', (d) => this.height - this.y(d))
      this.bars
        .transition()
        .duration(this.duration)
        .attr('x', (d, i) => this.x(i))
    }
    this.transition = this.transition
      .each(each)
      .transition()
      .each('start', () => this.tick())
  }
}

export class AreaChart extends Chart {
  constructor(id, getCurrent, config, interpolation = 'linear') {
    super(id, config, [0, config.n - 1])
    this.getCurrent = getCurrent
    this.interpolation = interpolation
    this.render()
  }

  render() {
    this.area = d3.svg
      .area()
      .interpolate(this.interpolation)
      .x((d, i) => this.x(i))
      .y0(this.y(0))
      .y1((d) => this.y(d))
    this.path = this.rootElement //
      .append('g')
      .attr('clip-path', 'url(#clip)')
      .append('path')
      .datum(this.data)
      .attr('class', 'area')
      .attr('d', this.area)
    this.tick()
  }

  tick() {
    const each = () => {
      this.data.push(this.getCurrent())
      this.updateAxis()
      const translate = this.x(-1)
      this.path //
        .attr('d', this.area)
        .attr('transform', null)
        .transition()
        .attr('transform', `translate(${translate})`)
      this.data.shift()
    }
    this.transition = this.transition
      .each(each)
      .transition()
      .each('start', () => this.tick())
  }
}
```