一个关于组件配置项的糟糕设计案例
缘由
最近在做范式组件,我写了动态柱状图和折线图,在考虑如何给使用者开放配置项的时候,考虑到如果全部配置都给用户,我感觉他们的理解成本会比较高,因此我想将配置封装为Theme类,只开放设计规范中允许的配置项给用户。
形式
默认的配置格式
组件默认的配置是类似这样的:
const option = {
animation: {
show: true,
// 事件回调函数
action: {
// 页面框架初始化完成
onInitialized() {
console.log('页面初始化完成');
},
// 动画播放完成的回调
onFinished() {
console.log('动画播放完了');
},
// 每次步进的回调
onStep(date, data, container, divContainer, scale) {},
},
},
// 播放相关的设置
play: {},
// 绘图区域
grid: {},
// 图形元素相关的设置
series: {},
// 坐标轴
axis: [],
};
Theme设计后的格式
我将配置用类包了一层,这是基类:
class Theme {
constructor(option) {
this.option = option;
}
}
export default Theme;
这是具体实现类:
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;