基于D3实现的选项式树状图生成器

基于 D3 的选项式树状图生成器,通过简单的数据计算和图形选项,帮助业务方快速实现一个 D3 树状图结构,无需关心展开收起逻辑,通过配置均能够快速实现。

核心模块

核心模块由容器,树组件,工具库组成。

容器模块主要负责为树状图提供显示容器,以及全局的交互,例如缩放,移动。

数组件提供了一个基类,并封装了通用的绘制流程,业务方通过重写核心计算方法,并通过图形配置达到自定义数图样式的效果

工具库提供了计算元素宽度,截取字符等常用方法。

绘制基本流程:

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let wrapper = new Wrapper("#law-relation", { scaleExtent: [0.5, 2] });
this.tree = new Tree(wrapper.rootG.append("svg:g"), data, [40, 300], {
toggleChildren(e, d) {
this.nextTick(() => {
wrapper.transformToCenter({ x: d.data.offset.x, y: d.data.offset.y });
});
},
contents: [
{
filter: (o) => o.depth === 0,
contents: [
renderer.textArrRenderer({
cursor: "pointer",
fontSize: 14,
lineHeight: 16,
fill: "#fff",
dx: macro.centerWidth / 2 + 5,
textAnchor: "middle",
}),
],
},
],
});
this.tree.refresh();
this.tree.handleData((data) => {
data.children = _.filter(originData.children, filter);
});

Wrapper

初始化视图容器:

1
let wrapper = new Wrapper(selector, config);

参数定义

  • selector:画布的容器,可使用 class 或者 id,必填
  • config:配置对象,选填
    • scaleExtent:缩放比例,不填会默认无限放大缩小,例如:[0.5, 2]

实例属性

属性 类型 描述
svg d3-selector svg 根节点,一般情况下用不到
svgWidth Number svg 根节点宽度
svgHeight Number svg 根节点高度
rootG d3-selector 图层根节点,所有内容添加在该层级内
zoom d3-zoom 支持拖拽缩放

实例方法

transformToCenter

将容器中某一点坐标移动到容器中心。

1
wrapper.transformToCenter({ x: d.data.offset.x, y: d.data.offset.y });

参数说明:

参数名 说明
{x, y} 坐标,必填
duration 动画时间,默认 500ms

Tree

基于 d3 树状图和集群图的二次封装,自动计算节点坐标,封装展开收起等基础交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import BaseTree from "d3-chart/Trees/Base";

class Tree extends BaseTree {
calcNodeOffset(o) {
return { x, y, width, height, transX, transY };
}
applyNodeContent(nodeEnter) {}
applyNodeUpdate(nodeUpdate) {}
applyLinkStyle(linkEnter) {}
pathGenerator(o) {
let hx = (o.target.x - o.source.x) / 2;
return `M${o.source.x},${o.source.y} H ${o.source.x + hx} V${o.target.y} H${
o.target.x
}`;
}
}
this.tree = new Tree(wrapper.rootG.append("svg:g"), data, [40, 300], {
toggleChildren(e, d) {
this.nextTick(() => {
wrapper.transformToCenter({ x: d.data.offset.x, y: d.data.offset.y });
});
},
contents: [
{
filter: (o) => o.depth === 0,
contents: [
renderer.textArrRenderer({
cursor: "pointer",
fontSize: 14,
lineHeight: 16,
fill: "#fff",
dx: macro.centerWidth / 2 + 5,
textAnchor: "middle",
}),
],
},
],
});

参数定义

配置项 类型 描述 是否必选 默认值
root d3-selector svg 容器,必须为<g>标签 -
data Object 数据 -
nodeSize Array 描述节点距离,Array[0]:同层节点间距 Array[1]:层级间距 -
config.layout String 使用的 d3 模型,暂时只支持 tree 和 cluster ‘tree’
config.contents Array 节点内容,下文会详细介绍
config.prefix String 节点 id 前缀 ‘node’
config.duration Number 动画过渡时间 500
config.toggleChildren Function 节点点击时触发
config.loadChildren Function 加载数据时触发

核心方法

calcNodeOffset

继承时重写该方法以计算图谱中节点的坐标

params:object: HierarchyNode

return:object: Offset

offset 会被注入到每个节点的 data 属性中去,其包含以下属性:

属性名 说明
x,y 节点基准点坐标,同时也是节点与父节点连线终点坐标,节点内部坐标均为与该点的相对坐标
width,height 节点真实占位
transX,transY 节点与子节点连线起点坐标,其值为相对于基准点的偏移,可以理解为节点实际占位的长宽
x0,y0 节点上一个生命周期的基准点坐标

除了 x0,y0 会自动生成,其余属性均需要在该方法中计算并返回。

