Merge pull request #1116 from mermaid-js/bug/1110_state_diagram_rendering_composit_state

Bug/1110 state diagram rendering composit state
This commit is contained in:
Knut Sveidqvist 2019-12-04 19:15:06 +01:00 committed by GitHub
commit 9fe0aa0604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 82 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [knsv]
#patreon: # Replace with a single Patreon username
#open_collective: # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#otechie: # Replace with a single Otechie username
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@ -2,19 +2,20 @@
import { imgSnapshotTest } from '../../helpers/util';
describe('State diagram', () => {
it('should render a flowchart full of circles', () => {
it('should render a state with states in it', () => {
imgSnapshotTest(
`
stateDiagram
State1: The state with a note
note right of State1
Important information! You\ncan write
notes with multiple lines...
Here is another line...
And another line...
end note
stateDiagram
state PersonalizedCockpit {
Other
state Parent {
C
}
}
`,
{}
{
logLevel: 0,
}
);
});
});

View File

@ -106,6 +106,22 @@ describe('State diagram', () => {
);
cy.get('svg');
});
it('should render a note with multiple lines in it', () => {
imgSnapshotTest(
`
stateDiagram
State1: The state with a note
note right of State1
Important information! You\ncan write
notes with multiple lines...
Here is another line...
And another line...
end note
`,
{}
);
});
it('should render a states with descriptions including multi-line descriptions', () => {
imgSnapshotTest(
`
@ -276,4 +292,33 @@ describe('State diagram', () => {
);
cy.get('svg');
});
it('should render a state with states in it', () => {
imgSnapshotTest(
`
stateDiagram
state PilotCockpit {
state Parent {
C
}
}
`,
{
logLevel: 0,
}
);
});
it('Simplest compone state', () => {
imgSnapshotTest(
`
stateDiagram
state Parent {
C
}
`,
{
logLevel: 0,
}
);
});
});

View File

@ -8,12 +8,18 @@
</head>
<body>
<h1>info below</h1>
<div class="mermaid">graph TB
a --> b
a --> c
a --> d
</div>
<script src="./mermaid.js"></script>
<div style="display: flex;">
<div class="mermaid">stateDiagram
[*] --> State1
State1 --> State2 : Transition 1
State1 --> State3 : Transition 2
State1 --> State4 : Transition 3
State1 --> State5 : Transition 4
State2 --> State3 : Transition 5
State1 --> [*]
</div>
</div>
<script src="./mermaid.js"></script>
<script>
mermaid.initialize({
theme: 'forest',

View File

@ -73,7 +73,7 @@ export const drawDescrState = (g, stateDef) => {
const title = g
.append('text')
.attr('x', 2 * getConfig().state.padding)
.attr('y', getConfig().state.textHeight + 1.5 * getConfig().state.padding)
.attr('y', getConfig().state.textHeight + 1.3 * getConfig().state.padding)
.attr('font-size', getConfig().state.fontSize)
.attr('class', 'state-title')
.text(stateDef.descriptions[0]);
@ -87,7 +87,7 @@ export const drawDescrState = (g, stateDef) => {
.attr(
'y',
titleHeight +
getConfig().state.padding * 0.2 +
getConfig().state.padding * 0.4 +
getConfig().state.dividerMargin +
getConfig().state.textHeight
)
@ -129,71 +129,108 @@ export const drawDescrState = (g, stateDef) => {
* Adds the creates a box around the existing content and adds a
* panel for the id on top of the content.
*/
export const addIdAndBox = (g, stateDef) => {
// TODO Move hardcodings to conf
// const addTspan = function(textEl, txt, isFirst) {
// const tSpan = textEl
// .append('tspan')
// .attr('x', 2 * getConfig().state.padding)
// .text(txt);
// if (!isFirst) {
// tSpan.attr('dy', getConfig().state.textHeight);
// }
// };
/**
* Function that creates an title row and a frame around a substate for a composit state diagram.
* The function returns a new d3 svg object with updated width and height properties;
* @param {*} g The d3 svg object for the substate to framed
* @param {*} stateDef The info about the
*/
export const addTitleAndBox = (g, stateDef, altBkg) => {
const pad = getConfig().state.padding;
const dblPad = 2 * getConfig().state.padding;
const orgBox = g.node().getBBox();
const orgWidth = orgBox.width;
const orgX = orgBox.x;
const title = g
.append('text')
.attr('x', 2 * getConfig().state.padding)
.attr('x', 0)
.attr('y', getConfig().state.titleShift)
.attr('font-size', getConfig().state.fontSize)
.attr('class', 'state-title')
.text(stateDef.id);
const titleBox = title.node().getBBox();
const lineY = 1 - getConfig().state.textHeight;
const descrLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', lineY)
.attr('y2', lineY)
.attr('class', 'descr-divider');
const titleWidth = titleBox.width + dblPad;
let width = Math.max(titleWidth, orgWidth); // + dblPad;
if (width === orgWidth) {
width = width + dblPad;
}
let startX;
// const lineY = 1 - getConfig().state.textHeight;
// const descrLine = g
// .append('line') // text label for the x axis
// .attr('x1', 0)
// .attr('y1', lineY)
// .attr('y2', lineY)
// .attr('class', 'descr-divider');
const graphBox = g.node().getBBox();
title.attr('x', graphBox.width / 2 - titleBox.width / 2);
descrLine.attr('x2', graphBox.width + getConfig().state.padding);
// console.warn(width / 2, titleWidth / 2, getConfig().state.padding, orgBox);
// descrLine.attr('x2', graphBox.width + getConfig().state.padding);
if (stateDef.doc) {
// cnsole.warn(
// stateDef.id,
// 'orgX: ',
// orgX,
// 'width: ',
// width,
// 'titleWidth: ',
// titleWidth,
// 'orgWidth: ',
// orgWidth,
// 'width',
// width
// );
}
startX = orgX - pad;
if (titleWidth > orgWidth) {
startX = (orgWidth - width) / 2 + pad;
}
if (Math.abs(orgX - graphBox.x) < pad) {
if (titleWidth > orgWidth) {
startX = orgX - (titleWidth - orgWidth) / 2;
}
}
const lineY = 1 - getConfig().state.textHeight;
// White color
g.insert('rect', ':first-child')
.attr('x', graphBox.x)
.attr('x', startX)
.attr('y', lineY)
.attr('class', 'composit')
.attr('width', graphBox.width + getConfig().state.padding)
.attr('class', altBkg ? 'alt-composit' : 'composit')
.attr('width', width)
.attr(
'height',
graphBox.height + getConfig().state.textHeight + getConfig().state.titleShift + 1
)
.attr('rx', '0');
title.attr('x', startX + pad);
if (titleWidth <= orgWidth) title.attr('x', orgX + (width - dblPad) / 2 - titleWidth / 2 + pad);
// Title background
g.insert('rect', ':first-child')
.attr('x', graphBox.x)
.attr('x', startX)
.attr(
'y',
getConfig().state.titleShift - getConfig().state.textHeight - getConfig().state.padding
)
.attr('width', graphBox.width + getConfig().state.padding)
.attr('width', width)
// Just needs to be higher then the descr line, will be clipped by the white color box
.attr('height', getConfig().state.textHeight * 3)
.attr('rx', getConfig().state.radius);
// Full background
g.insert('rect', ':first-child')
.attr('x', graphBox.x)
.attr('x', startX)
.attr(
'y',
getConfig().state.titleShift - getConfig().state.textHeight - getConfig().state.padding
)
.attr('width', graphBox.width + getConfig().state.padding)
.attr('width', width)
.attr('height', graphBox.height + 3 + 2 * getConfig().state.textHeight)
.attr('rx', getConfig().state.radius);

View File

@ -5,40 +5,19 @@ import { logger } from '../../logger';
import stateDb from './stateDb';
import { parser } from './parser/stateDiagram';
// import idCache from './id-cache';
import { drawState, addIdAndBox, drawEdge } from './shapes';
import { drawState, addTitleAndBox, drawEdge } from './shapes';
import { getConfig } from '../../config';
parser.yy = stateDb;
// TODO Move conf object to main conf in mermaidAPI
let conf;
// {
// // Used
// padding: 5,
// // Font size factor, this is used to guess the width of the edges labels before rendering by dagre
// // layout. This might need updating if/when switching font
// fontSizeFactor: 5.02,
// labelHeight: 16,
// edgeLengthFactor: '20',
// compositTitleSize: 35
// };
const transformationLog = {};
export const setConf = function() {};
// Todo optimize
// const getGraphId = function(label) {
// const keys = idCache.keys();
// for (let i = 0; i < keys.length; i++) {
// if (idCache.get(keys[i]).label === label) {
// return keys[i];
// }
// }
// return undefined;
// };
/**
* Setup arrow head and define the marker. The result is appended to the svg.
@ -68,11 +47,11 @@ export const draw = function(text, id) {
parser.parse(text);
logger.debug('Rendering diagram ' + text);
// /// / Fetch the default direction, use TD if none was found
// Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id='${id}']`);
insertMarkers(diagram);
// // Layout graph, Create a new directed graph
// Layout graph, Create a new directed graph
const graph = new graphlib.Graph({
multigraph: false,
compound: true,
@ -81,13 +60,13 @@ export const draw = function(text, id) {
// ranksep: '20'
});
// // Default to assigning a new object as a label for each new edge.
// Default to assigning a new object as a label for each new edge.
graph.setDefaultEdgeLabel(function() {
return {};
});
const rootDoc = stateDb.getRootDoc();
renderDoc(rootDoc, diagram);
renderDoc(rootDoc, diagram, undefined, false);
const padding = conf.padding;
const bounds = diagram.node().getBBox();
@ -100,7 +79,7 @@ export const draw = function(text, id) {
// diagram.attr('height', height);
// Zoom in a bit
diagram.attr('width', width * 2);
diagram.attr('width', width * 1.75);
// diagram.attr('height', bounds.height * 3 + conf.padding * 2);
diagram.attr(
'viewBox',
@ -128,12 +107,21 @@ const getRows = s => {
return str.split('#br#');
};
const renderDoc = (doc, diagram, parentId) => {
const renderDoc = (doc, diagram, parentId, altBkg) => {
// // Layout graph, Create a new directed graph
const graph = new graphlib.Graph({
compound: true
});
let i;
let edgeFreeDoc = true;
for (i = 0; i < doc.length; i++) {
if (doc[i].stmt === 'relation') {
edgeFreeDoc = false;
break;
}
}
// Set an object for the graph label
if (parentId)
graph.setGraph({
@ -142,8 +130,11 @@ const renderDoc = (doc, diagram, parentId) => {
compound: true,
// acyclicer: 'greedy',
ranker: 'tight-tree',
ranksep: conf.edgeLengthFactor
ranksep: edgeFreeDoc ? 1 : conf.edgeLengthFactor,
nodeSep: edgeFreeDoc ? 1 : 50
// isMultiGraph: false
// ranksep: 5,
// nodesep: 1
});
else {
graph.setGraph({
@ -152,7 +143,8 @@ const renderDoc = (doc, diagram, parentId) => {
// isCompound: true,
// acyclicer: 'greedy',
// ranker: 'longest-path'
ranksep: conf.edgeLengthFactor,
ranksep: edgeFreeDoc ? 1 : conf.edgeLengthFactor,
nodeSep: edgeFreeDoc ? 1 : 50,
ranker: 'tight-tree'
// ranker: 'network-simplex'
// isMultiGraph: false
@ -185,14 +177,14 @@ const renderDoc = (doc, diagram, parentId) => {
.append('g')
.attr('id', stateDef.id)
.attr('class', 'stateGroup');
node = renderDoc(stateDef.doc, sub, stateDef.id);
node = renderDoc(stateDef.doc, sub, stateDef.id, !altBkg);
if (first) {
// first = false;
sub = addIdAndBox(sub, stateDef);
sub = addTitleAndBox(sub, stateDef, altBkg);
let boxBounds = sub.node().getBBox();
node.width = boxBounds.width;
node.height = boxBounds.height + 2 * conf.padding;
node.height = boxBounds.height + conf.padding / 2;
transformationLog[stateDef.id] = { y: conf.compositTitleSize };
} else {
// sub = addIdAndBox(sub, stateDef);
@ -278,8 +270,8 @@ const renderDoc = (doc, diagram, parentId) => {
pShift = 0;
}
}
divider.setAttribute('x1', 0 - pShift);
divider.setAttribute('x2', pWidth - pShift);
divider.setAttribute('x1', 0 - pShift + 8);
divider.setAttribute('x2', pWidth - pShift - 8);
});
} else {
logger.debug('No Node ' + v + ': ' + JSON.stringify(graph.node(v)));

View File

@ -37,6 +37,11 @@ g.stateGroup line {
border-bottom: 1px
}
.stateGroup .alt-composit {
fill: #e0e0e0;
border-bottom: 1px
}
.state-note {
stroke: $noteBorderColor;
fill: $noteBkgColor;