diff --git a/.gitignore b/.gitignore index 69f442484..58579d79b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ dist/classTest.html dist/sequenceTest.html .vscode/ -cypress/platform/current.html \ No newline at end of file +cypress/platform/current.html +cypress/platform/experimental.html \ No newline at end of file diff --git a/cypress/platform/current.html b/cypress/platform/current.html index 3ed964921..2f5ec32b4 100644 --- a/cypress/platform/current.html +++ b/cypress/platform/current.html @@ -20,11 +20,8 @@

info below

- stateDiagram - O --> A : ong line using
should work
should work
should work - A --> B : ong line using
should work - B --> C : Sing line - + flowchart LR + A --> B
diff --git a/docs/gantt.md b/docs/gantt.md index 9d2124469..542f3a43e 100755 --- a/docs/gantt.md +++ b/docs/gantt.md @@ -1,8 +1,19 @@ # Gantt diagrams -> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule. Gantt charts illustrate the start and finish dates of the terminal elements and summary elements of a project. - -Mermaid can render Gantt diagrams. +> A Gantt chart is a type of bar chart, first developed by Karol Adamiecki in 1896, and independently by Henry Gantt in the 1910s, that illustrates a project schedule and the amount of time it would take for any one project to finish. Gantt charts illustrate number of days between the start and finish dates of the terminal elements and summary elements of a project. + + ## A note to users + Gannt Charts will record each scheduled task as one continuous bar that extends from the left to the right. The x axis represents time and the y records the different tasks and the order in which they are to be completed. + + It is important to remember that when a date, day or collection of dates specific to a task are "excluded", the Gannt Chart will accomodate those changes by extending an equal number of day, towards the right, not by creating a gap inside the task. + As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-excluded-days-within.png) + + However, if the excluded date/s is between two tasks that are set to start consecutively, the excluded dates will be skipped graphically and left blank, and the following task will begin after the end of the excluded date/s. + As shown here ![](https://github.com/NeilCuzon/mermaid/blob/develop/docs/img/Gantt-long-weekend-look.png) + + A Gantt chart is useful for tracking the amount of time it would take before a project is finished, but it can also be used to graphically represent "non-working days, with a few tweaks. + +Mermaid can render Gantt diagrams as SVG, PNG or a MarkDown link that can be pasted into docs. ``` gantt @@ -30,9 +41,12 @@ gantt ``` gantt - dateFormat YYYY-MM-DD - title Adding GANTT diagram functionality to mermaid + + dateFormat :YYYY-MM-DD + title :Adding GANTT diagram functionality to mermaid + excludes :excludes the named dates/days from being included in a charted task.. + (Accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".) section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d @@ -118,7 +132,7 @@ Tbd ### Date format -The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example: +The default date format is YYYY-MM-DD. You can define your ``dateFormat``. For example: 2020-3-7 ``` dateFormat YYYY MM DD diff --git a/docs/img/Gantt-excluded-days-within.png b/docs/img/Gantt-excluded-days-within.png new file mode 100644 index 000000000..2283bf99d Binary files /dev/null and b/docs/img/Gantt-excluded-days-within.png differ diff --git a/docs/img/Gantt-long-weekend-look.png b/docs/img/Gantt-long-weekend-look.png new file mode 100644 index 000000000..1b2fc8e17 Binary files /dev/null and b/docs/img/Gantt-long-weekend-look.png differ diff --git a/src/diagrams/flowchart-v2/flowChartShapes.js b/src/diagrams/flowchart-v2/flowChartShapes.js new file mode 100644 index 000000000..23cb53049 --- /dev/null +++ b/src/diagrams/flowchart-v2/flowChartShapes.js @@ -0,0 +1,261 @@ +import dagreD3 from 'dagre-d3'; + +function question(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const s = (w + h) * 0.9; + const points = [ + { x: s / 2, y: 0 }, + { x: s, y: -s / 2 }, + { x: s / 2, y: -s }, + { x: 0, y: -s / 2 } + ]; + const shapeSvg = insertPolygonShape(parent, s, s, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function hexagon(parent, bbox, node) { + const f = 4; + const h = bbox.height; + const m = h / f; + const w = bbox.width + 2 * m; + const points = [ + { x: m, y: 0 }, + { x: w - m, y: 0 }, + { x: w, y: -h / 2 }, + { x: w - m, y: -h }, + { x: m, y: -h }, + { x: 0, y: -h / 2 } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function rect_left_inv_arrow(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: -h / 2, y: 0 }, + { x: w, y: 0 }, + { x: w, y: -h }, + { x: -h / 2, y: -h }, + { x: 0, y: -h / 2 } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function lean_right(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: (-2 * h) / 6, y: 0 }, + { x: w - h / 6, y: 0 }, + { x: w + (2 * h) / 6, y: -h }, + { x: h / 6, y: -h } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function lean_left(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: (2 * h) / 6, y: 0 }, + { x: w + h / 6, y: 0 }, + { x: w - (2 * h) / 6, y: -h }, + { x: -h / 6, y: -h } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function trapezoid(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: (-2 * h) / 6, y: 0 }, + { x: w + (2 * h) / 6, y: 0 }, + { x: w - h / 6, y: -h }, + { x: h / 6, y: -h } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function inv_trapezoid(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: h / 6, y: 0 }, + { x: w - h / 6, y: 0 }, + { x: w + (2 * h) / 6, y: -h }, + { x: (-2 * h) / 6, y: -h } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function rect_right_inv_arrow(parent, bbox, node) { + const w = bbox.width; + const h = bbox.height; + const points = [ + { x: 0, y: 0 }, + { x: w + h / 2, y: 0 }, + { x: w, y: -h / 2 }, + { x: w + h / 2, y: -h }, + { x: 0, y: -h } + ]; + const shapeSvg = insertPolygonShape(parent, w, h, points); + node.intersect = function(point) { + return dagreD3.intersect.polygon(node, points, point); + }; + return shapeSvg; +} + +function stadium(parent, bbox, node) { + const h = bbox.height; + const w = bbox.width + h / 4; + + const shapeSvg = parent + .insert('rect', ':first-child') + .attr('rx', h / 2) + .attr('ry', h / 2) + .attr('x', -w / 2) + .attr('y', -h / 2) + .attr('width', w) + .attr('height', h); + + node.intersect = function(point) { + return dagreD3.intersect.rect(node, point); + }; + return shapeSvg; +} + +function cylinder(parent, bbox, node) { + const w = bbox.width; + const rx = w / 2; + const ry = rx / (2.5 + w / 50); + const h = bbox.height + ry; + + const shape = + 'M 0,' + + ry + + ' a ' + + rx + + ',' + + ry + + ' 0,0,0 ' + + w + + ' 0 a ' + + rx + + ',' + + ry + + ' 0,0,0 ' + + -w + + ' 0 l 0,' + + h + + ' a ' + + rx + + ',' + + ry + + ' 0,0,0 ' + + w + + ' 0 l 0,' + + -h; + + const shapeSvg = parent + .attr('label-offset-y', ry) + .insert('path', ':first-child') + .attr('d', shape) + .attr('transform', 'translate(' + -w / 2 + ',' + -(h / 2 + ry) + ')'); + + node.intersect = function(point) { + const pos = dagreD3.intersect.rect(node, point); + const x = pos.x - node.x; + + if ( + rx != 0 && + (Math.abs(x) < node.width / 2 || + (Math.abs(x) == node.width / 2 && Math.abs(pos.y - node.y) > node.height / 2 - ry)) + ) { + // ellipsis equation: x*x / a*a + y*y / b*b = 1 + // solve for y to get adjustion value for pos.y + let y = ry * ry * (1 - (x * x) / (rx * rx)); + if (y != 0) y = Math.sqrt(y); + y = ry - y; + if (point.y - node.y > 0) y = -y; + + pos.y += y; + } + + return pos; + }; + + return shapeSvg; +} + +export function addToRender(render) { + render.shapes().question = question; + render.shapes().hexagon = hexagon; + render.shapes().stadium = stadium; + render.shapes().cylinder = cylinder; + + // Add custom shape for box with inverted arrow on left side + render.shapes().rect_left_inv_arrow = rect_left_inv_arrow; + + // Add custom shape for box with inverted arrow on left side + render.shapes().lean_right = lean_right; + + // Add custom shape for box with inverted arrow on left side + render.shapes().lean_left = lean_left; + + // Add custom shape for box with inverted arrow on left side + render.shapes().trapezoid = trapezoid; + + // Add custom shape for box with inverted arrow on left side + render.shapes().inv_trapezoid = inv_trapezoid; + + // Add custom shape for box with inverted arrow on right side + render.shapes().rect_right_inv_arrow = rect_right_inv_arrow; +} + +function insertPolygonShape(parent, w, h, points) { + return parent + .insert('polygon', ':first-child') + .attr( + 'points', + points + .map(function(d) { + return d.x + ',' + d.y; + }) + .join(' ') + ) + .attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')'); +} + +export default { + addToRender +}; diff --git a/src/diagrams/flowchart-v2/flowChartShapes.spec.js b/src/diagrams/flowchart-v2/flowChartShapes.spec.js new file mode 100644 index 000000000..61e876d4b --- /dev/null +++ b/src/diagrams/flowchart-v2/flowChartShapes.spec.js @@ -0,0 +1,131 @@ +import { addToRender } from './flowChartShapes'; + +describe('flowchart shapes', function() { + // rect-based shapes + [ + ['stadium', useWidth, useHeight] + ].forEach(function([shapeType, getW, getH]) { + it(`should add a ${shapeType} shape that renders a properly positioned rect element`, function() { + const mockRender = MockRender(); + const mockSvg = MockSvg(); + addToRender(mockRender); + + [[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) { + const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {}); + const w = width + height / 4; + const h = height; + const dx = -getW(w, h) / 2; + const dy = -getH(w, h) / 2; + expect(shape.__tag).toEqual('rect'); + expect(shape.__attrs).toHaveProperty('x', dx); + expect(shape.__attrs).toHaveProperty('y', dy); + }); + }); + }); + + // path-based shapes + [ + ['cylinder', useWidth, useHeight] + ].forEach(function([shapeType, getW, getH]) { + it(`should add a ${shapeType} shape that renders a properly positioned path element`, function() { + const mockRender = MockRender(); + const mockSvg = MockSvg(); + addToRender(mockRender); + + [[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) { + const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {}); + expect(shape.__tag).toEqual('path'); + expect(shape.__attrs).toHaveProperty('d'); + }); + }); + }); + + // polygon-based shapes + [ + [ + 'question', + 4, + function(w, h) { + return (w + h) * 0.9; + }, + function(w, h) { + return (w + h) * 0.9; + } + ], + [ + 'hexagon', + 6, + function(w, h) { + return w + h / 2; + }, + useHeight + ], + ['rect_left_inv_arrow', 5, useWidth, useHeight], + ['rect_right_inv_arrow', 5, useWidth, useHeight], + ['lean_right', 4, useWidth, useHeight], + ['lean_left', 4, useWidth, useHeight], + ['trapezoid', 4, useWidth, useHeight], + ['inv_trapezoid', 4, useWidth, useHeight] + ].forEach(function([shapeType, expectedPointCount, getW, getH]) { + it(`should add a ${shapeType} shape that renders a properly translated polygon element`, function() { + const mockRender = MockRender(); + const mockSvg = MockSvg(); + addToRender(mockRender); + + [[100, 100], [123, 45], [71, 300]].forEach(function([width, height]) { + const shape = mockRender.shapes()[shapeType](mockSvg, { width, height }, {}); + const dx = -getW(width, height) / 2; + const dy = getH(width, height) / 2; + const points = shape.__attrs.points.split(' '); + expect(shape.__tag).toEqual('polygon'); + expect(shape.__attrs).toHaveProperty('transform', `translate(${dx},${dy})`); + expect(points).toHaveLength(expectedPointCount); + }); + }); + }); +}); + +function MockRender() { + const shapes = {}; + return { + shapes() { + return shapes; + } + }; +} + +function MockSvg(tag, ...args) { + const children = []; + const attributes = {}; + return { + get __args() { + return args; + }, + get __tag() { + return tag; + }, + get __children() { + return children; + }, + get __attrs() { + return attributes; + }, + insert: function(tag, ...args) { + const child = MockSvg(tag, ...args); + children.push(child); + return child; + }, + attr(name, value) { + this.__attrs[name] = value; + return this; + } + }; +} + +function useWidth(w, h) { + return w; +} + +function useHeight(w, h) { + return h; +} diff --git a/src/diagrams/flowchart-v2/flowDb.js b/src/diagrams/flowchart-v2/flowDb.js new file mode 100644 index 000000000..4917a54a7 --- /dev/null +++ b/src/diagrams/flowchart-v2/flowDb.js @@ -0,0 +1,644 @@ +import * as d3 from 'd3'; +import { logger } from '../../logger'; +import utils from '../../utils'; +import { getConfig } from '../../config'; +import common from '../common/common'; + +// const MERMAID_DOM_ID_PREFIX = 'mermaid-dom-id-'; +const MERMAID_DOM_ID_PREFIX = ''; + +const config = getConfig(); +let vertices = {}; +let edges = []; +let classes = []; +let subGraphs = []; +let subGraphLookup = {}; +let tooltips = {}; +let subCount = 0; +let firstGraphFlag = true; +let direction; +// Functions to be run after graph rendering +let funs = []; + +/** + * Function called by parser when a node definition has been found + * @param id + * @param text + * @param type + * @param style + * @param classes + */ +export const addVertex = function(_id, text, type, style, classes) { + let txt; + let id = _id; + if (typeof id === 'undefined') { + return; + } + if (id.trim().length === 0) { + return; + } + + if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + + if (typeof vertices[id] === 'undefined') { + vertices[id] = { id: id, styles: [], classes: [] }; + } + if (typeof text !== 'undefined') { + txt = common.sanitizeText(text.trim(), config); + + // strip quotes if string starts and ends with a quote + if (txt[0] === '"' && txt[txt.length - 1] === '"') { + txt = txt.substring(1, txt.length - 1); + } + + vertices[id].text = txt; + } else { + if (typeof vertices[id].text === 'undefined') { + vertices[id].text = _id; + } + } + if (typeof type !== 'undefined') { + vertices[id].type = type; + } + if (typeof style !== 'undefined') { + if (style !== null) { + style.forEach(function(s) { + vertices[id].styles.push(s); + }); + } + } + if (typeof classes !== 'undefined') { + if (classes !== null) { + classes.forEach(function(s) { + vertices[id].classes.push(s); + }); + } + } +}; + +/** + * Function called by parser when a link/edge definition has been found + * @param start + * @param end + * @param type + * @param linktext + */ +export const addSingleLink = function(_start, _end, type, linktext) { + let start = _start; + let end = _end; + if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start; + if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end; + logger.info('Got edge...', start, end); + + const edge = { start: start, end: end, type: undefined, text: '' }; + linktext = type.text; + + if (typeof linktext !== 'undefined') { + edge.text = common.sanitizeText(linktext.trim(), config); + + // strip quotes if string starts and exnds with a quote + if (edge.text[0] === '"' && edge.text[edge.text.length - 1] === '"') { + edge.text = edge.text.substring(1, edge.text.length - 1); + } + } + + if (typeof type !== 'undefined') { + edge.type = type.type; + edge.stroke = type.stroke; + } + edges.push(edge); +}; +export const addLink = function(_start, _end, type, linktext) { + let i, j; + for (i = 0; i < _start.length; i++) { + for (j = 0; j < _end.length; j++) { + addSingleLink(_start[i], _end[j], type, linktext); + } + } +}; + +/** + * Updates a link's line interpolation algorithm + * @param pos + * @param interpolate + */ +export const updateLinkInterpolate = function(positions, interp) { + positions.forEach(function(pos) { + if (pos === 'default') { + edges.defaultInterpolate = interp; + } else { + edges[pos].interpolate = interp; + } + }); +}; + +/** + * Updates a link with a style + * @param pos + * @param style + */ +export const updateLink = function(positions, style) { + positions.forEach(function(pos) { + if (pos === 'default') { + edges.defaultStyle = style; + } else { + if (utils.isSubstringInArray('fill', style) === -1) { + style.push('fill:none'); + } + edges[pos].style = style; + } + }); +}; + +export const addClass = function(id, style) { + if (typeof classes[id] === 'undefined') { + classes[id] = { id: id, styles: [], textStyles: [] }; + } + + if (typeof style !== 'undefined') { + if (style !== null) { + style.forEach(function(s) { + if (s.match('color')) { + const newStyle1 = s.replace('fill', 'bgFill'); + const newStyle2 = newStyle1.replace('color', 'fill'); + classes[id].textStyles.push(newStyle2); + } + classes[id].styles.push(s); + }); + } + } +}; + +/** + * Called by parser when a graph definition is found, stores the direction of the chart. + * @param dir + */ +export const setDirection = function(dir) { + direction = dir; + if (direction.match(/.*/)) { + direction = 'LR'; + } + if (direction.match(/.*v/)) { + direction = 'TB'; + } +}; + +/** + * Called by parser when a special node is found, e.g. a clickable element. + * @param ids Comma separated list of ids + * @param className Class to add + */ +export const setClass = function(ids, className) { + ids.split(',').forEach(function(_id) { + let id = _id; + if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (typeof vertices[id] !== 'undefined') { + vertices[id].classes.push(className); + } + + if (typeof subGraphLookup[id] !== 'undefined') { + subGraphLookup[id].classes.push(className); + } + }); +}; + +const setTooltip = function(ids, tooltip) { + ids.split(',').forEach(function(id) { + if (typeof tooltip !== 'undefined') { + tooltips[id] = common.sanitizeText(tooltip, config); + } + }); +}; + +const setClickFun = function(_id, functionName) { + let id = _id; + if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (config.securityLevel !== 'loose') { + return; + } + if (typeof functionName === 'undefined') { + return; + } + if (typeof vertices[id] !== 'undefined') { + funs.push(function() { + const elem = document.querySelector(`[id="${id}"]`); + if (elem !== null) { + elem.addEventListener( + 'click', + function() { + window[functionName](id); + }, + false + ); + } + }); + } +}; + +/** + * Called by parser when a link is found. Adds the URL to the vertex data. + * @param ids Comma separated list of ids + * @param linkStr URL to create a link for + * @param tooltip Tooltip for the clickable element + */ +export const setLink = function(ids, linkStr, tooltip) { + ids.split(',').forEach(function(_id) { + let id = _id; + if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (typeof vertices[id] !== 'undefined') { + vertices[id].link = utils.formatUrl(linkStr, config); + } + }); + setTooltip(ids, tooltip); + setClass(ids, 'clickable'); +}; +export const getTooltip = function(id) { + return tooltips[id]; +}; + +/** + * Called by parser when a click definition is found. Registers an event handler. + * @param ids Comma separated list of ids + * @param functionName Function to be called on click + * @param tooltip Tooltip for the clickable element + */ +export const setClickEvent = function(ids, functionName, tooltip) { + ids.split(',').forEach(function(id) { + setClickFun(id, functionName); + }); + setTooltip(ids, tooltip); + setClass(ids, 'clickable'); +}; + +export const bindFunctions = function(element) { + funs.forEach(function(fun) { + fun(element); + }); +}; +export const getDirection = function() { + return direction.trim(); +}; +/** + * Retrieval function for fetching the found nodes after parsing has completed. + * @returns {{}|*|vertices} + */ +export const getVertices = function() { + return vertices; +}; + +/** + * Retrieval function for fetching the found links after parsing has completed. + * @returns {{}|*|edges} + */ +export const getEdges = function() { + return edges; +}; + +/** + * Retrieval function for fetching the found class definitions after parsing has completed. + * @returns {{}|*|classes} + */ +export const getClasses = function() { + return classes; +}; + +const setupToolTips = function(element) { + let tooltipElem = d3.select('.mermaidTooltip'); + if ((tooltipElem._groups || tooltipElem)[0][0] === null) { + tooltipElem = d3 + .select('body') + .append('div') + .attr('class', 'mermaidTooltip') + .style('opacity', 0); + } + + const svg = d3.select(element).select('svg'); + + const nodes = svg.selectAll('g.node'); + nodes + .on('mouseover', function() { + const el = d3.select(this); + const title = el.attr('title'); + // Dont try to draw a tooltip if no data is provided + if (title === null) { + return; + } + const rect = this.getBoundingClientRect(); + + tooltipElem + .transition() + .duration(200) + .style('opacity', '.9'); + tooltipElem + .html(el.attr('title')) + .style('left', rect.left + (rect.right - rect.left) / 2 + 'px') + .style('top', rect.top - 14 + document.body.scrollTop + 'px'); + el.classed('hover', true); + }) + .on('mouseout', function() { + tooltipElem + .transition() + .duration(500) + .style('opacity', 0); + const el = d3.select(this); + el.classed('hover', false); + }); +}; +funs.push(setupToolTips); + +/** + * Clears the internal graph db so that a new graph can be parsed. + */ +export const clear = function() { + vertices = {}; + classes = {}; + edges = []; + funs = []; + funs.push(setupToolTips); + subGraphs = []; + subGraphLookup = {}; + subCount = 0; + tooltips = []; + firstGraphFlag = true; +}; +/** + * + * @returns {string} + */ +export const defaultStyle = function() { + return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'; +}; + +/** + * Clears the internal graph db so that a new graph can be parsed. + */ +export const addSubGraph = function(_id, list, _title) { + let id = _id.trim(); + let title = _title; + if (_id === _title && _title.match(/\s/)) { + id = undefined; + } + function uniq(a) { + const prims = { boolean: {}, number: {}, string: {} }; + const objs = []; + + return a.filter(function(item) { + const type = typeof item; + if (item.trim() === '') { + return false; + } + if (type in prims) { + return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); // eslint-disable-line + } else { + return objs.indexOf(item) >= 0 ? false : objs.push(item); + } + }); + } + + let nodeList = []; + + nodeList = uniq(nodeList.concat.apply(nodeList, list)); + for (let i = 0; i < nodeList.length; i++) { + if (nodeList[i][0].match(/\d/)) nodeList[i] = MERMAID_DOM_ID_PREFIX + nodeList[i]; + } + + id = id || 'subGraph' + subCount; + if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + title = title || ''; + title = common.sanitizeText(title, config); + subCount = subCount + 1; + const subGraph = { id: id, nodes: nodeList, title: title.trim(), classes: [] }; + subGraphs.push(subGraph); + subGraphLookup[id] = subGraph; + return id; +}; + +const getPosForId = function(id) { + for (let i = 0; i < subGraphs.length; i++) { + if (subGraphs[i].id === id) { + return i; + } + } + return -1; +}; +let secCount = -1; +const posCrossRef = []; +const indexNodes2 = function(id, pos) { + const nodes = subGraphs[pos].nodes; + secCount = secCount + 1; + if (secCount > 2000) { + return; + } + posCrossRef[secCount] = pos; + // Check if match + if (subGraphs[pos].id === id) { + return { + result: true, + count: 0 + }; + } + + let count = 0; + let posCount = 1; + while (count < nodes.length) { + const childPos = getPosForId(nodes[count]); + // Ignore regular nodes (pos will be -1) + if (childPos >= 0) { + const res = indexNodes2(id, childPos); + if (res.result) { + return { + result: true, + count: posCount + res.count + }; + } else { + posCount = posCount + res.count; + } + } + count = count + 1; + } + + return { + result: false, + count: posCount + }; +}; + +export const getDepthFirstPos = function(pos) { + return posCrossRef[pos]; +}; +export const indexNodes = function() { + secCount = -1; + if (subGraphs.length > 0) { + indexNodes2('none', subGraphs.length - 1, 0); + } +}; + +export const getSubGraphs = function() { + return subGraphs; +}; + +export const firstGraph = () => { + if (firstGraphFlag) { + firstGraphFlag = false; + return true; + } + return false; +}; + +const destructStartLink = _str => { + const str = _str.trim(); + + switch (str) { + case '<--': + return { type: 'arrow', stroke: 'normal' }; + case 'x--': + return { type: 'arrow_cross', stroke: 'normal' }; + case 'o--': + return { type: 'arrow_circle', stroke: 'normal' }; + case '<-.': + return { type: 'arrow', stroke: 'dotted' }; + case 'x-.': + return { type: 'arrow_cross', stroke: 'dotted' }; + case 'o-.': + return { type: 'arrow_circle', stroke: 'dotted' }; + case '<==': + return { type: 'arrow', stroke: 'thick' }; + case 'x==': + return { type: 'arrow_cross', stroke: 'thick' }; + case 'o==': + return { type: 'arrow_circle', stroke: 'thick' }; + case '--': + return { type: 'arrow_open', stroke: 'normal' }; + case '==': + return { type: 'arrow_open', stroke: 'thick' }; + case '-.': + return { type: 'arrow_open', stroke: 'dotted' }; + } +}; + +const destructEndLink = _str => { + const str = _str.trim(); + + switch (str) { + case '--x': + return { type: 'arrow_cross', stroke: 'normal' }; + case '-->': + return { type: 'arrow', stroke: 'normal' }; + case '<-->': + return { type: 'double_arrow_point', stroke: 'normal' }; + case 'x--x': + return { type: 'double_arrow_cross', stroke: 'normal' }; + case 'o--o': + return { type: 'double_arrow_circle', stroke: 'normal' }; + case 'o.-o': + return { type: 'double_arrow_circle', stroke: 'dotted' }; + case '<==>': + return { type: 'double_arrow_point', stroke: 'thick' }; + case 'o==o': + return { type: 'double_arrow_circle', stroke: 'thick' }; + case 'x==x': + return { type: 'double_arrow_cross', stroke: 'thick' }; + case 'x.-x': + return { type: 'double_arrow_cross', stroke: 'dotted' }; + case 'x-.-x': + return { type: 'double_arrow_cross', stroke: 'dotted' }; + case '<.->': + return { type: 'double_arrow_point', stroke: 'dotted' }; + case '<-.->': + return { type: 'double_arrow_point', stroke: 'dotted' }; + case 'o-.-o': + return { type: 'double_arrow_circle', stroke: 'dotted' }; + case '--o': + return { type: 'arrow_circle', stroke: 'normal' }; + case '---': + return { type: 'arrow_open', stroke: 'normal' }; + case '-.-x': + return { type: 'arrow_cross', stroke: 'dotted' }; + case '-.->': + return { type: 'arrow', stroke: 'dotted' }; + case '-.-o': + return { type: 'arrow_circle', stroke: 'dotted' }; + case '-.-': + return { type: 'arrow_open', stroke: 'dotted' }; + case '.-x': + return { type: 'arrow_cross', stroke: 'dotted' }; + case '.->': + return { type: 'arrow', stroke: 'dotted' }; + case '.-o': + return { type: 'arrow_circle', stroke: 'dotted' }; + case '.-': + return { type: 'arrow_open', stroke: 'dotted' }; + case '==x': + return { type: 'arrow_cross', stroke: 'thick' }; + case '==>': + return { type: 'arrow', stroke: 'thick' }; + case '==o': + return { type: 'arrow_circle', stroke: 'thick' }; + case '===': + return { type: 'arrow_open', stroke: 'thick' }; + } +}; + +const destructLink = (_str, _startStr) => { + const info = destructEndLink(_str); + let startInfo; + if (_startStr) { + startInfo = destructStartLink(_startStr); + + if (startInfo.stroke !== info.stroke) { + return { type: 'INVALID', stroke: 'INVALID' }; + } + + if (startInfo.type === 'arrow_open') { + // -- xyz --> - take arrow type form ending + startInfo.type = info.type; + } else { + // x-- xyz --> - not supported + if (startInfo.type !== info.type) return { type: 'INVALID', stroke: 'INVALID' }; + + startInfo.type = 'double_' + startInfo.type; + } + + if (startInfo.type === 'double_arrow') { + startInfo.type = 'double_arrow_point'; + } + + return startInfo; + } + + return info; +}; + +export default { + addVertex, + addLink, + updateLinkInterpolate, + updateLink, + addClass, + setDirection, + setClass, + getTooltip, + setClickEvent, + setLink, + bindFunctions, + getDirection, + getVertices, + getEdges, + getClasses, + clear, + defaultStyle, + addSubGraph, + getDepthFirstPos, + indexNodes, + getSubGraphs, + destructLink, + lex: { + firstGraph + } +}; diff --git a/src/diagrams/flowchart-v2/flowRenderer.js b/src/diagrams/flowchart-v2/flowRenderer.js new file mode 100644 index 000000000..1baeeb9ad --- /dev/null +++ b/src/diagrams/flowchart-v2/flowRenderer.js @@ -0,0 +1,487 @@ +import graphlib from 'graphlib'; +import * as d3 from 'd3'; + +import flowDb from '../flowchart/flowDb'; +import flow from '../flowchart/parser/flow'; +import { getConfig } from '../../config'; + +import dagreD3 from 'dagre-d3'; +import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js'; +import { logger } from '../../logger'; +import { interpolateToCurve, getStylesFromArray } from '../../utils'; +import flowChartShapes from '../flowchart/flowChartShapes'; + +const conf = {}; +export const setConf = function(cnf) { + const keys = Object.keys(cnf); + for (let i = 0; i < keys.length; i++) { + conf[keys[i]] = cnf[keys[i]]; + } +}; + +/** + * Function that adds the vertices found in the graph definition to the graph to be rendered. + * @param vert Object containing the vertices. + * @param g The graph that is to be drawn. + */ +export const addVertices = function(vert, g, svgId) { + const svg = d3.select(`[id="${svgId}"]`); + const keys = Object.keys(vert); + + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + keys.forEach(function(id) { + const vertex = vert[id]; + + /** + * Variable for storing the classes for the vertex + * @type {string} + */ + let classStr = 'default'; + if (vertex.classes.length > 0) { + classStr = vertex.classes.join(' '); + } + + const styles = getStylesFromArray(vertex.styles); + + // Use vertex id as text in the box if no text is provided by the graph definition + let vertexText = vertex.text !== undefined ? vertex.text : vertex.id; + + // We create a SVG label, either by delegating to addHtmlLabel or manually + let vertexNode; + if (getConfig().flowchart.htmlLabels) { + // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? + const node = { + label: vertexText.replace( + /fa[lrsb]?:fa-[\w-]+/g, + s => `` + ) + }; + vertexNode = addHtmlLabel(svg, node).node(); + vertexNode.parentNode.removeChild(vertexNode); + } else { + const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); + + const rows = vertexText.split(//gi); + + for (let j = 0; j < rows.length; j++) { + const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); + tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); + tspan.setAttribute('dy', '1em'); + tspan.setAttribute('x', '1'); + tspan.textContent = rows[j]; + svgLabel.appendChild(tspan); + } + vertexNode = svgLabel; + } + + let radious = 0; + let _shape = ''; + // Set the shape based parameters + switch (vertex.type) { + case 'round': + radious = 5; + _shape = 'rect'; + break; + case 'square': + _shape = 'rect'; + break; + case 'diamond': + _shape = 'question'; + break; + case 'hexagon': + _shape = 'hexagon'; + break; + case 'odd': + _shape = 'rect_left_inv_arrow'; + break; + case 'lean_right': + _shape = 'lean_right'; + break; + case 'lean_left': + _shape = 'lean_left'; + break; + case 'trapezoid': + _shape = 'trapezoid'; + break; + case 'inv_trapezoid': + _shape = 'inv_trapezoid'; + break; + case 'odd_right': + _shape = 'rect_left_inv_arrow'; + break; + case 'circle': + _shape = 'circle'; + break; + case 'ellipse': + _shape = 'ellipse'; + break; + case 'stadium': + _shape = 'stadium'; + break; + case 'cylinder': + _shape = 'cylinder'; + break; + case 'group': + _shape = 'rect'; + break; + default: + _shape = 'rect'; + } + // Add the node + g.setNode(vertex.id, { + labelType: 'svg', + labelStyle: styles.labelStyle, + shape: _shape, + label: vertexNode, + rx: radious, + ry: radious, + class: classStr, + style: styles.style, + id: vertex.id + }); + }); +}; + +/** + * Add edges to graph based on parsed graph defninition + * @param {Object} edges The edges to add to the graph + * @param {Object} g The graph object + */ +export const addEdges = function(edges, g) { + let cnt = 0; + + let defaultStyle; + let defaultLabelStyle; + + if (typeof edges.defaultStyle !== 'undefined') { + const defaultStyles = getStylesFromArray(edges.defaultStyle); + defaultStyle = defaultStyles.style; + defaultLabelStyle = defaultStyles.labelStyle; + } + + edges.forEach(function(edge) { + cnt++; + const edgeData = {}; + + // Set link type for rendering + if (edge.type === 'arrow_open') { + edgeData.arrowhead = 'none'; + } else { + edgeData.arrowhead = 'normal'; + } + + let style = ''; + let labelStyle = ''; + + if (typeof edge.style !== 'undefined') { + const styles = getStylesFromArray(edge.style); + style = styles.style; + labelStyle = styles.labelStyle; + } else { + switch (edge.stroke) { + case 'normal': + style = 'fill:none'; + if (typeof defaultStyle !== 'undefined') { + style = defaultStyle; + } + if (typeof defaultLabelStyle !== 'undefined') { + labelStyle = defaultLabelStyle; + } + break; + case 'dotted': + style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; + break; + case 'thick': + style = ' stroke-width: 3.5px;fill:none'; + break; + } + } + + edgeData.style = style; + edgeData.labelStyle = labelStyle; + + if (typeof edge.interpolate !== 'undefined') { + edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear); + } else if (typeof edges.defaultInterpolate !== 'undefined') { + edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear); + } else { + edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear); + } + + if (typeof edge.text === 'undefined') { + if (typeof edge.style !== 'undefined') { + edgeData.arrowheadStyle = 'fill: #333'; + } + } else { + edgeData.arrowheadStyle = 'fill: #333'; + edgeData.labelpos = 'c'; + + if (getConfig().flowchart.htmlLabels) { + edgeData.labelType = 'html'; + edgeData.label = '' + edge.text + ''; + } else { + edgeData.labelType = 'text'; + edgeData.label = edge.text.replace(//gi, '\n'); + + if (typeof edge.style === 'undefined') { + edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'; + } + + edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); + } + } + // Add the edge to the graph + g.setEdge(edge.start, edge.end, edgeData, cnt); + }); +}; + +/** + * Returns the all the styles from classDef statements in the graph definition. + * @returns {object} classDef styles + */ +export const getClasses = function(text) { + logger.info('Extracting classes'); + flowDb.clear(); + const parser = flow.parser; + parser.yy = flowDb; + + // Parse the graph definition + parser.parse(text); + return flowDb.getClasses(); +}; + +/** + * Draws a flowchart in the tag with id: id based on the graph definition in text. + * @param text + * @param id + */ +export const draw = function(text, id) { + logger.info('Drawing flowchart'); + flowDb.clear(); + const parser = flow.parser; + parser.yy = flowDb; + + // Parse the graph definition + try { + parser.parse(text); + } catch (err) { + logger.debug('Parsing failed'); + } + + // Fetch the default direction, use TD if none was found + let dir = flowDb.getDirection(); + if (typeof dir === 'undefined') { + dir = 'TD'; + } + + const conf = getConfig().flowchart; + const nodeSpacing = conf.nodeSpacing || 50; + const rankSpacing = conf.rankSpacing || 50; + + // Create the input mermaid.graph + const g = new graphlib.Graph({ + multigraph: true, + compound: true + }) + .setGraph({ + rankdir: dir, + nodesep: nodeSpacing, + ranksep: rankSpacing, + marginx: 8, + marginy: 8 + }) + .setDefaultEdgeLabel(function() { + return {}; + }); + + let subG; + const subGraphs = flowDb.getSubGraphs(); + for (let i = subGraphs.length - 1; i >= 0; i--) { + subG = subGraphs[i]; + flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes); + } + + // Fetch the verices/nodes and edges/links from the parsed graph definition + const vert = flowDb.getVertices(); + + const edges = flowDb.getEdges(); + + let i = 0; + for (i = subGraphs.length - 1; i >= 0; i--) { + subG = subGraphs[i]; + + d3.selectAll('cluster').append('text'); + + for (let j = 0; j < subG.nodes.length; j++) { + g.setParent(subG.nodes[j], subG.id); + } + } + addVertices(vert, g, id); + addEdges(edges, g); + + // Create the renderer + const Render = dagreD3.render; + const render = new Render(); + + // Add custom shapes + flowChartShapes.addToRender(render); + + // Add our custom arrow - an empty arrowhead + render.arrows().none = function normal(parent, id, edge, type) { + const marker = parent + .append('marker') + .attr('id', id) + .attr('viewBox', '0 0 10 10') + .attr('refX', 9) + .attr('refY', 5) + .attr('markerUnits', 'strokeWidth') + .attr('markerWidth', 8) + .attr('markerHeight', 6) + .attr('orient', 'auto'); + + const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z'); + dagreD3.util.applyStyle(path, edge[type + 'Style']); + }; + + // Override normal arrowhead defined in d3. Remove style & add class to allow css styling. + render.arrows().normal = function normal(parent, id) { + const marker = parent + .append('marker') + .attr('id', id) + .attr('viewBox', '0 0 10 10') + .attr('refX', 9) + .attr('refY', 5) + .attr('markerUnits', 'strokeWidth') + .attr('markerWidth', 8) + .attr('markerHeight', 6) + .attr('orient', 'auto'); + + marker + .append('path') + .attr('d', 'M 0 0 L 10 5 L 0 10 z') + .attr('class', 'arrowheadPath') + .style('stroke-width', 1) + .style('stroke-dasharray', '1,0'); + }; + + // Set up an SVG group so that we can translate the final graph. + const svg = d3.select(`[id="${id}"]`); + + // Run the renderer. This is what draws the final graph. + const element = d3.select('#' + id + ' g'); + render(element, g); + + element.selectAll('g.node').attr('title', function() { + return flowDb.getTooltip(this.id); + }); + + const padding = 8; + const svgBounds = svg.node().getBBox(); + const width = svgBounds.width + padding * 2; + const height = svgBounds.height + padding * 2; + logger.debug( + `new ViewBox 0 0 ${width} ${height}`, + `translate(${padding - g._label.marginx}, ${padding - g._label.marginy})` + ); + + if (conf.useMaxWidth) { + svg.attr('width', '100%'); + svg.attr('style', `max-width: ${width}px;`); + } else { + svg.attr('height', height); + svg.attr('width', width); + } + + svg.attr('viewBox', `0 0 ${width} ${height}`); + svg + .select('g') + .attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`); + + // Index nodes + flowDb.indexNodes('subGraph' + i); + + // reposition labels + for (i = 0; i < subGraphs.length; i++) { + subG = subGraphs[i]; + + if (subG.title !== 'undefined') { + const clusterRects = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"] rect'); + const clusterEl = document.querySelectorAll('#' + id + ' [id="' + subG.id + '"]'); + + const xPos = clusterRects[0].x.baseVal.value; + const yPos = clusterRects[0].y.baseVal.value; + const width = clusterRects[0].width.baseVal.value; + const cluster = d3.select(clusterEl[0]); + const te = cluster.select('.label'); + te.attr('transform', `translate(${xPos + width / 2}, ${yPos + 14})`); + te.attr('id', id + 'Text'); + + for (let j = 0; j < subG.classes.length; j++) { + clusterEl[0].classList.add(subG.classes[j]); + } + } + } + + // Add label rects for non html labels + if (!conf.htmlLabels) { + const labels = document.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); + for (let k = 0; k < labels.length; k++) { + const label = labels[k]; + + // Get dimensions of label + const dim = label.getBBox(); + + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('rx', 0); + rect.setAttribute('ry', 0); + rect.setAttribute('width', dim.width); + rect.setAttribute('height', dim.height); + rect.setAttribute('style', 'fill:#e8e8e8;'); + + label.insertBefore(rect, label.firstChild); + } + } + + // If node has a link, wrap it in an anchor SVG object. + const keys = Object.keys(vert); + keys.forEach(function(key) { + const vertex = vert[key]; + + if (vertex.link) { + const node = d3.select('#' + id + ' [id="' + key + '"]'); + if (node) { + const link = document.createElementNS('http://www.w3.org/2000/svg', 'a'); + link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); + link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); + link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); + + const linkNode = node.insert(function() { + return link; + }, ':first-child'); + + const shape = node.select('.label-container'); + if (shape) { + linkNode.append(function() { + return shape.node(); + }); + } + + const label = node.select('.label'); + if (label) { + linkNode.append(function() { + return label.node(); + }); + } + } + } + }); +}; + +export default { + setConf, + addVertices, + addEdges, + getClasses, + draw +}; diff --git a/src/diagrams/flowchart/parser/flow.jison b/src/diagrams/flowchart/parser/flow.jison index f867e5713..58e4664a6 100644 --- a/src/diagrams/flowchart/parser/flow.jison +++ b/src/diagrams/flowchart/parser/flow.jison @@ -21,7 +21,8 @@ "classDef" return 'CLASSDEF'; "class" return 'CLASS'; "click" return 'CLICK'; -"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} +"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} +"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';} "subgraph" return 'subgraph'; "end"\b\s* return 'end'; \s*"LR" { this.popState(); return 'DIR'; } diff --git a/src/experimental.js b/src/experimental.js new file mode 100644 index 000000000..e6c3bd806 --- /dev/null +++ b/src/experimental.js @@ -0,0 +1,41 @@ +import dagre from 'dagre'; + +// Create a new directed graph +var g = new dagre.graphlib.Graph({ compound: true }); + +// Set an object for the graph label +g.setGraph({}); + +// Default to assigning a new object as a label for each new edge. +g.setDefaultEdgeLabel(function() { + return {}; +}); + +// Add nodes to the graph. The first argument is the node id. The second is +// metadata about the node. In this case we're going to add labels to each of +// our nodes. +g.setNode('root', { label: 'Cluster' }); +g.setNode('kspacey', { label: 'Kevin Spacey', width: 144, height: 100, x: 200 }); +// g.setParent('kspacey', 'root'); +g.setNode('swilliams', { label: 'Saul Williams', width: 160, height: 100 }); +// g.setNode('bpitt', { label: 'Brad Pitt', width: 108, height: 100 }); +// g.setNode('hford', { label: 'Harrison Ford', width: 168, height: 100 }); +// g.setNode('lwilson', { label: 'Luke Wilson', width: 144, height: 100 }); +// g.setNode('kbacon', { label: 'Kevin Bacon', width: 121, height: 100 }); + +// Add edges to the graph. +g.setEdge('kspacey', 'swilliams'); +g.setEdge('swilliams'); +// g.setEdge('swilliams', 'kbacon'); +// g.setEdge('bpitt', 'kbacon'); +// g.setEdge('hford', 'lwilson'); +// g.setEdge('lwilson', 'kbacon'); + +dagre.layout(g); + +g.nodes().forEach(function(v) { + console.log('Node ' + v + ': ' + JSON.stringify(g.node(v))); +}); +g.edges().forEach(function(e) { + console.log('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))); +}); diff --git a/src/mermaid.js b/src/mermaid.js index 0cf24e81f..6f2311390 100644 --- a/src/mermaid.js +++ b/src/mermaid.js @@ -6,7 +6,6 @@ import he from 'he'; import mermaidAPI from './mermaidAPI'; import { logger } from './logger'; - /** * ## init * Function that goes through the document to find the chart definitions in there and render them. diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 0731ae166..af52927f0 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -17,6 +17,7 @@ import { setConfig, getConfig } from './config'; import { logger, setLogLevel } from './logger'; import utils from './utils'; import flowRenderer from './diagrams/flowchart/flowRenderer'; +import flowRendererV2 from './diagrams/flowchart-v2/flowRenderer'; import flowParser from './diagrams/flowchart/parser/flow'; import flowDb from './diagrams/flowchart/flowDb'; import sequenceRenderer from './diagrams/sequence/sequenceRenderer'; @@ -414,6 +415,11 @@ function parse(text) { parser = flowParser; parser.parser.yy = flowDb; break; + case 'flowchart-v2': + flowDb.clear(); + parser = flowRendererV2; + parser.parser.yy = flowDb; + break; case 'sequence': parser = sequenceParser; parser.parser.yy = sequenceDb; @@ -624,6 +630,11 @@ const render = function(id, _txt, cb, container) { flowRenderer.setConf(config.flowchart); flowRenderer.draw(txt, id, false); break; + case 'flowchart-v2': + config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute; + flowRendererV2.setConf(config.flowchart); + flowRendererV2.draw(txt, id, false); + break; case 'sequence': config.sequence.arrowMarkerAbsolute = config.arrowMarkerAbsolute; if (config.sequenceDiagram) { diff --git a/src/utils.js b/src/utils.js index 055bb867a..faff453f2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -41,6 +41,9 @@ export const detectType = function(text) { if (text.match(/^\s*gitGraph/)) { return 'git'; } + if (text.match(/^\s*flowchart/)) { + return 'flowchart-v2'; + } if (text.match(/^\s*info/)) { return 'info';