D3 形状

d3

D3 形状

参考

本文主要介绍 Shapes 模块

一个完整的可视化作品是由多种基本的图形符号构成的,例如饼图由多个扇形 arcs 构成,折线图由多段线 lines 构成,面积图由一个或多个面积 areas 构成,而且图中还可能使用符号 symbols(如星形、三角形、圆形等)作注释等。

SVG 提供了不同的元素构建基本的图形符号,例如 <rect> 元素可以绘制出矩形(或圆角矩形),<path> 元素可以绘制出复杂的图形。但是「手动」设置这些元素的参数以绘制出期望的形状(例如 <path> 元素需要设置属性 d,该属性值是由一系列 ML 等命令和 x y 坐标点数据组成的字符串)是十分繁琐的。

d3-shape 模块中包含各种基本图形生成器,可以方便地基于抽象数据生成所需的图形。

可以直接用于生成图形的生成器,一般会生成可用于 <path> 元素属性 d 的值:

  • Arcs 扇形/环形生成器:基于内外半径和开角,绘制出扇形
  • Lines 线段生成器:基于坐标点,直接将一系列的坐标点相连绘制出折线
  • Areas 面积生成器:和线段生成器类似,不过它需要基于上下两条边界线,用以绘制出面积
  • Links 连线生成器:基于两个坐标点,绘制出平滑的三次贝塞尔曲线作为两点之间的连线
  • Symbols 符号生成器:提供了一系列常用的符号,一般用于散点图中以表示数据的类别

不可直接用于生成图形的生成器,需要配合上方的生成器使用:

  • Pie 饼图角度生成器:返回一个关于角度的数组,然后将这些角度数据用于 Arcs 扇形/环形生成器
  • Curves 曲线插值生成器:配合 Lines 线段生成器使用,设置坐标点之间的插值方式(D3 内置了多种方式),以绘制出更平滑的线段
  • Custom Curves 自定义曲线插值生成器:自定义坐标点之间的插值函数,配合 Lines 线段生成器使用
  • Custom Symbol Types 自定义符号类型生成器:通过 symbol.type 将自定义的图形符号设置为符号的类型
  • Stacks 堆叠生成器:基于数据计算出堆叠的方式,需要将这些数据传递给 Areas 面积生成器

扇形生成器 Arcs

扇形/弧线生成器 Arcs 用以生成一个个扇形或环形,然后将多个扇形或环形拼起来就构成了饼图或圆环图。

