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

Add custom Gantt Date Range #4563

108 changes: 108 additions & 0 deletions cypress/integration/rendering/gantt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,112 @@ describe('Gantt diagram', () => {
{}
);
});

it('should render when there is a dateRange', () => {
imgSnapshotTest(
`
---
displayMode: compact
---
gantt
title GANTT compact
dateFormat HH:mm:ss
dateRange 12:30:00,13:30:00
axisFormat %Hh%M
Comment on lines +495 to +504
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the imgSnapshotTests, for the other scenarios of dateRange?

  • Actual date
  • Only start
  • Only end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly, but how do these work? Where are the images produced so that I can check them? I suspect that without altering the tasks, the display is going to be messed up, with the task bars invading the margins, but I don't know how to produce any diagram output from this repo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To verify snapshots:

pnpm e2e
open cypress/snapshots

Can you also add this into the contribution docs if it's not present there?


section DB Clean
Clean: 12:00:00, 10m
Clean: 12:30:00, 12m
Clean: 13:00:00, 8m
Clean: 13:30:00, 9m
Clean: 14:00:00, 13m
Clean: 14:30:00, 10m
Clean: 15:00:00, 11m

section Sessions
A: 12:00:00, 63m
B: 12:30:00, 12m
C: 13:05:00, 12m
D: 13:06:00, 33m
E: 13:15:00, 55m
F: 13:20:00, 12m
G: 13:32:00, 18m
H: 13:50:00, 20m
I: 14:10:00, 10m
`,
{}
);
});

it('should render with a start-only dateRange', () => {
imgSnapshotTest(
`
---
displayMode: compact
---
gantt
title GANTT compact
dateFormat HH:mm:ss
dateRange 12:30:00
axisFormat %Hh%M

section DB Clean
Clean: 12:00:00, 10m
Clean: 12:30:00, 12m
Clean: 13:00:00, 8m
Clean: 13:30:00, 9m
Clean: 14:00:00, 13m
Clean: 14:30:00, 10m
Clean: 15:00:00, 11m

section Sessions
A: 12:00:00, 63m
B: 12:30:00, 12m
C: 13:05:00, 12m
D: 13:06:00, 33m
E: 13:15:00, 55m
F: 13:20:00, 12m
G: 13:32:00, 18m
H: 13:50:00, 20m
I: 14:10:00, 10m
`,
{}
);
});

it('should render with an end-only dateRange', () => {
imgSnapshotTest(
`
---
displayMode: compact
---
gantt
title GANTT compact
dateFormat HH:mm:ss
dateRange ,13:30:00
axisFormat %Hh%M

section DB Clean
Clean: 12:00:00, 10m
Clean: 12:30:00, 12m
Clean: 13:00:00, 8m
Clean: 13:30:00, 9m
Clean: 14:00:00, 13m
Clean: 14:30:00, 10m
Clean: 15:00:00, 11m

section Sessions
A: 12:00:00, 63m
B: 12:30:00, 12m
C: 13:05:00, 12m
D: 13:06:00, 33m
E: 13:15:00, 55m
F: 13:20:00, 12m
G: 13:32:00, 18m
H: 13:50:00, 20m
I: 14:10:00, 10m
`,
{}
);
});
});
30 changes: 30 additions & 0 deletions docs/syntax/gantt.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,36 @@ The pattern is:

More info in: <https://github.com/d3/d3-time#interval_every>

### Date Ranges

The default behavior is to set the x range to include all of the data in the chart. You can override
the range by specifying `dateRange`. Any tasks falling outside the specified range will be removed
from the plot, and tasks overlapping with the boundaries will be truncated. The `dateRange` command
should follow the `dateFormat` and use the same format.

```mermaid-example
gantt
dateFormat HH:mm
dateRange 13:00, 14:00
axisFormat %H:%M
Task A : 13:24, 13:39
Task B : 13:50, 14:35
Dropped : 14:01, 14:20
```

```mermaid
gantt
dateFormat HH:mm
dateRange 13:00, 14:00
axisFormat %H:%M
Task A : 13:24, 13:39
Task B : 13:50, 14:35
Dropped : 14:01, 14:20
```

It is also possible to specify only a lower bound using `dateRange lower` or only an upper bound
using `dateRange ,upper`.

## Output in compact mode

The compact mode allows you to display multiple tasks in the same row. Compact mode can be enabled for a gantt chart by setting the display mode of the graph via preceeding YAML settings.
Expand Down
67 changes: 66 additions & 1 deletion packages/mermaid/src/diagrams/gantt/ganttDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ dayjs.extend(dayjsCustomParseFormat);
dayjs.extend(dayjsAdvancedFormat);

