zoukankan      html  css  js  c++  java
  • 基于 ECharts 封装甘特图并实现自动滚屏

    项目中需要用到甘特图组件,之前的图表一直基于 EChart 开发,但 EChart 本身没有甘特图组件,需要自行封装

    经过一番鏖战,终于完成了...

    我在工程中参考 v-chart 封装了一套图表组件,所以这里只介绍甘特图组件的实现,图表的初始化、数据更新、自适应等不在这里介绍

     

    一、约定数据格式

    EChart 本身没有甘特图,但可以通过 EChart 提供的“自定义”方法 type: 'custom' 开发

    const option = {
      series: [{
        type: 'custom',
        renderItem: (params, api) => {
          // do sth
        },
        data,
      }]
    }

    这里的 data 就是数据集,它是一个二维数组,主要需要两个参数:

    name: 名称,可以在 legend 和 tooltip 中展示

    value:参数集合,自定义的图表时需要的参数都可以放到这个数组里

    如果需要其它的配置,也可以按照 ECharts 的 series 结构添加别的字段

    我自定义的数据结构是这样的:

    {
      name,
      itemStyle: {
        normal: {
          color: color || defaultColor,
        },
      },
      // value 为约定写法,依序为“类目对应的索引”、“状态类型”、“状态名称”、“开始时间”、“结束时间”
      value: [
        index,
        type,
        name,
        new Date(start).getTime(),
        new Date(end || Date.now()).getTime(),
      ],
    }

    注意:series.data 中的元素需要根据状态划分,不能根据类目(Y轴)划分,这样才能保证图例 legend 的正常显示

    最终的 data 结构如图:

    自定义的核心是 renderItem 函数,这个函数的本质就是:将 data 中的参数 value 处理之后,映射到对应的坐标轴上,具体处理参数的逻辑完全自定义

    甘特图就需要计算出各个数据块的高度和宽度,然后映射到对应的类目轴(Y轴)和时间轴(X轴)上

    由于甘特图会用到时间轴(X轴),所以定义的 value 中需要开始时间和结束时间的时间戳

    为了区分该数据属于类目轴(Y轴)的哪一条类目,还需要对应类目的索引 index

    如果还有其它的需要,比如自定义 tooltip,还可以在 value 中添加其它的参数

    但一定要约定好参数的顺序,因为 renderItem 函数是根据 value 的索引去取对应的参数

     

    二、处理数据 Series

    // 处理数据
    function getGantSeries(args) {
      const { innerRows, columns } = args
      const baseItem = {
        type: 'custom',
        renderItem: (params, api) => renderGanttItem(params, api),
        dimensions: columns,
      };
      return innerRows.map(row => {
        return {
          ...baseItem,
          name: row[0].name,
          data: row,
        };
      });
    }

    当 type 指定为 'custom' 的时候,series 的元素可以添加 dimensions 字段,用来定义每个维度的信息

    处理数据的核心是 renderItem 方法,该方法提供了 paramsapi 两个参数,最后需要返回对应的图形元素信息

    const DIM_CATEGORY_INDEX = 0; // value 中类目标识的索引
    const DIM_CATEGORY_NAME_INDEX = 1; // value 中对应元素类型的索引
    const DIM_START_TIME_INDEX = 3; // value 中开始时间的索引
    const DIM_END_TIME_INDEX = 4; // value 中结束时间的索引
    
    const HEIGHT_RATIO = 0.6; // 甘特图矩形元素高度缩放比例
    const CATEGORY_NAME_PADDING_WIDTH = 20; // 在甘特图矩形元素上展示文字时,左右 padding 的最小长度
    
    /**
     * 计算元素位置及宽高
     * 如果元素超出了当前坐标系的包围盒,则剪裁这个元素
     * 如果元素完全被剪掉,会返回 undefined
     */
    function clipRectByRect(params, rect) {
      return echarts.graphic.clipRectByRect(rect, {
        x: params.coordSys.x,
        y: params.coordSys.y,
         params.coordSys.width,
        height: params.coordSys.height,
      });
    }
    
    // 渲染甘特图元素
    function renderGanttItem(params, api, extra) {
      const { isShowText, barMaxHeight, barHeight } = extra;
      // 使用 api.value(index) 取出当前 dataItem 的维度
      const categoryIndex = api.value(DIM_CATEGORY_INDEX);
      // 使用 api.coord(...) 将数值在当前坐标系中转换成为屏幕上的点的像素值
      const startPoint = api.coord([api.value(DIM_START_TIME_INDEX), categoryIndex]);
      const endPoint = api.coord([api.value(DIM_END_TIME_INDEX), categoryIndex]);
      // 使用 api.size(...) 取得坐标系上一段数值范围对应的长度
      const baseHeight = Math.min(api.size([0, 1])[1], barMaxHeight);
      const height = barHeight * HEIGHT_RATIO || baseHeight * HEIGHT_RATIO;
      const width = endPoint[0] - startPoint[0];
      const x = startPoint[0];
      const y = startPoint[1] - height / 2;
    
      // 处理类目名,用于在图形上展示
      const categoryName = api.value(DIM_CATEGORY_NAME_INDEX) + '';
      const categoryNameWidth = echarts.format.getTextRect(categoryName).width;
      const text = width > categoryNameWidth + CATEGORY_NAME_PADDING_WIDTH ? categoryName : '';
    
      const rectNormal = clipRectByRect(params, { x, y, width, height });
      const rectText = clipRectByRect(params, { x, y, width, height });
    
      return {
        type: 'group',
        children: [
          {
            // 图形元素形状: 'rect', circle', 'sector', 'polygon'
            type: 'rect',
            ignore: !rectNormal, // 是否忽略(忽略即不渲染)
            shape: rectNormal,
            // 映射 option 中 itemStyle 样式
            style: api.style(),
          },
          {
            // 在图形上展示类目名
            type: 'rect',
            ignore: !isShowText || !rectText,
            shape: rectText,
            style: api.style({
              fill: 'transparent',
              stroke: 'transparent',
              text: text,
              textFill: '#fff',
            }),
          },
        ],
      };
    }

    上面是我用的 renderItem 方法全貌,主要是使用 api 提供的工具函数计算出元素的视觉宽高

    再使用 echarts 提供的 graphic.clipRectByRect 方法,结合参数 params 提供的坐标系信息,截取出元素的图形信息

     

    三、自定义 tooltip

    如果数据格式正确,到这里已经能渲染出甘特图了,但一个图表还需要其它的细节,比如 tooltip 的自定义

    在 renderItem 中有一个字段 encode 可以用来自定义 tooltip,但只能定义展示的文字

    具体的 tooltip 排版和图例颜色(特别是渐变色)无法通过 encode 实现自定义,最终还是得通过 formatter 函数

    formatter: params => {
      const { value = [], marker, name, color } = params;
      const axis = this.columns; // 类目轴(Y轴)数据
      // 删除空标题
      let str = '';
      isArray(axis[value[0]]) && axis[value[0]].map(item => {
        item && (str += `${item}/`);
      });
      str = str.substr(0, str.length - 1);
      // 颜色为对象时,为渐变颜色,需要手动拼接
      let mark = marker;
      if (isObject(color)) {
        const { colorStops = [] } = color;
        const endColor = colorStops[0] && colorStops[0].color;
        const startColor = colorStops[1] && colorStops[1].color;
        const colorStr = `background-image: linear-gradient(90deg, ${startColor}, ${endColor});`;
        mark = `
          <span style="
            display:inline-block;
            margin-right:5px;
            border-radius:10px;
            10px;
            height:10px;
            ${colorStr}
          "></span>`;
      }
      // 计算时长
      const startTime = moment(value[3]);
      const endTime = moment(value[4]);
      let unit = '小时';
      let duration = endTime.diff(startTime, 'hours');
      return `
        <div>${str}</div>
        <div>${mark}${name}: ${duration}${unit}</div>
        <div>开始时间:${startTime.format('YYYY-MM-DD HH:mm')}</div>
        <div>结束时间:${endTime.format('YYYY-MM-DD HH:mm')}</div>
      `;
    },
    },

     

    四、自动滚屏

    如果甘特图的数据过多,堆在一屏展示就会显得很窄,这时候可以结合 dataZoom 实现滚屏

    首先需要在组件中引入 dataZoom

    import 'echarts/lib/component/dataZoom';
    
    // 配置项
    const option = {
      ...,
      dataZoom: {
        type: 'slider',
        id: 'insideY01',
        yAxisIndex: 0,
        zoomLock: true,
        bottom: -10,
        startValue: this.dataZoomStartVal,
        endValue: this.dataZoomEndVal,
        handleSize: 0,
        borderColor: 'transparent',
        backgroundColor: 'transparent',
        fillerColor: 'transparent',
        showDetail: false,
      },
      {
        type: 'inside',
        id: 'insideY02',
        yAxisIndex: 0,
        startValue: this.dataZoomStartVal,
        endValue: this.dataZoomEndVal,
        zoomOnMouseWheel: false,
        moveOnMouseMove: true,
        moveOnMouseWheel: true,
      }
    }

    然后需要设定甘特图每一行的高度 barHeight,同时获取甘特图组件的高度

    通过这两个高度计算出每屏可以展示的甘特图数据的数量 pageSize

    const GANT_ITEM_HEIGHT = 56;
    const height = this.$refs.chartGantRef.$el.clientHeight;
    this.pageSize = Math.floor(height / GANT_ITEM_HEIGHT);
    // 设置 dataZoom 的起点
    this.dataZoomStartVal = 0;
    this.dataZoomEndVal = this.pageSize - 1;

    然后通过定时器派发事件,修改 dataZoom 的 startValue 和 endValue,实现自动滚屏的效果

    const Timer = null;
    dataZoomAutoScoll() {
      Timer = setInterval(() => {
        const max = this.total - 1;
        if (
          this.dataZoomEndVal > max ||
          this.dataZoomStartVal > max - this.pageSize
        ) {
          this.dataZoomStartVal = 0;
          this.dataZoomEndVal = this.pageSize - 1;
        } else {
          this.dataZoomStartVal += 1;
          this.dataZoomEndVal += 1;
        }
        echarts.dispatchAction({
          type: 'dataZoom',
          dataZoomIndex: 0,
          startValue: this.dataZoomStartVal,
          endValue: this.dataZoomEndVal
        });
      }, 2000);
    },

     

  • 相关阅读:
    Codeforces Round #307 (Div. 2)E. GukiZ and GukiZiana
    bzoj2957: 楼房重建,分块
    分块入门。
    poj3690 Constellations
    Codeforces Round #451 (Div. 2)F. Restoring the Expression 字符串hash
    Codeforces Round #456 (Div. 2)D. Fishes
    Codeforces Round #450 (Div. 2)D. Unusual Sequences
    快速排序+分治法
    哈夫曼编码课程设计+最小优先对列建树。
    浅谈差分约束系统
  • 原文地址:https://www.cnblogs.com/wisewrong/p/11960267.html
Copyright © 2011-2022 走看看