diff --git a/README.zh-CN.md b/README.zh-CN.md index fcaa1f523..e88c54e7f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -39,7 +39,7 @@ Mermaid 甚至能让非程序员也能通过 [Mermaid Live Editor](https://merma -### 流程图 [文档 - live editor] +### 流程图 [文档 - live editor] ``` flowchart LR @@ -57,7 +57,7 @@ C -->|One| D[Result 1] C -->|Two| E[Result 2] ``` -### 时序图 [文档 - live editor] +### 时序图 [文档 - live editor] ``` sequenceDiagram @@ -83,7 +83,7 @@ John->>Bob: How about you? Bob-->>John: Jolly good! ``` -### 甘特图 [文档 - live editor] +### 甘特图 [文档 - live editor] ``` gantt @@ -107,7 +107,7 @@ gantt Parallel 4 : des6, after des4, 1d ``` -### 类图 [文档 - live editor] +### 类图 [文档 - live editor] ``` classDiagram @@ -147,7 +147,7 @@ class Class10 { } ``` -### 状态图 [[docs - live editor] +### 状态图 [[docs - live editor] ``` stateDiagram-v2 @@ -169,7 +169,7 @@ Moving --> Crash Crash --> [*] ``` -### 饼图 [文档 - live editor] +### 饼图 [文档 - live editor] ``` pie @@ -185,9 +185,9 @@ pie "Rats" : 15 ``` -### Git 图 [实验特性 - live editor] +### Git 图 [实验特性 - live editor] -### 用户体验旅程图 [文档 - live editor] +### 用户体验旅程图 [文档 - live editor] ``` journey diff --git a/cypress/integration/rendering/gantt.spec.js b/cypress/integration/rendering/gantt.spec.js index 16a70ece0..b75e682c6 100644 --- a/cypress/integration/rendering/gantt.spec.js +++ b/cypress/integration/rendering/gantt.spec.js @@ -341,4 +341,130 @@ describe('Gantt diagram', () => { expect(descriptionEl.textContent).to.equal(expectedAccDescription); }); }); + + it('should render a gantt diagram with tick is 15 minutes', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %H:%M + tickInterval 15minute + excludes weekends + + section Section + A task : a1, 2022-10-03, 6h + Another task : after a1, 6h + section Another + Task in sec : 2022-10-03, 3h + another task : 3h + `, + {} + ); + }); + + it('should render a gantt diagram with tick is 6 hours', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %d %H:%M + tickInterval 6hour + excludes weekends + + section Section + A task : a1, 2022-10-03, 1d + Another task : after a1, 2d + section Another + Task in sec : 2022-10-04, 2d + another task : 2d + `, + {} + ); + }); + + it('should render a gantt diagram with tick is 1 day', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %m-%d + tickInterval 1day + excludes weekends + + section Section + A task : a1, 2022-10-01, 30d + Another task : after a1, 20d + section Another + Task in sec : 2022-10-20, 12d + another task : 24d + `, + {} + ); + }); + + it('should render a gantt diagram with tick is 1 week', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %m-%d + tickInterval 1week + excludes weekends + + section Section + A task : a1, 2022-10-01, 30d + Another task : after a1, 20d + section Another + Task in sec : 2022-10-20, 12d + another task : 24d + `, + {} + ); + }); + + it('should render a gantt diagram with tick is 1 month', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %m-%d + tickInterval 1month + excludes weekends + + section Section + A task : a1, 2022-10-01, 30d + Another task : after a1, 20d + section Another + Task in sec : 2022-10-20, 12d + another task : 24d + `, + {} + ); + }); + + it('should render a gantt diagram with tick is 1 day and topAxis is true', () => { + imgSnapshotTest( + ` + gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + axisFormat %m-%d + tickInterval 1day + excludes weekends + + section Section + A task : a1, 2022-10-01, 30d + Another task : after a1, 20d + section Another + Task in sec : 2022-10-20, 12d + another task : 24d + `, + { gantt: { topAxis: true } } + ); + }); }); diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index e4b34e9bc..aea949fab 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -14,7 +14,7 @@ #### Defined in -[defaultConfig.ts:1869](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L1869) +[defaultConfig.ts:1882](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L1882) --- diff --git a/docs/syntax/gantt.md b/docs/syntax/gantt.md index 07907981f..11034d2e7 100644 --- a/docs/syntax/gantt.md +++ b/docs/syntax/gantt.md @@ -234,6 +234,18 @@ The following formatting strings are supported: More info in: https://github.com/mbostock/d3/wiki/Time-Formatting +### Axis ticks + +The default output ticks are auto. You can custom your `tickInterval`, like `1day` or `1week`. + + tickInterval 1day + +The pattern is: + + /^([1-9][0-9]*)(minute|hour|day|week|month)$/ + +More info in: + ## 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 diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 2343bdd34..540fc6760 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -297,6 +297,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig { sectionFontSize?: string | number; numberSectionStyles?: number; axisFormat?: string; + tickInterval?: string; topAxis?: boolean; } diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 8b09eed1a..5a7f6c071 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -661,6 +661,19 @@ const config: Partial = { */ axisFormat: '%Y-%m-%d', + /** + * | Parameter | Description | Type | Required | Values | + * | ------------ | ------------| ------ | -------- | ------- | + * | tickInterval | axis ticks | string | Optional | string | + * + * **Notes:** + * + * Pattern is /^([1-9][0-9]*)(minute|hour|day|week|month)$/ + * + * Default value: undefined + */ + tickInterval: undefined, + /** * | Parameter | Description | Type | Required | Values | * | ----------- | ----------- | ------- | -------- | ----------- | diff --git a/packages/mermaid/src/diagrams/er/erRenderer.js b/packages/mermaid/src/diagrams/er/erRenderer.js index a4d5c8bd1..323bb4607 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer.js +++ b/packages/mermaid/src/diagrams/er/erRenderer.js @@ -77,31 +77,27 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { // Add a text node for the attribute type const typeNode = groupNode .append('text') - .attr('class', 'er entityLabel') + .classed('er entityLabel', true) .attr('id', `${attrPrefix}-type`) .attr('x', 0) .attr('y', 0) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'left') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' - ) + .style('dominant-baseline', 'middle') + .style('text-anchor', 'left') + .style('font-family', getConfig().fontFamily) + .style('font-size', attrFontSize + 'px') .text(attributeType); // Add a text node for the attribute name const nameNode = groupNode .append('text') - .attr('class', 'er entityLabel') + .classed('er entityLabel', true) .attr('id', `${attrPrefix}-name`) .attr('x', 0) .attr('y', 0) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'left') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' - ) + .style('dominant-baseline', 'middle') + .style('text-anchor', 'left') + .style('font-family', getConfig().fontFamily) + .style('font-size', attrFontSize + 'px') .text(item.attributeName); const attributeNode = {}; @@ -118,16 +114,14 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { if (hasKeyType) { const keyTypeNode = groupNode .append('text') - .attr('class', 'er entityLabel') + .classed('er entityLabel', true) .attr('id', `${attrPrefix}-key`) .attr('x', 0) .attr('y', 0) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'left') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' - ) + .style('dominant-baseline', 'middle') + .style('text-anchor', 'left') + .style('font-family', getConfig().fontFamily) + .style('font-size', attrFontSize + 'px') .text(item.attributeKeyType || ''); attributeNode.kn = keyTypeNode; @@ -139,16 +133,14 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { if (hasComment) { const commentNode = groupNode .append('text') - .attr('class', 'er entityLabel') + .classed('er entityLabel', true) .attr('id', `${attrPrefix}-comment`) .attr('x', 0) .attr('y', 0) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'left') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + attrFontSize + 'px' - ) + .style('dominant-baseline', 'middle') + .style('text-anchor', 'left') + .style('font-family', getConfig().fontFamily) + .style('font-size', attrFontSize + 'px') .text(item.attributeComment || ''); attributeNode.cn = commentNode; @@ -217,10 +209,10 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { // Insert a rectangle for the type const typeRect = groupNode .insert('rect', '#' + attributeNode.tn.node().id) - .attr('class', `er ${attribStyle}`) - .attr('fill', conf.fill) - .attr('fill-opacity', '100%') - .attr('stroke', conf.stroke) + .classed(`er ${attribStyle}`, true) + .style('fill', conf.fill) + .style('fill-opacity', '100%') + .style('stroke', conf.stroke) .attr('x', 0) .attr('y', heightOffset) .attr('width', maxTypeWidth + widthPadding * 2 + spareColumnWidth) @@ -237,10 +229,10 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { // Insert a rectangle for the name const nameRect = groupNode .insert('rect', '#' + attributeNode.nn.node().id) - .attr('class', `er ${attribStyle}`) - .attr('fill', conf.fill) - .attr('fill-opacity', '100%') - .attr('stroke', conf.stroke) + .classed(`er ${attribStyle}`, true) + .style('fill', conf.fill) + .style('fill-opacity', '100%') + .style('stroke', conf.stroke) .attr('x', nameXOffset) .attr('y', heightOffset) .attr('width', maxNameWidth + widthPadding * 2 + spareColumnWidth) @@ -259,10 +251,10 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { // Insert a rectangle for the key type const keyTypeRect = groupNode .insert('rect', '#' + attributeNode.kn.node().id) - .attr('class', `er ${attribStyle}`) - .attr('fill', conf.fill) - .attr('fill-opacity', '100%') - .attr('stroke', conf.stroke) + .classed(`er ${attribStyle}`, true) + .style('fill', conf.fill) + .style('fill-opacity', '100%') + .style('stroke', conf.stroke) .attr('x', keyTypeAndCommentXOffset) .attr('y', heightOffset) .attr('width', maxKeyWidth + widthPadding * 2 + spareColumnWidth) @@ -282,10 +274,10 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => { // Insert a rectangle for the comment groupNode .insert('rect', '#' + attributeNode.cn.node().id) - .attr('class', `er ${attribStyle}`) - .attr('fill', conf.fill) - .attr('fill-opacity', '100%') - .attr('stroke', conf.stroke) + .classed(`er ${attribStyle}`, 'true') + .style('fill', conf.fill) + .style('fill-opacity', '100%') + .style('stroke', conf.stroke) .attr('x', keyTypeAndCommentXOffset) .attr('y', heightOffset) .attr('width', maxCommentWidth + widthPadding * 2 + spareColumnWidth) @@ -335,16 +327,14 @@ const drawEntities = function (svgNode, entities, graph) { const textId = 'text-' + entityId; const textNode = groupNode .append('text') - .attr('class', 'er entityLabel') + .classed('er entityLabel', true) .attr('id', textId) .attr('x', 0) .attr('y', 0) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'middle') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px' - ) + .style('dominant-baseline', 'middle') + .style('text-anchor', 'middle') + .style('font-family', getConfig().fontFamily) + .style('font-size', conf.fontSize + 'px') .text(entityName); const { width: entityWidth, height: entityHeight } = drawAttributes( @@ -356,10 +346,10 @@ const drawEntities = function (svgNode, entities, graph) { // Draw the rectangle - insert it before the text so that the text is not obscured const rectNode = groupNode .insert('rect', '#' + textId) - .attr('class', 'er entityBox') + .classed('er entityBox', true) .style('fill', conf.fill) - .attr('fill-opacity', '100%') - .attr('stroke', conf.stroke) + .style('fill-opacity', '100%') + .style('stroke', conf.stroke) .attr('x', 0) .attr('y', 0) .attr('width', entityWidth) @@ -460,10 +450,10 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { // Insert the line at the right place const svgPath = svg .insert('path', '#' + insert) - .attr('class', 'er relationshipLine') + .classed('er relationshipLine', true) .attr('d', lineFunction(edge.points)) - .attr('stroke', conf.stroke) - .attr('fill', 'none'); + .style('stroke', conf.stroke) + .style('fill', 'none'); // ...and with dashes if necessary if (rel.relSpec.relType === diagObj.db.Identification.NON_IDENTIFYING) { @@ -537,16 +527,14 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { const labelNode = svg .append('text') - .attr('class', 'er relationshipLabel') + .classed('er relationshipLabel', true) .attr('id', labelId) .attr('x', labelPoint.x) .attr('y', labelPoint.y) - .attr('text-anchor', 'middle') - .attr('dominant-baseline', 'middle') - .attr( - 'style', - 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px' - ) + .style('text-anchor', 'middle') + .style('dominant-baseline', 'middle') + .style('font-family', getConfig().fontFamily) + .style('font-size', conf.fontSize + 'px') .text(rel.roleA); // Figure out how big the opaque 'container' rectangle needs to be @@ -555,13 +543,13 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { // Insert the opaque rectangle before the text label svg .insert('rect', '#' + labelId) - .attr('class', 'er relationshipLabelBox') + .classed('er relationshipLabelBox', true) .attr('x', labelPoint.x - labelBBox.width / 2) .attr('y', labelPoint.y - labelBBox.height / 2) .attr('width', labelBBox.width) .attr('height', labelBBox.height) - .attr('fill', 'white') - .attr('fill-opacity', '85%'); + .style('fill', 'white') + .style('fill-opacity', '85%'); }; /** diff --git a/packages/mermaid/src/diagrams/gantt/ganttDb.js b/packages/mermaid/src/diagrams/gantt/ganttDb.js index 99c93ea04..a0f18c3b8 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDb.js +++ b/packages/mermaid/src/diagrams/gantt/ganttDb.js @@ -17,6 +17,7 @@ import { let dateFormat = ''; let axisFormat = ''; +let tickInterval = undefined; let todayMarker = ''; let includes = []; let excludes = []; @@ -47,6 +48,7 @@ export const clear = function () { rawTasks = []; dateFormat = ''; axisFormat = ''; + tickInterval = undefined; todayMarker = ''; includes = []; excludes = []; @@ -65,6 +67,14 @@ export const getAxisFormat = function () { return axisFormat; }; +export const setTickInterval = function (txt) { + tickInterval = txt; +}; + +export const getTickInterval = function () { + return tickInterval; +}; + export const setTodayMarker = function (txt) { todayMarker = txt; }; @@ -647,6 +657,8 @@ export default { topAxisEnabled, setAxisFormat, getAxisFormat, + setTickInterval, + getTickInterval, setTodayMarker, getTodayMarker, setAccTitle, diff --git a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js index c9f6836a5..9501dd024 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttRenderer.js +++ b/packages/mermaid/src/diagrams/gantt/ganttRenderer.js @@ -10,6 +10,11 @@ import { axisBottom, axisTop, timeFormat, + timeMinute, + timeHour, + timeDay, + timeWeek, + timeMonth, } from 'd3'; import common from '../common/common'; import { getConfig } from '../../config'; @@ -495,6 +500,33 @@ export const draw = function (text, id, version, diagObj) { .tickSize(-h + theTopPad + conf.gridLineStartPadding) .tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d')); + const reTickInterval = /^([1-9][0-9]*)(minute|hour|day|week|month)$/; + const resultTickInterval = reTickInterval.exec( + diagObj.db.getTickInterval() || conf.tickInterval + ); + + if (resultTickInterval !== null) { + const every = resultTickInterval[1]; + const interval = resultTickInterval[2]; + switch (interval) { + case 'minute': + bottomXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + bottomXAxis.ticks(timeHour.every(every)); + break; + case 'day': + bottomXAxis.ticks(timeDay.every(every)); + break; + case 'week': + bottomXAxis.ticks(timeWeek.every(every)); + break; + case 'month': + bottomXAxis.ticks(timeMonth.every(every)); + break; + } + } + svg .append('g') .attr('class', 'grid') @@ -512,6 +544,28 @@ export const draw = function (text, id, version, diagObj) { .tickSize(-h + theTopPad + conf.gridLineStartPadding) .tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d')); + if (resultTickInterval !== null) { + const every = resultTickInterval[1]; + const interval = resultTickInterval[2]; + switch (interval) { + case 'minute': + topXAxis.ticks(timeMinute.every(every)); + break; + case 'hour': + topXAxis.ticks(timeHour.every(every)); + break; + case 'day': + topXAxis.ticks(timeDay.every(every)); + break; + case 'week': + topXAxis.ticks(timeWeek.every(every)); + break; + case 'month': + topXAxis.ticks(timeMonth.every(every)); + break; + } + } + svg .append('g') .attr('class', 'grid') diff --git a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison index f25656453..2223aa378 100644 --- a/packages/mermaid/src/diagrams/gantt/parser/gantt.jison +++ b/packages/mermaid/src/diagrams/gantt/parser/gantt.jison @@ -82,6 +82,7 @@ that id. "inclusiveEndDates" return 'inclusiveEndDates'; "topAxis" return 'topAxis'; "axisFormat"\s[^#\n;]+ return 'axisFormat'; +"tickInterval"\s[^#\n;]+ return 'tickInterval'; "includes"\s[^#\n;]+ return 'includes'; "excludes"\s[^#\n;]+ return 'excludes'; "todayMarker"\s[^\n;]+ return 'todayMarker'; @@ -125,6 +126,7 @@ statement | inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);} | topAxis {yy.TopAxis();$$=$1.substr(8);} | axisFormat {yy.setAxisFormat($1.substr(11));$$=$1.substr(11);} + | tickInterval {yy.setTickInterval($1.substr(13));$$=$1.substr(13);} | excludes {yy.setExcludes($1.substr(9));$$=$1.substr(9);} | includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);} | todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);} diff --git a/packages/mermaid/src/docs/syntax/gantt.md b/packages/mermaid/src/docs/syntax/gantt.md index 755f50b1e..8b200da7d 100644 --- a/packages/mermaid/src/docs/syntax/gantt.md +++ b/packages/mermaid/src/docs/syntax/gantt.md @@ -174,6 +174,22 @@ The following formatting strings are supported: More info in: https://github.com/mbostock/d3/wiki/Time-Formatting +### Axis ticks + +The default output ticks are auto. You can custom your `tickInterval`, like `1day` or `1week`. + +``` +tickInterval 1day +``` + +The pattern is: + +``` +/^([1-9][0-9]*)(minute|hour|day|week|month)$/ +``` + +More info in: [https://github.com/d3/d3-time#interval_every](https://github.com/d3/d3-time#interval_every) + ## 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