美文网首页
D3数据可视化-stack_bar

D3数据可视化-stack_bar

作者: 一拾五 | 来源:发表于2019-06-03 21:31 被阅读0次

    案例

    与饼图相似的是,stack bar 图也适合表现各个项目在总体中的比例。但同时 stack bar 还可以表现随着时间的推移各个项目比例的变化情况,这是饼图难以做到的。

    下图展现了从2017年2季度到2018年1季度全球手机市场中各厂商的占有率情况,数据来源于 IDC

    stack-bar-market-share.png

    解析

    普通的柱形图,一个柱子就是一个 rect 元素。而在 stack bar 中,每个柱子都是由多个 rect 组成的,每个 rect 我们需要知道其 y坐标轴上的基点(baseline)和顶点(topline)。我们的原始数据如下:

    const data = [
            {
              year: 2017,
              quarter: 2,
              samsung: 0.229,
              apple: 0.118,
              huawei: 0.110,
              oppo: 0.08,
              xiaomi: 0.062,
              others: 0.401,
            },
            {
              year: 2017,
              quarter: 3,
              samsung: 0.221,
              apple: 0.124,
              huawei: 0.104,
              oppo: 0.081,
              xiaomi: 0.075,
              others: 0.396,
            },
             ...
          ]
    

    我们需要把这些数据转化为相应的 series,结构如下:

    [
      [[0,0.118],[0,0.124],[0,0.196],[0,0.157]], // apple
      [[0.118,0.347],[0.124,0.345],[0.196,0.385],[0.157,0.392]], // samsung
      ...
    ]
    

    每个 series 代表一个属性在各个 x 轴点上的范围,每个数组元素的两个取值分别是 baseline 和 topline。不过现在这里的每个点代表的是值范围,如果我们要实际绘制图形,需要把值 scale 到实际图形的长度。假设一段数据的值范围是 [0, 0.118],而 y 轴的总长度是 100px,那么这一段数据渲染出来的实际y轴范围就是 [0, 11.8px]。

    在柱形绘制完成之后,我们还需要添加 x 轴,可以用 scalePoint 帮助我们定位到相应的刻度(ticks)。需要注意的是,有些例子会使用 scaleBand 来定位 x 轴位置,但是 scaleBand 是不能指定柱宽度的,所以如果要让柱形和x轴刻度对齐,最好使用 scalePoint 来定位,scalePoint 是宽度为 0 的 scaleBand。

    实现

    源代码 Git 地址在这里。

    完整实现如下:

    <!DOCTYPE html>
    <html>
      <body>
        <style>
          svg {
            border: 1px solid lightgrey;
          }
    
          .caption {
            margin-top: 20px;
            width: 600px;
            text-align: center;
          }
        </style>
        <script src="http://d3js.org/d3.v5.min.js"></script>
        <script type="text/javascript">
          const maxHeight = 400;
          const maxWidth = 600;
          const barWidth = 20;
    
          const data = [
            {
              year: 2017,
              quarter: 2,
              samsung: 0.229,
              apple: 0.118,
              huawei: 0.110,
              oppo: 0.08,
              xiaomi: 0.062,
              others: 0.401,
            },
            {
              year: 2017,
              quarter: 3,
              samsung: 0.221,
              apple: 0.124,
              huawei: 0.104,
              oppo: 0.081,
              xiaomi: 0.075,
              others: 0.396,
            },
    
            {
              year: 2017,
              quarter: 4,
              samsung: 0.189,
              apple: 0.196,
              huawei: 0.107,
              oppo: 0.069,
              xiaomi: 0.071,
              others: 0.368,
            },
            {
              year: 2018,
              quarter: 1,
              samsung: 0.235,
              apple: 0.157,
              huawei: 0.118,
              oppo: 0.074,
              xiaomi: 0.084,
              others: 0.332,
            },
          ]
    
          const keys = ['apple', 'samsung', 'huawei', 'oppo', 'xiaomi', 'others'];
    
          const getTimePoint = (d) => {
            const _d = d.data ? d.data : d;
            return `${_d.year}-${_d.quarter}`;
          }
    
          const stack = d3.stack().keys(keys).order(d3.stackOrderNone).offset(d3.stackOffsetNone);
          const series = stack(data);
          const colorArray = ['#38CCCB', '#0074D9', '#2FCC40', '#FEDC00', '#FF4036', 'lightgrey'];
    
          function renderVerticalStack() {
            const svg = d3.select('body')
              .append('svg')
              .attr('width', maxWidth)
              .attr('height', maxHeight + 50);
    
            const xScale = d3.scalePoint()
              .domain(data.map(getTimePoint))
              .range([0, maxWidth])
              .padding(0.2);
    
            const xScalePoint = d3.scalePoint()
              .domain(data.map(getTimePoint))
              .range([0, maxWidth])
              .padding(0.2)
    
            const stackMax = (serie) => d3.max(serie, (d) => d ? d[1] : 0)
            const stackMin = (serie) => d3.min(serie, (d) => d? d[0]: 0)
    
            const y = d3.scaleLinear()
              .domain([d3.max(series, stackMax), d3.min(series, stackMin)])
              .range([0, maxHeight])
    
            const g = svg.selectAll('g')
              .data(series)
              .enter()
              .append('g')
              .attr('fill', (d, i) => colorArray[i % colorArray.length])
              .selectAll('rect')
              .data((d) => d)
              .enter()
              .append('rect')
              .attr('x', (d) => {
                const scaledX = xScale(getTimePoint(d));
                return scaledX - barWidth / 2;
              })
              .attr('y', (d) => y(d[1]))
              .attr('width', barWidth)
              .attr('height', (d) => {
                return y(d[0]) - y(d[1])
              })
    
            const axis = d3.axisBottom(xScalePoint);
            svg.append('g')
              .attr('transform', `translate(0, ${maxHeight})`)
              .call(axis)
          }
    
          renderVerticalStack()
    
          d3.select('body')
            .append('div')
            .style('width', maxWidth + 'px')
            .style('display', 'flex')
            .style('justify-content', 'space-around')
            .selectAll('.legend')
            .data(keys)
            .enter()
            .append('div')
            .attr('class', 'legend')
            .text((d) => d)
            .style('color', (d, i) => colorArray[i % colorArray.length])
        </script>
    
        <div class='caption'>
          Worldwide Top 5 Smartphone Shipment Company Market Share<br/>
          Data Source: <a href='https://www.idc.com/promo/smartphone-market-share/vendor'>IDC</a>
        </div>
      </body>
    </html>
    
    

    相关文章

      网友评论

          本文标题:D3数据可视化-stack_bar

          本文链接:https://www.haomeiwen.com/subject/ttegtctx.html