可视化组件的配置设计思路

刚接触数据可视化的组件开发时,往往会遇到一个问题,就是不知道该如何设计组件对外开放的配置项。我根据最近写组件的一些经验,对配置项的设计做个总结,方便后面参考。

我们一定要明确,组件的终态是什么样的。

原则

不包含数据联动的HTML结构不应该开放,包含数据联动的HTML结构应该以formatter格式开放

交互不应该开放,这是组件内部实现

样式应该以组件为单位进行开放,统一以style字段开放出去;当然,我们应该有一个默认的样式

风格应该通过引入不同的css文件来实现

根据CSS三大目的设计配置

前端开发人员众所周知,CSS的三大精髓是:定位、美化、移动。一般这三个内容,组件应用人员都会有一些个性化的需求,因此我们设计组件的配置项的时候,就需要把这三部分都暴露出去。

因此我们的组件,针对每个元素,需要有类似这样的配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
// 定位:相对本身位置的x、y偏移量
offset: [10, 10],
// 美化
style: {
color: '#CCCCD9',
opacity: 1,
fontWeight: 'bold',
fontSize: '100px',
position: 'absolute',
right: '10px',
bottom: '100px',
textAlign: 'right',
},
// 移动:即动画
animation: {
enable: true,
type: 'animationName',
ease: 'linear',
}
}

这里只是给了一个简单的示例,实际应用中可能会比这个复杂很多;另外一些配置项也可以暴露为自定义函数。

如何简化样式配置项的处理逻辑

样式(style)的配置一般都会比较多,如果我们在代码里面,针对每个组件都一个配置一个配置的去处理,那代码量就太多了,因此我们需要用更加简洁、可复用的方式来实现样式配置项的处理。

以给D3.js画的元素绑定样式和函数为例:

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
/**
* 给元素绑定自定义样式和自定义函数
* @param {*} element
* @param {*} style
*/
export function customize(element, style) {
Object.keys(style).forEach((key) => {
if (typeof style[key] === 'function') {
element.style(capitalToDash(key), (...args) => {
style[key](args);
});
} else {
element.style(capitalToDash(key), style[key]);
}
});
}

/**
* 将驼峰格式的字符串转为破折号分割的小写字符串
* @param {*} content
*/
function capitalToDash(content) {
const chars = [];
for (let i = 0; i < content.length; i++) {
let char = content[i];
const charCode = char.charCodeAt();
if (charCode >= 65 && charCode <= 90) {
char = `-${char.toLowerCase()}`;
}
chars.push(char);
}
return chars.join('');
}

SVG和DOM的样式属性有哪些差异?

SVG本质上也是DOM,因此DOM的属性SVG都支持;然后SVG还有一些扩展的属性,是DOM所没有的,这些属性一般和具体的几何图形相关联。

单个组件的数据结构设计

我们不应该在某个具体的绘图小组件(比如tooltip)里面做太多的数据处理,绘图小组件应该只专注于展示和事件交互,因此我们需要给每个绘图小组件设计一个固定的数据结构。

这一点很重要,如果我们没有做好小组件的数据结构设计,到后面很容易就把数据处理逻辑也放到具体的组件中了,导致代码耦合度很高,组件无法复用。

内容

比如柱状图中,每个柱子上面的数值,有的时候需要保留2位小数,有的时候需要加上一个单位后缀,类似这种内容自定义需求也是非常多的,因此我们需要开放自定义内容的能力给用户:

1
2
3
4
5
6
7
8
{
formatter(data) {
if (data.name.length > 3) {
return `${data.name.substr(0, 3)}...`;
}
return data.name;
},
};

事件交互

组件大多都会带有交互功能,这部分也是用户自定义需求较多的内容,因此事件相关的配置也是必不可少的,类似这样:

1
2
3
4
5
6
7
8
9
10
{
action: {
// 点击的回调函数
onClick(data, date) {
console.log(
`您点击了${data.name}这个柱子,当前播放到了这个日期:${date}`,
);
},
},
}

数值类全部开放为配置

我们在写组件的初期,一般都是以实现功能为主,这个时候往往会在代码中写很多的硬编码,比如计算坐标轴的位置、图形元素的位置等等。这些硬编码中的数值,都是需要暴露出去作为配置项的。

比如页面绘图区域,一般和页面四侧会有一定的距离,那么就可以将这个距离暴露出去:

1
2
3
4
5
6
7
{
// 绘图区域
grid: {
// 图表左右上下间距
margin: [40, 40, 40, 25],
},
};

参考配置

综上,我们就可以得出一份基本的配置项了。

比如下面这个就是一个折线图组件的配置示例:

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
{
// 为了避免组件内部的样式/id和应用方页面上的冲突,这里设计了一个前缀,会加到组件所有的样式名/id名前面
prefix: 'this_is_a_prefix_of_the_line_component_',
// 绘图区域
grid: {
// 图表左右上下间距,注意,left_margin不包括左侧的label,修改数值较小会导致左侧label不显示
margin: [40, 40, 40, 25],
background: {
show: true,
style: {
// 画布的背景色
fill: '#000',
},
},
},
series: {
line: {
// 定位:相对本身位置的x、y偏移量
offset: [10, 10],
// 美化
style: {
color: '#CCCCD9',
opacity: 1,
fontWeight: 'bold',
fontSize: '100px',
position: 'absolute',
right: '10px',
bottom: '100px',
textAlign: 'right',
},
// 移动:即动画
animation: {
enable: true,
type: 'animationName',
ease: 'linear',
},
formatter(data) {
if (data.name.length > 3) {
return `${data.name.substr(0, 3)}...`;
}
return data.name;
},
action: {
// 点击的回调函数
onClick(data, date) {
console.log(
`您点击了${data.name}这个柱子,当前播放到了这个日期:${date}`
);
},
},
},
},
};

思考

先有数据结构和数据流程图,再进行编码

这样可以保证我们的组件是具有一定设计的,而不只是简单的功能堆积。否则到后面代码会乱得一塌糊涂,返工率极高。