react中利用highcharts绘制甘特图,如何绘制?遇到了哪些问题?

932 阅读5分钟

早年在项目上使用到过highcharts、echarts,这些图表类的插件为一些报表系统提供了高效支持。这两年从数据分析类项目转移到ToB类项目后,逐渐没再使用过这类插件。最近项目中,有新的需求,用户方指定使用highcharts,说是看到了highcharts中的图表美观大方,因此必须要在项目中引入。好吧,客户需求大于天,又开始重拾highcharts。

一、聊聊需求

产品:用户现在为每个项目,维护了n个阶段,需要用图表把这n个阶段的开始、结束信息展示出来。

开发:可以啊,用图表显示就行了,每个阶段的项目信息放在一个单元格内,格式化单元格即可。

产品:这种是可以实现,但用户想要看每个阶段的图形化信息,比如阶段1持续时间长,阶段2持续时间短,直观上一眼能看过去。

开发:那开始和结束时间都展示出来了,持续时间用户一目了然啊。

产品:用户想看图形化的,说白了,就是类似甘特图那种颜色分块的区域。

开发:甘特图?甘特图不是做项目进度控制的图形么,用户仅仅为了查看阶段信息,用不着甘特图吧。

产品:但客户的意思是,通过甘特图可以一眼看出来每个阶段,这样对进度把控了然一新。

开发:好吧,可以尝试。

开发:不对,你这个维护的是多个项目信息啊,怎么展示它的进度呢,甘特图是竖向展示的,数轴一般是任务阶段, 横轴是时间,进度条从上到下排列,直观上可以看到当前走到了哪个进度。

产品:额,我知道你说的意思,但客户现在想让纵轴展示项目名,即用表格形式展示项目信息,横轴是时间轴信息,阶段根据横轴来进行绘制。可以实现不?

开发:那我先查阅下文档吧。

就是上面描述的需求,总结下来就是,一个图表,左侧是列表信息,展示列名,右侧是时间信息,时间信息展示不同的阶段,阶段用不同颜色绘制,且根据横轴时间信息完整表示。如下图所示。

image.png

二、highcharts是否能满足

打开highcharts中文官网,找到甘特图区示例资料,很幸运的是,甘特图提供了上图描述的样式,基于官网的示例,开始在项目中实践。

2.1 react中引入highcharts

  • 通过npm安装,npm install highcharts --save
  • 页面中引入highcharts相关图表模块,这里注意单独引入highcharts并不能生效,需要直接引入highcharts-gantt模块文件。import Highcharts from "highcharts/highcharts-gantt.src

2.2 接口获取数据,组装数据

注意: 项目维护的阶段数据,有明确的早晚关系,如项目阶段1的结束时间是不能晚于阶段2的开始时间的。

  • series数据获取

由于数据是通过模块维护的,因此需要走接口获取实时数据。数据方面,需要和后端确认接口返回结构。该需求,针对每个项目,维护了n个阶段的时间信息,因此在接口中,必须包括每个阶段的数组对象结构,该数组中,存放的是n个阶段对应的对象信息,包括阶段名称、起始、结束时间。

接口获取,根据项目制定的请求标准去获取,前端需要包装数据。根据官网API文档介绍,series数据必须包括start、end、index属性,start、end将标记阶段信息如何绘制,因此需要将后端数据按照官网要求,二次包装。特别注意start、end值为时间的毫秒数,需转换。

image.png

const wrapChartData = (lists) => {
  if (lists.length) {
    const wrapSeriesData = lists.map((list, index) => {
      if (list.stageList.length) {
        // 阶段视图需要转换
        const data = list.stageList.map(stage => {
          return {
            projectName: list.projectName,
            color: renderColor(stage.stageName), // 每个阶段的颜色信息
            stageName: stage.stageName,
            start: stage.start ? moment(stage.start).toDate().getTime() : null, // 注意start和end的时间转换
            end: stage.end ? moment(stage.end).toDate().getTime() : null,
            y: index
          }
        })
        return {
          name: list.projectName,
          data
        }
      }
    })
    setSeries(wrapSeriesData)
  } else {
    setSeries([])
  }
}
  • x轴数据获取

x轴,展示的是时间信息,该时间信息根据用户设置的项目阶段时间来确定,大概思路如下。

  1. 寻找多个项目初始阶段的开始时间的最小值。
  2. 根据最小值,设置最小值年度的下一年度第一季度信息。(该需求为用户提出,需在图标上看到的是最小年度和第二年度第一季度信息)
  3. 设置highcharts中x轴的min、max属性,min属性表示x轴的起始时间、max展示x轴的最大时间。
  4. max的值决定了横轴能看到的数据,如果有比max大的值,可添加横向滚动条,拉动展示。

