D3.js基础

概述

D3 在操作元素时的方式是转换而不是表示,即 D3 在操作元素时的语法描述是指将当前的状态转换为期望的状态所需要的更改(插入、更新和删除),而不是直接表示元素的期望状态;例如,在 div 元素中插入 span 元素,在 D3 中的语法是这样的:

d3.select('div')      // 选中要插入 span 的 div 元素
  .selectAll('span')  // 选中 div 元素中的所有 span 元素
  .data([1,2,3,4,5])  // 为 span 绑定数据
  .enter()            // 获取所有没有绑定数据的空元素
  .append('span')     // 将 span 插入空元素的位置

虽然这样很繁琐,但这样允许更精细的操作。

DOM对象DOM对象D3 定义的用以描述 DOM 元素的对象,包含 DOM 的信息和其他一些方法属性;在选择元素时,D3DOM 元素翻译为 DOM对象 ;渲染时,D3DOM对象 翻译成供浏览器渲染的 DOM


选择器

D3 的选择器选中 DOM 元素时,会返回一个对象,该对象包含 _group(分组)_parent(父级)

分组 是一个数组,由 DOM对象 组成的数组组成。

父级 也是一个数组,由所有分组中元素的父级的 DOM对象 组成。

分组的多少不是由真实元素的父元素数量来决定的,而是由 D3 的选择器来决定的,如:

<div>
  <span>1</span>
</div>
<div>
  <span>2</span>
</div>
d3.selectAll('span') // 这时会获得一个分组,分组中有两个span元素对象,父级为空。
d3.selectAll('div').selectAll('span') // 这时会获得两个分组,每个分组中包含一个span元素对象,父级包含两个div元素对象。

数据绑定

D3 将数据与 DOM 绑定是通过在 DOM对象 中创建 __data__ 属性来实现的。

D3 中的数据是由数组构成的,如:const data = [1, 2, 3],使用 selection.data(data),可以使数据和元素进行绑定,但需要注意,数组中的项是和 分组 进行绑定,而不是和每个 元素 绑定。

如:const data = [0, 1, 2] 的绑定结果是这样的:

如:const data = [[3, 4, 5], [6, 7, 8, 9]] 的绑定结果:

数据和 DOM对象 之间默认以数据的 index 进行识别和关联,当然也可以自定义关联关系。


EnterUpdateExit

