d3.js基础与平行桑基图说明

D3.js 基础与平行桑基图

1. D3.js 是什么

D3.js(Data-Driven Documents)是一个面向 数据可视化 的前端库,核心能力是:

  • 通过数据驱动 DOM / SVG 更新
  • 适合做高自由度自定义图表
  • 支持动画、交互、布局计算、颜色映射等

ECharts 这类图表库不同,D3.js 更像是底层绘图工具箱,适合:

  • 标准图表库不容易实现的图
  • 图形结构复杂、交互复杂的场景
  • 需要精细控制布局和渲染细节的业务图表

2. 基础安装与引入

2.1 安装

1
npm install d3

2.2 在 Vue + TS 中引入

1
import * as d3 from 'd3'

3. D3.js 的基础使用

D3 最核心的思想是:先计算数据与布局,再把结果映射到 SVG 图形上

常见开发流程:

  1. 准备容器
  2. 获取尺寸
  3. 绑定数据
  4. 计算坐标 / 路径 / 颜色
  5. 渲染图形
  6. 绑定交互

3.1 选择元素

1
const svg = d3.select(svgRef.value)

常见 API:

  • d3.select():选中单个元素
  • d3.selectAll():选中多个元素
  • .append():追加元素
  • .attr():设置属性
  • .style():设置样式
  • .text():设置文本

3.2 数据绑定

1
2
3
4
5
6
7
const data = [10, 20, 30]

d3.select('svg')
  .selectAll('rect')
  .data(data)
  .enter()
  .append('rect')

常用概念:

  • .data(data):绑定数据
  • .enter():处理新增元素
  • .exit():处理删除元素
  • .join():统一处理增删改

3.3 比例尺(Scale)

比例尺用于把数据值转成像素值。

常见类型:

  • d3.scaleLinear():线性映射
  • d3.scaleBand():分类轴
  • d3.scaleOrdinal():离散映射,常用于颜色
  • d3.scaleTime():时间轴

示例:

1
const x = d3.scaleLinear().domain([0, 100]).range([0, 500])

3.4 坐标轴

1
2
const axis = d3.axisBottom(xScale)
svg.append('g').call(axis)

常见坐标轴:

  • d3.axisBottom()
  • d3.axisTop()
  • d3.axisLeft()
  • d3.axisRight()

3.5 路径生成

如果图表不是简单矩形,而是折线、面积、弧线、流带,通常会使用路径。

常见工具:

  • d3.line()
  • d3.area()
  • d3.arc()

平行桑基图,使用的是 自定义 SVG path + 贝塞尔曲线 的方式绘制流带。


4. D3.js 图表常见配置项

D3 本身没有统一 option,但在业务里通常会把影响图形展示的参数整理成配置对象。

4.1 尺寸与边距

1
const MARGIN = { top: 20, right: 80, bottom: 40, left: 80 }

常见作用:

  • 给图例、标签、列名预留空间
  • 避免图形贴边

4.2 颜色相关

1
2
3
4
5
colorMap: Map<string, string>
paletteColors: string[]
fontColor: string
borderColor?: string
borderWidth?: number

说明:

  • colorMap:分类值到颜色的映射
  • paletteColors:默认调色板
  • fontColor:文字颜色
  • borderColor / borderWidth:边框控制

4.3 图例相关

1
2
3
4
legendVisible: boolean
legendPosition?: string
legendLeft?: string
legendData: Array<{ name: string; color: string }>

说明:

  • 是否显示图例
  • 图例在顶部还是底部
  • 左对齐 / 居中 / 右对齐
  • 图例文本与颜色

4.4 文本标签相关

1
2
3
4
5
6
7
8
9
labelConfig?: {
  show: boolean
  position: string
  overlap: boolean
  fontSize: number
  color: string
  markArray: Array<any>
  stateDocHtml: string
}

说明:

  • show:是否显示标签
  • position:左 / 中 / 右
  • overlap:是否允许重叠
  • fontSize / color:样式控制
  • markArray:动态字段定义
  • stateDocHtml:富文本模板

4.5 Tooltip 相关

1
2
3
4
5
tooltipConfig?: {
  markArray: Array<any>
  prosemirrorDOM: string
  fieldMap: Map<string, string>
}

说明:

  • 支持动态字段展示
  • 支持富文本模板替换
  • 支持字段别名映射

4.6 图形结构相关

1
2
3
curveness?: number
gapRatio?: number
bandOpacity?: number