这里寻找最小值和下一年度的第一季度数据,不在这里详细描述了,大家可通过moment官网进行学习。

xAxis: [{
  min: minYear, 
  max: maxYear,
  grid: {
    borderWidth: 1, // 右侧表头边框宽度
    cellHeight: 35, // 右侧日期表头高度
  },
  labels: {
    formatter: function () {
      const text = moment(this.value).format('MM')
      return text;
    }
  },
  scrollbar: {
    enabled: true,
  }
},
{
   labels: {
     align: 'center',
     formatter: function () {
       return `${moment(this.value).format('YYYY')}`;
     }
   },
}],
  • y轴数据获取

y轴数据相对简单,展示的列表信息,这里的列表数据在二次构造中需要暴露,如在上图中我return了name属性,在y轴上可以根据属性去获取。

  1. 需要展示的信息,在data数据中返回,如name、code、date等。
  2. yaxis在grid中,根据categories设置,展示数据,注意多个项目展示多行数据,利用categories设置才会生效,使用label设置时,只有第一行数据生效。
yAxis: {
  type: 'category',
  grid: {
    columns: [{
      title: {
        text: '项目名称' // 可以理解为列名
      },
      categories: map(series, function (s) {
        return s.name // 设置列表中的展示数据
      }),
    }]
  }
 }

2.3 绘制图表

绘制图表和其他图形插件没有区别,需要定义绘制区域,区域宽高设定。然后将甘特图选项数据设置即可。

const container = document.getElementById("container")
if (container) {
    Highcharts.ganttChart(container, chartOptions)
}
// 容器
<div id="container" style={{ width: '100%' }}></div>

三、遇到了哪些问题

  • 直接引用highcharts文件,会找不到ganttChart方法

image.png

在node_modules上,查看highcharts的源码,可以看到highcharts开放了需要ts和js文件,除了基本的highcharts文件之外,针对复杂图形,如map、gantt图等都有各自的模块文件,文中需要创建的是甘特图,因此在基本图形组件的文件中无法找到方法。 这里需要引入的是import Highcharts from "highcharts/highcharts-gantt.src"

  • 初次进入页面的时候,未找到绘图dom节点,当调用甘特图方法时报错

image.png

我们的绘图区域,依赖的数据来源于接口,接口数据变化之后,就要重新绘制,因此我们把方法放到了useEffect的hook中,该hook在初次渲染页面时会读取绘制图表的方法,读取时由于dom节点还未在页面上渲染,因此会出现读取不到的情况,从而报错。处理该问题,需要判断下绘图id是否获取到,获取到之后再进行操作。

useEffect(() => {
  renderGanttChart()
}, [series, minYear, maxYear])

function renderGanttChart() {
    const container = document.getElementById("container")
    if (container) { // 通过判断语句,确保container已渲染到页面
        Highcharts.ganttChart(container, chartOptions)
    }
}
  • 左边表格区域,控制每列宽度

highcharts的官方配置文档,没有找到yaxis宽度的配置,都是通过显示信息自动适应宽度,这里需要在categories方法中,自定义宽度。由于highcharts支持html标签的操作,因此在categories中可以返回html内容,通过对html标签设置样式,来控制。

如控制固定宽度,超出部分用省略号显示,可如下图示例

categories: map(series, function (s) {
    return `<div style="font-size: 12px;  // 当然这里也可以自己抽成css去控制,通过内联样式做个示例
                        width: 135px;
                        text-overflow: ellipsis;
                        white-space: nowrap;
                        overflow: hidden;"
            >
                ${s.name}
            </div>
})
  • 表格内容添加事件,如鼠标移到单元表格上方,显示提示框

还是继续用上面的示例往下进行,我们可以为上图所示的div添加class样式,然后通过原生js的addEventListener去添加事件,做相应处理。

const yaxisName = document.getElementsByClassName("yaxis-name")
if (yaxisName.length) {
    for (let idx = 0; idx < yaxisName.length; idx++ ) {
      ganttYAxisEvent(yaxisName[idx], 'mouseenter') // 抽象绑定的事件方法
    }
}
// 抽象的绑定事件
const ganttYAxisEvent = (dom, event) => {
    switch (event) {
        case "mouseenter":
            dom.addEventListener('mouseenter', function() {
                ......
            })
            break;
    }
}

四、最终效果

image.png

本文详述了如何在react项目中完成highcharts甘特图的绘制,包括数据的抽取,数据的封装,同时提供一些自定义操作,也将开发过程中遇到的部分问题进行分析,希望对大家有用。