diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index cb65f73b0b..f3e4db8601 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -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 + + 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 + `, + {} + ); + }); }); diff --git a/docs/syntax/gantt.md b/docs/syntax/gantt.md index 8e64a268aa..b6c9b03ddb 100644 --- a/docs/syntax/gantt.md +++ b/docs/syntax/gantt.md @@ -257,6 +257,36 @@ The pattern is: More info in: +### 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. diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 3964027025..cdc54c3288 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -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 = ''; @@ -55,6 +58,9 @@ export const clear = function () { lastTaskID = undefined; rawTasks = []; dateFormat = ''; + dateRange = ''; + startDateRange = ''; + endDateRange = ''; axisFormat = ''; displayMode = ''; tickInterval = undefined; @@ -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; }; @@ -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,]+/); }; @@ -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; }; @@ -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; } @@ -726,6 +787,10 @@ export default { setDateFormat, getDateFormat, enableInclusiveEndDates, + setDateRange, + getDateRange, + getStartRange, + getEndRange, endDatesAreInclusive, enableTopAxis, topAxisEnabled, diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts index 416368e8f9..9aef743b1a 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts @@ -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'); @@ -49,6 +50,9 @@ describe('when using the ganttDb', function () { ${'getAccTitle'} | ${''} ${'getAccDescription'} | ${''} ${'getDateFormat'} | ${''} + ${'getDateRange'} | ${''} + ${'getStartRange'} | ${''} + ${'getEndRange'} | ${''} ${'getAxisFormat'} | ${''} ${'getTodayMarker'} | ${''} ${'getExcludes'} | ${[]} @@ -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'); @@ -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); }); }); diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index 215a4df29d..34434389cc 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -3,8 +3,6 @@ import { log } from '../../logger.js'; import { select, scaleTime, - min, - max, scaleLinear, interpolateHcl, axisBottom, @@ -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]); /** diff --git a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison index 0eb45ec416..4a00c876b7 100644 --- a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison +++ b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison @@ -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'; @@ -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);} diff --git a/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js b/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js index 020bab0ed3..2710327a9f 100644 --- a/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js +++ b/packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js @@ -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'; diff --git a/packages/mermaid/src/docs/syntax/gantt.md b/packages/mermaid/src/docs/syntax/gantt.md index 710b39e521..ee02e3065b 100644 --- a/packages/mermaid/src/docs/syntax/gantt.md +++ b/packages/mermaid/src/docs/syntax/gantt.md @@ -189,6 +189,26 @@ The pattern is: More info in: [https://github.com/d3/d3-time#interval_every](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 +``` + +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.