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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/4060 gantt working hours #5403

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/syntax/gantt.md
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
```
Comment on lines +483 to +492
Copy link
Member

Choose a reason for hiding this comment

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

Can we use the YAML config for this, instead of adding more custom syntax?

Suggested change
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
```
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.
```
---
title: A Gantt Diagram
config:
gantt:
dateFormat: YYYY-MM-DD
workdayStartTime: 08:00
workdayEndTime: 17:00
---
gantt
accTitle: A simple sample gantt diagram
accDescr: 2 sections with 2 tasks each, from 2014
```

Copy link
Author

Choose a reason for hiding this comment

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

Sorry can you explain in a bit more detail what you mean? Does this relate to not using jison?


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
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 = {};
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
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
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
Expand Up @@ -21,6 +21,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
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