let dateFormat = '';
let dateRange = '';
let startDateRange = '';
let endDateRange = '';
let axisFormat = '';
let tickInterval = undefined;
let todayMarker = '';
Expand Down Expand Up @@ -55,6 +58,9 @@ export const clear = function () {
lastTaskID = undefined;
rawTasks = [];
dateFormat = '';
dateRange = '';
startDateRange = '';
endDateRange = '';
axisFormat = '';
displayMode = '';
tickInterval = undefined;
Expand Down Expand Up @@ -100,6 +106,22 @@ export const enableInclusiveEndDates = function () {
inclusiveEndDates = true;
};

export const setDateRange = function (txt) {
dateRange = txt;

if (!dateRange) {
return;
}
const [startStr, endStr] = dateRange.split(',');

if (startStr) {
startDateRange = getStartDate(undefined, dateFormat, startStr);
}
if (endStr) {
endDateRange = getEndDate(undefined, dateFormat, endStr);
}
};

export const endDatesAreInclusive = function () {
return inclusiveEndDates;
};
Expand All @@ -124,6 +146,34 @@ export const getDateFormat = function () {
return dateFormat;
};

export const getDateRange = function () {
return dateRange;
};

export const getStartRange = function () {
if (startDateRange) {
return startDateRange;
}
if (getTasks().length > 0) {
return getTasks().reduce((min, task) => {
return task.startTime < min ? task.startTime : min;
}, Infinity);
}
return '';
};

export const getEndRange = function () {
if (endDateRange) {
return endDateRange;
}
if (getTasks().length > 0) {
return getTasks().reduce((max, task) => {
return task.endTime > max ? task.endTime : max;
}, -Infinity);
}
return '';
};

export const setIncludes = function (txt) {
includes = txt.toLowerCase().split(/[\s,]+/);
};
Expand Down Expand Up @@ -162,6 +212,17 @@ export const getTasks = function () {
}

tasks = rawTasks;
if (dateRange != '') {
tasks = tasks.filter(function (task) {
if (
(startDateRange && task.endTime <= startDateRange) ||
(endDateRange && task.startTime >= endDateRange)
) {
return false;
}
return true;
});
}

return tasks;
};
Expand Down Expand Up @@ -298,7 +359,7 @@ const getStartDate = function (prevTime, dateFormat, str) {
d.getFullYear() < -10000 ||
d.getFullYear() > 10000
) {
throw new Error('Invalid date:' + str);
throw new Error(`Invalid date: '${str}' with date format: '${dateFormat}'`);
}
return d;
}
Expand Down Expand Up @@ -726,6 +787,10 @@ export default {
setDateFormat,
getDateFormat,
enableInclusiveEndDates,
setDateRange,
getDateRange,
getStartRange,
getEndRange,
endDatesAreInclusive,
enableTopAxis,
topAxisEnabled,
Expand Down
52 changes: 51 additions & 1 deletion packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('when using the ganttDb', function () {
describe('when calling the clear function', function () {
beforeEach(function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.setDateRange('2019-02-01, 2019-03-01');
ganttDb.enableInclusiveEndDates();
ganttDb.setDisplayMode('compact');
ganttDb.setTodayMarker('off');
Expand All @@ -49,6 +50,9 @@ describe('when using the ganttDb', function () {
${'getAccTitle'} | ${''}
${'getAccDescription'} | ${''}
${'getDateFormat'} | ${''}
${'getDateRange'} | ${''}
${'getStartRange'} | ${''}
${'getEndRange'} | ${''}
${'getAxisFormat'} | ${''}
${'getTodayMarker'} | ${''}
${'getExcludes'} | ${[]}
Expand Down Expand Up @@ -158,6 +162,28 @@ 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 fixed date ranges', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.setDateRange('2023-07-01, 2023-07-30');
ganttDb.addSection('testa1');
ganttDb.addTask('test1', 'id1,2013-07-01,2w');
ganttDb.addTask('test2', 'id2,2023-06-25,2w');
ganttDb.addSection('testa2');
ganttDb.addTask('test3', 'id3,after id2,2d');

const tasks = ganttDb.getTasks();

expect(tasks.length).toEqual(2);
expect(tasks[0].startTime).toEqual(new Date(2023, 5, 25));
expect(tasks[0].endTime).toEqual(new Date(2023, 6, 9));
expect(tasks[0].id).toEqual('id2');
expect(tasks[0].task).toEqual('test2');

expect(tasks[1].id).toEqual('id3');
expect(tasks[1].task).toEqual('test3');
expect(tasks[1].startTime).toEqual(new Date(2023, 6, 9));
expect(tasks[1].endTime).toEqual(new Date(2023, 6, 11));
});
it('should ignore weekends', function () {
ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.setExcludes('weekends 2019-02-06,friday');
Expand Down Expand Up @@ -436,6 +462,30 @@ describe('when using the ganttDb', function () {
it('should reject dates with ridiculous years', function () {
ganttDb.setDateFormat('YYYYMMDD');
ganttDb.addTask('test1', 'id1,202304,1d');
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
expect(() => ganttDb.getTasks()).toThrowError(
"Invalid date: '202304' with date format: 'YYYYMMDD'"
);
});

it.each(convert`
testName | dateRange | expStartRange | expEndRange | expTasksLength
${'No dateRange'} | ${''} | ${'2023-06-01'} | ${'2023-07-07'} | ${2}
${'Wide dateRange'} | ${'2023-01-01, 2023-12-31'} | ${'2023-01-01'} | ${'2023-12-31'} | ${2}
${'Narrow dateRange'} | ${'2023-06-29, 2023-06-30'} | ${'2023-06-29'} | ${'2023-06-30'} | ${0}
${'Overlapping dateRange'} | ${'2023-06-06, 2023-07-03'} | ${'2023-06-06'} | ${'2023-07-03'} | ${2}
${'Starting dateRange'} | ${'2023-06-06'} | ${'2023-06-06'} | ${'2023-07-07'} | ${2}
${'Ending dateRange'} | ${',2023-06-06'} | ${'2023-06-01'} | ${'2023-06-06'} | ${1}
`)('$testName', ({ dateFormat, dateRange, expStartRange, expEndRange, expTasksLength }) => {
ganttDb.setDateFormat('YYYY-MM-DD');
expect('').toEqual(ganttDb.getDateRange());
ganttDb.setDateRange(dateRange);
ganttDb.addTask('task1', 't1, 2023-06-01, 2023-06-07');
ganttDb.addTask('task2', 't2, 2023-07-02, 2023-07-07');
const tasks = ganttDb.getTasks();
const startRange = ganttDb.getStartRange();
const endRange = ganttDb.getEndRange();
expect(expTasksLength).toEqual(tasks.length);
expect(dayjs(expStartRange, 'YYYY-MM-DD').toDate()).toEqual(startRange);
expect(dayjs(expEndRange, 'YYYY-MM-DD').toDate()).toEqual(endRange);
});
});
11 changes: 1 addition & 10 deletions packages/mermaid/src/diagrams/gantt/ganttRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { log } from '../../logger.js';
import {
select,
scaleTime,
min,
max,
scaleLinear,
interpolateHcl,
axisBottom,
Expand Down Expand Up @@ -126,14 +124,7 @@ export const draw = function (text, id, version, diagObj) {

// Set timescale
const timeScale = scaleTime()
.domain([
min(taskArray, function (d) {
return d.startTime;
}),
max(taskArray, function (d) {
return d.endTime;
}),
])
.domain([diagObj.db.getStartRange(), diagObj.db.getEndRange()])
.rangeRound([0, w - conf.leftPadding - conf.rightPadding]);

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/mermaid/src/diagrams/gantt/parser/gantt.jison
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ that id.
"gantt" return 'gantt';
"dateFormat"\s[^#\n;]+ return 'dateFormat';
"inclusiveEndDates" return 'inclusiveEndDates';
"dateRange"\s[^#\n;]+ return 'dateRange';
"topAxis" return 'topAxis';
"axisFormat"\s[^#\n;]+ return 'axisFormat';
"tickInterval"\s[^#\n;]+ return 'tickInterval';
Expand Down Expand Up @@ -124,6 +125,7 @@ line
statement
: dateFormat {yy.setDateFormat($1.substr(11));$$=$1.substr(11);}
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
| dateRange {yy.setDateRange($1.substr(11));$$=$1.substr(11);}
| topAxis {yy.TopAxis();$$=$1.substr(8);}
| axisFormat {yy.setAxisFormat($1.substr(11));$$=$1.substr(11);}
| tickInterval {yy.setTickInterval($1.substr(13));$$=$1.substr(13);}
Expand Down
5 changes: 5 additions & 0 deletions packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ describe('when parsing a gantt diagram it', function () {

expect(parserFnConstructor(str)).not.toThrow();
});
it('should handle a dateRange definition', function () {
const str = 'gantt\ndateRange : 2023-06-01, 2023-07-01';

expect(parserFnConstructor(str)).not.toThrow();
});
it('should handle a title definition', function () {
const str = 'gantt\ndateFormat yyyy-mm-dd\ntitle Adding gantt diagram functionality to mermaid';

Expand Down
Loading