dataDOM对象 关联的时候,存在三种关系:

  • Enter :进入,即数据与 DOM对象 没有关联。这时要创建新的 DOM对象 与数据进行关联。

    示例:

    首先在 div 中创建四个 span ,并为 span 绑定数据,key 值为数据内容,颜色为 blue

    d3.select('div')
      .selectAll('span')
      .data('ABCD'.split(''), d => d)
      .enter()
      .append('span')
      .style('width', '20px')
      .style('height', '20px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'blue')
    

    再次 div 中的 span ,并绑定数据 ACGH ,由于原有的 spankey = Gkey = H 的不在 ABCD 中(即这两个数据与 DOM对象 没有关联),所以会得到两个需要 enterDOM 元素,使用 enter().append() 将元素加入(颜色为 red),最终得到六个 span (原有的四个蓝色的和新加入的两个红色的)。

    d3.select('div')
      .selectAll('span')
      .data('ACEF'.split(''), d => d)
      .enter()
      .append('span')
      .style('width', '20px')
      .style('height', '20px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'red')
    
  • Update :更新,即 DOM对象 的数据在 data 中存在,这时要根据数据更新 DOM对象

    示例:

    首先在 div 中创建四个 span ,并为 span 绑定数据,key 值为数据内容,颜色为 blue

    d3.select('div')
      .selectAll('span')
      .data('ABCD'.split(''), d => d)
      .enter()
      .append('span')
      .style('width', '20px')
      .style('height', '20px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'blue')
    

    再次 div 中的 span ,并绑定数据 AC ,由于原有的 spankey 值有 key = Akey = C(即数据与 DOM对象 有关联),这时会得到两个需要 UpdateDOM 元素。将这两个元素改为红色,最终得到四个 span (原有的两个蓝色的和两个改为红色的)。

    d3.select('div')
      .selectAll('span')
      .data('AC'.split(''), d => d)
      .style('background', 'red')
    
  • Exit :退出,即 DOM对象 的数据在 data 中不存在,这时要移除多余的 DOM对象

    示例:

    首先在 div 中创建四个 span ,并为 span 绑定数据,key 值为数据内容,颜色为 blue

    d3.select('div')
      .selectAll('span')
      .data('ABCD'.split(''), d => d)
      .enter()
      .append('span')
      .style('width', '20px')
      .style('height', '20px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'blue')
    

    再次 div 中的 span ,并绑定数据 AC ,由于原有的 spankey 值有 key = Akey = C(即数据与 DOM对象 有关联),这时会得到两个需要 ExitDOM 元素,使用 enter().append() 将元素移除,最后剩下两个 span

    d3.select('div')
      .selectAll('span')
      .data('AC'.split(''), d => d)
      .exit()
      .remove()
    
  • 元素更新示例

    初始化

    d3.select('div')
      .selectAll('span')
      .data('ABCD'.split(''), d => d)
      .enter()
      .append('span')
      .style('width', '20px')
      .style('height', '20px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'blue')
    
  • 修改

    const span = d3.select('div')
      .selectAll('span')
      .data('ACEF'.split(''), d => d)
    span.exit().remove()                // 退出
    span.style('background', 'yellow')  // 更新
    span.enter()                        // 进入
      .append('span')
      .style('width', '40px')
      .style('height', '40px')
      .style('margin-right', '5px')
      .style('display', 'inline-block')
      .style('background', 'red')
    

动画

概述:连续动画通常由离散的关键帧和插值或补间生成的中间帧来定义。D3 中的动画也是这样生成,并提供了 Interpolators(插值器) 来生成中间帧,中间帧和关键帧的时间是 0-1

Interpolators(插值器)

d3.interpolate(a, b) :接收两个参数,返回中间值。他会根据参数 b 的类型使用相应的算法。如:当 bnullundefinedboolean 时,直接使用常熟 b;当 b 是颜色时,使用 d3.interpolateRgb ; 具体可查看官方文档。

示例:创建一个从红色到蓝色的插值器。

const fx = d3.interpolate('red', 'blue') // 创建一个插值器
fx(0)   // 'rgb(255, 0, 0)'   // 起始值,即 red
fx(0.5) // 'rgb(128, 0, 128)' // 中间值
fx(1)   // 'rgb(0, 0, 255)'   // 结束值,即 blue
fx(1.1) // 'rgb(0, 0, 255)'   // 结束值,即 blue

生成动画

  • Transitions(过渡):使用 D3Transitions 生成动画, Transitions 类似于 Selections 用于在 DOM 更改时生成动画,它支持大多数的选择方法,但在开始之前必须插入元素和绑定数据。 Transitions 可以自动使用 D3Interpolators(插值器) 计算中间帧,也可以自定义中间帧。

    示例:将 div 的颜色由 red 过渡到 blue ,持续时间为 1s 。注意,假如 div 元素没有设置 background 属性的话,在过渡之前要先设置 background 属性,不然 transition 会找不到起始帧,导致没有过渡效果。

    d3.select('div')
      .style('background', 'red')
      .transition()
      .duration(1000)
      .style('background', 'blue')
    
  • Generators(生成器):自定义中间帧,每次生成中间帧时刷新整个视图的方法。

    示例:将 divred 变为 blue,中间帧为 blackyellow

    const generators = () => {
        const color = ['red', 'black', 'yellow', 'blue']
        let index = 0
        let timer = null
        const div = d3.select(divRef.current)
        timer = d3.interval(
          () => {
            div.style('background', color[index])
            index += 1
            console.log(99999999999)
            if(index === 4 ){
              timer.stop()
            }
          }, 1000)
      }
    

Scales(尺度)

D3 中的 scale 的作用是将抽象的维度数据映射为可视的变量,抽象的维度数据指我们手中的数据,可视的变量指图纸中的坐标点。

如有以下数据:

const data = [
    {name: '张三', age: 18},
    {name: '李四', age: 20},
    {name: '王五', age: 22}
]

生成的条形图:

name 属性映射为条形图的 Y轴点age 属性映射为条形图的 X轴点

D3 中有多种 scale 使用哪一种取决维度数据要映射的可视变量。上面的 X轴linear scale ,因为 age 是量化数据;Y轴band scale ,因为 name 是指标数据。

维度变量与可视变量之间的映射关系

D3 中的 Scales 系列函数,会跟据抽象的维度数据和可视的变量数据,生成一个映射函数,映射函数接收抽象的维度数据返回可视的变量数据。

d3.scaleLinear 为例,将 [0, 9] 的范围映射到 [18, 36] 的范围中去,得到映射函数 x,这样的话,每个单位是 2,起点是 18,则 x(4) = 18 + 4 * 2 = 26x(10) = 18 + 10 * 2 =38D3 代码如下:

const x = d3.scaleLinear()
    .domain([0, 9])
    .range([18, 36])
x(4) // 输出 26
x(10) // 输出 38

d3.scaleLinear() 中维度写在 domain 中,将可视变量写在 range, 这样就可以建立起维度数据与可视变量之间的映射关系了。

d3.scaleBand 为例,将 [张三, 李四, 王五] 的映射到 [18, 36] 的范围中去,得到映射函数 x,这样的话,将[18, 36] 的范围分为三份,[张三, 李四, 王五] 分别对应 [18, 27, 36],则 x(张三) = 18x(李四) = 27,而 x(马六) = undefined ,应为 马六 在建立映射的时候不存在,所以没有相对应的可视变量,这点是 量化映射指标映射 的区别,D3 代码如下:

const x = d3.scaleBand()
    .domain(['张三', '李四', '王五'])
    .range([18, 36])
x('张三') // 输出 18
x('李四') // 输出 27
x('马六') // 输出 undefined

思考:维度变量 既可以是 指标变量 也可以是 量化变量可视变量 则一定是 量化变量

Axis(坐标轴)

D3 提供了将 维度变量和可视变量的映射关系Axis(坐标轴) 的形式表示的方法。

示例:

// 生成维度变量和可视变量的映射关系
const x = d3.scaleLinear()
    .domain([0, 9])
    .range([18, 360])
// 将映射关系以坐标轴表示
d3.select(svgRef.current)
    .append('g')
    .call(d3.axisBottom(x))