可视化D3专题系列(一)初见

1,815 阅读10分钟

Author: arcsin1
Time: 2020-04-22 00:01

前言

一直以来,都想写D3可视化系列文章,不用担心我会结合D3的V4,V5版本写下去。 让更多喜欢可视化的能够轻松用D3上手定制自己喜欢的可视化。

那么如何学习D3.js?

  我分为4个部分:(目前只介绍svg篇,后续再介绍canvas篇)
  
  1. svg入门(包括svg的基本元素等)
  2. d3中的数据驱动和dom的操作
  3. d3中数据的绑定
  4. d3中的比例尺

SVG入门

在对D3.js进行学习之前,有必要先了解一下SVG,因为D3.js基本上是对SVG元素进行操作从而进行可视化展示的。当然现在D3.js同样支持canvas输出,不过我们更建议先从SVG进行入门。

网上对于SVG的教程非常多,你只需了解SVG中的基础元素,线、圆、矩形、三角形等等。并熟悉SVG的DOM结构。推荐几个基本教程:

利用svg基本元素绘制一只猫

本节我们使用d3.jssvg中利用基础元素画一只猫。

首先添加一张svg画布:(demo示例地址在后面)

const svg = d3.select('body')
              .append('svg')
              .attr('width', 600)
              .attr('height', 300)

利用rect元素绘制猫的头部:

const head = svg.append('g')
                 .attr('id', 'head')
                 .attr('transform', 'translate(60, 80)')

head.append('rect')
     .attr('width', 100)
     .attr('height', 60)
     .attr('stroke-width', 1)
     .attr('stroke', '#000')
     .attr('fill', '#fff')

利用circle元素绘制猫的眼睛和嘴巴:

// eyes
head.append('circle')
    .attr('cx', 20)
    .attr('cy', 20)
    .attr('r', 10)
    .attr('fill', '#529fca')

head.append('circle')
    .attr('cx', 80)
    .attr('cy', 20)
    .attr('r', 10)
    .attr('fill', '#529fca')

// mouse
head.append('circle')
    .attr('cx', 50)
    .attr('cy', 50)
    .attr('r', 5)
    .attr('fill', '#529fca')

通过path元素绘制猫的两只耳朵:

// ears
svg.append('path')
    .attr('d', 'M65 80 L80 60 L95 80 Z')
    .attr('fill', '#fff')
    .attr('stroke', '#000')

svg.append('path')
    .attr('d', 'M125 80 L140 60 L155 80 Z')
    .attr('fill', '#fff')
    .attr('stroke', '#000')

绘制猫的身体:

// body
const body = svg.append('g')
                .attr('id', 'body')
                .attr('transform', 'translate(110, 110)')

body.append('rect')
    .attr('width', 120)
    .attr('height', 60)
    .attr('stroke-width', 1)
    .attr('stroke', '#000')
    .attr('fill', '#fff')

四只脚:

// legs
svg.append('line')
   .attr('x1', '110')
   .attr('y1', '170')
   .attr('x2', '100')
   .attr('y2', '200')
   .attr('stroke', '#000')
   .attr('stroke-width', 1.5)

svg.append('line')
   .attr('x1', '130')
   .attr('y1', '170')
   .attr('x2', '140')
   .attr('y2', '200')
   .attr('stroke', '#000')
   .attr('stroke-width', 1.5)

svg.append('line')
   .attr('x1', '210')
   .attr('y1', '170')
   .attr('x2', '200')
   .attr('y2', '200')
   .attr('stroke', '#000')
   .attr('stroke-width', 1.5)

svg.append('line')
   .attr('x1', '230')
   .attr('y1', '170')
   .attr('x2', '240')
   .attr('y2', '200')
   .attr('stroke', '#000')
   .attr('stroke-width', 1.5) 

加上尾巴:

// tail
svg.append('path')
   .attr('d', 'M230 140 L250 140 L250 120 L270 120 L270 100')
   .attr('fill', '#fff')
   .attr('stroke', '#000')

svg.append('circle')
   .attr('cx', 270)
   .attr('cy', 95)
   .attr('r', 5)
   .attr('fill', '#fff')
   .attr('stroke', '#000')

最后再加点小小的星星花纹:

body.append('polygon')
    .attr('points', '9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78')
    .attr('transform', 'translate(80, 20)')

这样,一只小猫就画完了:

demo地址codepen

