背景

  • 工作中需要在网页上实现甘特图,以展示进度数据。通过网上调研相关项目,找到一款 dhtmlx-gantt 组件,在低程度上满足项目需求,但在部分定制功能(如时间轴自定义、编辑弹窗样式风格等)并不能完全满足项目需求。此外,使用此类开源项目,若遇到功能无法满足需求时,解决起来较为麻烦,基本只有在需求上进行妥协。
  • 个人在工作后暂时没有开发过相对复杂且功能较为完整的组件,开发甘特图组件既可以满足工作需要、方便开发人员,也可以加深自己对前端技术的理解。
    基于以上原因,开始着手开发一款甘特图组件 m-gantt,第一版首先以完成项目需求为目标,实现项目需要的功能,尽可能将配置项进行提取。后续将继续完善拓展应有功能,实现可配置化。

开发准备

  • 本甘特图开发的基本思路源于这两篇文章:
    【参考链接1】
    【参考链接2】
  • 调研含 dhtmlx-gantt 在内的多款甘特图组件,了解甘特图组件所需要的基本功能以及数据的在展示方法、交互方法等。

其他说明

  • 本甘特图组件目前仅支持 Angular 开发
  • 除 Angular 框架外,本组件无其他依赖包
  • 甘特图基于svg绘画,不依赖其他工具,可塑性强,且相较于使用标签加定位的布局方式,该方法代码量较少且逻辑清晰
  • 样式使用less语法

开发内容概述

基本思路

  1. 布局
    布局需要实现如下几项功能
    ① 主要分为左右两个部分,每个部分分上部固定区域和下部垂直滚动区域
    ② 左右部分的下部区域需要同时滚动
    ③ 右部需要横向滚动
    ④(可选)左侧部分支持缩放
  2. 表格区域
    ① 基本为常规表格,将表头固定在上部,表体放在下部
    ② 点击行数据可使进度图横向滚动到该项任务所在起始位置
  3. 时间轴区域
    ① 分多层,默认分为 年-月层、日层、自定义层
    ② 使用svg语法进行绘制
  4. 进度图区域
    ① 使用svg语法绘制
    ② 进度图根据实际数据实时渲染
    ③ 鼠标移动到单个任务进度条上显示数据详情

布局

① 主要分为左右两个部分,每个部分分上部固定区域(吸顶)和下部垂直滚动区域
② 左右部分的下部区域需要同时滚动(共用滚动条)
③ 右部需要横向滚动

