Merge pull request #1334 from russellgeraghty/feature/user-journey
Feature/user journey
This commit is contained in:
commit
397f57accb
23
README.md
23
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
|
|||
<td colspan="2" align="center"><i>Coming soon!</i></td>
|
||||
</tr>
|
||||
<!-- </Git> -->
|
||||
<!-- <Journey> -->
|
||||
<tr>
|
||||
<td><pre>
|
||||
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
|
||||
</pre></td>
|
||||
<td align="center">
|
||||
<img alt="User Journey Diagram" src="https://raw.githubusercontent.com/mermaid-js/mermaid/master/img/gray-journey.png" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- </Journey> -->
|
||||
|
||||
</table>
|
||||
|
||||
## Related projects
|
||||
|
|
|
@ -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
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
<html>
|
||||
<head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>User Journey</h1>
|
||||
<div class="mermaid">
|
||||
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
|
||||
</div>
|
||||
<script src="./mermaid.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
theme: 'forest',
|
||||
logLevel: 3,
|
||||
journey: { taskMargin: 30 },
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -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: <score>: <comma separated list of actors>`
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -27,7 +27,8 @@
|
|||
"test": "yarn lint && jest src/.*",
|
||||
"test:watch": "jest --watch src",
|
||||
"prepublishOnly": "yarn build && yarn release && yarn test && yarn e2e",
|
||||
"prepush": "yarn test"
|
||||
"prepush": "yarn test",
|
||||
"prepare": "yarn build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
let title = '';
|
||||
let currentSection = '';
|
||||
|
||||
const sections = [];
|
||||
const tasks = [];
|
||||
const rawTasks = [];
|
||||
|
||||
export const clear = function() {
|
||||
sections.length = 0;
|
||||
tasks.length = 0;
|
||||
currentSection = '';
|
||||
title = '';
|
||||
rawTasks.length = 0;
|
||||
};
|
||||
|
||||
export const setTitle = function(txt) {
|
||||
title = txt;
|
||||
};
|
||||
|
||||
export const getTitle = function() {
|
||||
return title;
|
||||
};
|
||||
|
||||
export const addSection = function(txt) {
|
||||
currentSection = txt;
|
||||
sections.push(txt);
|
||||
};
|
||||
|
||||
export const getSections = function() {
|
||||
return sections;
|
||||
};
|
||||
|
||||
export const getTasks = function() {
|
||||
let allItemsProcessed = compileTasks();
|
||||
const maxDepth = 100;
|
||||
let iterationCount = 0;
|
||||
while (!allItemsProcessed && iterationCount < maxDepth) {
|
||||
allItemsProcessed = compileTasks();
|
||||
iterationCount++;
|
||||
}
|
||||
|
||||
tasks.push(...rawTasks);
|
||||
|
||||
return tasks;
|
||||
};
|
||||
|
||||
const updateActors = function() {
|
||||
const tempActors = [];
|
||||
tasks.forEach(task => {
|
||||
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
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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 ':';
|
||||
<<EOF>> 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';}
|
||||
;
|
|
@ -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<br>Line2<br/>Line3</br />Line4<br\t/>Line5';
|
||||
|
||||
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'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(/<br\s*\/?>/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(/<br\s*\/?>/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
|
||||
};
|
|
@ -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}"]`)
|
||||
|
|
|
@ -59,6 +59,10 @@ export const detectType = function(text) {
|
|||
return 'er';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*journey/)) {
|
||||
return 'journey';
|
||||
}
|
||||
|
||||
return 'flowchart';
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue