一个关于组件配置项的糟糕设计案例

缘由

最近在做范式组件,我写了动态柱状图和折线图,在考虑如何给使用者开放配置项的时候,考虑到如果全部配置都给用户,我感觉他们的理解成本会比较高,因此我想将配置封装为Theme类,只开放设计规范中允许的配置项给用户。

形式

默认的配置格式

组件默认的配置是类似这样的:

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
const option = {
animation: {
show: true,
// 事件回调函数
action: {
// 页面框架初始化完成
onInitialized() {
console.log('页面初始化完成');
},
// 动画播放完成的回调
onFinished() {
console.log('动画播放完了');
},
// 每次步进的回调
onStep(date, data, container, divContainer, scale) {},
},
},

// 播放相关的设置
play: {},

// 绘图区域
grid: {},

// 图形元素相关的设置
series: {},
// 坐标轴
axis: [],
};

Theme设计后的格式

我将配置用类包了一层,这是基类:

1
2
3
4
5
6
7
class Theme {
constructor(option) {
this.option = option;
}
}

export default Theme;

这是具体实现类:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import Theme from '../Theme';

class LineTheme extends Theme {
constructor(option) {
super(option);
this.maxValue = option.maxValue;
this.xTicks = option.xTicks;
// 图表在X方向上的位移,一般用于右移0点
this.translateXOfDiagram = option.translateXOfDiagram || 10;

/**
* 允许用户自定义的配置
*/
this.customConfig = {
logo: null,
text: null,
line: null,
axis: null,
chartName: null,
tooltip: null,
legend: null,
};
}

setText(callback) {
this.customConfig.name = callback;
}
getText() {
if (this.customConfig.name && typeof this.customConfig.name === 'function') {
return this.customConfig.name;
}
return (datum) => `<div style="font-size:10px; margin-left:${this.translateXOfDiagram + 5}px;margin-top:-8px;white-space:nowrap;color:#1B1B1B">
<img src='../assets/1@2x.png' style="width:10px; height:10px; display:inline" />
<div style="display:inline">${datum.value.toFixed(2)}亿</div>
</div>`
}

/**
* 根据当前的配置,动态生成Option
*/
getOption() {
return {
// 组件初始化后默认展示的日期;如果不设置该配置项,则默认展示第一个日期
defaultDate: '',
// 是否反向绘图(Y轴从小到大)
reverse: false,
// 动画相关的设置
animation: {
show: true,
// 事件回调函数
action: {
// 页面框架初始化完成
onInitialized() {
console.log('页面初始化完成');
},
// 动画播放完成的回调
onFinished() {
console.log('动画播放完了');
},
// 每次步进的回调
onStep(date, data, container, divContainer, scale) {

},
},
},

// 播放相关的设置
play: {
// 自动播放的间隔时间
intervalTime: 1000,
},

// 绘图区域
grid: {
// 图表左右上下间距,注意,left_margin不包括左侧的label,修改数值较小会导致左侧label不显示
margin: [20, 80, 20, 20],
background: {
show: true,
style: {
// 画布的背景色
// fill: '#F00',
// background: '#FFF'
// background: 'url(https://t.zhouchangju.com/test/images/travel.jpeg)',
// backgroundColor: 'rgba(255, 0, 0, 0.3)',
// opacity: 0.2
},
},
},

// 图形元素相关的设置
series: {
// 柱子的标题,一般显示在柱子左侧
line: this.getLine(),
// 阴影区域
area: {
show: false,
style: {
fill: '#597FFF',
// 设置为0就没有阴影了
opacity: 0,
// 阴影的颜色,键名对应数据中的name
color: {
// 同花顺: '#B3A6F2',
// 东方财富: '#FE9D9D',
},
},
formatter(data) {
if (data.name.length > 3) {
return `${data.name.substr(0, 3)}...`;
}
return data.name;
},
},
axisPointer: {
show: true,
line: {
style: {
fill: '#597FFF',
opacity: 1,
stroke: '#597FFF',
strokeWidth: 1,
// 虚线设置
strokeDasharray: '2,2',
// 线上面的菱形的宽度
pointerWidth: 100,
pointerStroke: '#FFFFFF',
transform: `translate(${this.translateXOfDiagram}px, 0px)`,
},
},
pointer: {
style: {
fill: '#1B1B1B',
opacity: 1,
stroke: '#FFF',
strokeWidth: 2,
radius: 3,
transform: `translate(${this.translateXOfDiagram}px, 0px)`,
},
},
},
// 终点标记
endPointer: {
show: true,
style: {
fill: '#1B1B1B',
opacity: 1,
stroke: '#FFF',
strokeWidth: 2,
radius: 3,
},
collision: {
// 两个文本的纵向最小空隙(低于这个值则认为存在文本重叠)
minGap: 5,
// 当文本出现重叠时,执行碰撞检测的最大次数
maxTimes: 10,
},
// 文本格式化
// formatter(datum) {
// return `<div style="font-size:10px; margin-left:${this.translateXOfDiagram + 5}px;margin-top:-8px;white-space:nowrap;color:#1B1B1B">
// <img src='../assets/1@2x.png' style="width:10px; height:10px; display:inline" />
// <div style="display:inline">${datum.value.toFixed(2)}亿</div>
// </div>`
// },
formatter: this.getText(),
},
},
// 坐标轴
axis: [
{
// Y轴,为了避免Y轴的0刻度线把X轴盖掉了,因此先画Y轴,后画X轴
show: true,
position: 'left',
// 坐标轴的x、y偏移量,用于微调坐标文字的位置
offset: [0, 0],
// 坐标轴的范围,默认是按照原始数据的最大最小进行设置;如果设置为null,则采用默认值
// domain: [0, 200],
max: this.maxValue,
min: 0,
// 显示几个刻度/数值
tickNumber: 5,
tickFormat(d) {
if (d !== 0) {
return `${d}亿`;
}
return d;
},
// 刻度与数值之间的间隙
tickPadding: 5,
style: {
// 刻度线和刻度上的文本的颜色
// color: '#70717D',
color: 'rgba(0,0,0,0.32)',
opacity: 1,
// stroke: '#00F',
// fontWeight: 'bold',
fontSize: 10,
textAnchor: 'start',
// 刻度线的颜色
tickColor: '#DADADA',
// 刻度线(网格线)的宽度
tickWidth: 1,
// 虚线设置
strokeDasharray: '2,2',
// 刻度数值的偏移量
transform: 'translate(0px, -6px)',
},
// 轴线
domain: {
style: {
color: '#F00',
stroke: '#F00',
strokeWidth: 1,
// 隐藏X轴线
opacity: 0,
},
},
},
{
show: true,
// 坐标轴(axis)位于直角坐标系(grid)中的位置, 可选类型有 bottom | left | top | right
position: 'bottom',
// 坐标轴的x、y偏移量,用于微调坐标文字的位置
offset: [this.translateXOfDiagram, 0],
// 显示几个刻度/数值(大致数值,不一定精准)
tickNumber: 6,
tickValues: this.xTicks,
// 刻度数值的格式自定义
tickFormat(d) {
return d;
},
// 刻度与数值之间的间隙
tickPadding: 5,
style: {
// 刻度线和刻度上的文本的颜色
color: 'rgba(0,0,0,0.32)',
opacity: 0,
fontSize: 10,
textAnchor: 'middle',
// 刻度线(网格线)的颜色
tickColor: '#ECECF7',
// 刻度线(网格线)的宽度
tickWidth: 1,
// 虚线设置
strokeDasharray: '2,2',
},
// 轴线
domain: {
style: {
// color: '#0F0',
stroke: '#EBEBEB',
strokeWidth: 2,
// 隐藏X轴线
opacity: 1,
},
},
},
],
// 页面上的自定义文本,比如坐标轴的单位、标题、右下角随着柱子变动的日期等等
label: [
// 右下角的日期
{
show: false,
style: {
color: '#FEBB86',
opacity: 1,
fontWeight: 'bold',
fontSize: '20px',
position: 'absolute',
right: '10px',
bottom: '0px',
textAlign: 'right',
pointerEvents: 'none',
},
formatter(data, date) {
return date;
},
},
// Y轴单位
{
show: false,
style: {
color: '#70717D',
opacity: 1,
// fontWeight: 'bold',
fontSize: '10px',
position: 'absolute',
left: '5px',
top: '5px',
textAlign: 'left',
},
text: '单位(元)',
},
],
};
}
}

export default LineTheme;