diff --git a/.changeset/add-vert-tag-bar-chart.md b/.changeset/add-vert-tag-bar-chart.md new file mode 100644 index 000000000..4ab74059c --- /dev/null +++ b/.changeset/add-vert-tag-bar-chart.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +feat: Add Vertical Line To Gantt Plot At Specified Time diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index fc64f45d3..2cc67918c 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -358,6 +358,23 @@ describe('Gantt diagram', () => { ); }); + it('should render a gantt diagram with a vert tag', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat ss + axisFormat %Ss + + section Section + A task : a1, 00, 6s + Milestone : vert, 01, + section Another + Task in sec : 06, 3s + another task : 3s + ` + ); + }); it('should render a gantt diagram with tick is 2 milliseconds', () => { imgSnapshotTest( ` diff --git a/docs/syntax/gantt.md b/docs/syntax/gantt.md index 5f7a5cee1..552c1658a 100644 --- a/docs/syntax/gantt.md +++ b/docs/syntax/gantt.md @@ -229,6 +229,30 @@ gantt Final milestone : milestone, m2, 18:08, 4m ``` +### Vertical Markers + +The `vert` keyword lets you add vertical lines to your Gantt chart, making it easy to highlight important dates like deadlines, events, or checkpoints. These markers extend across the entire chart and are positioned based on the date you provide. Unlike milestones, vertical markers don’t take up a row. They’re purely visual reference points that help break up the timeline and make important moments easier to spot. + +```mermaid-example +gantt + dateFormat HH:mm + axisFormat %H:%M + Initial vert : vert, v1, 17:30, 2m + Task A : 3m + Task B : 8m + Final vert : vert, v2, 17:58, 4m +``` + +```mermaid +gantt + dateFormat HH:mm + axisFormat %H:%M + Initial vert : vert, v1, 17:30, 2m + Task A : 3m + Task B : 8m + Final vert : vert, v2, 17:58, 4m +``` + ## Setting dates `dateFormat` defines the format of the date **input** of your gantt elements. How these dates are represented in the rendered chart **output** are defined by `axisFormat`. diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 15c7fab97..3ae55fb25 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -33,7 +33,7 @@ let sections = []; let tasks = []; let currentSection = ''; let displayMode = ''; -const tags = ['active', 'done', 'crit', 'milestone']; +const tags = ['active', 'done', 'crit', 'milestone', 'vert']; let funs = []; let inclusiveEndDates = false; let topAxis = false; @@ -422,7 +422,7 @@ const compileData = function (prevTask, dataStr) { const task = {}; - // Get tags like active, done, crit and milestone + // Get tags like active, done, crit, milestone, and vert getTaskTags(data, task, tags); for (let i = 0; i < data.length; i++) { @@ -470,7 +470,7 @@ const parseData = function (prevTaskId, dataStr) { const task = {}; - // Get tags like active, done, crit and milestone + // Get tags like active, done, crit, milestone, and vert getTaskTags(data, task, tags); for (let i = 0; i < data.length; i++) { @@ -538,6 +538,7 @@ export const addTask = function (descr, data) { rawTask.done = taskInfo.done; rawTask.crit = taskInfo.crit; rawTask.milestone = taskInfo.milestone; + rawTask.vert = taskInfo.vert; rawTask.order = lastOrder; lastOrder++; @@ -570,6 +571,7 @@ export const addTaskOrg = function (descr, data) { newTask.done = taskInfo.done; newTask.crit = taskInfo.crit; newTask.milestone = taskInfo.milestone; + newTask.vert = taskInfo.vert; lastTask = newTask; tasks.push(newTask); }; diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 683fdbe1b..f5c8c2e38 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -231,10 +231,11 @@ export const draw = function (text, id, version, diagObj) { * @param w */ function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w) { + // Sort theArray so that tasks with `vert` come last + theArray.sort((a, b) => (a.vert === b.vert ? 0 : a.vert ? 1 : -1)); // Get unique task orders. Required to draw the background rects when display mode is compact. const uniqueTaskOrderIds = [...new Set(theArray.map((item) => item.order))]; const uniqueTasks = uniqueTaskOrderIds.map((id) => theArray.find((item) => item.order === id)); - // Draw background rects covering the entire width of the graph, these form the section rows. svg .append('g') @@ -259,7 +260,8 @@ export const draw = function (text, id, version, diagObj) { } } return 'section section0'; - }); + }) + .enter(); // Draw the rects representing the tasks const rectangles = svg.append('g').selectAll('rect').data(theArray).enter(); @@ -289,15 +291,26 @@ export const draw = function (text, id, version, diagObj) { .attr('y', function (d, i) { // Ignore the incoming i value and use our order instead i = d.order; + if (d.vert) { + return conf.gridLineStartPadding; + } return i * theGap + theTopPad; }) .attr('width', function (d) { if (d.milestone) { return theBarHeight; } + if (d.vert) { + return 0.08 * theBarHeight; + } return timeScale(d.renderEndTime || d.endTime) - timeScale(d.startTime); }) - .attr('height', theBarHeight) + .attr('height', function (d) { + if (d.vert) { + return taskArray.length * (conf.barHeight + conf.barGap) + conf.barHeight * 2; + } + return theBarHeight; + }) .attr('transform-origin', function (d, i) { // Ignore the incoming i value and use our order instead i = d.order; @@ -354,6 +367,9 @@ export const draw = function (text, id, version, diagObj) { if (d.milestone) { taskClass = ' milestone ' + taskClass; } + if (d.vert) { + taskClass = ' vert ' + taskClass; + } taskClass += secNum; @@ -377,10 +393,13 @@ export const draw = function (text, id, version, diagObj) { let endX = timeScale(d.renderEndTime || d.endTime); if (d.milestone) { startX += 0.5 * (timeScale(d.endTime) - timeScale(d.startTime)) - 0.5 * theBarHeight; - } - if (d.milestone) { endX = startX + theBarHeight; } + + if (d.vert) { + return timeScale(d.startTime) + theSidePad; + } + const textWidth = this.getBBox().width; // Check id text width > width of rectangle @@ -396,6 +415,9 @@ export const draw = function (text, id, version, diagObj) { }) .attr('y', function (d, i) { // Ignore the incoming i value and use our order instead + if (d.vert) { + return conf.gridLineStartPadding + taskArray.length * (conf.barHeight + conf.barGap) + 60; + } i = d.order; return i * theGap + conf.barHeight / 2 + (conf.fontSize / 2 - 2) + theTopPad; }) @@ -406,6 +428,7 @@ export const draw = function (text, id, version, diagObj) { if (d.milestone) { endX = startX + theBarHeight; } + const textWidth = this.getBBox().width; let classStr = ''; @@ -445,6 +468,10 @@ export const draw = function (text, id, version, diagObj) { taskType += ' milestoneText'; } + if (d.vert) { + taskType += ' vertText'; + } + // Check id text width > width of rectangle if (textWidth > endX - startX) { if (endX + textWidth + 1.5 * conf.leftPadding > w) { diff --git a/packages/mermaid/src/diagrams/gantt/styles.js b/packages/mermaid/src/diagrams/gantt/styles.js index 5b53a1b07..776083a9c 100644 --- a/packages/mermaid/src/diagrams/gantt/styles.js +++ b/packages/mermaid/src/diagrams/gantt/styles.js @@ -237,6 +237,16 @@ const getStyles = (options) => fill: ${options.taskTextDarkColor} !important; } + .vert { + stroke: ${options.vertLineColor}; + } + + .vertText { + font-size: 15px; + text-anchor: middle; + fill: ${options.vertLineColor} !important; + } + .activeCritText0, .activeCritText1, .activeCritText2, diff --git a/packages/mermaid/src/docs/syntax/gantt.md b/packages/mermaid/src/docs/syntax/gantt.md index 5742f73f5..919be2874 100644 --- a/packages/mermaid/src/docs/syntax/gantt.md +++ b/packages/mermaid/src/docs/syntax/gantt.md @@ -150,6 +150,20 @@ gantt Final milestone : milestone, m2, 18:08, 4m ``` +### Vertical Markers + +The `vert` keyword lets you add vertical lines to your Gantt chart, making it easy to highlight important dates like deadlines, events, or checkpoints. These markers extend across the entire chart and are positioned based on the date you provide. Unlike milestones, vertical markers don’t take up a row. They’re purely visual reference points that help break up the timeline and make important moments easier to spot. + +```mermaid-example +gantt + dateFormat HH:mm + axisFormat %H:%M + Initial vert : vert, v1, 17:30, 2m + Task A : 3m + Task B : 8m + Final vert : vert, v2, 17:58, 4m +``` + ## Setting dates `dateFormat` defines the format of the date **input** of your gantt elements. How these dates are represented in the rendered chart **output** are defined by `axisFormat`. diff --git a/packages/mermaid/src/themes/theme-base.js b/packages/mermaid/src/themes/theme-base.js index 73ffef070..0b90bd8d7 100644 --- a/packages/mermaid/src/themes/theme-base.js +++ b/packages/mermaid/src/themes/theme-base.js @@ -98,6 +98,7 @@ class Theme { this.critBorderColor = this.critBorderColor || '#ff8888'; this.critBkgColor = this.critBkgColor || 'red'; this.todayLineColor = this.todayLineColor || 'red'; + this.vertLineColor = this.vertLineColor || 'navy'; this.taskTextColor = this.taskTextColor || this.textColor; this.taskTextOutsideColor = this.taskTextOutsideColor || this.textColor; this.taskTextLightColor = this.taskTextLightColor || this.textColor; diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index c452eea9f..23e0fa33d 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -79,6 +79,7 @@ class Theme { this.critBkgColor = '#E83737'; this.taskTextDarkColor = 'calculated'; this.todayLineColor = '#DB5757'; + this.vertLineColor = '#00BFFF'; /* C4 Context Diagram variables */ this.personBorder = this.primaryBorderColor; diff --git a/packages/mermaid/src/themes/theme-default.js b/packages/mermaid/src/themes/theme-default.js index eba3ff101..6bdbc5f8c 100644 --- a/packages/mermaid/src/themes/theme-default.js +++ b/packages/mermaid/src/themes/theme-default.js @@ -88,6 +88,7 @@ class Theme { this.critBorderColor = 'calculated'; this.critBkgColor = 'calculated'; this.todayLineColor = 'calculated'; + this.vertLineColor = 'calculated'; this.sectionBkgColor = rgba(102, 102, 255, 0.49); this.altSectionBkgColor = 'white'; @@ -107,6 +108,7 @@ class Theme { this.critBorderColor = '#ff8888'; this.critBkgColor = 'red'; this.todayLineColor = 'red'; + this.vertLineColor = 'navy'; /* C4 Context Diagram variables */ this.personBorder = this.primaryBorderColor; diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index 853b4d032..f34478795 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -81,6 +81,7 @@ class Theme { this.critBorderColor = '#ff8888'; this.critBkgColor = 'red'; this.todayLineColor = 'red'; + this.vertLineColor = '#00BFFF'; /* C4 Context Diagram variables */ this.personBorder = this.primaryBorderColor; diff --git a/packages/mermaid/src/themes/theme-neutral.js b/packages/mermaid/src/themes/theme-neutral.js index 633a26849..c2a43d035 100644 --- a/packages/mermaid/src/themes/theme-neutral.js +++ b/packages/mermaid/src/themes/theme-neutral.js @@ -93,6 +93,7 @@ class Theme { this.critBkgColor = 'calculated'; this.critBorderColor = 'calculated'; this.todayLineColor = 'calculated'; + this.vertLineColor = 'calculated'; /* C4 Context Diagram variables */ this.personBorder = this.primaryBorderColor; @@ -209,6 +210,7 @@ class Theme { this.critBorderColor = darken(this.critBkgColor, 10); this.todayLineColor = this.critBkgColor; + this.vertLineColor = this.critBkgColor; /* Architecture Diagram variables */ this.archEdgeColor = this.lineColor;