d3.js 入門學習筆記( 八) 實現 面積圖(線圖) 散點圖 氣泡圖 條形圖

WanFengZ發表於2019-11-24

線圖 面積圖

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .line {
            fill: none;
            stroke: steelblue;
            stroke-width: 2;
        }
        .grid-line{
            stroke: black;
            shape-rendering: crispEdges;
            stroke-opacity: .2;
        }
        .area {
            stroke: none;
            fill: steelblue;
            fill-opacity: .2;
        }
        .dot {
            fill: #fff;
            stroke: steelblue;
        }
    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
    function areaChart() {
        const _chart = {}

        let _width = 600,
            _height = 300,
            _margins = {
                top: 30,
                right: 30,
                bottom: 30,
                left: 30
            },
            _colors = d3.scaleOrdinal(d3.schemeCategory10),
            _data = []

        let _x,
            _y,
            _svg,
            _body

        _chart.width = function (width) {
            if (!arguments.length) return _width
            _width = width
            return _chart
        }

        _chart.height = function (height) {
            if (!arguments.length) return _height
            _height = height
            return _chart
        }

        _chart.margins = function (margins) {
              if (!arguments.length) return _margins
              _margins = margins
              return _chart
        }

        _chart.colors = function (colorScale) {
            if (!arguments.length) return _colors
            _colors = colorScale
            return _chart
        }

        _chart.x = function (xScale) {
            if (!arguments.length) return _x
            _x = xScale
            return _chart
        }

        _chart.y = function (yScale) {
          if (!arguments.length) return _y
          _y = yScale
          return _chart
        }

        _chart.addSeries = function (series) {
            _data.push(series)
            return _chart
        }

        _chart.render = function () {
            if (!_svg) {
                _svg = d3.select('body')
                    .append('svg')
                    .attr('width', _width)
                    .attr('height', _height)
                renderAxes()
            }
            renderBody()
        }

        function renderAxes() {
            const axes = _svg.append('g')
                .classed('axes', true)

            axes.append('g')
                .classed('x-axis', true)
                .attr('transform', `translate(${_margins.left}, ${_height - _margins.bottom})`)
                .call(d3.axisBottom().scale(_x.range([0, _width - _margins.left - _margins.right])))

            axes.selectAll('g.x-axis g.tick')
                .append('line')
                    .classed('grid-line', true)
                    .attr('x1', 0)
                    .attr('y1', 0)
                    .attr('x2', 0)
                    .attr('y2', -(_height - _margins.top - _margins.bottom))

            axes.append('g')
                .classed('y-axis', true)
                .attr('transform', `translate(${_margins.left}, ${_margins.top})`)
                .call(d3.axisLeft().scale(_y.range([_height - _margins.top - _margins.bottom, 0])))

            axes.selectAll('g.y-axis g.tick')
                .append('line')
                    .classed('grid-line', true)
                    .attr('x1', 0)
                    .attr('y1', 0)
                    .attr('x2', _width - _margins.left - _margins.right)
                    .attr('y2', 0)
        }

        function renderBody() {
            if (!_body) {
              _body = _svg.append('g')
                .classed('body', true)
                .attr('transform', `translate(${_margins.left}, ${_margins.top})`)
            }
            renderLines()
            renderAreas()
            renderDots()
        }

        function renderLines() {
            const line = d3.line()
                .x((d) => _x(d.x))
                .y((d) => _y(d.y))

            const pathLines = _body.selectAll('path.line')
                .data(data)

            pathLines.enter()
                .append('path')
                .merge(pathLines)
                    .classed('line', true)
                    .style('stroke', (d, i) => _colors(i))
                .transition()
                    .attr('d', d => line(d))
        }

        function renderAreas() {
            const area = d3.area()
                .x(d => _x(d.x))
                .y0(_y(0))
                .y1(d => _y(d.y))

            const pathAreas = _body.selectAll('path.area')
                .data(data)

            pathAreas.enter()
                .append('path')
                .merge(pathAreas)
                    .classed('area', true)
                    .style('fill', (d, i) => _colors(i))
                .transition()
                    .attr('d', d => area(d))

        }

        function renderDots() {
            _data.forEach((data, index) => {
                const circles =_body.selectAll(`circle._${index}`)
                    .data(data)

                circles.enter()
                    .append('circle')
                    .merge(circles)
                        .attr('class', `dot _${index}`)
                        .style('stroke', () => _colors(index))
                    .transition()
                        .attr('cx', d => _x(d.x))
                        .attr('cy', d => _y(d.y))
                        .attr('r', 4.5)
            })
        }

        return _chart
    }

    const numberOfSeries = 2,
          numberOfDataPoint = 11
          data = []

    for (let i = 0; i < numberOfSeries; i++) {
        data.push(d3.range(numberOfDataPoint).map((i) => ({x: i, y: Math.random() * 10})))
    }

    const chart = areaChart()
        .x(d3.scaleLinear().domain([0, 10]))
        .y(d3.scaleLinear().domain([0, 10]))

    data.forEach(series => {
        chart.addSeries(series)
    })

    chart.render()

    function update() {
        for (let i = 0; i < data.length; i++) {
            let series = data[i]
            series.length = 0
            for (let i = 0; i < numberOfDataPoint; i++) {
                series.push({x: i, y: Math.random() * 10})
            }
        }
        chart.render()
    }
