早年在项目上使用到过highcharts、echarts,这些图表类的插件为一些报表系统提供了高效支持。这两年从数据分析类项目转移到ToB类项目后,逐渐没再使用过这类插件。最近项目中,有新的需求,用户方指定使用highcharts,说是看到了highcharts中的图表美观大方,因此必须要在项目中引入。好吧,客户需求大于天,又开始重拾highcharts。
一、聊聊需求
产品:用户现在为每个项目,维护了n个阶段,需要用图表把这n个阶段的开始、结束信息展示出来。
开发:可以啊,用图表显示就行了,每个阶段的项目信息放在一个单元格内,格式化单元格即可。
产品:这种是可以实现,但用户想要看每个阶段的图形化信息,比如阶段1持续时间长,阶段2持续时间短,直观上一眼能看过去。
开发:那开始和结束时间都展示出来了,持续时间用户一目了然啊。
产品:用户想看图形化的,说白了,就是类似甘特图那种颜色分块的区域。
开发:甘特图?甘特图不是做项目进度控制的图形么,用户仅仅为了查看阶段信息,用不着甘特图吧。
产品:但客户的意思是,通过甘特图可以一眼看出来每个阶段,这样对进度把控了然一新。
开发:好吧,可以尝试。
开发:不对,你这个维护的是多个项目信息啊,怎么展示它的进度呢,甘特图是竖向展示的,数轴一般是任务阶段, 横轴是时间,进度条从上到下排列,直观上可以看到当前走到了哪个进度。
产品:额,我知道你说的意思,但客户现在想让纵轴展示项目名,即用表格形式展示项目信息,横轴是时间轴信息,阶段根据横轴来进行绘制。可以实现不?
开发:那我先查阅下文档吧。
就是上面描述的需求,总结下来就是,一个图表,左侧是列表信息,展示列名,右侧是时间信息,时间信息展示不同的阶段,阶段用不同颜色绘制,且根据横轴时间信息完整表示。如下图所示。
二、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值为时间的毫秒数,需转换。
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轴,展示的是时间信息,该时间信息根据用户设置的项目阶段时间来确定,大概思路如下。
- 寻找多个项目初始阶段的开始时间的最小值。
- 根据最小值,设置最小值年度的下一年度第一季度信息。(该需求为用户提出,需在图标上看到的是最小年度和第二年度第一季度信息)
- 设置highcharts中x轴的min、max属性,min属性表示x轴的起始时间、max展示x轴的最大时间。
- 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轴上可以根据属性去获取。
- 需要展示的信息,在data数据中返回,如name、code、date等。
- 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方法。
在node_modules上,查看highcharts的源码,可以看到highcharts开放了需要ts和js文件,除了基本的highcharts文件之外,针对复杂图形,如map、gantt图等都有各自的模块文件,文中需要创建的是甘特图,因此在基本图形组件的文件中无法找到方法。 这里需要引入的是import Highcharts from "highcharts/highcharts-gantt.src"
- 初次进入页面的时候,未找到绘图dom节点,当调用甘特图方法时报错。
我们的绘图区域,依赖的数据来源于接口,接口数据变化之后,就要重新绘制,因此我们把方法放到了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;
}
}
四、最终效果
本文详述了如何在react项目中完成highcharts甘特图的绘制,包括数据的抽取,数据的封装,同时提供一些自定义操作,也将开发过程中遇到的部分问题进行分析,希望对大家有用。