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 #5403

Open
wants to merge 11 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
```
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?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it was to move new configurations into the yaml config section, and not to add more keywords in jison.
I only edited the documentation, to reflect how it'll look after the change.

Copy link
Author

Choose a reason for hiding this comment

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

@nirname
The below is created within the config.type.ts automatically by the YAML config file (wdStartTime and wdEndTime are the two new pieces of code. However, I want to import this into the GanttDB.js file but as i understand it 'interface' is a typescript definition that cannot be imported in a plain JS file. What methods are there for getting around this? Or is it purely a need to re-write the GanttDB file?

export interface GanttDiagramConfig extends BaseDiagramConfig {
  /**
   * Margin top for the text over the diagram
   */
  titleTopMargin?: number;
  /**
   * The height of the bars in the graph
   */
  barHeight?: number;
  /**
   * The margin between the different activities in the gantt diagram
   */
  barGap?: number;
  /**
   * Margin between title and gantt diagram and between axis and gantt diagram.
   *
   */
  topPadding?: number;
  /**
   * The space allocated for the section name to the right of the activities
   *
   */
  rightPadding?: number;
  /**
   * The space allocated for the section name to the left of the activities
   *
   */
  leftPadding?: number;
  /**
   * Vertical starting position of the grid lines
   */
  gridLineStartPadding?: number;
  /**
   * Font size
   */
  fontSize?: number;
  /**
   * Font size for sections
   */
  sectionFontSize?: string | number;
  /**
   * The number of alternating section styles
   */
  numberSectionStyles?: number;
  /**
   * Date/time format of the axis
   *
   * This might need adjustment to match your locale and preferences.
   *
   */
  axisFormat?: string;
  /**
   * axis ticks
   *
   * Pattern is:
   *
   * ```javascript
   * /^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
   * ```
   *
   */
  tickInterval?: string;
  /**
   * When this flag is set, date labels will be added to the top of the chart
   *
   */
  topAxis?: boolean;
  /**
   * Controls the display mode.
   *
   */
  displayMode?: '' | 'compact';
  /**
   * On which day a week-based interval should start
   *
   */
  weekday?: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
  /**
   * Allows user to set the start time for the Workday. Used in calculating end times for tasks when set in hours
   *
   */
  wdStartTime?: string;
  /**
   * Allows user to set the end time for workday
   *
   */
  wdEndTime?: string;


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 = {};
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 @@ -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
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