【实现自己的可视化引擎08】条形图

588 阅读3分钟

【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图

前言

条形图使用水平的柱子显示类别之间的数值比较。其中一个轴表示需要对比的分类维度,另一个轴代表相应的数值。效果图如图所示:

组件使用代码:

render() {
    return (
      <Bar
        className="chart"
        data={[{
          label: '第一季度',
          data: [{
            key: 'a',
            value: 80
          }, {
            key: 'b',
            value: 100,
          }, {
            key: 'c',
            value: 120
          }]
        }, {
          label: '第二季度',
          data: [{
            key: 'a',
            value: 100,
          }, {
            key: 'b',
            value: 80,
          }, {
            key: 'c',
            value: 100
          }]
        }, {
          label: '第三季度',
          data: [{
            key: 'a',
            value: 120,
          }, {
            key: 'b',
            value: 30,
          }, {
            key: 'c',
            value: 90
          }]
        }, {
          label: '第四季度',
          data: [{
            key: 'a',
            value: 40,
          }, {
            key: 'b',
            value: 60,
          }, {
            key: 'c',
            value: 100
          }]
        }]}
        style={{
          color: '#666666',
          axisColor: '#666666',
          fontColor: '#666666',
          colors: {
            a: '#4169E1',
            b: '#43CD80',
            c: '#FFC0CB',
          },
          xAxisAttr: 'height',
          yAxisAttr: 'weight',
        }}
      />)
 }

条形图层

从效果图上可以看出,条形图由不同的长方形组合而成。其算法如下:

  1. 计算数据的最大值,并计算出单位数值的单位长度:xStep=width/max;
  2. 遍历数据依据数据值value计算长方形的长度width;
  3. 绘制长方形,并加入图层。 根据上述流程,编写代码:
make() {
    this.childs.splice(0, this.childs.length);
    if (this.data.length === 0) {
      return;
    }
    let max = -9999999999999;
    let min = 9999999999999;
    // 计算所有数据的最大值与最小值
    for (let i = 0; i < this.data.length; i++) {
      for (let j = 0; j < this.data[i].data.length; j++) {
        if (this.data[i].data[j].value > max) {
          max = this.data[i].data[j].value;
        }
        if (this.data[i].data[j].value < min) {
          min = this.data[i].data[j].value;
        }
      }
    }
    // 计算
    let xStep = this.width / max;
    let yStep = this.height / this.data.length;
    // 遍历数据,绘制柱形(长方形)
    for(let i = 0; i < this.data.length; i++) {
      // 长方形高度
      let rectHeight = yStep * 0.8 / this.data[i].data.length;
      for(let j = 0; j < this.data[i].data.length; j++) {
          // 长方形宽度
        let rectWidth = this.data[i].data[j].value * xStep;
        console.log(this.position.x);
        const rect = new Rectangle(this.canvas, {
          width: rectWidth,
          height: rectHeight,
          type: Rectangle.TYPE.FILL,
          color: this.colors[this.data[i].data[j].key],
          position: new Point(this.position.x + rectWidth / 2, this.position.y + (i + 0.1) * yStep + j * rectHeight),
        });
        console.log(rect);
        this.addChild(rect);
      }
      let txt = new Text(this.canvas, {
        text: this.data[i].label,
        font: this.fontFamily,
        size: this.fontSize,
        color: this.fontColor,
      });
      txt.setPosition(new Point(0, this.position.y + (i + 0.5) * yStep - txt.height))
      this.addChild(txt);
    }
    this.onMaked && this.onMaked(this, {
      max,
      xStep,
      yStep
    });
  }

React 封装

React封装需要DOM的挂载完成,所以我们在生命周期componentDidMount函数中构建我们的图层。代码如下:

componentDidMount () {
    const { data = [], style } = this.props;
    this.canvas = new Canvas({
      ele: this.ref.current,
      canAction: false,
    });
    const xFontSize = Number(style.xFontSize || 20);
    const yFontSize = Number(style.yFontSize || 20);
    let width = 0;
    for (let i = 0;i < data.length; i++) {
      const txt = new Text(this.canvas, {
        text: data[i].label,
        size: xFontSize,
        color: style.axisColor || '#999999',
        font: style.fontFamily || 'PingFang SC',
      });
      if (width < txt.width) {
        width = txt.width;
      }
    }
    // 坐标轴标准配置
    this.axisLayer = new AxisLayer(this.canvas, {
      yAxisType: AxisLayer.AxisType.LABEL, // y轴为数值型
      xAxisType: AxisLayer.AxisType.NUMBER,  // x轴时间为字符型
      xAxisGraduations: style.xAxis || 5,   // 网格5列
      yAxisGraduations: style.yAxis || 5,   // 网格5行
      xAxisPosition: AxisLayer.AxisPosition.BLOCK,  // X轴坐标不计算
      yAxisPosition: AxisLayer.AxisPosition.BLOCK,  // Y轴坐标计算
      yAxisLabels: [],
      position: new Point(width + 10, 0),
      xAxisRender: (value) => {
        const enob = style.yEnob || 2;
        return {
          text: Number(value).toFixed(enob),
          size: xFontSize,
          color: style.axisColor || '#999999',
          font: style.fontFamily || 'PingFang SC',
        };
      },
      color: style.color,
    });
    // 条形图层
    this.barLayer = new BarLayer(this.canvas, {
      width: (this.canvas.width - yFontSize * 4 * this.canvas.ratio) * 0.9, // 预留20%的空白空间
      height: (this.canvas.height - xFontSize * this.canvas.ratio) * 0.8, // 预留20%的空白空间
      colors: style.colors,
      fontColor: style.color,
      fontSize: yFontSize,
      fontFamily: style.fontFamily || 'PingFang SC',
      position: new Point(
        width + 10,
        xFontSize * this.canvas.ratio * 0.9 + this.canvas.height * 0.1
      )
    }, data);
    this.barLayer.onMaked = (layer, option) => {
      const { max, xStep } = option;
      // 计算坐标系坐标数值
      const xAxisMax = max + (this.canvas.width - yFontSize * 4 * this.canvas.ratio) * 0.1 / xStep;
      // 设置坐标轴的最大最小值
      this.axisLayer.xAxisMin = 0;
      this.axisLayer.xAxisMax = xAxisMax;
      this.axisLayer.make();
    }
    this.canvas.addChild(this.axisLayer, this.barLayer);
    this.barLayer.make();
    this.canvas.paint();
  }