此外,该方法还需要计算绘制过程中需要的数值,例如文字的长宽等,数据建议均放在 data 字段中,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export function calcNodePos(o) {
let base = {
x: o.x,
y: o.y,
width: this.nodeSize[0],
height: this.nodeSize[1],
};
let offset = base;
if (o.depth === 0) {
o.data.textArr = splitTextByWidth(
o.data.name,
macro.centerFontSize,
macro.centerWidth
);
offset = {
...base,
width: macro.centerWidth + 10,
height: o.data.textArr.length * 16 + 8 * 2,
};
} else if (o.depth === 1) {
o.data.themeColor = _.sample(macro.colorList);
o.data.textWidth = calcTextWidth(o.data.name, macro.fontSizeSecondTitle);
offset = {
...base,
width: 12 + 5 + o.data.textWidth,
height: 16 + 5 + 12,
};
} else if (o.depth > 1) {
o.data.lineWidth = 30;
o.data.percentWidth = o.data.percent
? calcTextWidth(o.data.percent, 14)
: 0;
o.data.percentHeight = o.data.percent ? 16 + o.data.lineWidth : 0;
o.data.textContainerWidth =
calcTextWidth(o.data.name, macro.fontSizeSecondTitle) + 10 * 2;
offset = {
...base,
width: o.data.textContainerWidth,
height: o.data.percentHeight + macro.barHeight + 18,
};
}
offset.transX = 0;
offset.transY = offset.height;
return offset;
}

核心配置

contents 包含了对树状图中每个节点的内容描述,可以通过图形组合嵌套达到节点样式的自定义,且用法非常灵活,在功能上可以通过配置特殊的标记让节点拥有特定交互的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import * as d3 from "d3";

export declare interface RenderOption {
transform?: String | ((d: d3.Datum) => String);
className?: String | ((d: d3.Datum) => String);
[String]?: String;
}

export declare function renderer(
params: RenderOption
): (node: d3.Selection) => d3.Selection;

export declare function filter(node: d3.Selection): Boolean;

export declare interface ContentOption extends RenderOption {
renderer?: renderer;
filter?: filter;
contents?: Array<ContentOption>;
expend?: Boolean;
fold?: Boolean;
group?: Boolean;
}

const content: renderer | ContentOption[];

配置说明:

配置项 说明
renderer 提供图形渲染函数。为树状图添加一个图形节点,sdk 内置了一些基本图形,业务方可自行扩展。当该节点为虚拟节点(不存在实际标签)或者集合节点(<g>)时可忽略
transform 可选,为当前节点添加一个 transform 属性,或者为返回 transform 属性的函数,函数参数为 d3 当前节点的属性
className 可选,为当前节点添加一个 class 属性
filter 可选,添加一个过滤器,为符合条件的节点添加相应的图形
contents 当 renderer 为空时可选,为当前虚拟节点/集合节点添加子节点,子节点配置方法和当前节点相同,可以多次嵌套
group 当 renderer 为空时可选,当设置为 true 时会添加一个<g>标签作为子节点的父元素
expend 可选,为当前节点添加展开/收起子节点交互,如果当前节点存在_children 属性则会直接将其展开,如果不存在则会调用 loadChildren 钩子异步加载子节点
fold 可选,为当前节点添加展开.收起同层节点交互,如果当前节点的父节点存在_foldChildren 则会直接将其全部展开,如果不存在则会调用 loadChildren 钩子异步加载子节点

完整参考配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const content = [
{
filter: (o) => o.data.percent,
contents: [
renderer.textRenderer({
transform: (o) =>
`translate(${-o.data.percentWidth / 2}, ${16 / 2 + 5})`,
text: (o) => o.data.percent,
fontSize: 14,
}),
renderer.lineRenderer({
x1: 0,
x2: 0,
y1: 16,
y2: (o) => o.data.percentHeight,
stroke: macro.linkColor,
}),
],
},
{
group: true,
transform: (o) =>
`translate(${-o.data.textContainerWidth / 2}, ${o.data.percentHeight})`,
contents: [
renderer.rectRenderer({
width: (o) => o.data.textContainerWidth,
height: macro.barHeight,
x: 0,
y: 0,
rx: 2,
ry: 2,
stroke: (d) => {
if (d.parent && d.parent.data) {
d.data.themeColor = d.parent.data.themeColor;
return d.parent.data.themeColor;
}
},
}),
{
expend: true,
renderer: renderer.textRenderer({
text: (o) => cutCompanyName(o.data.name, 100),
fontSize: 14,
x: 10,
y: 5 + macro.barHeight / 2,
}),
},
],
},
{
filter: (o) => o.data.hasChildren,
expend: true,
renderer: renderer.circleToggleRenderer({
transform: (o) =>
`translate(-7, ${o.data.percentHeight + macro.barHeight + 5})`,
...circleToggleParams,
}),
},
];

基于D3实现的选项式树状图生成器
https://www.wobushi.top/2021/基于D3实现的选项式树状图生成器/
作者
Pride Su
发布于
2021年10月29日
许可协议