Merge branch 'develop' into feature/117_er_diagram

This commit is contained in:
Adrian Hall 2020-03-17 09:28:10 +00:00
commit a0bbbe1404
14 changed files with 1604 additions and 14 deletions

3
.gitignore vendored
View File

@ -18,4 +18,5 @@ dist/classTest.html
dist/sequenceTest.html
.vscode/
cypress/platform/current.html
cypress/platform/current.html
cypress/platform/experimental.html

View File

@ -20,11 +20,8 @@
<h1>info below</h1>
<div style="display: flex;width: 100%; height: 100%">
<div class="mermaid" style="width: 100%; height: 100%">
stateDiagram
O --> A : ong line using<br/>should work<br/>should work<br/>should work
A --> B : ong line using<br/>should work
B --> C : Sing line
flowchart LR
A --> B
</div>
</div>
<script src="./mermaid.js"></script>

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

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

View File

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

View File

@ -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 = 'RL';
}
if (direction.match(/.*\^/)) {
direction = 'BT';
}
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
}
};

View File

@ -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 => `<i class='${s.replace(':', ' ')}'></i>`
)
};
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(/<br\s*\/?>/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 = '<span class="edgeLabel">' + edge.text + '</span>';
} else {
edgeData.labelType = 'text';
edgeData.label = edge.text.replace(/<br\s*\/?>/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
};

View File

@ -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';
<dir>\s*"LR" { this.popState(); return 'DIR'; }

41
src/experimental.js Normal file
View File

@ -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)));
});

View File

@ -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.

View File

@ -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) {

View File

@ -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';