From f081527731b5c4d2c5c01818f4051f61d741a450 Mon Sep 17 00:00:00 2001 From: Russell Geraghty Date: Sat, 4 Apr 2020 16:38:30 +0100 Subject: [PATCH 1/2] User journey handler --- README.md | 23 + cypress/integration/rendering/journey.spec.js | 33 ++ cypress/platform/user-journey.html | 41 ++ docs/mermaidAPI.md | 113 +++++ img/gray-journey.png | Bin 0 -> 27310 bytes package.json | 3 +- src/diagrams/user-journey/journeyDb.js | 123 +++++ src/diagrams/user-journey/journeyDb.spec.js | 90 ++++ src/diagrams/user-journey/journeyRenderer.js | 284 ++++++++++++ .../user-journey/parser/journey.jison | 52 +++ .../user-journey/parser/journey.spec.js | 117 +++++ src/diagrams/user-journey/svgDraw.js | 429 ++++++++++++++++++ 12 files changed, 1307 insertions(+), 1 deletion(-) create mode 100644 cypress/integration/rendering/journey.spec.js create mode 100644 cypress/platform/user-journey.html create mode 100644 img/gray-journey.png create mode 100644 src/diagrams/user-journey/journeyDb.js create mode 100644 src/diagrams/user-journey/journeyDb.spec.js create mode 100644 src/diagrams/user-journey/journeyRenderer.js create mode 100644 src/diagrams/user-journey/parser/journey.jison create mode 100644 src/diagrams/user-journey/parser/journey.spec.js create mode 100644 src/diagrams/user-journey/svgDraw.js diff --git a/README.md b/README.md index 83fe09ec7..8f6a6b102 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ For more information and help in getting started, please view our [documentation :trophy: **Mermaid was nominated and won the [JS Open Source Awards (2019)](https://osawards.com/javascript/#nominees) in the category "The most exciting use of technology"!!! Thanks to all involved, people committing pull requests, people answering questions and special thanks to Tyler Long who is helping me maintain the project.** +## New diagram + +This version comes with a new diagram type, user journey diagrams. + ## New diagrams in 8.4 With version 8.4 class diagrams have got some new features, bug fixes and documentation. Another new feature in 8.4 is the new diagram type, state diagrams. @@ -182,6 +186,25 @@ pie Coming soon! + + +
+  journey
+    title Hello
+    section Go to work
+      Make tea: 5: Me
+      Go upstairs: 3: Me
+      Do work: 1: Me, Cat
+    section Go home
+      Go downstairs: 5: Me
+      Sit down: 3: Me
+
+ + User Journey Diagram + + + + ## Related projects diff --git a/cypress/integration/rendering/journey.spec.js b/cypress/integration/rendering/journey.spec.js new file mode 100644 index 000000000..5fa822060 --- /dev/null +++ b/cypress/integration/rendering/journey.spec.js @@ -0,0 +1,33 @@ +/* eslint-env jest */ +import { imgSnapshotTest } from '../../helpers/util.js'; + +describe('User journey diagram', () => { + it('Simple test', () => { + imgSnapshotTest( + `journey +title Adding journey diagram functionality to mermaid +section Order from website + `, + {} + ); + }); + + it('should render a user journey chart', () => { + imgSnapshotTest( + ` + journey + title Go shopping + + section Get to the shops + Get car keys: Dad + Get into car: Dad, Mum, Child#1, Child#2 + Drive to supermarket: Dad + + section Do shopping + Do actual shop: Mum + Get in the way: Dad, Child#1, Child#2 + `, + {} + ); + }); +}); diff --git a/cypress/platform/user-journey.html b/cypress/platform/user-journey.html new file mode 100644 index 000000000..2fcd48046 --- /dev/null +++ b/cypress/platform/user-journey.html @@ -0,0 +1,41 @@ + + + + + + +

User Journey

+
+ journey + title Go shopping + + section Get to the shops + Get car keys:5: Dad + Get into car:5: Dad, Mum, Child 1, Child 2 + Really drive to supermarket:3: Dad + + section Do shopping + Do actual shop:3: Mum + Get in the way:2: Dad, Child 1, Child 2 + Pay: 2: Dad + + section Go home + Lose keys:3: Dad + Get cross:1: Dad, Child 1 + Find keys:4: Mum + Get into car:4: Dad, Mum, Child 1, Child 2 + Drive home:3: Dad +
+ + + + diff --git a/docs/mermaidAPI.md b/docs/mermaidAPI.md index ff25fd2f1..2469edd12 100644 --- a/docs/mermaidAPI.md +++ b/docs/mermaidAPI.md @@ -263,6 +263,119 @@ The number of alternating section styles. Datetime format of the axis. This might need adjustment to match your locale and preferences **Default value '%Y-%m-%d'**. +## journey + +The object containing configurations specific for sequence diagrams + +### diagramMarginX + +margin to the right and left of the sequence diagram. +**Default value 50**. + +### diagramMarginY + +margin to the over and under the sequence diagram. +**Default value 10**. + +### actorMargin + +Margin between actors. +**Default value 50**. + +### width + +Width of actor boxes +**Default value 150**. + +### height + +Height of actor boxes +**Default value 65**. + +### boxMargin + +Margin around loop boxes +**Default value 10**. + +### boxTextMargin + +margin around the text in loop/alt/opt boxes +**Default value 5**. + +### noteMargin + +margin around notes. +**Default value 10**. + +### messageMargin + +Space between messages. +**Default value 35**. + +### messageAlign + +Multiline message alignment. Possible values are: + +- left +- center **default** +- right + +### bottomMarginAdj + +Depending on css styling this might need adjustment. +Prolongs the edge of the diagram downwards. +**Default value 1**. + +### useMaxWidth + +when this flag is set the height and width is set to 100% and is then scaling with the +available space if not the absolute space required is used. +**Default value true**. + +### rightAngles + +This will display arrows that start and begin at the same node as right angles, rather than a curve +**Default value false**. + +## er + +The object containing configurations specific for entity relationship diagrams + +### layoutDirection + +Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL', +where T = top, B = bottom, L = left, and R = right. + +### minEntityWidth + +The mimimum width of an entity box + +### minEntityHeight + +The minimum height of an entity box + +### entityPadding + +The minimum internal padding between the text in an entity box and the enclosing box borders + +### stroke + +Stroke color of box edges and lines + +### fill + +Fill color of entity boxes + +### fillOpacity + +Opacity of entity boxes - if you want to see how the crows feet +retain their elegant joins to the boxes regardless of the angle of incidence +then override this to something less than 100% + +### fontSize + +Font size + ## render Function that renders an svg with a graph from a chart definition. Usage example below. diff --git a/img/gray-journey.png b/img/gray-journey.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe2be03a7df960ea7f97019c7daaa41773df506 GIT binary patch literal 27310 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 z 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 { + if (task.people) { + tempActors.push(...task.people); + } + }); + + const unique = new Set(tempActors); + return [...unique].sort(); +}; + +export const addTask = function(descr, taskData) { + const pieces = taskData.substr(1).split(':'); + + let score = 0; + let peeps = []; + if (pieces.length === 1) { + score = Number(pieces[0]); + peeps = []; + } else { + score = Number(pieces[0]); + peeps = pieces[1].split(','); + } + const peopleList = peeps.map(s => s.trim()); + + const rawTask = { + section: currentSection, + type: currentSection, + people: peopleList, + task: descr, + score + }; + + rawTasks.push(rawTask); +}; + +export const addTaskOrg = function(descr) { + const newTask = { + section: currentSection, + type: currentSection, + description: descr, + task: descr, + classes: [] + }; + tasks.push(newTask); +}; + +const compileTasks = function() { + const compileTask = function(pos) { + return rawTasks[pos].processed; + }; + + let allProcessed = true; + for (let i = 0; i < rawTasks.length; i++) { + compileTask(i); + + allProcessed = allProcessed && rawTasks[i].processed; + } + return allProcessed; +}; + +const getActors = function() { + return updateActors(); +}; + +export default { + clear, + setTitle, + getTitle, + addSection, + getSections, + getTasks, + addTask, + addTaskOrg, + getActors +}; diff --git a/src/diagrams/user-journey/journeyDb.spec.js b/src/diagrams/user-journey/journeyDb.spec.js new file mode 100644 index 000000000..8308e64f9 --- /dev/null +++ b/src/diagrams/user-journey/journeyDb.spec.js @@ -0,0 +1,90 @@ +/* eslint-env jasmine */ +import journeyDb from './journeyDb'; + +describe('when using the journeyDb', function() { + beforeEach(function() { + journeyDb.clear(); + }); + + describe('when calling the clear function', function() { + beforeEach(function() { + journeyDb.addSection('weekends skip test'); + journeyDb.addTask('test1', '4: id1, id3'); + journeyDb.addTask('test2', '2: id2'); + journeyDb.clear(); + }); + + it.each` + fn | expected + ${'getTasks'} | ${[]} + ${'getTitle'} | ${''} + ${'getSections'} | ${[]} + ${'getActors'} | ${[]} + `('should clear $fn', ({ fn, expected }) => { + expect(journeyDb[fn]()).toEqual(expected); + }); + }); + + describe('when calling the clear function', function() { + beforeEach(function() { + journeyDb.addSection('weekends skip test'); + journeyDb.addTask('test1', '3: id1, id3'); + journeyDb.addTask('test2', '1: id2'); + journeyDb.clear(); + }); + + it.each` + fn | expected + ${'getTasks'} | ${[]} + ${'getTitle'} | ${''} + ${'getSections'} | ${[]} + `('should clear $fn', ({ fn, expected }) => { + expect(journeyDb[fn]()).toEqual(expected); + }); + }); + + describe('tasks and actors should be added', function() { + journeyDb.setTitle('Shopping'); + journeyDb.addSection('Journey to the shops'); + journeyDb.addTask('Get car keys', ':5:Dad'); + journeyDb.addTask('Go to car', ':3:Dad, Mum, Child#1, Child#2'); + journeyDb.addTask('Drive to supermarket', ':4:Dad'); + journeyDb.addSection('Do shopping'); + journeyDb.addTask('Go shopping', ':5:Mum'); + + expect(journeyDb.getTitle()).toEqual('Shopping'); + expect(journeyDb.getTasks()).toEqual([ + { + score: 5, + people: ['Dad'], + section: 'Journey to the shops', + task: 'Get car keys', + type: 'Journey to the shops' + }, + { + score: 3, + people: ['Dad', 'Mum', 'Child#1', 'Child#2'], + section: 'Journey to the shops', + task: 'Go to car', + type: 'Journey to the shops' + }, + { + score: 4, + people: ['Dad'], + section: 'Journey to the shops', + task: 'Drive to supermarket', + type: 'Journey to the shops' + }, + { + score: 5, + people: ['Mum'], + section: 'Do shopping', + task: 'Go shopping', + type: 'Do shopping' + } + ]); + expect(journeyDb.getActors()).toEqual(['Child#1', 'Child#2', 'Dad', 'Mum']); + + expect(journeyDb.getSections()).toEqual(['Journey to the shops', 'Do shopping']); + }); +}); diff --git a/src/diagrams/user-journey/journeyRenderer.js b/src/diagrams/user-journey/journeyRenderer.js new file mode 100644 index 000000000..f41d835be --- /dev/null +++ b/src/diagrams/user-journey/journeyRenderer.js @@ -0,0 +1,284 @@ +import * as d3 from 'd3'; + +import { parser } from './parser/journey'; +import journeyDb from './journeyDb'; +import svgDraw from './svgDraw'; + +parser.yy = journeyDb; + +const conf = { + leftMargin: 150, + diagramMarginX: 50, + diagramMarginY: 20, + // Margin between tasks + taskMargin: 50, + // Width of task boxes + width: 150, + // Height of task boxes + height: 50, + taskFontSize: 14, + taskFontFamily: '"Open-Sans", "sans-serif"', + // Margin around loop boxes + boxMargin: 10, + boxTextMargin: 5, + noteMargin: 10, + // Space between messages + messageMargin: 35, + // Multiline message alignment + messageAlign: 'center', + // Depending on css styling this might need adjustment + // Projects the edge of the diagram downwards + bottomMarginAdj: 1, + + // width of activation box + activationWidth: 10, + + // text placement as: tspan | fo | old only text as before + textPlacement: 'fo', + + actorColours: ['#8FBC8F', '#7CFC00', '#00FFFF', '#20B2AA', '#B0E0E6', '#FFFFE0'], + + sectionFills: ['#191970', '#8B008B', '#4B0082', '#2F4F4F', '#800000', '#8B4513', '#00008B'], + sectionColours: ['#fff'] +}; + +export const setConf = function(cnf) { + const keys = Object.keys(cnf); + + keys.forEach(function(key) { + conf[key] = cnf[key]; + }); +}; + +const actors = {}; + +function drawActorLegend(diagram) { + // Draw the actors + let yPos = 60; + Object.keys(actors).forEach(person => { + const colour = actors[person]; + + const circleData = { + cx: 20, + cy: yPos, + r: 7, + fill: colour, + stroke: '#000' + }; + svgDraw.drawCircle(diagram, circleData); + + const labelData = { + x: 40, + y: yPos + 7, + fill: '#666', + text: person + }; + svgDraw.drawText(diagram, labelData); + + yPos += 20; + }); +} + +const LEFT_MARGIN = conf.leftMargin; +export const draw = function(text, id) { + parser.yy.clear(); + parser.parse(text + '\n'); + + bounds.init(); + const diagram = d3.select('#' + id); + diagram.attr('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + + svgDraw.initGraphics(diagram); + + const tasks = parser.yy.getTasks(); + const title = parser.yy.getTitle(); + + const actorNames = parser.yy.getActors(); + for (let member in actors) delete actors[member]; + let actorPos = 0; + actorNames.forEach(actorName => { + actors[actorName] = conf.actorColours[actorPos % conf.actorColours.length]; + actorPos++; + }); + + drawActorLegend(diagram); + bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50); + + drawTasks(diagram, tasks, 0); + + const box = bounds.getBounds(); + if (title) { + diagram + .append('text') + .text(title) + .attr('x', LEFT_MARGIN) + .attr('font-size', '4ex') + .attr('font-weight', 'bold') + .attr('y', 25); + } + const height = box.stopy - box.starty + 2 * conf.diagramMarginY; + const width = LEFT_MARGIN + box.stopx + 2 * conf.diagramMarginX; + if (conf.useMaxWidth) { + diagram.attr('height', '100%'); + diagram.attr('width', '100%'); + diagram.attr('style', 'max-width:' + width + 'px;'); + } else { + diagram.attr('height', height); + diagram.attr('width', width); + } + + // Draw activity line + diagram + .append('line') + .attr('x1', LEFT_MARGIN) + .attr('y1', conf.height * 4) // One section head + one task + margins + .attr('x2', width - LEFT_MARGIN - 4) // Subtract stroke width so arrow point is retained + .attr('y2', conf.height * 4) + .attr('stroke-width', 4) + .attr('stroke', 'black') + .attr('marker-end', 'url(#arrowhead)'); + + const extraVertForTitle = title ? 70 : 0; + diagram.attr('viewBox', `${box.startx} -25 ${width} ${height + extraVertForTitle}`); + diagram.attr('preserveAspectRatio', 'xMinYMin meet'); +}; + +export const bounds = { + data: { + startx: undefined, + stopx: undefined, + starty: undefined, + stopy: undefined + }, + verticalPos: 0, + + sequenceItems: [], + init: function() { + this.sequenceItems = []; + this.data = { + startx: undefined, + stopx: undefined, + starty: undefined, + stopy: undefined + }; + this.verticalPos = 0; + }, + updateVal: function(obj, key, val, fun) { + if (typeof obj[key] === 'undefined') { + obj[key] = val; + } else { + obj[key] = fun(val, obj[key]); + } + }, + updateBounds: function(startx, starty, stopx, stopy) { + const _self = this; + let cnt = 0; + function updateFn(type) { + return function updateItemBounds(item) { + cnt++; + // The loop sequenceItems is a stack so the biggest margins in the beginning of the sequenceItems + const n = _self.sequenceItems.length - cnt + 1; + + _self.updateVal(item, 'starty', starty - n * conf.boxMargin, Math.min); + _self.updateVal(item, 'stopy', stopy + n * conf.boxMargin, Math.max); + + _self.updateVal(bounds.data, 'startx', startx - n * conf.boxMargin, Math.min); + _self.updateVal(bounds.data, 'stopx', stopx + n * conf.boxMargin, Math.max); + + if (!(type === 'activation')) { + _self.updateVal(item, 'startx', startx - n * conf.boxMargin, Math.min); + _self.updateVal(item, 'stopx', stopx + n * conf.boxMargin, Math.max); + + _self.updateVal(bounds.data, 'starty', starty - n * conf.boxMargin, Math.min); + _self.updateVal(bounds.data, 'stopy', stopy + n * conf.boxMargin, Math.max); + } + }; + } + + this.sequenceItems.forEach(updateFn()); + }, + insert: function(startx, starty, stopx, stopy) { + const _startx = Math.min(startx, stopx); + const _stopx = Math.max(startx, stopx); + const _starty = Math.min(starty, stopy); + const _stopy = Math.max(starty, stopy); + + this.updateVal(bounds.data, 'startx', _startx, Math.min); + this.updateVal(bounds.data, 'starty', _starty, Math.min); + this.updateVal(bounds.data, 'stopx', _stopx, Math.max); + this.updateVal(bounds.data, 'stopy', _stopy, Math.max); + + this.updateBounds(_startx, _starty, _stopx, _stopy); + }, + bumpVerticalPos: function(bump) { + this.verticalPos = this.verticalPos + bump; + this.data.stopy = this.verticalPos; + }, + getVerticalPos: function() { + return this.verticalPos; + }, + getBounds: function() { + return this.data; + } +}; + +const fills = conf.sectionFills; +const textColours = conf.sectionColours; + +export const drawTasks = function(diagram, tasks, verticalPos) { + let lastSection = ''; + const sectionVHeight = conf.height * 2 + conf.diagramMarginY; + const taskPos = verticalPos + sectionVHeight; + + let sectionNumber = 0; + let fill = '#CCC'; + let colour = 'black'; + + // Draw the tasks + for (let i = 0; i < tasks.length; i++) { + let task = tasks[i]; + if (lastSection !== task.section) { + fill = fills[sectionNumber % fills.length]; + colour = textColours[sectionNumber % textColours.length]; + + const section = { + x: i * conf.taskMargin + i * conf.width + LEFT_MARGIN, + y: 50, + text: task.section, + fill, + colour + }; + + svgDraw.drawSection(diagram, section, conf); + lastSection = task.section; + sectionNumber++; + } + + // Collect the actors involved in the task + const taskActors = task.people.reduce((acc, actorName) => { + if (actors[actorName]) { + acc[actorName] = actors[actorName]; + } + + return acc; + }, {}); + + // Add some rendering data to the object + task.x = i * conf.taskMargin + i * conf.width + LEFT_MARGIN; + task.y = taskPos; + task.width = conf.diagramMarginX; + task.height = conf.diagramMarginY; + task.colour = colour; + task.fill = fill; + task.actors = taskActors; + + // Draw the box with the attached line + svgDraw.drawTask(diagram, task, conf); + bounds.insert(task.x, task.y, task.x + task.width + conf.taskMargin, 300 + 5 * 30); // stopy is the length of the descenders. + } +}; + +export default { + setConf, + draw +}; diff --git a/src/diagrams/user-journey/parser/journey.jison b/src/diagrams/user-journey/parser/journey.jison new file mode 100644 index 000000000..0de6045aa --- /dev/null +++ b/src/diagrams/user-journey/parser/journey.jison @@ -0,0 +1,52 @@ +/** mermaid + * https://mermaidjs.github.io/ + * (c) 2015 Knut Sveidqvist + * MIT license. + */ +%lex +%options case-insensitive +%% + +[\n]+ return 'NL'; +\s+ /* skip whitespace */ +\#[^\n]* /* skip comments */ +\%%[^\n]* /* skip comments */ + +"journey" return 'journey'; +"title"\s[^#\n;]+ return 'title'; +"section"\s[^#:\n;]+ return 'section'; +[^#:\n;]+ return 'taskName'; +":"[^#\n;]+ return 'taskData'; +":" return ':'; +<> return 'EOF'; +. return 'INVALID'; + +/lex + +%left '^' + +%start start + +%% /* language grammar */ + +start + : journey document 'EOF' { return $2; } + ; + +document + : /* empty */ { $$ = [] } + | document line {$1.push($2);$$ = $1} + ; + +line + : SPACE statement { $$ = $2 } + | statement { $$ = $1 } + | NL { $$=[];} + | EOF { $$=[];} + ; + +statement + : title {yy.setTitle($1.substr(6));$$=$1.substr(6);} + | section {yy.addSection($1.substr(8));$$=$1.substr(8);} + | taskName taskData {yy.addTask($1, $2);$$='task';} + ; diff --git a/src/diagrams/user-journey/parser/journey.spec.js b/src/diagrams/user-journey/parser/journey.spec.js new file mode 100644 index 000000000..fd19e8fa6 --- /dev/null +++ b/src/diagrams/user-journey/parser/journey.spec.js @@ -0,0 +1,117 @@ +/* eslint-env jasmine */ +/* eslint-disable no-eval */ +import { parser } from './journey'; +import journeyDb from '../journeyDb'; + +const parserFnConstructor = str => { + return () => { + parser.parse(str); + }; +}; + +describe('when parsing a journey diagram it', function() { + beforeEach(function() { + parser.yy = journeyDb; + parser.yy.clear(); + }); + + it('should handle a title definition', function() { + const str = 'journey\ntitle Adding journey diagram functionality to mermaid'; + + expect(parserFnConstructor(str)).not.toThrow(); + }); + + it('should handle a section definition', function() { + const str = + 'journey\n' + + 'title Adding journey diagram functionality to mermaid\n' + + 'section Order from website'; + + expect(parserFnConstructor(str)).not.toThrow(); + }); + it('should handle multiline section titles with different line breaks', function() { + const str = + 'journey\n' + + 'title Adding gantt diagram functionality to mermaid\n' + + 'section Line1
Line2
Line3
Line4Line5'; + + expect(parserFnConstructor(str)).not.toThrow(); + }); + + it('should handle a task definition', function() { + const str = + 'journey\n' + + 'title Adding journey diagram functionality to mermaid\n' + + 'section Documentation\n' + + 'A task: 5: Alice, Bob, Charlie\n' + + 'B task: 3:Bob, Charlie\n' + + 'C task: 5\n' + + 'D task: 5: Charlie, Alice\n' + + 'E task: 5:\n' + + 'section Another section\n' + + 'P task: 5:\n' + + 'Q task: 5:\n' + + 'R task: 5:'; + expect(parserFnConstructor(str)).not.toThrow(); + + const tasks = parser.yy.getTasks(); + expect(tasks.length).toEqual(8); + + expect(tasks[0]).toEqual({ + score: 5, + people: ['Alice', 'Bob', 'Charlie'], + section: 'Documentation', + task: 'A task', + type: 'Documentation' + }); + expect(tasks[1]).toEqual({ + score: 3, + people: ['Bob', 'Charlie'], + section: 'Documentation', + type: 'Documentation', + task: 'B task' + }); + expect(tasks[2]).toEqual({ + score: 5, + people: [], + section: 'Documentation', + type: 'Documentation', + task: 'C task' + }); + expect(tasks[3]).toEqual({ + score: 5, + people: ['Charlie', 'Alice'], + section: 'Documentation', + task: 'D task', + type: 'Documentation' + }); + expect(tasks[4]).toEqual({ + score: 5, + people: [''], + section: 'Documentation', + type: 'Documentation', + task: 'E task' + }); + expect(tasks[5]).toEqual({ + score: 5, + people: [''], + section: 'Another section', + type: 'Another section', + task: 'P task' + }); + expect(tasks[6]).toEqual({ + score: 5, + people: [''], + section: 'Another section', + type: 'Another section', + task: 'Q task' + }); + expect(tasks[7]).toEqual({ + score: 5, + people: [''], + section: 'Another section', + type: 'Another section', + task: 'R task' + }); + }); +}); diff --git a/src/diagrams/user-journey/svgDraw.js b/src/diagrams/user-journey/svgDraw.js new file mode 100644 index 000000000..db2e0aad5 --- /dev/null +++ b/src/diagrams/user-journey/svgDraw.js @@ -0,0 +1,429 @@ +import * as d3 from 'd3'; + +export const drawRect = function(elem, rectData) { + const rectElem = elem.append('rect'); + rectElem.attr('x', rectData.x); + rectElem.attr('y', rectData.y); + rectElem.attr('fill', rectData.fill); + rectElem.attr('stroke', rectData.stroke); + rectElem.attr('width', rectData.width); + rectElem.attr('height', rectData.height); + rectElem.attr('rx', rectData.rx); + rectElem.attr('ry', rectData.ry); + + if (typeof rectData.class !== 'undefined') { + rectElem.attr('class', rectData.class); + } + + return rectElem; +}; + +export const drawFace = function(element, faceData) { + const radius = 15; + const circleElement = element + .append('circle') + .attr('cx', faceData.cx) + .attr('cy', faceData.cy) + .attr('fill', '#FFF8DC') + .attr('stroke', '#999') + .attr('r', radius) + .attr('stroke-width', 2) + .attr('overflow', 'visible'); + + const face = element.append('g'); + + //left eye + face + .append('circle') + .attr('cx', faceData.cx - radius / 3) + .attr('cy', faceData.cy - radius / 3) + .attr('r', 1.5) + .attr('stroke-width', 2) + .attr('fill', '#666') + .attr('stroke', '#666'); + + //right eye + face + .append('circle') + .attr('cx', faceData.cx + radius / 3) + .attr('cy', faceData.cy - radius / 3) + .attr('r', 1.5) + .attr('stroke-width', 2) + .attr('fill', '#666') + .attr('stroke', '#666'); + + function smile(face) { + const arc = d3 + .arc() + .startAngle(Math.PI / 2) + .endAngle(3 * (Math.PI / 2)) + .innerRadius(radius / 2) + .outerRadius(radius / 2.2); + //mouth + face + .append('path') + .attr('d', arc) + .attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 2) + ')'); + } + + function sad(face) { + const arc = d3 + .arc() + .startAngle((3 * Math.PI) / 2) + .endAngle(5 * (Math.PI / 2)) + .innerRadius(radius / 2) + .outerRadius(radius / 2.2); + //mouth + face + .append('path') + .attr('d', arc) + .attr('transform', 'translate(' + faceData.cx + ',' + (faceData.cy + 7) + ')'); + } + + function ambivalent(face) { + face + .append('line') + .attr('stroke', 2) + .attr('x1', faceData.cx - 5) + .attr('y1', faceData.cy + 7) + .attr('x2', faceData.cx + 5) + .attr('y2', faceData.cy + 7) + .attr('class', 'task-line') + .attr('stroke-width', '1px') + .attr('stroke', '#666'); + } + + if (faceData.score > 3) { + smile(face); + } else if (faceData.score < 3) { + sad(face); + } else { + ambivalent(face); + } + + return circleElement; +}; + +export const drawCircle = function(element, circleData) { + const circleElement = element.append('circle'); + circleElement.attr('cx', circleData.cx); + circleElement.attr('cy', circleData.cy); + circleElement.attr('fill', circleData.fill); + circleElement.attr('stroke', circleData.stroke); + circleElement.attr('r', circleData.r); + + if (typeof circleElement.class !== 'undefined') { + circleElement.attr('class', circleElement.class); + } + + if (typeof circleData.title !== 'undefined') { + circleElement.append('title').text(circleData.title); + } + + return circleElement; +}; + +export const drawText = function(elem, textData) { + // Remove and ignore br:s + const nText = textData.text.replace(//gi, ' '); + + const textElem = elem.append('text'); + textElem.attr('x', textData.x); + textElem.attr('y', textData.y); + textElem.attr('fill', textData.fill); + textElem.style('text-anchor', textData.anchor); + + if (typeof textData.class !== 'undefined') { + textElem.attr('class', textData.class); + } + + const span = textElem.append('tspan'); + span.attr('x', textData.x + textData.textMargin * 2); + span.text(nText); + + return textElem; +}; + +export const drawLabel = function(elem, txtObject) { + function genPoints(x, y, width, height, cut) { + return ( + x + + ',' + + y + + ' ' + + (x + width) + + ',' + + y + + ' ' + + (x + width) + + ',' + + (y + height - cut) + + ' ' + + (x + width - cut * 1.2) + + ',' + + (y + height) + + ' ' + + x + + ',' + + (y + height) + ); + } + const polygon = elem.append('polygon'); + polygon.attr('points', genPoints(txtObject.x, txtObject.y, 50, 20, 7)); + polygon.attr('class', 'labelBox'); + + txtObject.y = txtObject.y + txtObject.labelMargin; + txtObject.x = txtObject.x + 0.5 * txtObject.labelMargin; + drawText(elem, txtObject); +}; + +export const drawSection = function(elem, section, conf) { + const g = elem.append('g'); + + const rect = getNoteRect(); + rect.x = section.x; + rect.y = section.y; + rect.fill = section.fill; + rect.width = conf.width; + rect.height = conf.height; + rect.class = 'journey-section'; + rect.rx = 3; + rect.ry = 3; + drawRect(g, rect); + + _drawTextCandidateFunc(conf)( + section.text, + g, + rect.x, + rect.y, + rect.width, + rect.height, + { class: 'journey-section' }, + conf, + section.colour + ); +}; + +let taskCount = -1; +/** + * Draws an actor in the diagram with the attaced line + * @param elem The HTML element + * @param task The task to render + * @param conf The global configuration + */ +export const drawTask = function(elem, task, conf) { + const center = task.x + conf.width / 2; + const g = elem.append('g'); + taskCount++; + const maxHeight = 300 + 5 * 30; + g.append('line') + .attr('id', 'task' + taskCount) + .attr('x1', center) + .attr('y1', task.y) + .attr('x2', center) + .attr('y2', maxHeight) + .attr('class', 'task-line') + .attr('stroke-width', '1px') + .attr('stroke-dasharray', '4 2') + .attr('stroke', '#666'); + + drawFace(g, { + cx: center, + cy: 300 + (5 - task.score) * 30, + score: task.score + }); + + const rect = getNoteRect(); + rect.x = task.x; + rect.y = task.y; + rect.fill = task.fill; + rect.width = conf.width; + rect.height = conf.height; + rect.class = 'task'; + rect.rx = 3; + rect.ry = 3; + drawRect(g, rect); + + let xPos = task.x + 14; + task.people.forEach(person => { + const colour = task.actors[person]; + + const circle = { + cx: xPos, + cy: task.y, + r: 7, + fill: colour, + stroke: '#000', + title: person + }; + + drawCircle(g, circle); + xPos += 10; + }); + + _drawTextCandidateFunc(conf)( + task.task, + g, + rect.x, + rect.y, + rect.width, + rect.height, + { class: 'task' }, + conf, + task.colour + ); +}; + +/** + * Draws a background rectangle + * @param elem The html element + * @param bounds The bounds of the drawing + */ +export const drawBackgroundRect = function(elem, bounds) { + const rectElem = drawRect(elem, { + x: bounds.startx, + y: bounds.starty, + width: bounds.stopx - bounds.startx, + height: bounds.stopy - bounds.starty, + fill: bounds.fill, + class: 'rect' + }); + rectElem.lower(); +}; + +export const getTextObj = function() { + return { + x: 0, + y: 0, + fill: undefined, + 'text-anchor': 'start', + width: 100, + height: 100, + textMargin: 0, + rx: 0, + ry: 0 + }; +}; + +export const getNoteRect = function() { + return { + x: 0, + y: 0, + width: 100, + anchor: 'start', + height: 100, + rx: 0, + ry: 0 + }; +}; + +const _drawTextCandidateFunc = (function() { + function byText(content, g, x, y, width, height, textAttrs, colour) { + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y + height / 2 + 5) + .style('font-color', colour) + .style('text-anchor', 'middle') + .text(content); + _setTextAttrs(text, textAttrs); + } + + function byTspan(content, g, x, y, width, height, textAttrs, conf, colour) { + const { taskFontSize, taskFontFamily } = conf; + + const lines = content.split(//gi); + for (let i = 0; i < lines.length; i++) { + const dy = i * taskFontSize - (taskFontSize * (lines.length - 1)) / 2; + const text = g + .append('text') + .attr('x', x + width / 2) + .attr('y', y) + .attr('fill', colour) + .style('text-anchor', 'middle') + .style('font-size', taskFontSize) + .style('font-family', taskFontFamily); + text + .append('tspan') + .attr('x', x + width / 2) + .attr('dy', dy) + .text(lines[i]); + + text + .attr('y', y + height / 2.0) + .attr('dominant-baseline', 'central') + .attr('alignment-baseline', 'central'); + + _setTextAttrs(text, textAttrs); + } + } + + function byFo(content, g, x, y, width, height, textAttrs, conf, colour) { + const body = g.append('switch'); + const f = body + .append('foreignObject') + .attr('x', x) + .attr('y', y) + .attr('width', width) + .attr('height', height) + .attr('position', 'fixed'); + + const text = f + .append('div') + .style('display', 'table') + .style('height', '100%') + .style('width', '100%'); + + text + .append('div') + .style('display', 'table-cell') + .style('text-align', 'center') + .style('vertical-align', 'middle') + .style('color', colour) + .text(content); + + byTspan(content, body, x, y, width, height, textAttrs, conf); + _setTextAttrs(text, textAttrs); + } + + function _setTextAttrs(toText, fromTextAttrsDict) { + for (const key in fromTextAttrsDict) { + if (key in fromTextAttrsDict) { + // eslint-disable-line + // noinspection JSUnfilteredForInLoop + toText.attr(key, fromTextAttrsDict[key]); + } + } + } + + return function(conf) { + return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan; + }; +})(); + +const initGraphics = function(graphics) { + graphics + .append('defs') + .append('marker') + .attr('id', 'arrowhead') + .attr('refX', 5) + .attr('refY', 2) + .attr('markerWidth', 6) + .attr('markerHeight', 4) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 0,0 V 4 L6,2 Z'); // this is actual shape for arrowhead +}; + +export default { + drawRect, + drawCircle, + drawSection, + drawText, + drawLabel, + drawTask, + drawBackgroundRect, + getTextObj, + getNoteRect, + initGraphics +}; From e4a2d7dfb75f7517e5bf335206cb6d08ad8b9550 Mon Sep 17 00:00:00 2001 From: Russell Geraghty Date: Sat, 4 Apr 2020 17:50:02 +0100 Subject: [PATCH 2/2] Updated API --- cypress/platform/user-journey.html | 2 +- docs/mermaidAPI.md | 91 +++------------------------ docs/user-journey.md | 23 +++++++ src/mermaidAPI.js | 98 ++++++++++++++++++++++++++++++ src/utils.js | 4 ++ 5 files changed, 134 insertions(+), 84 deletions(-) create mode 100644 docs/user-journey.md diff --git a/cypress/platform/user-journey.html b/cypress/platform/user-journey.html index 2fcd48046..8c4a7b63b 100644 --- a/cypress/platform/user-journey.html +++ b/cypress/platform/user-journey.html @@ -33,7 +33,7 @@ diff --git a/docs/mermaidAPI.md b/docs/mermaidAPI.md index 2469edd12..6dd749e27 100644 --- a/docs/mermaidAPI.md +++ b/docs/mermaidAPI.md @@ -263,84 +263,14 @@ The number of alternating section styles. Datetime format of the axis. This might need adjustment to match your locale and preferences **Default value '%Y-%m-%d'**. -## journey - -The object containing configurations specific for sequence diagrams - -### diagramMarginX - -margin to the right and left of the sequence diagram. -**Default value 50**. - -### diagramMarginY - -margin to the over and under the sequence diagram. -**Default value 10**. - -### actorMargin - -Margin between actors. -**Default value 50**. - -### width - -Width of actor boxes -**Default value 150**. - -### height - -Height of actor boxes -**Default value 65**. - -### boxMargin - -Margin around loop boxes -**Default value 10**. - -### boxTextMargin - -margin around the text in loop/alt/opt boxes -**Default value 5**. - -### noteMargin - -margin around notes. -**Default value 10**. - -### messageMargin - -Space between messages. -**Default value 35**. - -### messageAlign - -Multiline message alignment. Possible values are: - -- left -- center **default** -- right - -### bottomMarginAdj - -Depending on css styling this might need adjustment. -Prolongs the edge of the diagram downwards. -**Default value 1**. - -### useMaxWidth - -when this flag is set the height and width is set to 100% and is then scaling with the -available space if not the absolute space required is used. -**Default value true**. - -### rightAngles - -This will display arrows that start and begin at the same node as right angles, rather than a curve -**Default value false**. - ## er The object containing configurations specific for entity relationship diagrams +### diagramPadding + +The amount of padding around the diagram as a whole so that embedded diagrams have margins, expressed in pixels + ### layoutDirection Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL', @@ -348,15 +278,15 @@ where T = top, B = bottom, L = left, and R = right. ### minEntityWidth -The mimimum width of an entity box +The mimimum width of an entity box, expressed in pixels ### minEntityHeight -The minimum height of an entity box +The minimum height of an entity box, expressed in pixels ### entityPadding -The minimum internal padding between the text in an entity box and the enclosing box borders +The minimum internal padding between the text in an entity box and the enclosing box borders, expressed in pixels ### stroke @@ -366,12 +296,6 @@ Stroke color of box edges and lines Fill color of entity boxes -### fillOpacity - -Opacity of entity boxes - if you want to see how the crows feet -retain their elegant joins to the boxes regardless of the angle of incidence -then override this to something less than 100% - ### fontSize Font size @@ -431,6 +355,7 @@ mermaidAPI.initialize({ boxTextMargin:5, noteMargin:10, messageMargin:35, + messageAlign:'center', mirrorActors:true, bottomMarginAdj:1, useMaxWidth:true, diff --git a/docs/user-journey.md b/docs/user-journey.md new file mode 100644 index 000000000..2446c6db0 --- /dev/null +++ b/docs/user-journey.md @@ -0,0 +1,23 @@ +# User Journey Diagram + +> User journeys describe at a high level of detail exactly what steps different users take to complete a specific task within a system, application or website. This technique shows the current (as-is) user workflow, and reveals areas of improvement for the to-be workflow. (Wikipedia) + +Mermaid can render user journey diagrams: + +```markdown +journey + title My working day + section Go to work + Make tea: 5: Me + Go upstairs: 3: Me + Do work: 1: Me, Cat + section Go home + Go downstairs: 5: Me + Sit down: 5: Me +``` + +Each user journey is split into sections, these describe the part of the task +the user is trying to complete. + +Tasks syntax is `Task name: : ` + diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 9dd4c77ab..fabc9e096 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -45,6 +45,9 @@ import pieDb from './diagrams/pie/pieDb'; import erDb from './diagrams/er/erDb'; import erParser from './diagrams/er/parser/erDiagram'; import erRenderer from './diagrams/er/erRenderer'; +import journeyParser from './diagrams/user-journey/parser/journey'; +import journeyDb from './diagrams/user-journey/journeyDb'; +import journeyRenderer from './diagrams/user-journey/journeyRenderer'; const themes = {}; for (const themeName of ['default', 'forest', 'dark', 'neutral']) { @@ -337,6 +340,92 @@ const config = { */ axisFormat: '%Y-%m-%d' }, + /** + * The object containing configurations specific for sequence diagrams + */ + journey: { + /** + * margin to the right and left of the sequence diagram. + * **Default value 50**. + */ + diagramMarginX: 50, + + /** + * margin to the over and under the sequence diagram. + * **Default value 10**. + */ + diagramMarginY: 10, + + /** + * Margin between actors. + * **Default value 50**. + */ + actorMargin: 50, + + /** + * Width of actor boxes + * **Default value 150**. + */ + width: 150, + + /** + * Height of actor boxes + * **Default value 65**. + */ + height: 65, + + /** + * Margin around loop boxes + * **Default value 10**. + */ + boxMargin: 10, + + /** + * margin around the text in loop/alt/opt boxes + * **Default value 5**. + */ + boxTextMargin: 5, + + /** + * margin around notes. + * **Default value 10**. + */ + noteMargin: 10, + + /** + * Space between messages. + * **Default value 35**. + */ + messageMargin: 35, + + /** + * Multiline message alignment. Possible values are: + * * left + * * center **default** + * * right + */ + messageAlign: 'center', + + /** + * Depending on css styling this might need adjustment. + * Prolongs the edge of the diagram downwards. + * **Default value 1**. + */ + bottomMarginAdj: 1, + + /** + * when this flag is set the height and width is set to 100% and is then scaling with the + * available space if not the absolute space required is used. + * **Default value true**. + */ + useMaxWidth: true, + + /** + * This will display arrows that start and begin at the same node as right angles, rather than a curve + * **Default value false**. + */ + rightAngles: false + }, class: {}, git: {}, state: { @@ -465,6 +554,11 @@ function parse(text) { parser = erParser; parser.parser.yy = erDb; break; + case 'journey': + logger.debug('Journey'); + parser = journeyParser; + parser.parser.yy = journeyDb; + break; } parser.parser.yy.parseError = (str, hash) => { @@ -696,6 +790,10 @@ const render = function(id, _txt, cb, container) { erRenderer.setConf(config.er); erRenderer.draw(txt, id, pkg.version); break; + case 'journey': + journeyRenderer.setConf(config.journey); + journeyRenderer.draw(txt, id, pkg.version); + break; } d3.select(`[id="${id}"]`) diff --git a/src/utils.js b/src/utils.js index 823191ffc..b1073952f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -59,6 +59,10 @@ export const detectType = function(text) { return 'er'; } + if (text.match(/^\s*journey/)) { + return 'journey'; + } + return 'flowchart'; };