diff --git a/dist/index.html b/dist/index.html index d6a856f49..2da7d74d4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -291,7 +291,7 @@ graph TB
graph TD A[Christmas] -->|Get money| B(Go shopping) -B --> C{Let me think} +B --> C{{Let me think...
Do I want something for work,
something to spend every free second with,
or something to get around?}} C -->|One| D[Laptop] C -->|Two| E[iPhone] C -->|Three| F[Car] diff --git a/docs/flowchart.md b/docs/flowchart.md index b793a86ab..665e3b3f7 100644 --- a/docs/flowchart.md +++ b/docs/flowchart.md @@ -112,6 +112,17 @@ graph LR id1{This is the text in the box} ``` +### A hexagon node + +``` +graph LR + id1{{This is the text in the box}} +``` +```mermaid +graph LR + id1{{This is the text in the box}} +``` + ### Trapezoid ```mermaid diff --git a/docs/mermaidAPI.md b/docs/mermaidAPI.md index bfad444f4..59aafba4f 100644 --- a/docs/mermaidAPI.md +++ b/docs/mermaidAPI.md @@ -77,7 +77,7 @@ This option decides the amount of logging to be used. Sets the level of trust to be used on the parsed diagrams. -- **strict**: (**default**) tags in text are encoded, click functionality is disabled +- **strict**: (**default**) tags in text are encoded, click functionality is disabeled - **loose**: tags in text are allowed, click functionality is enabled ## startOnLoad diff --git a/src/diagrams/flowchart/flowChartShapes.js b/src/diagrams/flowchart/flowChartShapes.js new file mode 100644 index 000000000..e9b17ae89 --- /dev/null +++ b/src/diagrams/flowchart/flowChartShapes.js @@ -0,0 +1,177 @@ +import dagreD3 from 'dagre-d3-renderer'; + +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; +} + +export function addToRender(render) { + render.shapes().question = question; + render.shapes().hexagon = hexagon; + + // 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/flowChartShapes.spec.js b/src/diagrams/flowchart/flowChartShapes.spec.js new file mode 100644 index 000000000..de3f05a1d --- /dev/null +++ b/src/diagrams/flowchart/flowChartShapes.spec.js @@ -0,0 +1,91 @@ +import { addToRender } from './flowChartShapes'; + +describe('flowchart shapes', function() { + [ + [ + '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/flowRenderer.js b/src/diagrams/flowchart/flowRenderer.js index ef0bbbf61..7bb136287 100644 --- a/src/diagrams/flowchart/flowRenderer.js +++ b/src/diagrams/flowchart/flowRenderer.js @@ -8,6 +8,7 @@ import dagreD3 from 'dagre-d3-renderer'; import addHtmlLabel from 'dagre-d3-renderer/lib/label/add-html-label.js'; import { logger } from '../../logger'; import { interpolateToCurve } from '../../utils'; +import flowChartShapes from './flowChartShapes'; const conf = {}; export const setConf = function(cnf) { @@ -121,6 +122,9 @@ export const addVertices = function(vert, g, svgId) { case 'diamond': _shape = 'question'; break; + case 'hexagon': + _shape = 'hexagon'; + break; case 'odd': _shape = 'rect_left_inv_arrow'; break; @@ -328,199 +332,8 @@ export const draw = function(text, id) { const Render = dagreD3.render; const render = new Render(); - // Add custom shape for rhombus type of boc (decision) - render.shapes().question = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('rx', 5) - .attr('ry', 5) - .attr('transform', 'translate(' + -s / 2 + ',' + (s * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on left side - render.shapes().rect_left_inv_arrow = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on left side - render.shapes().lean_right = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on left side - render.shapes().lean_left = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on left side - render.shapes().trapezoid = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on left side - render.shapes().inv_trapezoid = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; - - // Add custom shape for box with inverted arrow on right side - render.shapes().rect_right_inv_arrow = function(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 = parent - .insert('polygon', ':first-child') - .attr( - 'points', - points - .map(function(d) { - return d.x + ',' + d.y; - }) - .join(' ') - ) - .attr('transform', 'translate(' + -w / 2 + ',' + (h * 2) / 4 + ')'); - node.intersect = function(point) { - return dagreD3.intersect.polygon(node, points, point); - }; - return shapeSvg; - }; + // Add custom shapes + flowChartShapes.addToRender(render); // Add our custom arrow - an empty arrowhead render.arrows().none = function normal(parent, id, edge, type) { diff --git a/src/diagrams/flowchart/flowRenderer.spec.js b/src/diagrams/flowchart/flowRenderer.spec.js new file mode 100644 index 000000000..3b8c72a44 --- /dev/null +++ b/src/diagrams/flowchart/flowRenderer.spec.js @@ -0,0 +1,57 @@ +import { addVertices } from './flowRenderer'; +import { setConfig } from '../../config'; + +setConfig({ + flowchart: { + htmlLabels: false + } +}); + +describe('the flowchart renderer', function() { + describe('when adding vertices to a graph', function() { + [ + ['round', 'rect', 5], + ['square', 'rect'], + ['diamond', 'question'], + ['hexagon', 'hexagon'], + ['odd', 'rect_left_inv_arrow'], + ['lean_right', 'lean_right'], + ['lean_left', 'lean_left'], + ['trapezoid', 'trapezoid'], + ['inv_trapezoid', 'inv_trapezoid'], + ['odd_right', 'rect_left_inv_arrow'], + ['circle', 'circle'], + ['ellipse', 'ellipse'], + ['group', 'rect'] + ].forEach(function([type, expectedShape, expectedRadios = 0]) { + it(`should add the correct shaped node to the graph for vertex type ${type}`, function() { + const addedNodes = []; + const mockG = { + setNode: function(id, object) { + addedNodes.push([id, object]); + } + }; + addVertices( + { + v1: { + type, + id: 'my-node-id', + classes: [], + styles: [], + text: 'my vertex text' + } + }, + mockG, + 'svg-id' + ); + expect(addedNodes).toHaveLength(1); + expect(addedNodes[0][0]).toEqual('my-node-id'); + expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id'); + expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg'); + expect(addedNodes[0][1]).toHaveProperty('shape', expectedShape); + expect(addedNodes[0][1]).toHaveProperty('rx', expectedRadios); + expect(addedNodes[0][1]).toHaveProperty('ry', expectedRadios); + }); + }); + }); +}); diff --git a/src/diagrams/flowchart/parser/flow.jison b/src/diagrams/flowchart/parser/flow.jison index bdc8d2869..da86b50f8 100644 --- a/src/diagrams/flowchart/parser/flow.jison +++ b/src/diagrams/flowchart/parser/flow.jison @@ -313,6 +313,10 @@ vertex: idString SQS text SQE {$$ = $1;yy.addVertex($1,$3,'diamond');} | idString DIAMOND_START text DIAMOND_STOP spaceList {$$ = $1;yy.addVertex($1,$3,'diamond');} + | idString DIAMOND_START DIAMOND_START text DIAMOND_STOP DIAMOND_STOP + {$$ = $1;yy.addVertex($1,$4,'hexagon');} + | idString DIAMOND_START DIAMOND_START text DIAMOND_STOP DIAMOND_STOP spaceList + {$$ = $1;yy.addVertex($1,$4,'hexagon');} | idString TAGEND text SQE {$$ = $1;yy.addVertex($1,$3,'odd');} | idString TAGEND text SQE spaceList diff --git a/src/diagrams/flowchart/parser/flow.spec.js b/src/diagrams/flowchart/parser/flow.spec.js index 778691e89..2d08f7d83 100644 --- a/src/diagrams/flowchart/parser/flow.spec.js +++ b/src/diagrams/flowchart/parser/flow.spec.js @@ -1391,6 +1391,16 @@ describe('when parsing ', function() { expect(edges.length).toBe(0); expect(vert['a'].type).toBe('diamond'); }); + it('should handle a single diamond node with whitespace after it', function() { + // Silly but syntactically correct + const res = flow.parser.parse('graph TD;a{A} ;'); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + + expect(edges.length).toBe(0); + expect(vert['a'].type).toBe('diamond'); + }); it('should handle a single diamond node with html in it', function() { // Silly but syntactically correct const res = flow.parser.parse('graph TD;a{A
end};'); @@ -1402,6 +1412,27 @@ describe('when parsing ', function() { expect(vert['a'].type).toBe('diamond'); expect(vert['a'].text).toBe('A
end'); }); + it('should handle a single hexagon node', function() { + // Silly but syntactically correct + const res = flow.parser.parse('graph TD;a{{A}};'); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + + expect(edges.length).toBe(0); + expect(vert['a'].type).toBe('hexagon'); + }); + it('should handle a single hexagon node with html in it', function() { + // Silly but syntactically correct + const res = flow.parser.parse('graph TD;a{{A
end}};'); + + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + + expect(edges.length).toBe(0); + expect(vert['a'].type).toBe('hexagon'); + expect(vert['a'].text).toBe('A
end'); + }); it('should handle a single round node with html in it', function() { // Silly but syntactically correct const res = flow.parser.parse('graph TD;a(A
end);');