<div class="gantt-table" #table><div class="header"></div><div class="body"></div>
</div>
<div class="gantt-chart" #chart><div class="header"></div><div class="body"></div>
</div>
.gantt-container {height: 800px;display: flex; // 使用flex布局overflow: hidden;.gantt-table, .gantt-chart {.header {position: sticky;height: @headHeight;top: 0;}.body {height: 900px;}}// 左侧表格.gantt-table {position: relative;overflow-x: hidden;overflow-y: scroll;}// 隐藏左侧滚动条.gantt-table::-webkit-scrollbar {width: 0;}// 右侧进度图.gantt-chart {overflow-x: scroll;flex: 1;}
}
  @ViewChild('table') table: any;@ViewChild('chart') chart: any;public scrollLock = {isTableScroll: false,isChartScroll: false}ngAfterViewInit(): void {// 监听左侧表格this.table.nativeElement.addEventListener('scroll', this.scrollChart);// 监听右侧表格this.chart.nativeElement.addEventListener('scroll', this.scrollTable);}private scrollChart = (e: any) => {// 当右侧进度图没有滚动时,使之随表格滚动if (!this.scrollLock.isChartScroll) {this.scrollLock.isTableScroll = true;this.chart.nativeElement.scroll({top: e.target?.scrollTop})}this.scrollLock.isTableScroll = false;}private scrollTable = (e: any) => {// 当左侧表格没有滚动时,使之随进度图滚动if (!this.scrollLock.isTableScroll) {this.scrollLock.isChartScroll = true;this.table.nativeElement.scroll({top: e.target?.scrollTop})}this.scrollLock.isChartScroll = false;}ngOnDestroy(): void {this.table.nativeElement.removeEventListener('scroll', this.scrollChart);this.chart.nativeElement.removeEventListener('scroll', this.scrollTable);}

SVG

本甘特图使用svg语法绘制,主要用到以下几种常用标签

  • react 矩形标签

    • x: 左侧距离
    • y: 顶部距离
    • width: 宽度
    • height: 高度
    • rx: x轴半径
    • rx: y轴半径
  • path 路径标签(eg: M 100 0 V 100)
    • M: move to 传入目标点的坐标 x y
    • H: horizontal lineto 平行线
    • V: vertical lineto 垂直线
  • line 线标签
    • x1 y1: 第一个点的坐标
    • x2 y2: 第二个点的坐标
  • text 文本标签
  • g 组合标签
    • 添加到g上的变化会应用到其子元素

更加详细的SVG图知识可以参考另一篇文章【svg学习】

时间轴

① 计算时间轴的长度
② 构造时间数组
③ 通过位置绘制时间轴

// 时间轴
public dateConfig: any = {startDate: new Date('2077-12-31'),endDate: new Date('1999-1-1'),total: 0, // 总天数svgWidth: 0, // 整体宽度svgHeight: 60, // 时间轴高度dateList: [], // 日轴monthList: [] // 月轴
}
// 配置时间轴数据
private setGanttData(): void {// 遍历任务数据 获取最大/最小值this.ganttConfig.data.forEach((task: any) => {const { startDate, endDate } = task;if (startDate && new Date(startDate) < this.dateConfig.startDate) {this.dateConfig.startDate = new Date(startDate)}if (endDate && new Date(endDate) > this.dateConfig.endDate) {this.dateConfig.endDate = new Date(endDate);}})// 前后加N天保证显示效果this.dateConfig.endDate = new Date(this.dateConfig.endDate.getTime() + 3 * 24 * 60 * 60 * 1000);this.dateConfig.startDate = new Date(this.dateConfig.startDate.getTime() - 3 * 24 * 60 * 60 * 1000);this.dateConfig.total = (this.dateConfig.endDate.getTime() - this.dateConfig.startDate.getTime()) / (24 * 60 * 60 * 1000);// 计算总宽度this.dateConfig.svgWidth = this.dateConfig.total * this.squareWidth;// 时间轴// 日const week = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];for (let i = 0; i < this.dateConfig.total; i++) {this.dateConfig.dateList.push({text: this.datePipe.transform(new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000), 'dd'),day: week[new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000).getDay()],month: this.datePipe.transform(new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000), 'yyyy-MM'),})}// 月const monthMap = new Map();this.dateConfig.dateList.forEach((date: any) => {const month = date.month;if (monthMap.has(month)) {monthMap.set(month, monthMap.get(month) + 1)} else {monthMap.set(month, 1)}})let lengthBefore: number = 0;monthMap.forEach((value, key) => {this.dateConfig.monthList.push({text: key,left: lengthBefore})lengthBefore += value;})
}
<!-- 时间轴 -->
<div class="header" [style.width]="dateConfig.svgWidth + 'px'"><!-- 月数据 --><svg [attr.width]="dateConfig.svgWidth" [attr.height]="timeLineHeight"><g class="date" *ngFor="let month of dateConfig.monthList; let i = index;"><!-- 文字 --><text [attr.x]="month.left * squareWidth + 5" [attr.y]="timeLineHeight / 2 + 4"style="font-size: 12px;">{{month.text}}</text><!-- 时间轴边框 --><path [attr.d]="'M ' + month.left * squareWidth + ' 0 V 30'" stroke="#d9dde0"></path><line x1="0" y1="30" [attr.x2]="dateConfig.svgWidth" y2="30" stroke="#d9dde0" /></g></svg><!-- 日数据 --><svg [attr.width]="dateConfig.svgWidth" [attr.height]="timeLineHeight"><g class="date" *ngFor="let date of dateConfig.dateList; let i = index;"><text [attr.x]="i * squareWidth + 5" [attr.y]="timeLineHeight / 2 + 4"style="font-size: 12px;">{{date.text}}</text><text [attr.x]="i * squareWidth + 20" [attr.y]="timeLineHeight / 2 + 4"style="font-size: 8px;">{{date.day}}</text><path [attr.d]="'M ' + i * squareWidth + ' 0 V 30'" stroke="#d9dde0"></path></g></svg>
</div>

进度图

  • 背景绘制
    ① 用 react 绘制格子
    ② 用 line 绘制横线
    ③ 用 path 绘制竖线
// 数据
public ganttConfig: any = {columns: columns,data: data,chartData: []
}
// 数据预处理
private preprocessData(data: Array<any>): Array<any> {data.forEach(row => {const startDay = (new Date(row.startDate).getTime() - this.dateConfig.startDate.getTime()) / (24 * 60 * 60 * 1000);row.startDay = startDay;})return data;
}
<div class="body"><svg [attr.width]="dateConfig.svgWidth" [attr.height]="ganttConfig.chartData.length * lineHeight"><rect *ngFor="let row of ganttConfig.chartData; let i = index;" x="0" [attr.y]="lineHeight * i"[attr.width]="dateConfig.svgWidth" [attr.heigth]="lineHeight" [attr.fill]="i % 2 === 0 ? '#fff' : '#f9fafb'"></rect><path *ngFor="let date of dateConfig.dateList; let i = index;"[attr.d]="'M ' + i * squareWidth + ' 0 V ' + ganttConfig.chartData.length * lineHeight" stoke="#d9dde0"></path><line *ngFor="let row of ganttConfig.chartData; let i = index;" x1="0" [attr.y1]="lineHeight * i + lineHeight"[attr.x2]="dateConfig.svgWidth" [attr.y2]="lineHeight * i + lineHeight" stroke="#d9dde0" /><!-- 进度图 --></svg>
</div>
  • 进度图 bar 绘制

① 用 rect 绘制每项任务的总计划 bar
② 用 rect 绘制每项任务的已完成 bar
③ 用 text 填充文字

<g class="bar" *ngFor="let row of ganttConfig.chartData; let i = index;" (mouseenter)="showDetail(row, true)"(mouseleave)="showDetail(row)"><!-- 全部 --><rect [id]="'bar_' + i" [attr.x]="row.startDay * squareWidth"[attr.y]="i * lineHeight + (lineHeight - barHeight) / 2" [attr.width]="row.duration * squareWidth"[attr.height]="barHeight" [attr.rx]="barHeight / 2" [attr.ry]="barHeight / 2"[attr.fill]="row.parentId ? subBarColor : barColor"></rect><!-- 进度 --><rect [attr.x]="row.startDay * squareWidth" [attr.y]="i * lineHeight + (lineHeight - barHeight) / 2"[attr.width]="(row.duration * squareWidth) * row.progress" [attr.height]="barHeight"[attr.rx]="barHeight / 2" [attr.ry]="barHeight / 2"[attr.fill]="row.parentId ? subProgressBarColor : progressBarColor"></rect><text [attr.x]="row.startDay * squareWidth + 20" [attr.y]="(i + 0.5) * lineHeight + 5"[attr.fill]="barFontColor" style="font-size: 12px;">{{row.name}}</text>
</g>

点击滚动

点击任务滚动到任务开始位置

// 点击任务自动滚动
public scrollToBar(row: any): void {const targetBar = document.querySelector(`#bar_${this.ganttConfig.chartData.indexOf(row)}`);if (targetBar && this.table) {// 目标进度条左侧与client距离const x = targetBar.getBoundingClientRect().left;// table右侧与client距离const parentX = this.table.nativeElement.getBoundingClientRect().right;const preScroll = this.chart.nativeElement.scrollLeft || 0;const diff = x - parentX;// 滚动this.chart.nativeElement.scrollTo({left: preScroll + diff,behavior: 'smooth'})}
}

显示详情

鼠标移动到任务上显示任务详情

① 创建一个modal标签,设置基本样式,在里面放置需要展示的详情
② 通过监听鼠标移动事件,将鼠标的位置传递给该元素,实现跟随鼠标移动
③ 在鼠标进入 bar 时绑定,在鼠标移出 bar 时解绑

// 弹窗显示详情
@ViewChild('msgModal') msgModal: any;
public showModal: boolean = false;
public modalData: any = {name: '任务1',startDate: '2022-10-1',status: '进行中',progress: ''
}
public showDetail(row: any, flag = false): void {if (flag) {this.showModal = true;// 绑定数据// ...document.addEventListener('mousemove', this.moveModal)} else {this.showModal = false}
}
private moveModal = (e: any) => {document.querySelector('#msg-modal')?.setAttribute('style', `top: ${e.clientY}px; left: ${e.clientX - 510}px`);
}

结构及样式代码略

树形表格

树形表格

① 表格支持点击 icon 展开与折叠
② 进度图的对应项根据表格的折叠与否决定是否显示
③ 为了支持父子级关系及控制显示,任务数据需添加以下字段:
a: id
b: parentId (仅子级数据需要,关联父子关系)
c: open (仅父级数据需要,控制是否展开状态,变换icon)
d: show (控制是否显示)

// 表格展开
public showSubData(id: string): void {this.ganttConfig.data.forEach((item: any) => {if (item.id === id) {item.open = !item.open;}if (item.parentId === id) {item.show = !item.show;}})this.ganttConfig.chartData = this.ganttConfig.data.filter((row: any) => {return row.show === true})
}

以上,甘特图组件基本功能开发完成,后续工作:
① 完善其他实用功能
② 修改已有问题
③ 将数据、功能、样式封装为可配置项

【项目GitHub地址】⭐️

原文地址
【个人博客】⭐️

相关文章
【前端甘特图组件开发(二)】

前端甘特图组件开发(一)相关推荐

  1. 前端甘特图组件开发(二)

    自定义时间轴功能 由于常见的甘特图时间轴只支持按照天/周/月/年或者固定的时间单位进行划分,但实际使用场景下有时候会需要按照特定且不规则的时间段对进度数据进行直观划分.因此本组件在原时间轴的基础上添加 ...

  2. vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享)

    语雀原文 有更好的排版体验~ 这篇文章主要讲述笔者开发 v-gantt 甘特图组件的经过. 起源 公司项目有个甘特图的需求. 笔者考察了世面上 常见的甘特图组件 后,本着 我上我也行 的心态,以及考虑 ...

  3. Twproject Gantt – 开源的 JavaScript 甘特图组件

    Twproject Gantt 是一款基于 jQuery 开发的甘特图组件,也可以创建其它图表,例如任务树(Task Trees).内置编辑.缩放和 CSS 皮肤等功能.更重要的是, 它是免费开源的. ...

  4. JQuery.Gantt(甘特图) 开发指南

    概述      JQuery.Gantt是一个开源的基于JQuery库的用于实现甘特图效果的可扩展功能的JS组件库. 源码下载 http://download.csdn.net/detail/aaak ...

  5. 强大js web甘特图制作之甘特图组件和数据对象

    引用CSS和JS 使用EdoGantt是一件简单轻松的事,首先我们在HTML页面内引用CSS和JS: <!--edo css--><link href="http://ww ...

  6. AnyGantt Flash甘特图组件免费下载及使用教程

    原文来自龙博方案网http://www.fanganwang.com/product/1455转载请注明出处 AnyGantt是一个强大的,基于Flash的数据可视化解决方案.允许任何人利用有力的动画 ...

  7. JavaScript多功能甘特图组件 - jsGantt

    jsGantt 是一个可定制的.灵活的.多语言的甘特图组件,由原生 JavaScript 构建.它使用客户端渲染以获得快速的性能和动态的交互性.非常适用于任何需要交互式时间线或时间表显示的项目. 更多 ...

  8. 基于 Compose Canvas 的蛛网图组件开发

    基于 Compose & Canvas 的蛛网图组件开发 1. 前言 2. 实现过程 2.1 准备工作 2.1.1创建Compose方法,确定参数 2.1.2 添加Canvas 2.1.3编写 ...

  9. 记一次 React 开源甘特图组件的性能优化,已合入 PR!

    背景 公司项目最近用到甘特图功能,于是集成了一款开源的甘特图插件. 甘特图的主要作用是项目管理,可以用图示的方式通过活动列表和时间刻度形象地表示出任何特定项目的活动顺序与持续时间,如下图 image. ...

最新文章

  1. IA-32 Intel手册学习笔记(一)系统架构概述
  2. linux 端口方法防火墙,Linux 打开端口方法(防火墙操作)
  3. android深度探索 HAL及驱动开发 第八章
  4. 如何在MySQL随机选择记录
  5. datagrid 重载本地数据_jQuery easyui datagrid重新加载数据
  6. 3.1 语音的产生与感知
  7. svn linux clean up,SVN清理失败 (svn cleanup) 的解决方法
  8. 如何恢复删除的文件?wps文件恢复,4种方法教你找回来
  9. TUIO学习笔记2-TUIO C++ Reference Implementation and Demo Application参考实现和Demo程序(图片版,防查重)
  10. 支付宝对账单和微信对账单下载保存数据库
  11. python xlsx转xls_Python 批量将xls文件转换为xlsx
  12. Java 面试题看这些就够了
  13. IBM 华为等薪资福利规定
  14. 信息系统项目管理师 第七章-项目成本管理
  15. 新想法 | 按需视觉识别:愿景和初步方案
  16. 2022年湖北中级工程师职称评审详细流程是哪些步骤呢? 甘建二
  17. Gitea在windows平台的安装和简单使用教程
  18. APP混合应用之web页面处理
  19. 详解Tomcat配置及使用
  20. 简单的医院挂号排队系统(c语言)

热门文章

  1. 一篇文章告诉你 1px 等于多少 cm
  2. Android 新版v2签名渠道包工具
  3. 二进制安全:转化大师漏洞复现:Boxoft Convert Master 1.3.0 - ‘wav‘ SEH Local Exploit
  4. 实力碾压!荣耀笔记本MagicBook Pro对比新鲜出炉
  5. 截取字符串的三个函数的简单使用
  6. SaaS化大潮,移动CRM带来的机遇和挑战!
  7. WPF之几何图形Geometry
  8. ​用互联网思维看待大数据
  9. Lvs负载均衡+健康检测
  10. activity工作流流程部署(自动部署与动态BPMN部署)