说明:

  • curveness:曲线弯曲程度
  • gapRatio:节点之间的间距比例
  • bandOpacity:流带透明度

5. 如何用 D3.js 绘制图表

无论是柱状图、折线图还是桑基图,思路基本一致。

5.1 准备容器

1
2
const svgRef = ref<SVGSVGElement | null>(null)
const containerRef = ref<HTMLDivElement | null>(null)

5.2 获取容器尺寸

1
2
const containerW = containerRef.value.offsetWidth || 800
const containerH = containerRef.value.offsetHeight || 500

5.3 计算绘图区

1
2
3
4
const plotLeft = MARGIN.left
const plotRight = svgW - MARGIN.right
const plotTop = MARGIN.top
const plotBottom = svgH - MARGIN.bottom

5.4 清空旧图重绘

1
2
const svg = d3.select(svgRef.value)
svg.selectAll('*').remove()

5.5 计算布局

布局计算通常包括:

  • 节点位置
  • 宽高分配
  • 路径控制点
  • 颜色映射
  • 标签位置

5.6 绘制图形元素

常见元素:

  • rect
  • circle
  • line
  • path
  • text
  • g

5.7 绑定交互

1
2
3
4
5
.on('mouseenter', fn)
.on('mousemove', fn)
.on('mouseleave', fn)
.on('click', fn)
.on('contextmenu', fn)

5.8 监听变化并自动重绘

1
2
3
4
5
watch(
  () => props.chartData,
  () => nextTick(draw),
  { deep: true }
)
1
resizeObserver = new ResizeObserver(() => nextTick(draw))

6. 平行桑基图是什么

本质上是一个 基于 D3 + SVG 实现的 Parallel Sets(平行集图)

它和传统桑基图类似,但更强调:

  • 每一列是一个维度
  • 每一列中有多个分类值节点
  • 相邻列之间通过流带展示流向关系
  • 流带颜色由 colorKey 区分

支持:

  • 多列维度流向展示
  • 颜色分类
  • 图例
  • Tooltip
  • 富文本标签
  • Hover 联动高亮
  • 点击与右键联动
  • 容器 resize 自动重绘

7. 平行桑基图输入结构

核心入参是 props.chartData

7.1 rows

1
2
3
4
5
6
rows: Array<{
  dimensionList: Array<{ id: string; value: string; tag?: boolean }>
  value: number
  colorKey?: string
  dynamicTooltipValue?: Array<{ fieldId: string; value: any }>
}>

含义:

  • dimensionList:该行经过的各个维度值
  • value:这条路径的数量
  • colorKey:分类颜色键
  • dynamicTooltipValue:Tooltip 附加字段

7.2 columns

1
columns: Array<{ id: string; name: string }>

含义:

  • 定义每一列代表哪个维度
  • name 用于底部列名渲染

8. 平行桑基图绘制流程

draw() 基本可以拆成以下几个阶段。

8.1 初始化和尺寸计算

主要完成:

  • 读取 rowscolumns
  • 读取图例与样式配置
  • 获取容器宽高
  • 计算绘图区坐标
  • 计算每列 X 坐标

重点变量:

  • svgW / svgH
  • plotTop / plotBottom
  • plotLeft / plotRight
  • colX

8.2 按列统计节点总量

通过以下结构记录各列数据:

  • colTotals:每列每个节点值的总量
  • colOrder:首次出现顺序
  • nodeFieldValMap:节点对应字段值映射

这一步的作用是:

  • 计算节点高度
  • 支撑排序
  • 支撑标签字段替换

8.3 为每列节点计算纵向区间

通过 colBandMaps 为每个节点值计算:

  • y0
  • y1

这表示节点在该列里占据的垂直区间。

这一步很关键,因为后续流带的上下边界都依赖它。

8.4 颜色映射计算

通过:

  • rowColorKeys
  • allColorKeys
  • rowColors

完成每条路径的颜色归属。

作用:

  • 保证相同分类颜色一致
  • 支撑图例显示
  • 支撑 hover 高亮分组

8.5 构建 flow 数据

最终把中间计算结果整理成 flows,每一项代表一段流带。

核心字段有:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Flow {
  sourceCol: number
  targetCol: number
  sourceValue: string
  targetValue: string
  sy0: number
  sy1: number
  ty0: number
  ty1: number
  color: string
  opacity: number
  rowKey: string
  value: number
  colorKey: string
  pathKey: string
  isForcedMin: boolean
  dynamicTooltipValue?: Array<{ fieldId: string; value: any }>
}

