0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-09

output.gif

<!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

// 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())
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?