如何用Echart正确的绘制甘特图(适用于React)

前言

之前实现作业车间调度问题甘特图绘制,搜索过如何用Echart绘制甘特图,发现绘制方法都一毛一样,都是通过叠加series来实现,但这样绘制出来的甘特图,不仅够丑,而且数据也难以描述,如果要在react这种mvvm中绘制的话,也没法对甘特图做数据管理,所以经过自己的思考,发现可以有更好的绘制甘特图的方法。首先先看绘制出来的甘特图效果(项目GitHub地址:https://github.com/sundial-dreams/BitMESClient

甘特图区域可控,可以通过看Echart配置项手册(https://www.echartsjs.com/option.html#title)改变外观、座标轴等

甘特图绘制思路

要绘制甘特图,首先想想我们需要定义怎样的数据格式,就以作业车间调度问题为例,我们的元素有:工件、机器、工序,对于机器里加工的每一个工件的每一道工序都有一个起始时间和终止时间,所以我们的数据格式定义如下:

// 工件编号,工序编号,机器编号,起始时间,终止时间
const mockData = [
  { workpiece: 0, process: 0, machine: 0, startTime: 2, endTime: 5 },
  { workpiece: 0, process: 1, machine: 1, startTime: 5, endTime: 7 },
  { workpiece: 0, process: 2, machine: 2, startTime: 7, endTime: 9 },
  { workpiece: 1, process: 0, machine: 0, startTime: 0, endTime: 2 },
  { workpiece: 1, process: 1, machine: 2, startTime: 2, endTime: 3 },
  { workpiece: 1, process: 2, machine: 1, startTime: 7, endTime: 11 },
  { workpiece: 2, process: 0, machine: 1, startTime: 0, endTime: 4 },
  { workpiece: 2, process: 1, machine: 2, startTime: 4, endTime: 7 }
];

定义好了数据格式,其实不用Echart,用原生的HTML/CSS/JS也能很轻松的绘制出甘特图,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Title</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<style>
    #gantt-chart {
        width: 60rem;
        height: 40rem;
    }
    

    .chart {
        position: relative;
        display: inline-block;
        width: 40rem;
        height: 40rem;
        vertical-align: top;
    }

    .block {
        position: absolute;
        display: inline-block;
    }

    .machine {
        display: inline-block;
        width: 8rem;
        color: gray;
        font-weight: bold;
        vertical-align: top;

    }

    .machine > .label {

    }

    .legend {
        width: 8rem;
        color: gray;
        font-weight: bold;
        display: inline-block;
        vertical-align: top;

    }

    .legend-block {
        width: 8rem;
    }

    .color {
        width: 1rem;
        height: 1rem;
        display: inline-block;
    }

    .text {
        width: 4rem;
        display: inline-block;
        color: gray;
        font-weight: bold;
    }
</style>
<body>
<div id="gantt-chart">
    <div class="machine">

    </div>
    <div class="chart">

    </div>
    <div class="legend">

    </div>
</div>
</body>
<script>
  void function () {
    function createEle (tag, options) {
      var el = document.createElement(tag);
      for (var k in options) {
        if (options.hasOwnProperty(k)) {
          el[k] = options[k]
        }
      }
      return el
    }

    function setStyle (el, cssObject) {
      for (var k in cssObject) {
        if (cssObject.hasOwnProperty(k)) {
          el.style[k] = cssObject[k]
        }
      }
    }

    var chart = document.querySelector(".chart");
    var legend = document.querySelector(".legend");
    var machine = document.querySelector(".machine");
    
    // 以一个机器为纵座标纵,
    var data = {
      0: [{ 'job': 0, 'process': 0, 'startTime': 2, 'endTime': 5 },
        { 'job': 1, 'process': 0, 'startTime': 0, 'endTime': 2 }],
      1: [{ 'job': 0, 'process': 1, 'startTime': 5, 'endTime': 7 },
        { 'job': 1, 'process': 2, 'startTime': 7, 'endTime': 11 },
        { 'job': 2, 'process': 0, 'startTime': 0, 'endTime': 4 }],
      2: [{ 'job': 0, 'process': 2, 'startTime': 7, 'endTime': 9 },
        { 'job': 1, 'process': 1, 'startTime': 2, 'endTime': 3 },
        { 'job': 2, 'process': 1, 'startTime': 4, 'endTime': 7 }]
    };
    var colors = [
      "#4bb5ef",
      "#bb3dff",
      "#ff4c5f",
      "#fffa51",
      "#4dff2b",
      "#ff9409",
      "#ff78b7",
      "#74ffff",
    ];
    var height = 3;
    var width = 3;
    var jobLegend = {};
    for (var k in data) {
      var label = createEle("div", { innerText: "machine" + k, className: "label" });
      setStyle(label, { height: height + "rem", lineHeight: height + "rem" });
      machine.appendChild(label);
      for (var i = 0; i < data[k].length; i ++) {
        var value = data[k][i];
        var block = createEle("div", { className: "block" }); // 绘制甘特图的每一个块
        setStyle(block, {
          height: height + "rem",
          width: (value.endTime - value.startTime) * width + "rem",
          left: value.startTime * width + "rem",
          top: k * height + "rem",
          backgroundColor: colors[value.job]
        });
        jobLegend[value.job] = colors[value.job];
        chart.appendChild(block);
      }
    }
    for (k in jobLegend) {
      var legendBlock = "<div class=\"legend-block\" style='height: " + height + "rem;line-height: " + height + "rem'>\n" +
        "                     <div class=\"color\" style='background: " + jobLegend[k] + "'></div>\n" +
        "                     <div class=\"text\">job" + k + "</div>\n" +
        "                </div>";
      legend.innerHTML += legendBlock;
    }

  }();
</script>
</html>

效果如下:(不要介意丑,意思到了就行了)

好了,开始正题,用Echart来绘制甘特图,打开Echart官方提供的例子,找到这个例子,我们甘特图的原型就是它 

 我们要做的是很简单,给这个例子传一个合适的data即可

代码里面的,options、函数啥的都不用改,只需要给他传一个合适的data,代码如下:


// 工件编号,工序编号,机器编号,起始时间,终止时间
const dataSource = [
  { workpiece: 0, process: 0, machine: 0, startTime: 2, endTime: 5 },
  { workpiece: 0, process: 1, machine: 1, startTime: 5, endTime: 7 },
  { workpiece: 0, process: 2, machine: 2, startTime: 7, endTime: 9 },
  { workpiece: 1, process: 0, machine: 0, startTime: 0, endTime: 2 },
  { workpiece: 1, process: 1, machine: 2, startTime: 2, endTime: 3 },
  { workpiece: 1, process: 2, machine: 1, startTime: 7, endTime: 11 },
  { workpiece: 2, process: 0, machine: 1, startTime: 0, endTime: 4 },
  { workpiece: 2, process: 1, machine: 2, startTime: 4, endTime: 7 }
];
// 一堆颜色集,画每一个图块需要
const Colors = [
    "#BB86D7",
    "#FFAFF0",
    "#5BC3EB",
    "#B5E2FA",
    "#A9D5C3",
    "#73DCFF",
    "#DCB0C6",
    "#F9CDA5",
    "#FBE6D2",
    "#B5E2FA",
    "#B8FFCE",
    "#FFE4E2",
    "#F7AF9D",
    "#BBF9B4",
    "#FFEE93",
    "#2CEAA3",
    "#ECC2C2",
    "#C8CACA"
];
const { keys } = Object;

// 以机器为纵座标轴绘制甘特图(这里还可以以工件为座标轴)

let machines = dataSource.reduce((acc, cur) => {
    acc[cur.machine] ? acc[cur.machine].push(cur) : acc[cur.machine] = [cur];
    return acc;
}, {}); 

let workpieces = dataSource.reduce((acc, cur) => {
    acc[cur.workpiece] ? acc[cur.workpiece].push(cur) : acc[cur.workpiece] = [cur];
    return acc;
}, {});

let workpieceColors = {}; // 颜色映射
keys(workpieces).forEach((v, i) => workpieceColors[v] = Colors[i]);
let data = [];
// 关键
keys(machines).forEach((k) => {
    machines[k].forEach(v => {
      let duration = v.endTime - v.startTime;
      data.push({
        name: v.workpiece, // 图块名称
        value: [k, v.startTime, v.endTime, duration], // 名称, 起始时间, 终止时间,持续时间
        itemStyle: {
          normal: {
            color: workpieceColors[v.workpiece] // 图块颜色
          }
        }
      });
    });
 });


function renderItem(params, api) {
    var categoryIndex = api.value(0);
    var start = api.coord([api.value(1), categoryIndex]);
    var end = api.coord([api.value(2), categoryIndex]);
    var height = api.size([0, 1])[1] * 0.6;

    var rectShape = echarts.graphic.clipRectByRect({
        x: start[0],
        y: start[1] - height / 2,
        width: end[0] - start[0],
        height: height
    }, {
        x: params.coordSys.x,
        y: params.coordSys.y,
        width: params.coordSys.width,
        height: params.coordSys.height
    });

    return rectShape && {
        type: 'rect',
        shape: rectShape,
        style: api.style()
    };
}


option = {
    tooltip: {
        formatter: function (params) {
            return params.marker + params.name + ': ' + params.value[3] + ' ms';
        }
    },
    title: {
        text: 'Profile',
        left: 'center'
    },
    grid: {
        height:300
    },
    xAxis: {
        min: startTime,
        scale: true,
    },
    yAxis: {
        data: keys(machines) // 机器编号为纵座标轴
    },
    series: [{
        type: 'custom',
        renderItem: renderItem,
        itemStyle: {
            normal: {
                opacity: 0.8
            }
        },
        encode: {
            x: [1, 2],
            y: 0
        },
        data: data
    }]
};

运行结果:

然后只需要按照配置项手册,改改相应的配置就是一个非常完整的甘特图了,下面附上我最开始那张截图里面的甘特图options(代码仅供参考,我的是在React中绘制的甘特图)

import echarts from 'echarts';
import Colors from '../constants/colors';

// export const mockData = [
//   { workpiece: 0, process: 0, machine: 0, startTime: 2, endTime: 5 },
//   { workpiece: 0, process: 1, machine: 1, startTime: 5, endTime: 7 },
//   { workpiece: 0, process: 2, machine: 2, startTime: 7, endTime: 9 },
//   { workpiece: 1, process: 0, machine: 0, startTime: 0, endTime: 2 },
//   { workpiece: 1, process: 1, machine: 2, startTime: 2, endTime: 3 },
//   { workpiece: 1, process: 2, machine: 1, startTime: 7, endTime: 11 },
//   { workpiece: 2, process: 0, machine: 1, startTime: 0, endTime: 4 },
//   { workpiece: 2, process: 1, machine: 2, startTime: 4, endTime: 7 }
// ];

export const Mode = {
  MACHINE: 'machine',
  WORKPIECE: 'workpiece'
};

function renderItem (params, api) {
  let categoryIndex = api.value(0);
  let start = api.coord([api.value(1), categoryIndex]);
  let end = api.coord([api.value(2), categoryIndex]);
  let height = api.size([0, 1])[1] * 0.6;

  let rectShape = echarts['graphic'].clipRectByRect({
    x: start[0],
    y: start[1] - height / 2,
    width: end[0] - start[0],
    height: height
  }, {
    x: params.coordSys.x,
    y: params.coordSys.y,
    width: params.coordSys.width,
    height: params.coordSys.height
  });

  return rectShape && {
    type: 'rect',
    shape: rectShape,
    style: api.style()
  };
}

const { keys } = Object;
export const makeGanttChartOption = ({ dataSource, mode = Mode.MACHINE, showGrid = false }) => {
  let data = [];

  let machines = dataSource.reduce((acc, cur) => {
    acc[cur.machine] ? acc[cur.machine].push(cur) : acc[cur.machine] = [cur];
    return acc;
  }, {});

  let workpieces = dataSource.reduce((acc, cur) => {
    acc[cur.workpiece] ? acc[cur.workpiece].push(cur) : acc[cur.workpiece] = [cur];
    return acc;
  }, {});

  let isByMachine = mode === Mode.MACHINE;
  let isByWorkpiece = mode === Mode.WORKPIECE;

  let machineColors = {};
  let workpieceColors = {};
  keys(workpieces).forEach((v, i) => workpieceColors[v] = Colors.light[i]);
  keys(machines).forEach((v, i) => machineColors[v] = Colors.light[i]);

  isByMachine && keys(machines).forEach((k) => {
    machines[k].forEach(v => {
      let duration = v.endTime - v.startTime;
      data.push({
        name: v.workpiece,
        value: [k, v.startTime, v.endTime, duration],
        itemStyle: {
          normal: {
            color: workpieceColors[v.workpiece]
          }
        }
      });
    });
  });

  isByWorkpiece && keys(workpieces).forEach(k => {
    workpieces[k].forEach(v => {
      let duration = v.endTime - v.startTime;
      data.push({
        name: v.machine,
        value: [k, v.startTime, v.endTime, duration],
        itemStyle: {
          normal: {
            color: machineColors[v.machine]
          }
        }
      });
    });
  });

  return {
    tooltip: {
      formatter: function(params) {
        return params.marker + params.name + ': ' + params.value[3] + ' h';
      }
    },
    dataZoom: [{
      type: 'slider',
      top: 60,
      height: 3,
      filterMode: 'weakFilter',
      textStyle: {
        color: 'gray'
      }

    },
      {
        type: 'inside',
        filterMode: 'weakFilter'
      },
      {
        type: 'slider',
        yAxisIndex: 0,
        zoomLock: true,
        width: 3,
        left: '10%',
        handleSize: 0,
        showDetail: false,
        filterMode: 'weakFilter',
        textStyle: {
          color: 'gray'
        }
      },
      {
        type: 'inside',
        id: 'insideY',
        yAxisIndex: 0,
        filterMode: 'weakFilter',
        zoomOnMouseWheel: true,
        moveOnMouseMove: true,
        moveOnMouseWheel: true
      }
    ],
    xAxis: {
      min: 0,
      scale: true,
      position: 'top',
      name: 'h',
      axisLine: {
        lineStyle: {
          color: 'gray'
        },
        symbol: ['none', 'arrow'],
        symbolSize: [5, 5],
        show: false
      },
      axisTick: {
        length: 3,
        lineStyle: {
          color: '#BB86D7',
          width: 1
        }
      },
      interval: 2,
      axisLabel: {
        formatter: function(val) {
          return Math.max(0, val);
        },
        color: 'gray'
      },
      splitLine: {
        show: showGrid
      }
    },
    yAxis: {
      data: keys(isByMachine ? machines : workpieces),
      axisTick: {
        length: 3,
        lineStyle: {
          color: '#BB86D7',
          width: 1
        }
      },
      splitLine: {
        show: showGrid
      },
      axisLabel: {
        formatter: (v, i) => {
          return v;
        },
        color: 'white',
        padding: [6, 6, 6, 6],
        backgroundColor: '#BB86D7',
        fontSize: '.7rem'
      },
      axisLine: {
        show: false
      }
    },

    series: [{
      type: 'custom',
      renderItem,
      itemStyle: {
        normal: {
          opacity: 0.8
        }
      },
      encode: {
        x: [1, 2],
        y: 0
      },
      data: data
    }]
  };
};

 最后,如果觉得这篇文章对你有帮助,别忘了点赞哦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章