</script>
<button onclick="update()">update</button>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習筆記( 八) 實現 面積圖(線圖) 散點圖 氣泡圖 條形圖

我們用工廠模式,areaChart 返回一個 chart物件,我們可以向其設定渲染資料和一些可以自定義的屬性項,渲染的邏輯放在 render() 函式中。

if (!_svg) {
    _svg = d3.select('body')
        .append('svg')
        .attr('width', _width)
        .attr('height', _height)
    renderAxes()
}
renderBody()
複製程式碼

座標軸只會在svg初始化的時候渲染一次。注意我們這裡是將座標軸和圖表內容主題分開的。

renderAxes() 裡面就是熟悉的渲染座標軸和柵格線的邏輯。

然後在初始化 body 時,位移到邊距的位置,這樣可以消除掉邊距的影響,因為 body 內的座標都是以 body 左上角為原點的。

接著,我們分別在renderLine() renderArea() renderDots() 內完成線條、面積、資料點的渲染。都是之前我們熟悉的程式碼。

散點圖

散點圖的工廠函式和上面類似,只實現了必須的功能,其餘的設定函式可以自行新增。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .grid-line{
            stroke: black;
            shape-rendering: crispEdges;
            stroke-opacity: .2;
        }
    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
    function scatterPlotChart() {
        const _chart = {}

        let _width = 500,
            _height = 500,
            _margins = {top: 30, right: 30, bottom: 30, left: 30},
            _colors = d3.scaleOrdinal(d3.schemeCategory10),
            _x,
            _y,
            _data = [],
            _svg,
            _body,
            _symbolTypes = d3.scaleOrdinal()
                .range(d3.symbols)


        _chart.render = function () {
            if (!_svg) {
                _svg = d3.select('body')
                    .append('svg')
                        .attr('width', _width)
                        .attr('height', _height)
                renderAxes()
            }
            renderBody()
        }

        function renderAxes() {
            const axes = _svg.append('g')
                .classed('axes', true)

            axes.append('g')
                .classed('x-axis', true)
                .attr('transform', `translate(${_margins.left}, ${_height - _margins.bottom})`)
                .call(d3.axisBottom().scale(_x.range([0, _width - _margins.left - _margins.right])))

            axes.selectAll('g.x-axis g.tick')
                .append('line')
                .classed('grid-line', true)
                .attr('x1', 0)
                .attr('y1', 0)
                .attr('x2', 0)
                .attr('y2', -(_height - _margins.top - _margins.bottom))

            axes.append('g')
                .classed('y-axis', true)
                .attr('transform', `translate(${_margins.left}, ${_margins.top})`)
                .call(d3.axisLeft().scale(_y.range([_height - _margins.top - _margins.bottom, 0])))

            axes.selectAll('g.y-axis g.tick')
                .append('line')
                    .classed('grid-line', true)
                    .attr('x1', 0)
                    .attr('y1', 0)
                    .attr('x2', _width - _margins.left - _margins.right)
                    .attr('y2', 0)
        }

        function renderBody() {
            if (!_body) {
                _body = _svg.append('g')
                    .classed('body', true)
                    .attr('transform', `translate(${_margins.left}, ${_margins.right})`)
            }
            renderSymbols()
        }

        function renderSymbols() {
            _data.forEach((data, i) => {
                const symbols = _body.selectAll('path._' + i)
                    .data(data)

                symbols.enter()
                    .append('path')
                    .merge(symbols)
                        .attr('class', 'symbol _' + i)
                        .style('fill', d => _colors(i))
                    .transition()
                        .attr('transform', d => `translate(${_x(d.x)}, ${_y(d.y)})`)
                        .attr('d', d3.symbol().type(_symbolTypes(i)))
            })
        }


        _chart.addSeries = function (series) {
            _data.push(series)
            return _chart
        }

        _chart.x = function (x) {
            if (!arguments.length) return _x
            _x = x
            return _chart
        }

        _chart.y = function (y) {
            if (!arguments.length) return _y
            _y = y
            return _chart
        }

        return _chart
    }

    const numberOfSeries = 5,
          numberOfDataPoint = 11,
          data = []

    for (let i = 0; i < numberOfSeries; i++) {
        data.push(d3.range(numberOfDataPoint).map(i => ({x: Math.random() * 10, y: Math.random() * 10})))
    }

    const chart = scatterPlotChart()
        .x(d3.scaleLinear().domain([0, 10]))
        .y(d3.scaleLinear().domain([0, 10]))

    data.forEach(series => {
        chart.addSeries(series)
    })

    chart.render()
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習筆記( 八) 實現 面積圖(線圖) 散點圖 氣泡圖 條形圖

