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

Feature/4060 gantt working hours #6149

Draft
wants to merge 13 commits into
base: develop
Choose a base branch
from
15 changes: 15 additions & 0 deletions docs/syntax/gantt.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,21 @@ To hide the marker, set `todayMarker` to `off`.
todayMarker off
```

## Working hours and durations

You can assign core working hours within the Gantt by providing a time value to `wdStartTime` and `wdEndTime`. It expects a time in the 24hour format as shown below.

```gantt
title A Gantt Diagram
accTitle: A simple sample gantt diagram
accDescr: 2 sections with 2 tasks each, from 2014
dateFormat YYYY-MM-DD
wdStartTime 08:00
wdEndTime 17:00
```

When a start and end time is provided alongside task durations in hours and/or minutes the task end date will be calculated using the working hours between the start and end time

## Configuration

It is possible to adjust the margins for rendering the gantt diagram.
Expand Down
80 changes: 76 additions & 4 deletions packages/mermaid/src/diagrams/gantt/ganttDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dayjs from 'dayjs';
import dayjsIsoWeek from 'dayjs/plugin/isoWeek.js';
import dayjsCustomParseFormat from 'dayjs/plugin/customParseFormat.js';
import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat.js';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js';
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import utils from '../../utils.js';
Expand All @@ -20,6 +21,7 @@ import {
dayjs.extend(dayjsIsoWeek);
dayjs.extend(dayjsCustomParseFormat);
dayjs.extend(dayjsAdvancedFormat);
dayjs.extend(isSameOrAfter);

const WEEKEND_START_DAY = { friday: 5, saturday: 6 };
let dateFormat = '';
Expand All @@ -38,6 +40,8 @@ let funs = [];
let inclusiveEndDates = false;
let topAxis = false;
let weekday = 'sunday';
let wdStartTime = undefined;
let wdEndTime = undefined;
let weekend = 'saturday';

// The serial order of the task in the script
Expand Down Expand Up @@ -65,6 +69,8 @@ export const clear = function () {
links = new Map();
commonClear();
weekday = 'sunday';
wdStartTime = undefined;
wdEndTime = undefined;
weekend = 'saturday';
};

Expand Down Expand Up @@ -96,6 +102,22 @@ export const setDateFormat = function (txt) {
dateFormat = txt;
};

export const setWDStartTime = function (txt) {
wdStartTime = dayjs(txt, 'HH:mm');
};

export const getWDStartTime = function () {
return wdStartTime;
};

export const setWDEndTime = function (txt) {
wdEndTime = dayjs(txt, 'HH:mm');
};

export const getWDEndTime = function () {
return wdEndTime;
};

export const enableInclusiveEndDates = function () {
inclusiveEndDates = true;
};
Expand Down Expand Up @@ -345,7 +367,7 @@ const parseDuration = function (str) {
return [NaN, 'ms'];
};

const getEndDate = function (prevTime, dateFormat, str, inclusive = false) {
const getEndDate = function (prevTime, dateFormat, str, inclusive = false, wdStartTime, wdEndTime) {
str = str.trim();

// test for until
Expand Down Expand Up @@ -382,6 +404,41 @@ const getEndDate = function (prevTime, dateFormat, str, inclusive = false) {
let endTime = dayjs(prevTime);
const [durationValue, durationUnit] = parseDuration(str);
if (!Number.isNaN(durationValue)) {
if (wdStartTime != undefined && wdEndTime != undefined && durationUnit == 'h') {
let currentTime = prevTime;
let durationTime = durationValue;
let wdEndTimeCompare = dayjs(currentTime)
.clone()
.hour(wdEndTime.hour())
.minute(wdEndTime.minute());
let wdStartTimeCompare = dayjs(currentTime)
.clone()
.hour(wdStartTime.hour())
.minute(wdStartTime.minute());
while (
durationTime > 0 &&
dayjs(currentTime).isBefore(wdEndTimeCompare, 'hour') &&
dayjs(currentTime).isSameOrAfter(wdStartTimeCompare, 'hour')
) {
currentTime = dayjs(currentTime).add(1, 'hour');
durationTime -= 1;

if (dayjs(currentTime).isSameOrAfter(wdEndTimeCompare, 'hour')) {
currentTime = dayjs(currentTime)
.add(1, 'day')
.clone()
.hour(wdStartTime.hour())
.minute(wdStartTime.minute());
wdEndTimeCompare = dayjs(currentTime)
.add(1, 'day')
.clone()
.hour(wdStartTime.hour())
.minute(wdStartTime.minute());
}
}
return currentTime;
}

const newEndTime = endTime.add(durationValue, durationUnit);
if (newEndTime.isValid()) {
endTime = newEndTime;
Expand Down Expand Up @@ -450,7 +507,14 @@ const compileData = function (prevTask, dataStr) {
}

if (endTimeData) {
task.endTime = getEndDate(task.startTime, dateFormat, endTimeData, inclusiveEndDates);
task.endTime = getEndDate(
task.startTime,
dateFormat,
endTimeData,
inclusiveEndDates,
wdStartTime,
wdEndTime
);
task.manualEndTime = dayjs(endTimeData, 'YYYY-MM-DD', true).isValid();
checkTaskDates(task, dateFormat, excludes, includes);
}
Expand Down Expand Up @@ -597,14 +661,18 @@ const compileTasks = function () {
rawTasks[pos].startTime,
dateFormat,
rawTasks[pos].raw.endTime.data,
inclusiveEndDates
inclusiveEndDates,
wdStartTime,
wdEndTime
);
if (rawTasks[pos].endTime) {
rawTasks[pos].processed = true;
rawTasks[pos].manualEndTime = dayjs(
rawTasks[pos].raw.endTime.data,
'YYYY-MM-DD',
true
true,
wdStartTime,
wdEndTime
).isValid();
checkTaskDates(rawTasks[pos], dateFormat, excludes, includes);
}
Expand Down Expand Up @@ -792,6 +860,10 @@ export default {
isInvalidDate,
setWeekday,
getWeekday,
setWDStartTime,
getWDStartTime,
setWDEndTime,
getWDEndTime,
setWeekend,
};

Expand Down
33 changes: 33 additions & 0 deletions packages/mermaid/src/diagrams/gantt/ganttDb.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,37 @@ describe('when using the ganttDb', function () {
ganttDb.addTask('test1', 'id1,202304,1d');
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
});

describe('when calculating task end times with working hours', function () {
beforeEach(function () {
ganttDb.clear();
ganttDb.setDateFormat('YYYY-MM-DD HH:mm');
ganttDb.setWDStartTime('09:00');
ganttDb.setWDEndTime('17:00');
});

it('should calculate end time extending to the next working day', function () {
ganttDb.addTask('task2', 'id2,2024-01-01 16:00, 3h');
const tasks = ganttDb.getTasks();
expect(dayjs(tasks[0].endTime).toISOString()).toEqual(
dayjs(new Date('2024-01-02 11:00')).toISOString()
);
});

it('should handle tasks spanning multiple days', function () {
ganttDb.addTask('task3', 'id3,2024-01-01 09:00, 16h');
const tasks = ganttDb.getTasks();
expect(dayjs(tasks[0].endTime).toISOString()).toEqual(
dayjs(new Date('2024-01-02 17:00')).toISOString()
);
});

it('should handle tasks within the same working day', function () {
ganttDb.addTask('task4', 'id4,2024-01-01 09:00, 3h');
const tasks = ganttDb.getTasks();
expect(dayjs(tasks[0].endTime).toISOString()).toEqual(
dayjs(new Date('2024-01-01 12:00')).toISOString()
);
});
});
});
4 changes: 4 additions & 0 deletions packages/mermaid/src/diagrams/gantt/parser/gantt.jison
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ that id.

"gantt" return 'gantt';
"dateFormat"\s[^#\n;]+ return 'dateFormat';
"wdStartTime"\s[^#\n;]+ return 'wdStartTime';
"wdEndTime"\s[^#\n;]+ return 'wdEndTime';
"inclusiveEndDates" return 'inclusiveEndDates';
"topAxis" return 'topAxis';
"axisFormat"\s[^#\n;]+ return 'axisFormat';
Expand Down Expand Up @@ -137,6 +139,8 @@ weekend

statement
: dateFormat {yy.setDateFormat($1.substr(11));$$=$1.substr(11);}
| wdStartTime {yy.setWDStartTime($1.substr(12));$$=$1.substr(12);}
| wdEndTime {yy.setWDEndTime($1.substr(10));$$=$1.substr(10);}
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
| topAxis {yy.TopAxis();$$=$1.substr(8);}
| axisFormat {yy.setAxisFormat($1.substr(11));$$=$1.substr(11);}
Expand Down
16 changes: 16 additions & 0 deletions packages/mermaid/src/diagrams/gantt/parser/gantt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ describe('when parsing a gantt diagram it', function () {
expect(parserFnConstructor(str)).not.toThrow();
});

it('should handle a wdStartTime definition', function () {
spyOn(ganttDb, 'setWDStartTime');
const str = 'gantt\nwdStartTime 09:00';

expect(parserFnConstructor(str)).not.toThrow();
expect(ganttDb.setWDStartTime).toHaveBeenCalledWith('09:00');
});

it('should handle a wdEndTime definition', function () {
spyOn(ganttDb, 'setWDEndTime');
const str = 'gantt\nwdEndTime 17:00';

expect(parserFnConstructor(str)).not.toThrow();
expect(ganttDb.setWDEndTime).toHaveBeenCalledWith('17:00');
});

it('should handle a inclusive end date definition', function () {
const str = 'gantt\ndateFormat yyyy-mm-dd\ninclusiveEndDates';

Expand Down
15 changes: 15 additions & 0 deletions packages/mermaid/src/docs/syntax/gantt.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,21 @@ To hide the marker, set `todayMarker` to `off`.
todayMarker off
```

## Working hours and durations

You can assign core working hours within the Gantt by providing a time value to `wdStartTime` and `wdEndTime`. It expects a time in the 24hour format as shown below.

```gantt
title A Gantt Diagram
accTitle: A simple sample gantt diagram
accDescr: 2 sections with 2 tasks each, from 2014
dateFormat YYYY-MM-DD
wdStartTime 08:00
wdEndTime 17:00
```

When a start and end time is provided alongside task durations in hours and/or minutes the task end date will be calculated using the working hours between the start and end time

## Configuration

It is possible to adjust the margins for rendering the gantt diagram.
Expand Down
Loading
Loading