刚接触数据可视化的组件开发时,往往会遇到一个问题,就是不知道该如何设计组件对外开放的配置项。我根据最近写组件的一些经验,对配置项的设计做个总结,方便后面参考。
我们一定要明确,组件的终态是什么样的。
原则
不包含数据联动的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
| { 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
|
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]); } }); }
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
| { prefix: 'this_is_a_prefix_of_the_line_component_', grid: { margin: [40, 40, 40, 25], background: { show: true, style: { fill: '#000', }, }, }, series: { line: { 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}` ); }, }, }, }, };
|
思考
先有数据结构和数据流程图,再进行编码
这样可以保证我们的组件是具有一定设计的,而不只是简单的功能堆积。否则到后面代码会乱得一塌糊涂,返工率极高。