React+D3 声明式可视化展示

在进行前端可视化图定制开发的时候,我们往往会使用d3.js来进行开发,其自包含了针对数据集合的处理以及操作节点集合的方式。下面我们将介绍在配合React使用时,借用React的思路,充分发挥React的组件化,声明式特性,来优化D3的开发过程。

旧的方式

d3提供了d3-selection,来使用数据对节点进行驱动。我们可以根据数据拿到需要对节点进行的变更。往往我们会在componentDidMount中来进行节点操作,大概分为几个步骤:

  1. 根据数据和容器宽度,获取比例尺
  2. 根据data join的enter和exit选择集,来添加,删除或更新元素
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
export default class Graph extends React.Component {
componentDidMount () {
const { data = [] } = this.props
this.renderBar(data)
}
renderBar (data) {
// 拿到比例尺
const scale = d3.scaleLinear()
.domain(d3.extent(data))
.range([0, 180])
const wrap = d3.select('#qps-graph')
wrap.selectAll(`g.bar`)
.data(data)
.enter()
.append('g')
.attr('class', 'bar')
.append('rect')
.attr('class', 'bar-rect')
wrap.selectAll(`g.bar`)
.data(data)
.attr("transform", (d, i) => `translate(${i * 20}, ${180 - scale(d)})`)
.select('rect')
.style('height', d => scale(d))
wrap.selectAll(`g.bar`)
.data(data)
.exit()
.remove()
}
render() {
return <svg height="180" width="1000" id="qps-graph"></svg>
}
}

这种方式虽然完成了任务,但是我们依旧会感觉有不舒服的地方:

  1. 我们很难对图形进行复用,图形稍微改动一点,就需要改动代码
  2. 在声明式代码中掺杂了很多命令式的操作过程,不直观,不利于维护
  3. 针对事件处理,会很难进行,我们往往需要使用d3创建新的元素,来控制它的内容、显示和隐藏

新的方式

我们会发现,我们根据data join的enter和exit选择集,来添加,删除或更新元素,这个步骤,其实React也可以进行,并且可以使用更加直观的声明式来书写,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render(){
const scale = d3.scaleLinear()
.domain(d3.extent(data))
.range([0, 180])
const h = d => scale(d)
const y = (d, i) => `translate(${i * 20}, ${180 - scale(d)})`
return <svg height="180" width="1000">
{data.map((d, i) => {
return (
<g key={`bar-${i}`} transform={y(d, i)}>
<rect
height={h(d)}
width={10}
fill='#fc2e1c'
/>
</g>
);
})}
</svg>
}

是不是很简单!同时,我们可以利用React的组件化特性,来对我们的每个单元模块进行封装,独立功能,方便各个组件的复用,来让我们的可视化代码更加直观,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render(){
const scale = d3.scaleLinear()
.domain(d3.extent(data))
.range([0, 180])
const h = d => scale(d)
const y = (d, i) => `translate(${i * 20}, ${180 - scale(d)})`
return <svg height="180" width="1000">
{data.map((d, i) => {
return (
<Group key={`bar-${i}`} left={i * 20} top={180 - scale(d)}>
<Bar
height={h(d)}
width={10}
fill='#fc2e1c'
/>
</Group>
);
})}
</svg>
}

我们有幸看到已经有人做了这个工作:vx,作者已经封装了很多的常用图形,我们的任务就是,对这些图形加上数据,进行拼接就可以了。同时,我们可以对其进行二次封装,以适应我们的项目。

经过一系列封装,我们写一个图形,这样操作即可:

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
// 定义getter
const x = d => new Date(d.date);
const y = d => +d.close;
// 定义容器配置
const padding = {
top: 20,
left: 40,
bottom: 20,
right: 20
};
export default class LineGraph {
render() {
return (
<Container width={1000} height={200} padding={padding} x={x} y={y}>
{({ width, height }) => {
// 根据数据定义x,y轴的scale
const xScale = scaleTime({
rangeRound: [0, width],
domain: d3.extent(data, x)
});
const yScale = scaleLinear({
rangeRound: [height, 0],
domain: d3.extent(data, y)
});
return (
<Group>
<Grid
width={width}
height={height}
xScale={xScale}
yScale={yScale}
/>
<AxisBottom top={height} scale={xScale} />
<AxisLeft scale={yScale} />
<AreaClosed
data={data}
xScale={xScale}
yScale={yScale}
x={x}
y={y}
/>
</Group>
);
}}
</Container>
);
}
}

More

其实不止这些基本的图形可以用这种方式,当我们进行更加复杂图形的书写时,也可以使用这种方式。
例如我们最近做了一个复杂的树形结构,其每个节点都包含了很复杂的内容和交互,我们可以将其进行简化成以下方式:

1
2
3
4
5
6
7
8
render () {
return <Container>
<Defs />
<RootNode />
<TopTree />
<BottomTree />
</Container>
}

其中Tree可以进行继续的封装,拼接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render () {
const nodeElements = nodes.map((node, index) => (
<Node direction={direction} node={node} methods={methods} />
));
const linkElements = links.map((link, index) => {
return (
<Path
active={active}
item={item}
direction={direction}
s={link.source}
d={link.target}
activeItem={activeItem}
/>
);
});
return <Group>
{nodeElements}
{linkElements}
</Group>
}

然后我们对Node,和Path组件,进行详细的书写,这样非常的简洁明了,不是吗?
同时,当我们其他可视化组件需要这样的Path活Node或者Tree时,我们直接拿来用即可!