主要程式碼如下:

const _symbolTypes = d3.scaleOrdinal()
        .range(d3.symbols)

function renderSymbols() {
    _data.forEach((data, i) => {
        const symbols = _body.selectAll('path._' + i)
            .data(data)

        symbols.enter()
            .append('path')
            .merge(symbols)
                .attr('class', 'symbol _' + i)
                .style('fill', d => _colors(i))
            .transition()
                .attr('transform', d => `translate(${_x(d.x)}, ${_y(d.y)})`)
                .attr('d', d3.symbol().type(_symbolTypes(i)))
    })
}
複製程式碼

我們先建立了個有序尺度,它對應著不同的符號形狀,d3.symbols 是 d3 內建符號形狀的陣列。

在 renderSymbol() 函式中 我們為每個資料項都建立 path 並呼叫 symbol 生成器為屬性 d 賦值。symbol.type(_symbolTypes(i))來設定 symbol 的型別為索引所對應著的尺度中的型別。

氣泡圖

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .grid-line{
            stroke: black;
            shape-rendering: crispEdges;
            stroke-opacity: .2;
        }
    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
  function scatterPlotChart() {
    const _chart = {}

    let _width = 500,
      _height = 500,
      _margins = {top: 30, right: 30, bottom: 30, left: 30},
      _colors = d3.scaleOrdinal(d3.schemeCategory10),
      _x,
      _y,
      _r,
      _data = [],
      _svg,
      _body


    _chart.render = function () {
      if (!_svg) {
        _svg = d3.select('body')
          .append('svg')
          .attr('width', _width)
          .attr('height', _height)
        renderAxes()
      }
      renderBody()
    }

    function renderAxes() {
      const axes = _svg.append('g')
        .classed('axes', true)

      axes.append('g')
        .classed('x-axis', true)
        .attr('transform', `translate(${_margins.left}, ${_height - _margins.bottom})`)
        .call(d3.axisBottom().scale(_x.range([0, _width - _margins.left - _margins.right])))

      axes.selectAll('g.x-axis g.tick')
        .append('line')
          .classed('grid-line', true)
          .attr('x1', 0)
          .attr('y1', 0)
          .attr('x2', 0)
          .attr('y2', -(_height - _margins.top - _margins.bottom))

      axes.append('g')
        .classed('y-axis', true)
        .attr('transform', `translate(${_margins.left}, ${_margins.top})`)
        .call(d3.axisLeft().scale(_y.range([_height - _margins.top - _margins.bottom, 0])))

      axes.selectAll('g.y-axis g.tick')
        .append('line')
          .classed('grid-line', true)
          .attr('x1', 0)
          .attr('y1', 0)
          .attr('x2', _width - _margins.left - _margins.right)
          .attr('y2', 0)
    }

    function renderBody() {
      if (!_body) {
        renderBodyClip()
        _body = _svg.append('g')
          .classed('body', true)
          .attr('transform', `translate(${_margins.left}, ${_margins.right})`)
          .attr('clip-path', 'url(#body-clip)')
      }
      renderBubbles()
    }

    function renderBodyClip() {
      _svg.append('defs')
        .append('clipPath')
          .attr('id', 'body-clip')
        .append('rect')
          .attr('x', 0)
          .attr('y', 0)
          .attr('width', _width - _margins.left - _margins.right)
          .attr('height', _height - _margins.top - _margins.bottom)
    }

    function renderBubbles() {
      _r.range([0, 20])

      _data.forEach((data, i) => {
        const bubbles = _body.selectAll('circle._' + i)
          .data(data)

        bubbles.enter()
          .append('circle')
          .merge(bubbles)
            .attr('class', 'circle _' + i)
            .style('stroke', _colors(i))
            .style('fill', _colors(i))
          .transition()
            .attr('cx', d => _x(d.x))
            .attr('cy', d => _x(d.y))
            .attr('r', d => _r(d.r))
      })
    }

    _chart.addSeries = function (series) {
      _data.push(series)
      return _chart
    }

    _chart.x = function (x) {
      if (!arguments.length) return _x
      _x = x
      return _chart
    }

    _chart.y = function (y) {
      if (!arguments.length) return _y
      _y = y
      return _chart
    }

    _chart.r = function (r) {
      if (!arguments.length) return _r
      _r = r
      return _chart
    }

    return _chart
  }


    const numberOfSeries = 5,
        numberOfDataPoint = 11,
        data = []

    for (let i = 0; i < numberOfSeries; i++) {
        data.push(d3.range(numberOfDataPoint).map(i => ({x: Math.random() * 10, y: Math.random() * 10, r: Math.random() * 10})))
    }

    const chart = scatterPlotChart()
        .x(d3.scaleLinear().domain([0, 10]))
        .y(d3.scaleLinear().domain([0, 10]))
        .r(d3.scalePow().exponent(2).domain([0, 10]))

    data.forEach(series => {
        chart.addSeries(series)
    })

    chart.render()
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習筆記( 八) 實現 面積圖(線圖) 散點圖 氣泡圖 條形圖