使用方法 d3.arc() 创建一个扇形生成器(以下称为 arc

它既是一个方法,可以直接调用,传递一个包含多个设置参数的对象作为入参(实际这些参数在内部被相应的方法调用),来创建一个扇形;它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用。

提示

调用扇形生成器时返回的结果,基于扇形生成器是否设置了绘图上下文 context 而不同

如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串

js
// 创建一个默认的扇形生成器
const arc = d3.arc();
// 在调用生成器时需要传递相关的设置参数(半径和角度)
// 适用于动态生成各种形状的扇形
arc({
  innerRadius: 0,
  outerRadius: 100,
  startAngle: 0,
  endAngle: Math.PI / 2
}); // "M0,-100A100,100,0,0,1,100,0L0,0Z"
js
// 创建一个扇形生成器,并设置了相应的参数
const arc = d3.arc()
    .innerRadius(0)
    .outerRadius(100)
    .startAngle(0)
    .endAngle(Math.PI / 2);
// 当扇形生成器已经设置了半径和角度,可以直接调用,生成路径
arc(); // "M0,-100A100,100,0,0,1,100,0L0,0Z"
提示

这两种创建和调用方式可混合使用,例如需要创建一系列半径相同,但角度不同的扇形,可以创建一个扇形生成器并预设了半径,然后在调用生成器时才传递角度信息

js
// 包含一系列扇形角度(开始和结束的角度,单位是弧度)的数组
const angleArr = [
    {
        start: 0 * 2 * Math.PI,
        end: 1/4 * 2 * Math.PI
    },
    {
        // ...
    },
    // ...
]
// 创建一个预设了半径的扇形生成器,开始角度和结束角度以参数的形式传入
arc = d3.arc()
    .innerRadius(210)
    .outerRadius(310)
    .startAngle(([startAngle, endAngle]) => startAngle)
    .endAngle(([startAngle, endAngle]) => endAngle)
const pathArr = []
angleArr.forEach(angle => {
    pathArr.push(arc([angleArr.start, angleArr.end]))
})
提示

生成的扇形的中心坐标默认是 (0, 0),可以为扇形的父容器(一般是元素 <g>)设置 transform 属性,移动到目标位置

提示

在官方样例文档中,常常见到 τ\tau 符号,它表示 2π2\pi

扇形生成器具有的方法,这些方法一般都返回扇形生成器本身,便于进行链式调用

  • arc.centroid() 计算扇形的「中点」,即位于弧度是 (startAngle+endAngle)/2(startAngle + endAngle) / 2 半径是 (innerRadius+outerRadius)/2(innerRadius + outerRadius) / 2 的点,一般将扇形的文本注释添加到此 ⚠️ 该点可能不在扇形/环形内部
  • arc.innerRadius([radius]) 设置内半径,入参可以是一个数值,或是返回数值的函数;如果没有入参,则返回当前的内半径。如果 radius=0 则生成扇形,如果 radius 不为零则生成环形
  • arc.outerRadius([radius]) 设置外半径
    提示

    如果内半径大于外半径,则 D3 会将两者调换;如果入参是负值则会被当作 00

  • arc.cornerRadius([radius]) 设置圆角的半径,对于扇形则设置外侧的两个角;对于环形则设置内外侧的四个角
    注意

    但是圆角的半径不会大于 (outerRadiusinnerRadius)/2(outerRadius - innerRadius)/2

    而且当两个相邻的圆角靠得太近时,圆角半径会减小,这种情况一般出现在环形的弧长小于 π\pi 的内侧圆角


    corner radius
    corner radius
  • arc.startAngle([angle]) 设置扇形的一边的起始角度,作为起始角,单位是弧度。其中在 12 点方向(即 SVG 坐标系统的 -y 方向)为 00。如果入参的是负值表示起始角度在逆时针方向上。
  • arc.endAngle([angle]) 设置扇形的另一边的起始角度,作为结束角。
    提示

    如果 endAnglestartAngle2π|endAngle - startAngle| \ge 2\pi 则生成一个完整的圆或圆环

  • arc.padAngle([angle]) 设置扇形的间隔角度
  • arc.padRadius([radius]) 设置扇形的间隔半径
    提示

    间隔角度结合间隔半径算出间隔弧长 padAngle×padRadiuspadAngle \times padRadius 对于环形的内外侧都会减去相同的间隔弧长,这样就会得到如下方右侧图的效果(两个环形的相邻的边是平行的);对于扇形或内半径较小的环形和开角较小的环形,只会在外侧减去间隔弧长,这样就得到如下方左侧图的效果(扇形或环形的内侧相连到一点)。但是如果扇形或环形的开角 endAnglestartAngle2π|endAngle - startAngle| \ge 2\pi(生成一个完整的圆或圆环)则会忽略间隔弧长

    pad arc
    pad arc

  • arc.context(canvasContext) 用于设置绘图上下文 context
    在使用扇形生成器时,如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串
提示

关于饼图或圆环图的各扇形的角度(起始角度和结束角度)的数据,一般不会「手动」设置,而是通过 Pies 饼图角度生成器产生一系列的角度,再将数据用于扇形生成器。

Pies 饼图角度生成器

使用方法 d3.pie() 创建一个 Pie 饼图角度生成器(以下称为 pie

调用 Pie 饼图角度生成器时,将需要可视化的数据(一个数组)作为入参

js
const data = [1, 1, 2, 3, 5, 8, 13, 21];
const pie = d3.pie();
const pieArcData = pie(data)
// 结果
// [
//  {"data":  1, "value":  1, "index": 6, "startAngle": 6.050474740247008, "endAngle": 6.166830023713296, "padAngle": 0},
//  {"data":  1, "value":  1, "index": 7, "startAngle": 6.166830023713296, "endAngle": 6.283185307179584, "padAngle": 0},
//  {"data":  2, "value":  2, "index": 5, "startAngle": 5.817764173314431, "endAngle": 6.050474740247008, "padAngle": 0},
//  {"data":  3, "value":  3, "index": 4, "startAngle": 5.468698322915565, "endAngle": 5.817764173314431, "padAngle": 0},
//  {"data":  5, "value":  5, "index": 3, "startAngle": 4.886921905584122, "endAngle": 5.468698322915565, "padAngle": 0},
//  {"data":  8, "value":  8, "index": 2, "startAngle": 3.956079637853813, "endAngle": 4.886921905584122, "padAngle": 0},
//  {"data": 13, "value": 13, "index": 1, "startAngle": 2.443460952792061, "endAngle": 3.956079637853813, "padAngle": 0},
//  {"data": 21, "value": 21, "index": 0, "startAngle": 0.000000000000000, "endAngle": 2.443460952792061, "padAngle": 0}
// ]

然后返回一个数组,它的长度和入参的数组长度一致,元素的次序也一样,其中每个元素(是一个对象)依次对应一个数据项,并包含以下属性

  • data 数据项的值
  • value 一个数值,它代表了该数据项,被用于在 Pie 饼图生成器里进行运算(以计算该数据项所需占据的角度)
  • index 数据项的索引,从 0 开始
  • starAngle 该数据项在扇形或环形中所对应的起始角
  • endAngle 该数据项在扇形或环形中所对应的结束角
  • padAngle 扇形或环形的间隔角度

然后这些元素就可以作为 Arc 扇形生成器(一般预设了 innerRadiusouterRadius,有时候还会预设绘图上下文 context)的入参,然后基于每个元素的 starAngleendAnglepadAngle 生成相应的扇形

js
pieArcData.forEach((d) => {
  arc(d); // arc 是扇形生成器
})

Pie 饼图角度生成器(以下的 pie)既是一个方法(可以接受数据作为入参),也是一个对象,具有多种方法设置不同的参数,一般这些方法都返回饼图角度生成器本身,便于进行链式调用

  • pie.value([valueMapFunction]) 设置每个数据项传递给 Pie 饼图角度生成器的值,默认直接传递整个数据项(因为 D3 假设每一个数据项都是一个数值),即默认的 valueMapFunction 如下
    js
    function value(d) {
      return d;
    }

    valueMapFunction 接受(每一行)数据项的数据 d、索引 i、整个数据表 data 作为入参,然后可以根据这些数据进行变换,返回代表该数据项的数值,传递给 Pie 饼图角度生成器。
    js
    const data = [
      {"number":  4, "name": "Locke"},
      {"number":  8, "name": "Reyes"},
      {"number": 15, "name": "Ford"},
      {"number": 16, "name": "Jarrah"},
      {"number": 23, "name": "Shephard"},
      {"number": 42, "name": "Kwon"}
    ];
    const arcs = d3.pie()
                  .value(d => d.number) // 只需要接受数据项作为入参,将数据项的 number 属性值,作为该数据项传递给 Pie 饼图角度生成器的值

    由于入参的数据表中(每一行)数据项可能有多个属性(例如以上示例中,每一个数据项都包含 numbername 两个属性),而不是单纯的数值,对于这种情况就需要设置 pie.value(),类似于 JS 数组的原始方法 map
    提示

    valueMapFunction 函数最后返回的值,相当于 Pie 饼图角度生成器所返回的数组中,每个元素的 value 属性。而每个数据项的原始值,相当于 Pie 饼图角度生成器所返回的数组中,每个元素的 data 属性。


    经过该方法的配置,可以直接将整个数据表传递给 Pie 饼图角度生成器(而不是先进行预处理),是为了在后续的绑定操作中,每个数据项的所有信息都可以绑定到相应的元素上,保留更大的拓展性。
  • pie.sort([compareFunction]) 设置数据项的排序对比函数(用于决定相应扇形的优先次序),其中排序方程 compareFunction 和 JS 数组的原始方法 sort 类似,入参是表示两个需要对比的数据项 ab,如果函数返回一个负值,则 a 排在在 b 前;如果函数返回一个正值,则 ab
    提示

    这里所说的排序,实际上体现在各数据项所对应的扇形的起始角度和结束角度上,而不会改变数组中的元素的次序(经过排序后返回的数组的元素次序,和数据表中数据项的顺序是相同的)


    compareFunction 默认值是 null
    提示

    如果调用了该方法 pie.sort(compareFunction) 设置了对比函数,则相当于隐式地调用 pie.sortValues(null) 将对比函数设置为 null,忽略按数据项的转换值进角度排序

    提示

    如果 pie.sort 数据项的对比函数 ,以及 pie.sortValues 它的转换值的对比函数都是 null,则各扇形的排序与原始数据集中各数据项顺序一致

    js
    const data = [10, 50, 22, 80, 11, 30, 130];
    d3.pie()(data);
    // [
    //  0: Object {data: 10, index: 6, value: 10, startAngle: 6.094501063720739, endAngle: 6.283185307179585, padAngle: 0}
    //  1: Object {data: 50, index: 2, value: 50, startAngle: 3.9623691126357747, endAngle: 4.9057903299300065, padAngle: 0}
    //  2: Object {data: 22, index: 4, value: 22, startAngle: 5.471843060306545, endAngle: 5.886948395916008, padAngle: 0}
    //  3: Object {data: 80, index: 1, value: 80, startAngle: 2.4528951649650033, endAngle: 3.9623691126357747, padAngle: 0}
    //  4: Object {data: 11, index: 5, value: 11, startAngle: 5.886948395916008, endAngle: 6.094501063720739, padAngle: 0}
    //  5: Object {data: 30, index: 3, value: 30, startAngle: 4.9057903299300065, endAngle: 5.471843060306545, padAngle: 0}
    //  6: Object {data: 130, index: 0, value: 130, startAngle: 0, endAngle: 2.4528951649650033, padAngle: 0}
    // ]
    d3.pie().sort((a,b) => a-b )(data);
    // [
    //  0: Object {data: 10, index: 0, value: 10, startAngle: 0, endAngle: 0.18868424345884643, padAngle: 0}
    //  1: Object {data: 50, index: 4, value: 50, startAngle: 1.3773949772495788, endAngle: 2.320816194543811, padAngle: 0}
    //  2: Object {data: 22, index: 2, value: 22, startAngle: 0.3962369112635775, endAngle: 0.8113422468730396, padAngle: 0}
    //  3: Object {data: 80, index: 5, value: 80, startAngle: 2.320816194543811, endAngle: 3.8302901422145825, padAngle: 0}
    // 4: Object {data: 11, index: 1, value: 11, startAngle: 0.18868424345884643, endAngle: 0.3962369112635775, padAngle: 0}
    //  5: Object {data: 30, index: 3, value: 30, startAngle: 0.8113422468730396, endAngle: 1.3773949772495788, padAngle: 0}
    //  6: Object {data: 130, index: 6, value: 130, startAngle: 3.8302901422145825, endAngle: 6.283185307179586, padAngle: 0}
    // ]
  • pie.sortValues([compareFunction]) 设置数据项的转换值的排序对比函数(用于决定相应扇形的优先次序),其中排序方程 compareFunction 的入参是表示两个需要对比的数据项所传递给 Pie 饼图角度生成器的值 valueAvalueB(即数据项经过 value accessor 数据访问器转换后所得到的值,而不是数据项的原始值)
    compareFunction 默认值是 d3.descending 降序排列(即角度较大的扇形会排在前面)
    提示

    如果调用了该方法 pie.sortValues(compareFunction) 设置了对比函数,则相当于隐式地调用 pie.sort(null) 将对比函数设置为 null,忽略按数据项的原始值进行角度排序

    提示

    如果 pie.sort 数据项的对比函数 ,以及 pie.sortValues 它的转换值的对比函数都是 null,则各扇形的排序与原始数据集中各数据项顺序一致

  • pie.startAngle([angle]) 设置 Pie 饼图角度生成器的起始角度(默认值是 0),它会被调用一次,用于设置第一个扇区的开始角度
    提示

    入参 angle 虽然可以是任意单位,但是如果打算将 Pie 饼图角度生成器的结果应用于 Arc 扇形生成器中,应该使用弧度作为单位,例如 angle=Math.PI/2

  • pie.endAngle([angle]) 设置 Pie 饼图角度生成器的结束角度(默认值是 2*Math.PI),它会被调用一次,用于设置最后一个扇区的结束角度
  • pie.padAngle([angle]) 设置相邻扇形之间的间隔角度,则在生成扇形时会预留出相应的角度空间(间隔角度与数据项的数量的乘积,最大值是 |endAngle-startAngle|)用于间隔,余下的角度空间再按比例分配给各个数据项。
    推荐做法

    虽然在扇形生成器中也可以使用方法 arc.padAngle(angle) 设置相邻扇形之间的间隔角度,但推荐在使用饼图角度生成器 pie 计算各数据项所占角度时,也同时设置间隔角度

    因为只采用 arc.padAngle() 方式设置饼图的间隔角度时,较小的弧形会受到不成比例的影响,导致失衡/比例误差

    具体介绍可以查看 Notebook Arc Pad Angle,它提供了一个交互式动画演示使用 arc.padAnglepie.padAngle 为饼图(不同部分之间)设置间隔的效果

线段生成器 Lines

线段生成器 Lines 基于给定的坐标点生成线段(或曲线),常用于构成一些可视化图形,如折线图 line chart 或分层的边缘捆绑图 hierarchical edge bundling。

使用方法 d3.line([xFunc], [yFunc]) 创建一个线段生成器(以下称为 line

它既是一个方法,可以直接调用,传递需要可视化的数据(一个数组)作为入参(实际这些参数在内部被相应的方法调用,例如在创建线段生成器时,传递横坐标和纵坐标的读取函数 xFuncyFunc 作为入参,然后数组每一个元素都会调用这两个函数,来获取相应的坐标值),来创建线段。

js
// 创建一个默认的线段生成器
const line = d3.line();
line([[10, 60], [40, 90]]); // "M10,60L40,90"
// 创建一个线段生成器,并设置了相应的参数
const line = d3.line(
    (d)=> d.xValue,
    (d)=> d.yValue
);
const lineData = line([
  {
    xValue: 10,
    yValue: 60
  },
  {
    xValue: 40,
    yValue: 90
  },
]); // "M10,60L40,90"
提示

调用线段生成器时,其返回的结果会基于生成器是否设置了渲染/绘图上下文 context(针对 Canvas 画布而言)而不同。

  • 如果通过方法 line.context(canvasContext) 设置了绘图上下文 context ,则调用线段生成器时生成一系列在画布绘制线条的命令/方法,形式如下
    js
    {
      context.moveTo(1, 3);
      context.lineTo(2, 7);
      context.lineTo(3, 2);
      context.lineTo(5, 2);
    }

    所以如果要调用线段生成器,在画布上绘制线段,其核心代码如下
    js
    line.context(context);
    context.beginPath(); // 开始绘制
    line(data); // 使用线段生成器生成一系列的具体绘制命令,并执行
    // 设置线段的样式
    context.fill();
    context.stroke();
  • 如果没有设置绘图上下文 context(默认值为 null),则调用线段生成器时生成字符串,用作 SVG 的 <path> 元素的属性 d 的属性值,形式如下
    js
    M10,108.04460008667525L28.163265306122447,85.83227209130348L46.326530612244895,87.72922523483525L64.48979591836735,71.37255731357486

    所以如果要调用线段生成器,在 SVG 上绘制线段,其核心代码如下
    js
    d3.select(…)
    .append("path")
    .attr("d", line(data))
    .attr("stroke", "black")

    也可以直接将生成的字符串应用到 <path> 元素的 d 属性上
    html
    <path d="line(data)" stroke="black" fill="none">
提示

如果数据点较多,在绘制折线图时,可以将元素 <path> 属性 stroke-miterlimit 设置为 1,以避免折线「锋利」交接处过渡延伸,导致该点的数据偏移

它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用。例如使用默认参数创建一个线段生成器 const line = d3.line(),然后在之后再调用相应的方法进行设置:

  • line.x(xFunc) 设置横坐标读取函数,该函数会在调用线段生成器时,为数组中的每一个元素都执行一次,以返回该数据所对应的横坐标。
    该函数有三个入参(线段生成器的其他方法的入参函数一般都是有这三个入参):当前的元素 d,该元素在数组中的索引 i,整个数组 data。通过这些入参数据,一般利用比例尺进行映射,返回相应的横坐标的值。
    D3 默认在使用线段生成器时,传递进来的数组的格式如 [[10, 60], [40, 90]],即每个元素都是由两个数值构成的数组,因此它的默认横坐标读取函数如下
    js
    function x(d) {
      return d[0];
    }
    提示

    如果数组中的元素并不是上述的默认结构,而是其他更复杂的结构,或者数据并不是坐标值,都需要配置 line.x(xFunc) 横坐标读取函数

  • line.y(xFunc) 设置纵坐标读取函数,设置方式和横坐标读取函数一样。
  • line.defined(definedState) 设置数据完整性检验函数,该函数会在调用线段生成器时,为数组中的每一个元素都执行一次,返回布尔值(默认值恒为 true),以判断该元素的数据是否完整。
    该函数也是有三个入参(当前的元素 d,该元素在数组中的索引 i,整个数组 data),当函数返回 true 时,线段线段生成器就会执行下一步(调用坐标读取函数),最后生成该元素相应的坐标数据;当函数返回 false 时,该元素就会就会跳过,当前线段就会截止,并在下一个有定义的元素再开始绘制,反映在图上就是一段段分离的线段
    提示

    如果一个点的两侧的元素(点)完整性欠缺,导致产生「孤立」的点,一般这些点在页面上不会显示出来,除非元素 <path> 的属性 stroke-linecap 路径两端的形状设置为 roundsquare(默认值是 butt

  • line.curve(curveInterpolator) 设置两点之间的曲线插值器
    所以线段生成器除了可以绘制折线,还可以绘制曲线
    D3 提供了一系列的内置的曲线插值器,也支持自定义曲线插值生成器
    提示

    除了使用曲线插值器,还可以从原始数据入手,通过方法 d3.blur(data, radius) 对数据进行处理 ❓,得到变化更平稳的数据集,也可以让绘制的折线更平滑

    具体示例可以参考官方的这个 Observable Notebook

  • line.context(canvasContext) 用于设置渲染/绘图上下文 context(针对 Canvas 画布而言)。在使用线段生成器时,如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串
    线段生成器的 context 的默认值为 null 即未设置绘图上下文
    js
    // 在画布中绘制线段的流程
    line.context(canvasContext);
    context.beginPath(); // 开始绘制
    line(data); // 使用线段生成器生成一系列的具体绘制命令,并执行
    // 设置线段的样式
    context.fill();
    context.stroke();
    // 在 SVG 中绘制线段的流程
    d3.select(target)
      .append("path")
      .attr("d", line(data))
      .attr("stroke", "black")
  • line.digits(number) 用于设置数据的精度,即路径 <path> 的属性 d 的值中描述画笔操作的数据的精度,参数 number 用于设置小数点后的最大位数(默认值3
    这个配置只在关上下文联 contextnull 时才会生效,因为这时候线段生成器返回的结果才是字符串(被用作元素 <path> 的属性 d 的属性值)

径向线段生成器

径向线段生成器和上述的线段生成器类似,也是用来生成线段或曲线,不同的是它的笛卡尔坐标系的横坐标换成角度(弧度为单位),纵坐标换成距离圆心的长度。

使用方法 d3.lineRadial() 创建一个线段生成器(以下称为 lineRadial

提示

调用径向线段生成器时,它生成的线段的中心坐标默认是 (0, 0),可以为其父容器(一般是元素 <g>)设置 transform 属性,移动到目标位置

它也具有多种方法设置不同的参数:

  • lineRadial.angle(angleFunc) 类似于 line.x(xFunc),设置角度读取函数,返回的角度单位是弧度。
  • lineRadial.radius(radiusFunc) 类似于 line.y(xFunc),设置径向距离读取函数
  • lineRadial.defined(definedState)
  • lineRadial.curve(curveInterpolator) 其中 curveMonotoneXcurveMonotoneY 单调性曲线插值器不适用
  • lineRadial.context(canvasContext)

面积生成器 Areas

面积生成器 Areas 和线段生成器类似,也是基于给定的坐标点生成线段(或曲线),不同的是它生成两条边界线

  • 上边界线 topline y1
  • 下边界线 baseline y0 一般下边界线采用横坐标轴,即设置为常量 y0=0

一般对于两条线段的横坐标相同 x0=x1 时,所对应的点的纵坐标 y0y1 不同,这样两条边界线之间区域就是目标面积

面积图的组成部分注释
面积图的组成部分注释

使用方法 d3.area([xFunc], [y0Func], [y1Func]) 创建一个面积生成器(以下称为 area

它既是一个方法,可以直接调用,传递需要可视化的数据(一个数组)作为入参

说明

实际这些数据在内部被相应的方法调用

在创建面积生成器时 const area = d3.area(xFunc, y0Func, y1Func) 可以传递三个参数,它们分别是:

  • 横坐标的读取函数 xFunc
  • 上边界线纵坐标的读取函数 y0Func
  • 下边界线纵坐标的读取函数y1Func

然后在调用面积生成器时 area(dataArr) 数组的每一个元素都会调用这三个函数,获取相应的坐标值,以创建面积

另外需要注意的一点是传入的数据需要根据 x 值排好序

它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用:

  • area.x(xFunc)area.x0(xFunc) 一样,用于设置下边界线横坐标读取函数。该方法会将 x0 读取函数设置为传入的参数 xFunc,并将 y1 读取函数设置为 null
  • area.x0(xFunc) 设置下边界线横坐标读取函数,该函数会在调用面积生成器时,为数组中的每一个元素都执行一次,以返回该数据所对应的横坐标。
    该函数也是有三个入参(面积生成器的其他方法的入参函数一般都是有这三个入参):当前的元素 d,该元素在数组中的索引 i,整个数组 data。通过这些入参数据,一般利用比例尺进行映射,返回相应的横坐标的值。
    提示

    D3 默认在使用面积生成器时,传递进来的数组的格式如 [[10, 60], [40, 90]],即每个元素都是由两个数值构成的数组,因此它的默认横坐标读取函数如下

    js
    function x(d) {
    return d[0];
    }

    如果数组中的元素并不是上述的默认结构,而是其他更复杂的结构,或者数据并不是坐标值,都需要配置横坐标读取函数


    该方法所传入的参数也可以是一个常数,对于纵向延伸的面积图,其左边界可能是一条平行于纵轴的线,所以 x0 值可以是一个常数
  • area.x1(xFunc) 设置上边界线横坐标读取函数。默认复用相应的 x0 值作为 x1 的值,一般都不需要进行配置。
    该方法所传入的参数也可以是一个常数,对于纵向延伸的面积图,其右边界可能是一条平行于纵轴的线,所以 x1 值可以是一个常数
  • area.y(yFunc)area.y0(yFunc) 一样,用于设置下边界线纵坐标读取函数。该方法会将 y0 读取函数设置为传入的参数 yFunc,并将 y1 读取函数设置为 null
  • area.y0(yFunc) 设置下边界线纵坐标读取函数(或一个常数,对于横向延伸的面积图,其下边界可能是一条平行于横轴的线,所以 y0 值可以是一个常数)
    它的默认值是 0,即默认以 svg 容器的顶部作为面积图的下边界线
    js
    function y() {
      return 0;
    }
    以横轴作为下边界线

    对于横向延伸的面积图,其下边界线一般是横坐标轴,而且横坐标轴所对应的 y 值为零点,则可以通过如下代码来设置下边界线

    js
    const area = d3.area().y0(y(0));

    其中 0 是横坐标轴所对应的 y 值(如果横坐标轴所对应的 y 值不是零点,则传递相应 y 值),再通过纵坐标轴比例尺 y 进行映射 y(0) 就得到横坐标轴在 svg 中的坐标位置


    如果下边界线也是折线,需要通过该方法配置下边界线纵坐标读取函数
  • area.y1(yFunc) 设置上边界线纵坐标读取函数(或一个常数,对于横向延伸的面积图,其上边界可能是一条平行于横轴的线,所以 y1 值可以是一个常数)
    D3 默认在使用面积生成器时,传递进来的数组的格式如 [[10, 60], [40, 90]],即每个元素都是由两个数值构成的数组,下边界线纵坐标读取函数的默认纵坐标读取函数如下
    js
    function y(d) {
      return d[1];
    }
  • area.defined(definedState) 设置数据完整性检验函数,其作用和线段生成器的相应方法一样,如果存在不完整的数据,也是会生成分离的多块面积
  • area.curve(curveInterpolator) 设置两点之间的曲线插值器
  • area.context(canvasContext) 用于设置渲染/绘图上下文 context(针对 Canvas 画布而言)
    在使用面积生成器时,如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串
    线段生成器的 context 的默认值为 null 即未设置绘图上下文
  • area.lineX0()area.lineY0() 返回一个线段生成器(它的横坐标读取函数返回的是 x0,而纵坐标读取函数返回的是 y1),一般用于在页面生成面积的下边界线,并对其设置样式以对下边界线进行强调显示
  • area.lineX1() 返回一个线段生成器,它的横坐标读取函数返回的是 x1,纵坐标读取函数返回的是 y1,一般用于在页面生成面积的上边界线
  • area.lineY1() 返回一个线段生成器,它的横坐标读取函数返回的是 x0,纵坐标读取函数返回的是 y1(一般与 area.lineX1() 所返回的线段生成器一样,因为面积生成器的横坐标 x0 的读取函数与 x1 的读取函数一般是一样的)

径向面积生成器

径向面积生成器和上述的面积生成器类似,不同的是它的笛卡尔坐标系的横坐标换成角度(弧度为单位),纵坐标换成距离圆心的长度,上边界线对应于外径 outerRadius,下边界线对应于内径 innerRadius。

使用方法 d3.areaRadial() 创建一个线段生成器(以下称为 areaRadial

提示

调用径向面积生成器时,它生成的线段的中心坐标默认是 (0, 0),可以为其父容器(一般是元素 <g>)设置 transform 属性,移动到目标位置

它也具有多种方法设置不同的参数:

  • areaRadial.angle(angleFunc) 类似于 area.x(xFunc),设置角度读取函数,返回的角度单位是弧度。
    提示

    也有分别设置内径角度读取函数 areaRadial.startAngle(angleFunc) 和外径角度读取函数 areaRadial.endAngle(angleFunc) 的方法,不过一般都不需要分开设置

  • areaRadial.radius(radiusFunc) 类似于 area.y(xFunc),设置内径距离读取函数
    提示

    相应地有分别设置内径距离读取函数 areaRadial.innerRadius(radiusFunc) 和外径距离读取函数 areaRadial.outerRadius(radiusFunc)

  • areaRadial.defined(definedState)
  • areaRadial.curve(curveInterpolator) 其中 curveMonotoneXcurveMonotoneY 单调性曲线插值器不适用
  • areaRadial.context(canvasContext) 用于设置渲染/绘图上下文 context(针对 Canvas 画布而言)
    径向面积生成器的 context 的默认值为 null 即未设置绘图上下文
  • areaRadial.lineStartAngle()areaRadial.lineInnerRadius() 返回内径线段生成器
  • areaRadial.lineEndAngle() 返回外径线段生成器
  • areaRadial.lineOuterRadius() 返回一个线段生成器,它的角度读取函数返回的是内径角度 startAngle,径向距离读取函数返回的是外径距离 outerRadius

堆叠生成器 Stacks

堆叠条形图、河流图(分别是条形图和面积图的一种变形)是一类适合同时对多个系列的数据进行对比的图表。D3 提供了堆叠生成器 Stacks,该生成器并不能直接生成形状,而是将原始数据转换为便于生成这类型图表的数据,一般将堆叠生成器与面积生成器配合使用(用于生成河流图,也可以直接使用,手动生成条形图)。

使用方法 d3.stack() 创建一个堆叠生成器(以下称为 stack

它既是一个方法,可以直接调用,传递需要可视化的数据。默认传递的数据格式是一个数组,其中每个元素都是一个对象,它包含了多个系列数据(属性名是系列名,属性值是当前记录中该系列对应的具体数据)

js
// 原始数据样例
// 数组每个元素都是一条数据记录,它包含了多个系列的数据(不同类型的水果在当月的销量)
const data = [
  {month: new Date(2015, 0, 1), apples: 3840, bananas: 1920},
  {month: new Date(2015, 1, 1), apples: 1600, bananas: 1440},
];

它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用,其中必须配置的参数是系列的名称(默认是一个空数组,则不会对数据进行转换生成相应的系列数据)

js
const stack = d3.stack()
    .keys(["apples", "bananas"])

然后在调用堆叠生成器时,会对原始数据进行转换,在返回的结果中为每一个系列生成一个数组

js
const series = stack(data);
js
// 返回的结果,转换后得到各系列的数据
[
  // 每一个系列的数据都独立「抽取」出来组成一个数组
  // apple 系列数据
  [
    // 每个元素都是 apple 的月销量数据点
    // 前两个数值类似于面积图中的下边界线和上边界线
    // data 对象保留了转换前该数据点对应的原始数据来源
    [0, 3840, data: {month: 2014-12-31T16:00Z, apples: 3840, bananas: 1920}], // 12 月销量
    [0, 1600, data: {month: 2015-01-31T16:00Z, apples: 1600, bananas: 1440}], // 1 月销量
    key: "apples", // 该系列对应的 key(名称)
    index: 0 // 该系列在 series 中的顺序(即对应于堆叠条形图中该系列叠放的次序)
  ],
  // bananas 系列数据
  [
    [3840, 5760, data: {month: 2014-12-31T16:00Z, apples: 3840, bananas: 1920}],
    [1600, 3040, data: {month: 2015-01-31T16:00Z, apples: 1600, bananas: 1440}],
    key: "bananas",
    index: 1
  ]
]
提示

从以上简单的样例可以知道,堆叠生成器的实际作用是对原始数据进行转换:基于系列的次序,通过累加的方式,从原始的数据得到 「堆叠」形式的数据(如果原始的数据不是坐标等图形变量,转换得到的数据也不能直接用于「绘图」,可能还需要在传递给面积生成器时,借助合适的比例尺进行转换映射处理),并按照系列返回结果

堆叠生成器提供多种方法以设置不同的参数:

  • stack.keys([keys]) 设置系列的名称,传递一个数组 keys 或一个返回数组 keys 的函数。一般数组元素的数据类型是字符串,但也可以是其他数据类型。
  • stack.value(valueFunc) 设置各系列的数据读取函数。在调用堆叠生成器对原始数据进行转换过程中,每一个原始数据 d 和系列名称 key(就是在 stack.keys([keys]) 设定的数组中的元素)会作为入参,分别调用该函数,以此获取相应系列的数据,因此该默认读取函数如下
js
function value(d, key) {
  return d[key];
}

如果原始数据中系列的名称,和设置的系列名称 key 不同;或原始数据的格式和默认格式不同(D3 默认原始数据中数组的每个元素都是一条数据记录(一个对象),它包含多个属性,而属性名就是系列的名称,属性值就是该系列在当前记录中的具体值),则需要手动设置数据读取函数。

  • stack.order(orderFunc) 设置系列堆叠排序函数,即对应于堆叠条形图中该系列叠放的次序
    函数 orderFunc 的入参是系列数据 series(原始数据先按照系列名称数组 keys 进行预排序生成了系列数据)
    返回的是一个数组(称为排序数组 order),里面的元素是一个表示索引的数值,依次对应于系列名称数组 keys 中各系列的排序/叠放优先次序
    默认使用 stack.order(d3.stackOrderNone) 排序方式,即按照系列名称数组来排序(不对排序/叠放次序进行改变),该排序函数源码如下
    js
    function orderNone(series) {
      let n = series.length;
      const o = new Array(n);
      while (--n >= 0) o[n] = n;
      return o;
    }

    D3 还提供了多种排序函数
    • d3.stackOrderAppearance 在所有数据点中,具有最大值的系列在底部
    • d3.stackOrderAscending 系列所有值之和最小的在底部
    • d3.stackOrderDescending 系列所有值之和最大的在底部
    • d3.stackOrderInsideOut 在所有数据点中,具有最大值的系列在中间(一般用于河流图中)
    • d3.stackOrderNone 默认值,堆叠次序按照系列名称数组 keys 的顺序
    • d3.stackOrderReverse 堆叠次序和系列名称数组 keys 次序相反

    如果内置的排序函数不能满足要求,也可以自定义排序函数
    提示

    运行完排序函数,系列数据 series 中各元素(也是一个数组,也是一个对象)就具有 index 属性

    而且排序函数在基线函数之前运行,因此在执行完排序函数时,各系列数据的基线还是默认值零。

  • stack.offset(offsetFunc) 设置系列堆叠基线函数。
    函数 offsetFunc 的入参是系列数据 series 和排序数组 order
    默认使用 stack.offset(d3.stackOffsetNone) 即以零为基线。
    D3 还提供了多种基线函数
    • d3.stackOffsetExpand 对数据进行标准化,基线是零,上边界线是 1
    • d3.stackOffsetDiverging 允许正值和负值分别进行堆叠,正值会在零之上进行堆叠,负值会在零之下堆叠
    • d3.stackOffsetNone 默认值,以零为基线
    • d3.stackOffsetSilhouette 将基线向下移动,使河流图的中心始终为零
    • d3.stackOffsetWiggle 移动基线,以最大程度地减少各系列在横轴方向上的「摆动」,一般用在河流图中,与 d3.stackOrderInsideOut 排序函数配合使用。

    如果内置的基线函数不能满足要求,也可以自定义基线函数

曲线插值生成器 Curves

曲线插值生成器并不能直接生成形状,而是定义了两个离散点之间的连线如何生成,D3 提供了多种不同的曲线插值生成器,对应于不同的连线方式,它们的作用一般都是基于离散的数据点模拟出顺滑的曲线。

它要与线段生成器或面积生成器配合使用,一般是作为线段生成器的方法 line.curve() 或面积生成器的方法 area.curve() 的入参。

js
const line = d3.line(d => d.date, d => d.value)
    .curve(d3.curveCatmullRom.alpha(0.5));
提示

默认使用 d3.curveLinear 曲线插值生成器,即两个相邻的离散点之间以直连的方式生成线段。

D3 提供了多种不同的曲线插值生成器,它们可进行的参数配置也不同,具体的效果可以参考官方文档

  • d3.curveBasis(context)
  • d3.curveBasisClosed(context)
  • d3.curveBasisOpen(context)
  • d3.curveBasisOpen(context)
  • d3.curveBumpX(context)
  • d3.curveBumpY(context)
  • d3.curveBundle(context) 常用于分层的边缘捆绑图 hierarchical edge bundling,只与线段生成器配合使用。该曲线插值生成器 bundle 可以设置 beta 系数 bundle.beta(bataValue),默认值是 0.85
  • d3.curveCardinal(context)
  • d3.curveCardinalClosed(context)
  • d3.curveCardinalOpen(context)
    以上三种曲线插值生成器 cardinal 可以设置张力 tension 系数 cardinal.tension(tensionValue),范围是 [1, 0]
  • d3.curveCatmullRom(context)
  • d3.curveCatmullRomClosed(context)
  • d3.curveCatmullRomOpen(context)
    以上三种曲线插值生成器 catmullRom 可以设置 alpha 系数 catmullRom.alpha(alpha),范围是 [1, 0]
  • d3.curveLinear(context)
  • d3.curveLinearClosed(context)
  • d3.curveMonotoneX(context)
  • d3.curveMonotoneY(context)
  • d3.curveNatural(context)
  • d3.curveStep(context)
  • d3.curveStepAfter(context)
  • d3.curveStepBefore(context)
提示

可以使用 D3 提供的相关接口,自定义曲线插值生成器

连线生成器 Links 基于两个坐标点,绘制出平滑的三次贝塞尔曲线作为两点之间(从起源点 source point 到目标点 target point)的连线,一般用于树形图 tree diagram 中。

所绘制的连线在起点和终点的处的切线可以有三种形式,分别通过三种不同的方法创建三种不同的连线生成器(以下称为 link),适用于不同方向的树形图中

  • 使用方法 d3.linkVertical() 创建(生成垂直切线的)连线生成器(以下称为 linkVertical,一般用于父节点和子节点是上下相对位置的树形图中
  • 使用方法 d3.linkHorizontal() 创建(生成水平切线的)连线生成器(以下称为 linkHorizontal,一般用于父节点和子节点是水平左右相对位置的树形图中
  • 使用方法 d3.linkRadial() 创建(生成径向切线的)连线生成器(以下称为 linkRadial,一般用于根节点在内部,后代节点在外部的环形树形图中

它既是一个方法,可以直接调用,传递需要可视化的数据,连线生成器会根据数据构建出连线。一般是以一个对象作为入参,它最基本的形式是包含 sourcetarget 这两个属性,分别表示连线的起点和终点,这两个属性的属性值最基本的形式就是一个数组,分别表示起点或终点的 xy

js
// D3 默认支持的连线生成器入参数据形式
link({
  source: [100, 100],
  target: [300, 300]
});
提示

调用连线生成器时返回的结果,也会基于生成器是否设置了绘图上下文 context 而不同

如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串

它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用:

对于 linkVerticallinkHorizontal 连线生成器有以下方法

  • link.source(sourceFunc) 设置起始点读取函数。该函数的入参是调用连线生成器时传递的数据 d,D3 默认该数据是一个对象,且具有 source 属性表示起始点,因此默认读取函数如下
    js
    function source(d) {
      return d.source;
    }
    提示

    如果入参的数据形式和 D3 的默认形式不同,需要手动设置起始点读取函数,返回起始点

  • link.target(targetFunc) 设置起始点读取函数。默认读取函数如下
    js
    function target(d) {
      return d.target;
    }
  • link.x(xFunc) 设置横坐标读取函数。D3 默认起点和终点都是一个含有两个元素的数组,第一个元素表示这些点的横坐标,第二个元素表示这些点的纵坐标,因此默认读取函数如下
    js
    function x(d) {
      return d[0];
    }
    提示

    如果入参的数据形式和 D3 的默认形式不同,需要手动设置点读取函数,返回横坐标

  • link.y(yFunc) 设置纵坐标读取函数。默认读取函数如下
    js
    function y(d) {
      return d[1];
    }
  • link.context(canvasContext) 用于设置渲染/绘图上下文 context(针对 Canvas 画布而言)
    在使用连线生成器时,如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串
    连线生成器的 context 的默认值为 null 即未设置绘图上下文

对于 linkRadial 连线生成器有类似的方法,不过它的横坐标变成角度(单位是弧度),纵坐标变成了径向距离

  • linkRadial.angle(angleFunc) 类似于 link.x(xFunc),设置角度读取函数,返回的角度单位是弧度
  • linkRadial.radius(radiusFunc) 类似于 link.y(yFunc),设置径向距离读取函数
    提示

    调用径向连线生成器时,它生成的连线的中心坐标默认是 (0, 0),可以为其父容器(一般是元素 <g>)设置 transform 属性,移动到目标位置

提示

如果希望从抽象的数据开始以构建树形图,可以使用 d3-hierarchy 模块。该模块基于抽象的数据和页面大小自动计算布局,即将抽象的数据映射到页面的可视化图形元素,生成各连线的起点 source 和终点 target 坐标数据。再将这些数据分别传递给相应的连线生成器即可分别生成连线。

符号生成器 Symbols

符号生成器 Symbols 提供了一系列常用的符号,一般在散点图中表示数据点,以区分不同的数据类别。

内置符号
内置符号

D3 内置了 7 种符号:

  • d3.symbolCircle 圆形
  • d3.symbolCross 十字形
  • d3.symbolDiamond 菱形
  • d3.symbolSquare 正方形
  • d3.symbolStar 星形
  • d3.symbolTriangle(向上的)三角形
  • d3.symbolWye Y 形
提示

可以通过 d3.symbols 获取具有这 7 种内置符号类型的数组,可用于创建排序比例尺 Ordinal Scales。

使用方法 d3.symbol([type][, size]) 创建符号生成器(以下称为 symbol

创建符号生成器时,第一个(可选)参数设置符号的类型,可以是 7 种内置符号类型之一,也可以是自定义的符号类型;第二个(可选)参数设置符号的尺寸(单位是像素);如果没有入参,则调用符号生成器时就会默认创建宽和高都为 64 px 的圆形符号。

它既是一个方法,可以直接调用,生成相应的符号。

它也是一个对象,具有多种方法设置不同的参数,一般通过链式调用的方式来使用:

  • symbol.type([type]) 设置符号类型,可以是 7 种内置符号类型之一,也可以是自定义的符号类型。默认值是圆形,其入参是函数如下:
    js
    function type() {
      return circle;
    }
  • symbol.size([size]) 设置符号的尺寸(宽和高都一样),单位是像素,这对于同时希望使用符号类型和符号大小两个视觉通道变量来编码数据是十分有用的。默认值是如下
    js
    function size() {
      return 64;
    }
  • symbol.context(canvasContext) 用于设置渲染/绘图上下文 context(针对 Canvas 画布而言)
    在使用符号生成器时,如果设置了绘图上下文 context,就会生成一系列在画布绘制线条的命令/方法;如果没有设置绘图上下文 context,则生成可用于作为 <path> 元素的属性 d 的属性值的字符串
提示

可以使用 D3 提供的相关接口,自定义符号类型。它是一个具有方法 draw 的对象,而该方法接收两个入参 contextsize,其中 context 是一个可以调用 canvas API 中与 path 相关方法的对象;而 size 则用于设置符号的大小。可以参考 7 种 D3 内置符号的源码来了解如何创建符号类型。


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes