搜索文章

输入关键词开始搜索

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

程序设计#程序设计#数据可视化

缘由

最近在做范式组件,我写了动态柱状图和折线图,在考虑如何给使用者开放配置项的时候,考虑到如果全部配置都给用户,我感觉他们的理解成本会比较高,因此我想将配置封装为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;