Merge pull request #4238 from JeremyFunk/feature/1618_repeating_tasks

Implement repeating tasks
This commit is contained in:
Knut Sveidqvist 2023-03-31 08:43:13 +02:00 committed by GitHub
commit 3823ecafb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 238 additions and 57 deletions

View File

@ -435,4 +435,39 @@ describe('Gantt diagram', () => {
{ gantt: { topAxis: true } } { gantt: { topAxis: true } }
); );
}); });
it('should render when compact is true', () => {
imgSnapshotTest(
`
---
displayMode: compact
---
gantt
title GANTT compact
dateFormat HH:mm:ss
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
`,
{}
);
});
}); });

View File

@ -78,7 +78,7 @@
axisFormat %d/%m axisFormat %d/%m
todayMarker off todayMarker off
section Section1 section Section1
Today: 1, -01:00, 5min Today: 1, 08-08-09-01:00, 5min
</pre> </pre>
<hr /> <hr />
@ -89,7 +89,7 @@
axisFormat %d/%m axisFormat %d/%m
todayMarker stroke-width:5px,stroke:#00f,opacity:0.5 todayMarker stroke-width:5px,stroke:#00f,opacity:0.5
section Section1 section Section1
Today: 1, -01:00, 5min Today: 1, 08-08-09-01:00, 5min
</pre> </pre>
<hr /> <hr />
@ -166,6 +166,37 @@
</pre> </pre>
<hr /> <hr />
<pre class="mermaid">
---
displayMode: compact
---
gantt
title GANTT compact
dateFormat HH:mm:ss
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
</pre>
<hr />
<script> <script>
function ganttTestClick(a, b, c) { function ganttTestClick(a, b, c) {
console.log('a:', a); console.log('a:', a);

View File

@ -14,7 +14,7 @@
#### Defined in #### Defined in
[defaultConfig.ts:2093](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2093) [defaultConfig.ts:2103](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L2103)
--- ---

View File

@ -88,6 +88,7 @@ const config = {
numberSectionStyles: 4, numberSectionStyles: 4,
axisFormat: '%Y-%m-%d', axisFormat: '%Y-%m-%d',
topAxis: false, topAxis: false,
displayMode: '',
}, },
}; };
mermaid.initialize(config); mermaid.initialize(config);

View File

@ -257,9 +257,41 @@ The pattern is:
More info in: <https://github.com/d3/d3-time#interval_every> More info in: <https://github.com/d3/d3-time#interval_every>
## 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.
```mermaid-example
---
displayMode: compact
---
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :a2, 2014-01-20, 25d
Another one :a3, 2014-02-10, 20d
```
```mermaid
---
displayMode: compact
---
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :a2, 2014-01-20, 25d
Another one :a3, 2014-02-10, 20d
```
## Comments ## Comments
Comments can be entered within a gantt chart, which will be ignored by the parser. Comments need to be on their own line and must be prefaced with `%%` (double percent signs). Any text after the start of the comment to the next newline will be treated as a comment, including any diagram syntax Comments can be entered within a gantt chart, which will be ignored by the parser. Comments need to be on their own line and must be prefaced with `%%` (double percent signs). Any text after the start of the comment to the next newline will be treated as a comment, including any diagram syntax.
```mermaid-example ```mermaid-example
gantt gantt

View File