代码主要的逻辑就是添加元素,并给元素赋予多种属性,最后组合在一起。在绘制的js代码中你会发现,头部的绘制是放在身体之后的,这是因为你需要将猫脸都露出来。如果先绘制身体,后绘制头部,那么猫脸就会被身体遮盖一部分。如果你熟知css样式,可能很容易就想到这是元素堆叠的问题,与z-index有关系。

不过很遗憾,z-indexsvg元素中不起作用svg中元素的堆叠关系只跟绘制的先后顺序有关。先绘制的在下面,后绘制的在上面。

d3.js使用了链式语法,让操作变得十分顺畅,相信这种语法对你来说已经非常熟悉了,在jQuery,Lodash等库中也有使用。当然你也可以把它拆开,在你需要的时候:

const body = d3.select('body')
const svg = body.append('svg')

在阅读完SVG的教程之后,我们可以开始对D3.js的学习。

  • 数据绑定 data ,enter, selectAll
  • 比例尺 xScale 、 yScale

数据与Dom

本章阐述如何将数据和DOM结构关联在一起

操作DOM

数据可视化的关键就在于将数据反应到视图上,也就是DOM结构上,D3.js就是天生为此而生的一件工具。

d3.select()

选中某个DOM,该语句是最为基本也最为常用的,用法基本与$.select()相同。

d3.select('body').style('background-color', 'black')

选中一个往往不够用,所以也可以使用d3.selectAll()选中所有的DOM。

d3.selectAll('p').style('color', 'white')
d3.append()

body标签中插入p标签,注意是在末尾。

d3.select('body').append('p')
d3.insert()

同样也是在选择集中插入标签,和d3.append()唯一一点不同之处在于多了一个用于指示插入位置的参数。对于如下DOM结构进行操作:

<ul class="list-group" id="list-group">
    <li class="list-group-item">0001</li>
    <li class="list-group-item">0002</li>
    <li class="list-group-item">0003</li>
</ul>
d3.select('#list-group')
  // 在#list-group的第2个子元素之前插入一个节点
  .insert('li', '.list-group-item:nth-child(2)')
d3.remove()

删除选中的元素。

d3.select('p').remove()

数据绑定

将数据与DOM绑定是核心之所在,那么如何绑定呢?

d3.data() 初识

对于如下代码:

先看看例子

在body中插入了5个p标签,并在p标签中插入了我们想要的数据。

d3.data() 详解

这里d3.v5版本把下面api合并到了d3.join()

在上一例中,我们在body元素中没有p元素的情况下,插入了5个p元素,并写入了对应的文本。实际上,选择的元素个数与绑定的数据个数存在3种关系,d3.data()返回三个函数进行对应:

当两者长度相等时,可以使用数据对原本元素中的数据进行更新;当数据的长度大于选择的元素的数量,那么就可以进行数据插入;相反,小于的情况时,可以获取多余的元素。见下图:

update 部分的处理办法一般是:更新属性值 enter 部分的处理办法一般是:添加元素后,赋予属性值 exit 一般适用于删除多余元素

d3.datum()

d3.datum()同样可以用来处理数据与元素的绑定

d3.datum() 与 d3.data() 的区别

  1. 如果datum()绑定的是数组,那么整个数组会绑定到每个被选择的元素上。而使用data()的话,那么会依次绑定数据。

  1. 传入的数据类型的不同

    • 关于data()传入的数据。

      • 最好传入数组。

      • 不可传入Object

        d3.select('text').data({id: 2}) // 无效,错误
        
      • 可传入字符串,不过只输入第一个字符。

        d3.select('text').data('234').text(e => {
           return e
         })
           
         // 只展示 '2'
        
    • 关于datum()传入的数据。

      • 可以传入字符串,并且全部输出。

        d3.select('text').data('234').text(e => {
             return e
         })
         
         // 展示 '234'
        
        
      • 也可以传入数组,对象。

      • datum()会将传入的值全部绑定到元素上。

  2. 更多区别

比例尺

D3中有个重要的概念就是比例尺。比例尺就是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。比如输入是1,输出是100;输入是5,输出是10000,那么这其中的映射关系就是你所定义的比例尺。

D3中有相当数量的比例尺函数,能满足你的各种需求,有连续性的,非连续性的,本文对于常用比例尺进行一一介绍。

1.d3.scaleLinear() 线性比例尺

使用d3.scaleLinear()创造一个线性比例尺,而domain()是输入域,range()是输出域,相当于将domain的数据集映射到range的数据集中。

const scale = d3.scaleLinear().domain([1,5]).range([0,100])

映射关系:

接下来,研究这个比例尺的输入与输出:

