如何用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
    }]
  };
};

 最後,如果覺得這篇文章對你有幫助,別忘了點贊哦

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