这些字段控制了:

  • 流带从哪一列连到哪一列
  • 从哪个值流向哪个值
  • 上下边界坐标
  • 流带颜色和透明度
  • hover 联动路径
  • tooltip 扩展内容

8.6 解决跨列连续对齐

核心思路:

  • 每列每个节点内部,按 colorKey 分配固定偏移区间
  • source 端和 target 端尽量保持堆叠顺序一致
  • 使用上一段的 target 位置,辅助下一段的 source 排序
  • 尽量减少交叉、保证路径连续

这部分主要依赖:

  • nodeColorOffset
  • prevTy0ByFlow
  • nextTy0ByFlow

这样做的效果是:

  • 流带更平滑
  • 同一路径视觉更连续
  • 不同颜色分类更稳定
  • 图更容易读

9. 流带是怎么画出来的

流带用自定义 path 拼出来的。

核心逻辑:

1
2
3
4
5
6
7
8
const makePath = (s0: number, s1: number, t0: number, t1: number) =>
  [
    `M ${x1} ${s0}`,
    `C ${cx} ${s0}, ${cx} ${t0}, ${x2} ${t0}`,
    `L ${x2} ${t1}`,
    `C ${cx} ${t1}, ${cx} ${s1}, ${x1} ${s1}`,
    `Z`
  ].join(' ')

含义:

  • 从 source 上边界开始
  • 用贝塞尔曲线连接到 target 上边界
  • 再连到 target 下边界
  • 再用另一条贝塞尔曲线回到 source 下边界
  • 最终闭合为一个带状区域

curveness 会决定控制点位置:

1
const cx = x1 + (x2 - x1) * curveness

也就是:

  • curveness 越小,越接近直线
  • curveness 越大,曲线越柔和

10. 组件交互设计

10.1 Hover 高亮

通过 pathKey 做了路径级别联动:

  • hover 当前流带时,不只高亮这一段
  • 同一路径的其他段也会一起高亮
  • 非相关流带降低透明度

这比单段高亮更符合平行集图的阅读习惯。

10.2 Tooltip 跟随鼠标

通过:

  • mouseenter
  • mousemove
  • mouseleave

控制 tooltip 的显示、位置更新和隐藏。

10.3 Click 联动

点击流带时会:

  • 找到匹配的原始数据行
  • 收集 dimensionList 中的字段值
  • 通过 emit('click', valueObj) 抛给外层

这很适合做图表联动筛选。

10.4 右键菜单

实现了:

1
emit('contextmenu', { x, y, valueObj })

这样外层就可以基于点击位置弹出菜单,例如:

  • 查看明细
  • 跳转页面
  • 追加筛选条件

11. 标签实现方式

这一部分做得也很完整。

11.1 普通文本标签

普通标签使用 SVG text 绘制,适合:

  • 纯文本
  • 样式简单
  • 性能要求更高

11.2 富文本标签

富文本标签没有直接画进 SVG,而是放到了覆盖层:

  • ps-rich-labels-layer
  • ps-rich-label

这样做的优点:

  • 不受 SVG overflow 限制
  • 更容易支持 HTML 富文本
  • 更容易兼容编辑器输出内容

11.3 标签配置能力

当前支持:

  • 是否显示标签
  • 左 / 中 / 右位置
  • 是否允许重叠
  • 字号与颜色
  • 动态字段占位
  • ProseMirror / HTML 模板替换

这已经属于比较完整的标签系统了。


12. Tooltip 实现方式

Tooltip 分成两种模式:

12.1 默认模式

展示:

  • sourceValue -> targetValue
  • 分类 colorKey
  • 数值 value

12.2 自定义字段模式

如果配置了 tooltipConfig.markArray,则会:

  • dynamicTooltipValue 里取字段值
  • 按字段配置格式化
  • 支持富文本模板 prosemirrorDOM

也就是说,Tooltip 已经不是静态提示,而是一个可配置的业务信息面板。


15. 总结

从项目角度看,D3.js 的意义主要在于:

  • 适合复杂自定义图表
  • 能精细控制布局和交互
  • 能很好地和 Vue 组件化结合

平行桑基图实现,具备完整业务组件能力,覆盖了:

  • 数据聚合
  • 列节点布局
  • 流带路径计算
  • 图例渲染
  • 标签渲染
  • Tooltip 渲染
  • Hover / Click / 右键联动
  • Resize 自适应
comments powered by Disqus
使用 Hugo 构建😊 主题 StackJimmy 设计