氣泡圖的渲染和之前相比也只是多了個 r 屬性的賦值,另外,我們還給 body 新增了個裁剪區域,用來限制圖形越出 body,如下:

_svg.append('defs')
    .append('clipPath')
      .attr('id', 'body-clip')
    .append('rect')
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', _width - _margins.left - _margins.right)
      .attr('height', _height - _margins.top - _margins.bottom)
          
_body = _svg.append('g')
    .classed('body', true)
    .attr('transform', `translate(${_margins.left}, ${_margins.right})`)
    .attr('clip-path', 'url(#body-clip)')
複製程式碼

條形圖

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .grid-line{
            stroke: black;
            shape-rendering: crispEdges;
            stroke-opacity: .2;
        }
        .bar {
            fill: #4682b4;
        }
    </style>
</head>
<body>
<script src="../d3.js"></script>
<script>
  function scatterPlotChart() {
    const _chart = {}

    let _width = 500,
      _height = 500,
      _margins = {top: 30, right: 30, bottom: 30, left: 30},
      _x,
      _y,
      _data = [],
      _svg,
      _body


    _chart.render = function () {
      if (!_svg) {
        _svg = d3.select('body')
          .append('svg')
          .attr('width', _width)
          .attr('height', _height)
        renderAxes()
      }
      renderBody()
    }

    function renderAxes() {
      const axes = _svg.append('g')
        .classed('axes', true)

      axes.append('g')
        .classed('x-axis', true)
        .attr('transform', `translate(${_margins.left}, ${_height - _margins.bottom})`)
        .call(d3.axisBottom().scale(_x.range([0, _width - _margins.left - _margins.right])))

      axes.selectAll('g.x-axis g.tick')
        .append('line')
        .classed('grid-line', true)
        .attr('x1', 0)
        .attr('y1', 0)
        .attr('x2', 0)
        .attr('y2', -(_height - _margins.top - _margins.bottom))

      axes.append('g')
        .classed('y-axis', true)
        .attr('transform', `translate(${_margins.left}, ${_margins.top})`)
        .call(d3.axisLeft().scale(_y.range([_height - _margins.top - _margins.bottom, 0])))

      axes.selectAll('g.y-axis g.tick')
        .append('line')
        .classed('grid-line', true)
        .attr('x1', 0)
        .attr('y1', 0)
        .attr('x2', _width - _margins.left - _margins.right)
        .attr('y2', 0)
    }

    function renderBody() {
      if (!_body) {
        _body = _svg.append('g')
          .classed('body', true)
          .attr('transform', `translate(${_margins.left}, ${_margins.right})`)
          .attr('clip-path', 'url(#body-clip)')
        renderBodyClip()
      }
      renderBubbles()
    }

    function renderBodyClip() {
      _svg.append('defs')
        .append('clipPath')
        .attr('id', 'body-clip')
        .append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', _width - _margins.left - _margins.right)
        .attr('height', _height - _margins.top - _margins.bottom)
    }

    function renderBubbles() {
        const padding = 2

        const bars = _body.selectAll('rect.bar').data(_data)

        bars.enter()
          .append('rect')
          .merge(bars)
          .classed('bar', true)
          .transition()
          .attr('x', d => _x(d.x))
          .attr('y', d => _y(d.y))
          .attr('height', d => _height - _margins.top - _margins.bottom - _y(d.y))
          .attr('width', d => Math.floor((_width - _margins.left - _margins.right) / _data.length) - padding)
    }


    _chart.addSeries = function (series) {
      _data = series
      return _chart
    }

    _chart.x = function (x) {
      if (!arguments.length) return _x
      _x = x
      return _chart
    }

    _chart.y = function (y) {
      if (!arguments.length) return _y
      _y = y
      return _chart
    }

    return _chart
  }


  const numberOfSeries = 5,
    numberOfDataPoint = 10,
    data = []

  for (let i = 0; i < numberOfSeries; i++) {
    data.push(d3.range(numberOfDataPoint).map(i => ({x: i, y: Math.random() * 10})))
  }

  const chart = scatterPlotChart()
    .x(d3.scaleLinear().domain([0, 10]))
    .y(d3.scaleLinear().domain([0, 10]))

  data.forEach(series => {
    chart.addSeries(series)
  })

  chart.render()
</script>
</body>
</html>
複製程式碼

效果如下:

d3.js 入門學習筆記( 八) 實現 面積圖(線圖) 散點圖 氣泡圖 條形圖

相關文章