@ -3,6 +3,7 @@ import { getConfig } from './config';
let title = ''; let title = '';
let diagramTitle = ''; let diagramTitle = '';
let description = ''; let description = '';
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig()); const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
export const clear = function (): void { export const clear = function (): void {
@ -36,10 +37,10 @@ export const getDiagramTitle = function (): string {
}; };
export default { export default {
setAccTitle,
getAccTitle, getAccTitle,
setAccTitle,
getDiagramTitle,
setDiagramTitle, setDiagramTitle,
getDiagramTitle: getDiagramTitle,
getAccDescription, getAccDescription,
setAccDescription, setAccDescription,
clear, clear,

View File

@ -335,6 +335,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
axisFormat?: string; axisFormat?: string;
tickInterval?: string; tickInterval?: string;
topAxis?: boolean; topAxis?: boolean;
displayMode?: string;
} }
export interface SequenceDiagramConfig extends BaseDiagramConfig { export interface SequenceDiagramConfig extends BaseDiagramConfig {

View File

@ -659,6 +659,17 @@ const config: Partial<MermaidConfig> = {
*/ */
numberSectionStyles: 4, numberSectionStyles: 4,
/**
* | Parameter | Description | Type | Required | Values |
* | ----------- | ------------------------- | ------ | -------- | --------- |
* | displayMode | Controls the display mode | string | 4 | 'compact' |
*
* **Notes**:
*
* - **compact**: Enables displaying multiple tasks on the same row.
*/
displayMode: '',
/** /**
* | Parameter | Description | Type | Required | Values | * | Parameter | Description | Type | Required | Values |
* | ---------- | ---------------------------- | ---- | -------- | ---------------- | * | ---------- | ---------------------------- | ---- | -------- | ---------------- |
@ -684,7 +695,6 @@ const config: Partial<MermaidConfig> = {
* Default value: undefined * Default value: undefined
*/ */
tickInterval: undefined, tickInterval: undefined,
/** /**
* | Parameter | Description | Type | Required | Values | * | Parameter | Description | Type | Required | Values |
* | ----------- | ----------- | ------- | -------- | ----------- | * | ----------- | ----------- | ------- | -------- | ----------- |

View File

@ -11,6 +11,8 @@ export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
type FrontMatterMetadata = { type FrontMatterMetadata = {
title?: string; title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string;
}; };
/** /**
@ -33,6 +35,10 @@ export function extractFrontMatter(text: string, db: DiagramDb): string {
db.setDiagramTitle?.(parsed.title); db.setDiagramTitle?.(parsed.title);
} }
if (parsed?.displayMode) {
db.setDisplayMode?.(parsed.displayMode);
}
return text.slice(matches[0].length); return text.slice(matches[0].length);
} else { } else {
return text; return text;

View File

@ -16,6 +16,7 @@ export interface InjectUtils {
export interface DiagramDb { export interface DiagramDb {
clear?: () => void; clear?: () => void;
setDiagramTitle?: (title: string) => void; setDiagramTitle?: (title: string) => void;
setDisplayMode?: (title: string) => void;
getAccTitle?: () => string; getAccTitle?: () => string;
getAccDescription?: () => string; getAccDescription?: () => string;
bindFunctions?: (element: Element) => void; bindFunctions?: (element: Element) => void;

View File

@ -32,6 +32,7 @@ let links = {};
let sections = []; let sections = [];
let tasks = []; let tasks = [];
let currentSection = ''; let currentSection = '';
let displayMode = '';
const tags = ['active', 'done', 'crit', 'milestone']; const tags = ['active', 'done', 'crit', 'milestone'];
let funs = []; let funs = [];
let inclusiveEndDates = false; let inclusiveEndDates = false;
@ -55,6 +56,7 @@ export const clear = function () {
rawTasks = []; rawTasks = [];
dateFormat = ''; dateFormat = '';
axisFormat = ''; axisFormat = '';
displayMode = '';
tickInterval = undefined; tickInterval = undefined;
todayMarker = ''; todayMarker = '';
includes = []; includes = [];
@ -110,6 +112,14 @@ export const topAxisEnabled = function () {
return topAxis; return topAxis;
}; };
export const setDisplayMode = function (txt) {
displayMode = txt;
};
export const getDisplayMode = function () {
return displayMode;
};
export const getDateFormat = function () { export const getDateFormat = function () {
return dateFormat; return dateFormat;
}; };
@ -143,11 +153,11 @@ export const getSections = function () {
}; };
export const getTasks = function () { export const getTasks = function () {
let allItemsPricessed = compileTasks(); let allItemsProcessed = compileTasks();
const maxDepth = 10; const maxDepth = 10;
let iterationCount = 0; let iterationCount = 0;
while (!allItemsPricessed && iterationCount < maxDepth) { while (!allItemsProcessed && iterationCount < maxDepth) {
allItemsPricessed = compileTasks(); allItemsProcessed = compileTasks();
iterationCount++; iterationCount++;
} }
@ -719,6 +729,8 @@ export default {
getAccTitle, getAccTitle,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
setDisplayMode,
getDisplayMode,
setAccDescription, setAccDescription,
getAccDescription, getAccDescription,
addSection, addSection,

View File

@ -34,6 +34,7 @@ describe('when using the ganttDb', function () {
beforeEach(function () { beforeEach(function () {
ganttDb.setDateFormat('YYYY-MM-DD'); ganttDb.setDateFormat('YYYY-MM-DD');
ganttDb.enableInclusiveEndDates(); ganttDb.enableInclusiveEndDates();
ganttDb.setDisplayMode('compact');
ganttDb.setTodayMarker('off'); ganttDb.setTodayMarker('off');
ganttDb.setExcludes('weekends 2019-02-06,friday'); ganttDb.setExcludes('weekends 2019-02-06,friday');
ganttDb.addSection('weekends skip test'); ganttDb.addSection('weekends skip test');
@ -53,6 +54,7 @@ describe('when using the ganttDb', function () {
${'getExcludes'} | ${[]} ${'getExcludes'} | ${[]}
${'getSections'} | ${[]} ${'getSections'} | ${[]}
${'endDatesAreInclusive'} | ${false} ${'endDatesAreInclusive'} | ${false}
${'getDisplayMode'} | ${''}
`)('should clear $fn', ({ fn, expected }) => { `)('should clear $fn', ({ fn, expected }) => {
expect(ganttDb[fn]()).toEqual(expected); expect(ganttDb[fn]()).toEqual(expected);
}); });

View File

@ -24,12 +24,43 @@ export const setConf = function () {
log.debug('Something is calling, setConf, remove the call'); log.debug('Something is calling, setConf, remove the call');
}; };
/**
* For this issue:
* https://github.com/mermaid-js/mermaid/issues/1618
*
* Finds the number of intersections between tasks that happen at any point in time.
* Used to figure out how many rows are needed to display the tasks when the display
* mode is set to 'compact'.
*
* @param tasks
* @param orderOffset
*/
const getMaxIntersections = (tasks, orderOffset) => {
let timeline = [...tasks].map(() => -Infinity);
let sorted = [...tasks].sort((a, b) => a.startTime - b.startTime || a.order - b.order);
let maxIntersections = 0;
for (const element of sorted) {
for (let j = 0; j < timeline.length; j++) {
if (element.startTime >= timeline[j]) {
timeline[j] = element.endTime;
element.order = j + orderOffset;
if (j > maxIntersections) {
maxIntersections = j;
}
break;
}
}
}
return maxIntersections;
};
let w; let w;
export const draw = function (text, id, version, diagObj) { export const draw = function (text, id, version, diagObj) {
const conf = getConfig().gantt; const conf = getConfig().gantt;
// diagObj.db.clear(); // diagObj.db.clear();
// parser.parse(text); // parser.parse(text);
const securityLevel = getConfig().securityLevel; const securityLevel = getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode // Handle root and Document for when rendering in sandbox mode
let sandboxElement; let sandboxElement;
@ -56,7 +87,40 @@ export const draw = function (text, id, version, diagObj) {
const taskArray = diagObj.db.getTasks(); const taskArray = diagObj.db.getTasks();
// Set height based on number of tasks // Set height based on number of tasks
const h = taskArray.length * (conf.barHeight + conf.barGap) + 2 * conf.topPadding;
let categories = [];
for (const element of taskArray) {
categories.push(element.type);
}
categories = checkUnique(categories);
const categoryHeights = {};
let h = 2 * conf.topPadding;
if (diagObj.db.getDisplayMode() === 'compact' || conf.displayMode === 'compact') {
const categoryElements = {};
for (const element of taskArray) {
if (categoryElements[element.section] === undefined) {
categoryElements[element.section] = [element];
} else {
categoryElements[element.section].push(element);
}
}
let intersections = 0;
for (const category of Object.keys(categoryElements)) {
const categoryHeight = getMaxIntersections(categoryElements[category], intersections) + 1;
intersections += categoryHeight;
h += categoryHeight * (conf.barHeight + conf.barGap);
categoryHeights[category] = categoryHeight;
}
} else {
h += taskArray.length * (conf.barHeight + conf.barGap);
for (const category of categories) {
categoryHeights[category] = taskArray.filter((task) => task.type === category).length;
}
}
// Set viewBox // Set viewBox
elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h); elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
@ -74,16 +138,6 @@ export const draw = function (text, id, version, diagObj) {
]) ])
.rangeRound([0, w - conf.leftPadding - conf.rightPadding]); .rangeRound([0, w - conf.leftPadding - conf.rightPadding]);
let categories = [];
for (const element of taskArray) {
categories.push(element.type);
}
const catsUnfiltered = categories; // for vert labels
categories = checkUnique(categories);
/** /**
* @param a * @param a
* @param b * @param b
@ -157,11 +211,15 @@ export const draw = function (text, id, version, diagObj) {
* @param w * @param w
*/ */
function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w) { function drawRects(theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w) {
// Get unique task orders. Required to draw the background rects when display mode is compact.
const uniqueTaskOrderIds = [...new Set(theArray.map((item) => item.order))];
const uniqueTasks = uniqueTaskOrderIds.map((id) => theArray.find((item) => item.order === id));
// Draw background rects covering the entire width of the graph, these form the section rows. // Draw background rects covering the entire width of the graph, these form the section rows.
svg svg
.append('g') .append('g')
.selectAll('rect') .selectAll('rect')
.data(theArray) .data(uniqueTasks)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', 0) .attr('x', 0)
@ -582,12 +640,9 @@ export const draw = function (text, id, version, diagObj) {
* @param theTopPad * @param theTopPad
*/ */
function vertLabels(theGap, theTopPad) { function vertLabels(theGap, theTopPad) {
const numOccurances = [];
let prevGap = 0; let prevGap = 0;
for (const [i, category] of categories.entries()) { const numOccurances = Object.keys(categoryHeights).map((d) => [d, categoryHeights[d]]);
numOccurances[i] = [category, getCount(category, catsUnfiltered)];
}
svg svg
.append('g') // without doing this, impossible to put grid lines behind text .append('g') // without doing this, impossible to put grid lines behind text
@ -625,7 +680,6 @@ export const draw = function (text, id, version, diagObj) {
} }
}) })
.attr('font-size', conf.sectionFontSize) .attr('font-size', conf.sectionFontSize)
.attr('font-size', conf.sectionFontSize)
.attr('class', function (d) { .attr('class', function (d) {
for (const [i, category] of categories.entries()) { for (const [i, category] of categories.entries()) {
if (d[0] === category) { if (d[0] === category) {
@ -682,31 +736,6 @@ export const draw = function (text, id, version, diagObj) {
} }
return result; return result;
} }
/**
* From this stack exchange question:
* http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
*
* @param arr
*/
function getCounts(arr) {
let i = arr.length; // const to loop over
const obj = {}; // obj to store results
while (i) {
obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
}
return obj;
}
/**
* Get specific from everything
*
* @param word
* @param arr
*/
function getCount(word, arr) {
return getCounts(arr)[word] || 0;
}
}; };
export default { export default {

View File

@ -131,9 +131,10 @@ statement
| includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);} | includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);}
| todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);} | todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);}
| title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);} | title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);} | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
| section { yy.addSection($1.substr(8));$$=$1.substr(8); }
| clickStatement | clickStatement
| taskTxt taskData {yy.addTask($1,$2);$$='task';} | taskTxt taskData {yy.addTask($1,$2);$$='task';}
| directive | directive