scale(1) // 输出:0
scale(4) // 输出:75
scale(5) // 输出:100

如果使用区域外的数据会得出什么结果呢?

scale(-1) // 输出:-50
scale(10) // 输出:225

所以,scale只是定义了一个映射规则,映射的输入值并不局限于domain()中的输入域。可以使用clamp()函数来限制输出的值域。该函数只针对连续性的比例尺有效。

scale.clamp(true)
scale(-1) // 输出:0
scale(10) // 输出:100

2.d3.scaleBand() 序数比例尺

d3.scaleBand()并不是一个连续性的比例尺,domain()中使用的是四个值,而range()需要的是一个连续域:

const scale = d3.scaleBand().domain([1,2,3,4]).range([0,100])

映射关系:

看一下输入与输出:

scale(1) // 输出:0
scale(2) // 输出:25
scale(4) // 输出:75

当输入不属于domain()中的数据集时:

scale(0)  // 输出:undefined
scale(10) // 输出:undefined

由此可见,d3.scaleBand()只针对domain()中的数据集映射相应的值。

3.d3.scaleOrdinal() 序数比例尺

d3.scaleOrdinal()的输入域和输出域都使用离散的数据。

const scale = d3.scaleOrdinal().domain(['jack', 'rose', 'john']).range([10, 20, 30])

映射关系:

输入与输出:

scale('jack') // 输出:10
scale('rose') // 输出:20
scale('john') // 输出:30

当输入不是domain()中的数据集时:

scale('tom') // 输出:10
scale('trump') // 输出:20

输入不相关的数据依然有输出值。所以在使用时,要注意输入数据的正确性。

我们从上面的映射关系中可以看出,domain()range()的数据是一一对应的。如果两边的值不一样呢?下面两张图说明这个问题:

domain()的值按照顺序循环依次对应range()的值。

4.d3.scaleQuantize() 量化比例尺

d3.scaleQuantize()也属于连续性的比例尺。定义域是连续的,而输出域是离散的。

const scale = d3.scaleQuantize().domain([0, 10]).range(['small', 'medium', 'long'])

映射关系:

输入与输出:

scale(1) // 输出:small
scale(5.5) // 输出:medium
scale(8) // 输出:long

而对于domain()域外的情况:

scale(-10) // 输出:small
scale(30)  // 输出:long

大概就是对domain()域的两侧的延展。

5.d3.scaleTime() 时间比例尺

d3.scaleTime()类似于d3.scaleLinear()线性比例尺,只不过输入域变成了一个时间轴。

const scale = d3.scaleTime()
              .domain([new Date(2017, 0, 1, 0), new Date(2017, 0, 1, 2)])
              .range([0,100])

输入与输出:

scale(new Date(2017, 0, 1, 0)) // 输出:0
scale(new Date(2017, 0, 1, 1)) // 输出:50

时间比例尺较多用在根据时间顺序变化的数据上。另外还有一个d3.scaleUtc()是根据世界标准时间(UTC)来计算的。

6.颜色比例尺

D3提供了一些颜色比例尺。10就是10种颜色,20就是20种:

d3.schemeCategory10
d3.schemeCategory20
d3.schemeCategory20b
d3.schemeCategory20c

// 定义一个序数颜色比例尺
let color = d3.scaleOrdinal(d3.schemeCategory10)

7.其他比例尺

另外有一些函数比例尺的功能,从名称上就可见一斑。产生的效果可以在实际操作中实验一下。

d3.scaleIdentity() // 恒等比例尺
d3.scaleSqrt()     // 乘方比例尺
d3.scalePow()      // 类似scaleSqrt的乘方比例尺
d3.scaleLog()      // 对数比例尺
d3.scaleQuantile() // 分位数比例尺

8.invert()invertExtent()方法

上述的各种使用比例尺的例子都是一个正序的过程,从domain的数据集映射到range的数据集中,那么有没有逆序的过程呢?D3中提供了invert()以及invertExtent()方法可以实现这个过程。

let scale = d3.scaleLinear().domain([1,5]).range([0,100])
scale.invert(50) // 输出:3

let scale2 = d3.scaleQuantize().domain([0,10]).range(['small', 'big'])
scale2.invertExtent('small') // 输出:[0,5]

不过,值得注意的是,这两种方法只针对连续性比例尺有效,即domain()域为连续性数据集的比例尺。

收尾

到此,d3基本的一些概念大概是这些,下一篇文章我会一步步用D3去画一个折线图的步骤。

可视化D3专题系列(二)折线图

希望我也能写下去。