D3数据可视化-stack_bar

案例

与饼图相似的是,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>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容