到目前为止,在我们的 css 图表系列教程中,我们已经学习了如何创建不同类型的图表,包括条形图、温度计图表和饼图。
今天,我们将通过在Gantt chart(甘特图)中构建和呈现数据来继续这一旅程。与其他图表教程不同,我们将大量使用 javascript 来实现图表的各个方面。您将能够将此基础用作未来项目的甘特图模板。
我们正在构建的甘特图
这是我们将要创建的图表(点击 重新运行 以查看动画)。它显示了一系列任务,明确了这些任务计划在一周内开始的时间——无论是在一天的开始还是中途——以及它们应该完成的时间:
See the Pen Build a Simple Gantt Chart With JavaScript by Envato Tuts+ (@tutsplus) on CodePen.
什么是甘特图?
由机械工程师亨利甘特在第一次世界大战前开发的甘特图最初用于管理动员美国士兵和弹药的后勤工作。如今,甘特的技术不仅仅用于战争。实际上,您会发现它们存在于大多数行业中。它们使您可以轻松地可视化任务列表、这些任务之间的依赖关系以及它们的完成状态。
“甘特图是一种说明项目进度的条形图。该图表在纵轴上列出了要执行的任务,在横轴上列出了时间间隔。图表中水平条的宽度显示了每个活动的持续时间。” – 维基百科
甘特图使管理人员能够管理项目、安排任务、观察进度、委派职责和分配资源。
实际上,您不必成为经理也可以使用它们。任何想要组织任务的人都可以从这种形式的项目可视化中受益。
不管把它当成管理工具,甘特图的布局还可以有其他用途。例如,您可以在公司站点中使用这种方法作为可视化公司历史的时间线。
甘特图解决方案
大多数项目管理平台都使用甘特图作为其产品的核心部分。ClickUp、Zapier和Monday.com都是您在讨论项目管理应用程序时会听到的名字(尽管 Monday.com 的创始人强调他们的 时间线“专注于人,而不是任务或项目”)。
有许多免费和专有的解决方案可用于构建您自己的甘特图。您可以使用 Microsoft Excel、Google 表格、 TeamGantt等 Web 应用程序、 Highcharts等 JavaScript 库 ,甚至编写自己的代码来创建一个。
在本教程中,我们将使用后者;让我们使用 CSS 和 javaScript 创建我们自己的简单甘特图!
1. 指定页面标记
我们将首先定义一个包含两个列表的包装器元素:
第一个列表定义图表范围(x 轴数据)。在我们的例子中,这将包含星期几。每天将代表正常的工作时间。
第二个列表设置图表数据(y 轴数据)。在我们的案例中,数据将包括需要在一周内执行的任务。每个描述任务的列表项都带有两个自定义属性:data-duration属性和data-color属性。第一个属性定义任务持续时间,而第二个属性定义其背景颜色。属性的值data-duration应该是[startDay]-[endDay]格式。话虽如此,我们将使用 data-duration="tue-wed"来完成一项应该在周二开始并在周三完成的任务。此外,一项任务也可以在一天的中间开始和完成。在这种情况下,data-duration属性值应该是[startDay]½-[endDay]½格式。例如,我们将使用data-duration="tue-wed½"对于应该在星期二开始并在星期三中间完成的任务。同样,data-duration="tue½-tue"将描述一项应在周二中旬开始并在同一天完成的任务。
这是所需的标记:
<div class="chart-wrapper">
<ul class="chart-values">
<li>sun</li>
<li>mon</li>
<li>tue</li>
<li>wed</li>
<li>thu</li>
<li>fri</li>
<li>sat</li>
</ul>
<ul class="chart-bars">
<li data-duration="tue-wed" data-color="#b03532">Task</li>
<li data-duration="wed-sat" data-color="#33a8a5">Task</li>
...
</ul>
</div>
2. 设置图表样式
为简单起见,我不会介绍初始重置样式,但可以通过单击 演示项目的CSS 选项卡 随意查看它们。
图表包装器将具有水平居中内容的最大宽度:
.chart-wrapper {
max-width: 1150px;
padding: 0 10px;
margin: 0 auto;
}
x 轴
该.chart-values列表将是一个弹性容器。它的弹性项目(天)将均匀分布在主轴上,最小宽度为 80 像素。由于该最小宽度,在小屏幕上,图表不会缩小超过该宽度,并且会出现一个水平滚动条。如果您不喜欢这种行为,请随意删除它。
为了更好地可视化每个项目的左右边界,我们将使用它们的::before伪元素。我们会给它一个很大的硬编码高度(510px),以确保它可以扩展到所有任务。无需对该值进行硬编码,而是始终可以选择通过 JavaScript 动态计算它。但是让我们暂时跳过该解决方案,因为它是次要的。
对应的样式:
:root {
--divider: lightgrey;
}
.chart-wrapper .chart-values {
position: relative;
display: flex;
margin-bottom: 20px;
font-weight: bold;
font-size: 1.2rem;
}
.chart-wrapper .chart-values li {
flex: 1;
min-width: 80px;
text-align: center;
}
.chart-wrapper .chart-values li:not(:last-child) {
position: relative;
}
.chart-wrapper .chart-values li:not(:last-child)::before {
content: '';
position: absolute;
right: 0;
height: 510px;
border-right: 1px solid var(--divider);
}
y 轴
第二个列表的项目(条)最初将被隐藏。具体来说,他们将拥有width: 0和opacity: 0。另外,我们会给他们 position: relative。稍后我们将left根据它们的data-duration属性值动态设置它们的位置。
以下是相关样式:
:root {
--white: #fff;
}
.chart-wrapper .chart-bars li {
position: relative;
color: var(--white);
margin-bottom: 15px;
font-size: 16px;
border-radius: 20px;
padding: 10px 20px;
width: 0;
opacity: 0;
transition: all 0.65s linear 0.2s;
}
@media screen and (max-width: 600px) {
.chart-wrapper .chart-bars li {
padding: 10px;
}
}
3. 添加 JavaScript
当页面加载或浏览器窗口调整大小时,createChart将执行该函数:
window.addeventListener("load", createChart);
window.addEventListener("resize", createChart);
注意: 正如我在其他教程中提到的,有不同的方法可以限制resize发出的事件。例如,一种有效的解决方案是使用 Lodash 的 _.debounce函数。不过,这超出了本教程的范围。
在这个函数内部,我们首先做以下事情:
抓住两个列表的项目。
days使用展开运算符将 nodeList 转换为实数数组。或者,我们可以使用该Array.from()方法。这种转换将使我们能够利用filter()数组中可用的方法来过滤日期。
循环执行任务。
function createChart(e) {
// 1
const days = document.queryselectorAll(".chart-values li");
const tasks = document.querySelectorAll(".chart-bars li");
// 2
const daysArray = [...days];
// 3
tasks.forEach(el => {
...
});
}
循环任务
接下来,对于每个任务:
我们获取其data-duration属性的值(例如 tue-wed)。另外,我们使用“-”作为分隔符来分割这个值。
返回数组的第一个字符串表示任务的开始日期(例如 tue),而第二个字符串表示任务的结束日期(例如 wed)。
知道它的开始日期,我们过滤daysArray以检索与这一天匹配的列表项(天)。在此测试期间,我们忽略了可能存在的“½”字符。然后,我们做一些计算来计算 left 相关任务所需的属性值。
知道它的结束日期,我们过滤 daysArray 以检索与这一天匹配的列表项(天)。在此测试期间,我们忽略了可能存在的“½”字符。然后,我们做一些计算来计算 width 相关任务所需的属性值。
tasks.forEach(el => {
// 1
const duration = el.dataset.duration.split("-");
// 2
const startDay = duration[0];
const endDay = duration[1];
let left = 0,
width = 0;
// 3
if (startDay.endsWith("½")) {
const filteredArray = daysArray.filter(day => day.textContent == startDay.slice(0, -1));
left = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth / 2;
} else {
const filteredArray = daysArray.filter(day => day.textContent == startDay);
left = filteredArray[0].offsetLeft;
}
// 4
if (endDay.endsWith("½")) {
const filteredArray = daysArray.filter(day => day.textContent == endDay.slice(0, -1));
width = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth / 2 - left;
} else {
const filteredArray = daysArray.filter(day => day.textContent == endDay);
width = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth - left;
}
...
});
注意: 在上面的代码中,我们使用该Element.offsetLeft属性来检索元素相对于其父元素的左侧位置。另外,我们利用该Element.offsetWidth属性来查找元素的宽度。作为获取其宽度的另一种选择,我们同样可以使用更精确的Element.getBoundingClientRect()方法。
设置样式
计算完每个任务的left和width值后,最后一步是执行以下操作:
应用相应的样式。
抓取data-color属性的值并将其设置为任务的背景颜色。
显示任务。请记住,所有任务最初都是隐藏的。
操作 2 和 3 应该只在页面加载时运行,因为每次我们调整浏览器窗口大小时它们的值都不会改变。
tasks.forEach(el => {
...
// 1
el.style.left = `${left}px`;
el.style.width = `${width}px`;
// 4
if (e.type == "load") {
// 2
el.style.backgroundColor = el.dataset.color;
// 3
el.style.opacity = 1;
}
});
结论
而已!我们设法构建了一个功能齐全的甘特图。感谢大家的关注,我希望这为刷新你的 JavaScript 技能提供了一个很好的练习。这里再看一下最终的演示:
See the Pen Build a Simple Gantt Chart With JavaScript by Envato Tuts+ (@tutsplus) on CodePen.
使用它,如果您找到任何增强其功能的方法,请务必分享!请继续关注等效的 CSS 甘特图。
一如既往,非常感谢您的阅读!
- x 轴
- y 轴
- 循环任务
- 设置样式