Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "until" keyword in gantt charts #5224

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions cypress/integration/rendering/gantt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ describe('Gantt diagram', () => {
excludes weekdays 2014-01-10

section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task :des3, after des2, 5d
Future task2 :des4, after des3, 5d
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved
Waiting for task 2 :after des1, until des4

section Critical tasks
Completed task in the critical line :crit, done, 2014-01-06,24h
Implement parser and jison :crit, done, after des1, 2d
Implement parser and jison :crit, done, after des1, 2d
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved

section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Expand Down Expand Up @@ -88,6 +88,7 @@ describe('Gantt diagram', () => {
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
kiwi :d, 2017-07-20, until b c
`,
{}
);
Expand Down
42 changes: 26 additions & 16 deletions docs/syntax/gantt.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ gantt
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
Functionality added :milestone, 2014-01-25, 0d
Add to mermaid :until isadded
Functionality added :milestone, isadded, 2014-01-25, 0d

section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Expand Down Expand Up @@ -100,8 +100,8 @@ gantt
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
Functionality added :milestone, 2014-01-25, 0d
Add to mermaid :until isadded
Functionality added :milestone, isadded, 2014-01-25, 0d

section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Expand All @@ -124,18 +124,26 @@ After processing the tags, the remaining metadata items are interpreted as follo
2. If two items are specified, the last item is interpreted as in the previous case. The first item can either specify an explicit start date/time (in the format specified by `dateFormat`) or reference another task using `after <otherTaskID> [[otherTaskID2 [otherTaskID3]]...]`. In the latter case, the start date of the task will be set according to the latest end date of any referenced task.
3. If three items are specified, the last two will be interpreted as in the previous case. The first item will denote the ID of the task, which can be referenced using the `later <taskID>` syntax.

| Metadata syntax | Start date | End date | ID |
| ------------------------------------------ | --------------------------------------------------- | ------------------------------------------- | -------- |
| `<taskID>, <startDate>, <endDate>` | `startdate` as interpreted using `dateformat` | `endDate` as interpreted using `dateformat` | `taskID` |
| `<taskID>, <startDate>, <length>` | `startdate` as interpreted using `dateformat` | Start date + `length` | `taskID` |
| `<taskID>, after <otherTaskId>, <endDate>` | End date of previously specified task `otherTaskID` | `endDate` as interpreted using `dateformat` | `taskID` |
| `<taskID>, after <otherTaskId>, <length>` | End date of previously specified task `otherTaskID` | Start date + `length` | `taskID` |
| `<startDate>, <endDate>` | `startdate` as interpreted using `dateformat` | `enddate` as interpreted using `dateformat` | n/a |
| `<startDate>, <length>` | `startdate` as interpreted using `dateformat` | Start date + `length` | n/a |
| `after <otherTaskID>, <endDate>` | End date of previously specified task `otherTaskID` | `enddate` as interpreted using `dateformat` | n/a |
| `after <otherTaskID>, <length>` | End date of previously specified task `otherTaskID` | Start date + `length` | n/a |
| `<endDate>` | End date of preceding task | `enddate` as interpreted using `dateformat` | n/a |
| `<length>` | End date of preceding task | Start date + `length` | n/a |
| Metadata syntax | Start date | End date | ID |
| ---------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------- | -------- |
| `<taskID>, <startDate>, <endDate>` | `startdate` as interpreted using `dateformat` | `endDate` as interpreted using `dateformat` | `taskID` |
| `<taskID>, <startDate>, <length>` | `startdate` as interpreted using `dateformat` | Start date + `length` | `taskID` |
| `<taskID>, after <otherTaskId>, <endDate>` | End date of previously specified task `otherTaskID` | `endDate` as interpreted using `dateformat` | `taskID` |
| `<taskID>, after <otherTaskId>, <length>` | End date of previously specified task `otherTaskID` | Start date + `length` | `taskID` |
| `<taskID>, <startDate>, until <otherTaskId>` | `startdate` as interpreted using `dateformat` | Start date of previously specified task `otherTaskID` | `taskID` |
| `<taskID>, after <otherTaskId>, until <otherTaskId>` | End date of previously specified task `otherTaskID` | Start date of previously specified task `otherTaskID` | `taskID` |
| `<startDate>, <endDate>` | `startdate` as interpreted using `dateformat` | `enddate` as interpreted using `dateformat` | n/a |
| `<startDate>, <length>` | `startdate` as interpreted using `dateformat` | Start date + `length` | n/a |
| `after <otherTaskID>, <endDate>` | End date of previously specified task `otherTaskID` | `enddate` as interpreted using `dateformat` | n/a |
| `after <otherTaskID>, <length>` | End date of previously specified task `otherTaskID` | Start date + `length` | n/a |
| `<startDate>, until <otherTaskId>` | `startdate` as interpreted using `dateformat` | Start date of previously specified task `otherTaskID` | n/a |
| `after <otherTaskId>, until <otherTaskId>` | End date of previously specified task `otherTaskID` | Start date of previously specified task `otherTaskID` | n/a |
| `<endDate>` | End date of preceding task | `enddate` as interpreted using `dateformat` | n/a |
| `<length>` | End date of preceding task | Start date + `length` | n/a |
| `until <otherTqskId>` | End date of preceding task | Start date of previously specified task `otherTaskID` | n/a |

> **Note**
> Support for keyword `until` was added in (v\<MERMAID_RELEASE_VERSION>+). This can be used to define a task which is running until some other specific task or milestone starts.

For simplicity, the table does not show the use of multiple tasks listed with the `after` keyword. Here is an example of how to use it and how it's interpreted:

Expand All @@ -144,13 +152,15 @@ gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
kiwi :d, 2017-07-20, until b c
```

```mermaid
gantt
apple :a, 2017-07-20, 1w
banana :crit, b, 2017-07-23, 1d
cherry :active, c, after b a, 1d
kiwi :d, 2017-07-20, until b c
```

### Title
Expand Down
63 changes: 40 additions & 23 deletions packages/mermaid/src/diagrams/gantt/ganttDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,31 +256,25 @@
str = str.trim();

// Test for after
const re = /^after\s+([\d\w- ]+)/;
const afterStatement = re.exec(str.trim());
const afterRePattern = /^after\s+(?<ids>[\d\w- ]+)/;
const afterStatement = afterRePattern.exec(str);

if (afterStatement !== null) {
// check all after ids and take the latest
let latestEndingTask = null;
afterStatement[1].split(' ').forEach(function (id) {
let latestTask = null;
for (const id of afterStatement.groups.ids.split(' ')) {
let task = findTaskById(id);
if (task !== undefined) {
if (!latestEndingTask) {
latestEndingTask = task;
} else {
if (task.endTime > latestEndingTask.endTime) {
latestEndingTask = task;
}
}
if (task !== undefined && (!latestTask || task.endTime > latestTask.endTime)) {
latestTask = task;
}
});
}

if (!latestEndingTask) {
const dt = new Date();
dt.setHours(0, 0, 0, 0);
return dt;
if (latestTask) {
return latestTask.endTime;
} else {
return latestEndingTask.endTime;
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
}
}

Expand Down Expand Up @@ -343,13 +337,36 @@
const getEndDate = function (prevTime, dateFormat, str, inclusive = false) {
str = str.trim();

// Check for actual date
let mDate = dayjs(str, dateFormat.trim(), true);
if (mDate.isValid()) {
// test for until
const untilRePattern = /^until\s+(?<ids>[\d\w- ]+)/;
const untilStatement = untilRePattern.exec(str);

if (untilStatement !== null) {
// check all until ids and take the earliest
let earliestTask = null;
for (const id of untilStatement.groups.ids.split(' ')) {
let task = findTaskById(id);
if (task !== undefined && (!earliestTask || task.startTime < earliestTask.startTime)) {
earliestTask = task;
}
}

if (earliestTask) {
return earliestTask.startTime;
} else {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
}

Check warning on line 360 in packages/mermaid/src/diagrams/gantt/ganttDb.js

View check run for this annotation

Codecov / codecov/patch

packages/mermaid/src/diagrams/gantt/ganttDb.js#L357-L360

Added lines #L357 - L360 were not covered by tests
fzag marked this conversation as resolved.
Show resolved Hide resolved
}

// check for actual date
let parsedDate = dayjs(str, dateFormat.trim(), true);
if (parsedDate.isValid()) {
if (inclusive) {
mDate = mDate.add(1, 'd');
parsedDate = parsedDate.add(1, 'd');
}
return mDate.toDate();
return parsedDate.toDate();
}

let endTime = dayjs(prevTime);
Expand Down
56 changes: 54 additions & 2 deletions packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ describe('when using the ganttDb', function () {

it('should handle relative start date based on id regardless of sections', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('testa1');
ganttDb.addSection('sec1');
ganttDb.addTask('test1', 'id1,2013-01-01,2w');
ganttDb.addTask('test2', 'id2,after id3,1d');
ganttDb.addSection('testa2');
ganttDb.addSection('sec2');
ganttDb.addTask('test3', 'id3,after id1,2d');

const tasks = ganttDb.getTasks();
Expand All @@ -158,6 +158,58 @@ describe('when using the ganttDb', function () {
expect(tasks[2].startTime).toEqual(new Date(2013, 0, 15));
expect(tasks[2].endTime).toEqual(new Date(2013, 0, 17));
});

it('should handle relative end date based on id regardless of sections', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('sec1');
ganttDb.addTask('task1', 'id1,2013-01-01,until id3');
ganttDb.addSection('sec2');
ganttDb.addTask('task2', 'id2,2013-01-10,until id3');
ganttDb.addTask('task3', 'id3,2013-02-01,2d');
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved

const tasks = ganttDb.getTasks();

expect(tasks[0].startTime).toEqual(new Date(2013, 0, 1));
expect(tasks[0].endTime).toEqual(new Date(2013, 1, 1));
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('task1');

expect(tasks[1].id).toEqual('id2');
expect(tasks[1].task).toEqual('task2');
expect(tasks[1].startTime).toEqual(new Date(2013, 0, 10));
expect(tasks[1].endTime).toEqual(new Date(2013, 1, 1));
});

it('should handle relative start date based on multiple id', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('sec1');
ganttDb.addTask('task1', 'id1,after id2 id3 id4,1d');
ganttDb.addTask('task2', 'id2,2013-01-01,1d');
ganttDb.addTask('task3', 'id3,2013-02-01,3d');
ganttDb.addTask('task4', 'id4,2013-02-01,2d');

const tasks = ganttDb.getTasks();

expect(tasks[0].endTime).toEqual(new Date(2013, 1, 5));
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('task1');
});

it('should handle relative end date based on multiple id', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.addSection('sec1');
ganttDb.addTask('task1', 'id1,2013-01-01,until id2 id3 id4');
ganttDb.addTask('task2', 'id2,2013-01-11,1d');
ganttDb.addTask('task3', 'id3,2013-02-10,1d');
ganttDb.addTask('task4', 'id4,2013-02-12,1d');

const tasks = ganttDb.getTasks();

expect(tasks[0].endTime).toEqual(new Date(2013, 0, 11));
expect(tasks[0].id).toEqual('id1');
expect(tasks[0].task).toEqual('task1');
});

it('should ignore weekends', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.setExcludes('weekends 2019-02-06,friday');
Expand Down
32 changes: 32 additions & 0 deletions packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,38 @@ describe('when parsing a gantt diagram it', function () {
expect(tasks[0].id).toEqual('des1');
expect(tasks[0].task).toEqual('Design jison grammar');
});
it('should handle a task with start/end time relative to other tasks', function () {
const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' +
'task A: a, 2024-01-27, 2024-01-28\n' +
'task B: b, after a, 2024-01-30\n' +
'task C: c, 2024-01-20, until a\n' +
'task D: d, after c, until b';

expect(parserFnConstructor(str)).not.toThrow();

const tasks = parser.yy.getTasks();

expect(tasks[0].startTime).toEqual(new Date(2024, 0, 27));
expect(tasks[0].endTime).toEqual(new Date(2024, 0, 28));
expect(tasks[0].id).toEqual('a');
expect(tasks[0].task).toEqual('task A');
expect(tasks[1].startTime).toEqual(new Date(2024, 0, 28));
expect(tasks[1].endTime).toEqual(new Date(2024, 0, 30));
expect(tasks[1].id).toEqual('b');
expect(tasks[1].task).toEqual('task B');
expect(tasks[2].startTime).toEqual(new Date(2024, 0, 20));
expect(tasks[2].endTime).toEqual(new Date(2024, 0, 27));
expect(tasks[2].id).toEqual('c');
expect(tasks[2].task).toEqual('task C');
expect(tasks[3].startTime).toEqual(new Date(2024, 0, 27));
expect(tasks[3].endTime).toEqual(new Date(2024, 0, 28));
expect(tasks[3].id).toEqual('d');
expect(tasks[3].task).toEqual('task D');
});
it.each(convert`
tags | milestone | done | crit | active
${'milestone'} | ${true} | ${false} | ${false} | ${false}
Expand Down
Loading
Loading