View File

@ -189,9 +189,27 @@ The pattern is:
More info in: [https://github.com/d3/d3-time#interval_every](https://github.com/d3/d3-time#interval_every) More info in: [https://github.com/d3/d3-time#interval_every](https://github.com/d3/d3-time#interval_every)
## 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.
```mmd
---
displayMode: compact
---
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :a2, 2014-01-20, 25d
Another one :a3, 2014-02-10, 20d
```
## Comments ## Comments
Comments can be entered within a gantt chart, which will be ignored by the parser. Comments need to be on their own line and must be prefaced with `%%` (double percent signs). Any text after the start of the comment to the next newline will be treated as a comment, including any diagram syntax Comments can be entered within a gantt chart, which will be ignored by the parser. Comments need to be on their own line and must be prefaced with `%%` (double percent signs). Any text after the start of the comment to the next newline will be treated as a comment, including any diagram syntax.
```mmd ```mmd
gantt gantt

View File

@ -650,6 +650,7 @@ function addA11yInfo(
* numberSectionStyles: 4, * numberSectionStyles: 4,
* axisFormat: '%Y-%m-%d', * axisFormat: '%Y-%m-%d',
* topAxis: false, * topAxis: false,
* displayMode: '',
* }, * },
* }; * };
* mermaid.initialize(config); * mermaid.initialize(config);