#5574 Adding support for edge ids and animations

This commit is contained in:
Knut Sveidqvist 2024-12-17 14:56:18 +01:00
parent df636c6d0a
commit 9b00f1f2fb
11 changed files with 314 additions and 74 deletions

View File

@ -84,29 +84,66 @@
/* tspan {
font-size: 6px !important;
} */
/* .flowchart-link {
stroke-dasharray: 4, 4 !important;
animation: flow 1s linear infinite;
animation: dashdraw 4.93282s linear infinite;
stroke-width: 2px !important;
} */
@keyframes dashdraw {
from {
stroke-dashoffset: 0;
}
}
/*stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: 4.932820s linear infinite;*/
/* stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: dashdraw 4.932820s linear infinite;*/
</style>
</head>
<body>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
<pre id="diagram4" class="mermaid2">
flowchart LR
subgraph S2
subgraph s1["APA"]
D{"Use the editor"}
end
D -- Mermaid js --> I{"fa:fa-code Text"}
D --> I
D --> I
end
A --> B
</pre>
<pre id="diagram4" class="mermaid">
flowchart LR
A e1@==> B
e1@{ animate: true}
</pre>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round
class e1 animate
</pre>
<h2>infinite</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class e1 animate
</pre>
<h2>Mermaid - edge-animation-slow</h2>
<pre id="diagram4" class="mermaid">
flowchart LR
A e1@--> B
e1@{ animation: fast}
</pre>
<h2>Mermaid - edge-animation-fast</h2>
<pre id="diagram4" class="mermaid2">
flowchart LR
A e1@--> B
classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear;
class e1 edge-animation-fast
</pre>
<pre id="diagram4" class="mermaid2">
info </pre
>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -131,7 +168,7 @@ config:
end
end
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -144,7 +181,7 @@ config:
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -183,7 +220,7 @@ flowchart LR
n8@{ shape: rect}
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -199,7 +236,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -208,7 +245,7 @@ flowchart LR
A{A} --> B & C
</pre
>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@ -220,7 +257,7 @@ flowchart LR
end
</pre
>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk

View File

@ -24,7 +24,7 @@ import type {
FlowLink,
FlowVertexTypeParam,
} from './types.js';
import type { NodeMetaData } from '../../types.js';
import type { NodeMetaData, EdgeMetaData } from '../../types.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
let vertexCounter = 0;
@ -71,12 +71,38 @@ export const addVertex = function (
classes: string[],
dir: string,
props = {},
shapeData: any
metadata: any
) {
// console.log('addVertex', id, shapeData);
if (!id || id.trim().length === 0) {
return;
}
// Extract the metadata from the shapeData, the syntax for adding metadata for nodes and edges is the same
// so at this point we don't know if it's a node or an edge, but we can still extract the metadata
let doc;
if (metadata !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
if (!metadata.includes('\n')) {
yamlData = '{\n' + metadata + '\n}';
} else {
yamlData = metadata + '\n';
}
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
}
// Check if this is an edge
const edge = edges.find((e) => e.id === id);
if (edge) {
const edgeDoc = doc as EdgeMetaData;
if (edgeDoc?.animate) {
edge.animate = edgeDoc.animate;
}
if (edgeDoc?.animation) {
edge.animation = edgeDoc.animation;
}
return;
}
let txt;
let vertex = vertices.get(id);
@ -128,19 +154,7 @@ export const addVertex = function (
Object.assign(vertex.props, props);
}
if (shapeData !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
// console.log('shapeData', shapeData);
if (!shapeData.includes('\n')) {
// console.log('yamlData shapeData has no new lines', shapeData);
yamlData = '{\n' + shapeData + '\n}';
} else {
// console.log('yamlData shapeData has new lines', shapeData);
yamlData = shapeData + '\n';
}
// console.log('yamlData', yamlData);
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
if (doc !== undefined) {
if (doc.shape) {
if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
@ -187,11 +201,18 @@ export const addVertex = function (
* Function called by parser when a link/edge definition has been found
*
*/
export const addSingleLink = function (_start: string, _end: string, type: any) {
export const addSingleLink = function (_start: string, _end: string, type: any, id?: string) {
const start = _start;
const end = _end;
const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
const edge: FlowEdge = {
start: start,
end: end,
type: undefined,
text: '',
labelType: 'text',
classes: [],
};
log.info('abc78 Got edge...', edge);
const linkTextObj = type.text;
@ -210,6 +231,9 @@ export const addSingleLink = function (_start: string, _end: string, type: any)
edge.stroke = type.stroke;
edge.length = type.length > 10 ? 10 : type.length;
}
if (id) {
edge.id = id;
}
if (edges.length < (config.maxEdges ?? 500)) {
log.info('Pushing edge...');
@ -225,11 +249,17 @@ You have to call mermaid.initialize.`
}
};
export const addLink = function (_start: string[], _end: string[], type: unknown) {
log.info('addLink', _start, _end, type);
export const addLink = function (_start: string[], _end: string[], linkData: unknown) {
const id =
linkData && typeof linkData === 'object' && 'id' in linkData
? linkData.id?.replace('@', '')
: undefined;
log.info('addLink', _start, _end, id);
for (const start of _start) {
for (const end of _end) {
addSingleLink(start, end, type);
addSingleLink(start, end, linkData, id);
}
}
};
@ -282,7 +312,13 @@ export const updateLink = function (positions: ('default' | number)[], style: st
});
};
export const addClass = function (ids: string, style: string[]) {
export const addClass = function (ids: string, _style: string[]) {
const style = _style
.join()
.replace(/\\,/g, '§§§')
.replace(/,/g, ';')
.replace(/§§§/g, ',')
.split(';');
ids.split(',').forEach(function (id) {
let classNode = classes.get(id);
if (classNode === undefined) {
@ -337,6 +373,10 @@ export const setClass = function (ids: string, className: string) {
if (vertex) {
vertex.classes.push(className);
}
const edge = edges.find((e) => e.id === id);
if (edge) {
edge.classes.push(className);
}
const subGraph = subGraphLookup.get(id);
if (subGraph) {
subGraph.classes.push(className);
@ -997,7 +1037,7 @@ export const getData = () => {
styles.push(...rawEdge.style);
}
const edge: Edge = {
id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }),
id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }, rawEdge.id),
start: rawEdge.start,
end: rawEdge.end,
type: rawEdge.type ?? 'normal',
@ -1009,14 +1049,20 @@ export const getData = () => {
rawEdge?.stroke === 'invisible'
? ''
: 'edge-thickness-normal edge-pattern-solid flowchart-link',
arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart,
arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd,
arrowTypeStart:
rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeStart,
arrowTypeEnd:
rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeEnd,
arrowheadStyle: 'fill: #333',
cssCompiledStyles: getCompiledStyles(rawEdge.classes),
labelStyle: styles,
style: styles,
pattern: rawEdge.stroke,
look: config.look,
animate: rawEdge.animate,
animation: rawEdge.animation,
};
edges.push(edge);
});

View File

@ -39,6 +39,27 @@ const doubleEndedEdges = [
{ edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' },
{ edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' },
];
const regularEdges = [
{ edgeStart: '--', edgeEnd: '--x', stroke: 'normal', type: 'arrow_cross' },
{ edgeStart: '==', edgeEnd: '==x', stroke: 'thick', type: 'arrow_cross' },
{ edgeStart: '-.', edgeEnd: '.-x', stroke: 'dotted', type: 'arrow_cross' },
{ edgeStart: '--', edgeEnd: '--o', stroke: 'normal', type: 'arrow_circle' },
{ edgeStart: '==', edgeEnd: '==o', stroke: 'thick', type: 'arrow_circle' },
{ edgeStart: '-.', edgeEnd: '.-o', stroke: 'dotted', type: 'arrow_circle' },
{ edgeStart: '--', edgeEnd: '-->', stroke: 'normal', type: 'arrow_point' },
{ edgeStart: '==', edgeEnd: '==>', stroke: 'thick', type: 'arrow_point' },
{ edgeStart: '-.', edgeEnd: '.->', stroke: 'dotted', type: 'arrow_point' },
{ edgeStart: '--', edgeEnd: '----x', stroke: 'normal', type: 'arrow_cross' },
{ edgeStart: '==', edgeEnd: '====x', stroke: 'thick', type: 'arrow_cross' },
{ edgeStart: '-.', edgeEnd: '...-x', stroke: 'dotted', type: 'arrow_cross' },
{ edgeStart: '--', edgeEnd: '----o', stroke: 'normal', type: 'arrow_circle' },
{ edgeStart: '==', edgeEnd: '====o', stroke: 'thick', type: 'arrow_circle' },
{ edgeStart: '-.', edgeEnd: '...-o', stroke: 'dotted', type: 'arrow_circle' },
{ edgeStart: '--', edgeEnd: '---->', stroke: 'normal', type: 'arrow_point' },
{ edgeStart: '==', edgeEnd: '====>', stroke: 'thick', type: 'arrow_point' },
{ edgeStart: '-.', edgeEnd: '...->', stroke: 'dotted', type: 'arrow_point' },
];
describe('[Edges] when parsing', () => {
beforeEach(function () {
@ -67,6 +88,74 @@ describe('[Edges] when parsing', () => {
expect(edges[0].type).toBe('arrow_circle');
});
describe('edges with ids', function () {
describe('open ended edges with ids and labels', function () {
regularEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
});
it('should handle normal edges where you also have a node with metadata', function () {
const res = flow.parser.parse(`flowchart LR
A id1@-->B
A@{ shape: 'rect' }
`);
const edges = flow.parser.yy.getEdges();
expect(edges[0].id).toBe('id1');
});
});
describe('double ended edges with ids and labels', function () {
doubleEndedEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
const res = flow.parser.parse(
`flowchart TD;\nA e1@${edgeType.edgeStart} label ${edgeType.edgeEnd} B;`
);
const vert = flow.parser.yy.getVertices();
const edges = flow.parser.yy.getEdges();
expect(vert.get('A').id).toBe('A');
expect(vert.get('B').id).toBe('B');
expect(edges.length).toBe(1);
expect(edges[0].id).toBe('e1');
expect(edges[0].start).toBe('A');
expect(edges[0].end).toBe('B');
expect(edges[0].type).toBe(`${edgeType.type}`);
expect(edges[0].text).toBe('label');
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
});
});
});
});
describe('edges', function () {
doubleEndedEdges.forEach((edgeType) => {
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {

View File

@ -141,6 +141,7 @@ that id.
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
[^\s]+\@(?=[^\{]) { return 'LINK_ID'; }
[0-9]+ return 'NUM';
\# return 'BRKT';
":::" return 'STYLE_SEPARATOR';
@ -201,7 +202,9 @@ that id.
"*" return 'MULT';
"#" return 'BRKT';
"&" return 'AMP';
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING';
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ {
return 'NODE_STRING';
}
"-" return 'MINUS'
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
@ -361,7 +364,7 @@ spaceList
statement
: vertexStatement separator
{ /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes}
{ $$=$vertexStatement.nodes}
| styleStatement separator
{$$=[];}
| linkStyleStatement separator
@ -472,6 +475,8 @@ link: linkStatement arrowText
{$$ = $linkStatement;}
| START_LINK edgeText LINK
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};}
| LINK_ID START_LINK edgeText LINK
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": $LINK_ID};}
;
edgeText: edgeTextToken
@ -487,6 +492,8 @@ edgeText: edgeTextToken
linkStatement: LINK
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
| LINK_ID LINK
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": $LINK_ID};}
;
arrowText:

View File

@ -62,6 +62,10 @@ export interface FlowEdge {
length?: number;
text: string;
labelType: 'text';
classes: string[];
id?: string;
animation?: 'fast' | 'slow';
animate?: boolean;
}
export interface FlowClass {

View File

@ -9,6 +9,7 @@ import { curveBasis, line, select } from 'd3';
import rough from 'roughjs';
import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
const edgeLabels = new Map();
const terminalLabels = new Map();
@ -429,6 +430,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
const tail = startNode;
var head = endNode;
const edgeClassStyles = [];
for (const key in edge.cssCompiledStyles) {
if (isLabelStyle(key)) {
continue;
}
edgeClassStyles.push(edge.cssCompiledStyles[key]);
}
if (head.intersect && tail.intersect) {
points = points.slice(1, edge.points.length - 1);
points.unshift(tail.intersect(points[0]));
@ -521,12 +530,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
svgPath.attr('d', d);
elem.node().appendChild(svgPath.node());
} else {
const stylesFromClasses = edgeClassStyles.join(';');
const styles = edge.edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '';
let animationClass = '';
if (edge.animate) {
animationClass = ' edge-animation-fast';
}
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
svgPath = elem
.append('path')
.attr('d', linePath)
.attr('id', edge.id)
.attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : ''))
.attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? animationClass : '')
)
.attr('style', stylesFromClasses + ';' + styles);
}
// DEBUG code, DO NOT REMOVE

View File

@ -32,7 +32,28 @@ export const styles2Map = (styles: string[]) => {
});
return styleMap;
};
export const isLabelStyle = (key: string) => {
return (
key === 'color' ||
key === 'font-size' ||
key === 'font-family' ||
key === 'font-weight' ||
key === 'font-style' ||
key === 'text-decoration' ||
key === 'text-align' ||
key === 'text-transform' ||
key === 'line-height' ||
key === 'letter-spacing' ||
key === 'word-spacing' ||
key === 'text-shadow' ||
key === 'text-overflow' ||
key === 'white-space' ||
key === 'word-wrap' ||
key === 'word-break' ||
key === 'overflow-wrap' ||
key === 'hyphens'
);
};
export const styles2String = (node: Node) => {
const { stylesArray } = compileStyles(node);
const labelStyles: string[] = [];
@ -42,26 +63,7 @@ export const styles2String = (node: Node) => {
stylesArray.forEach((style) => {
const key = style[0];
if (
key === 'color' ||
key === 'font-size' ||
key === 'font-family' ||
key === 'font-weight' ||
key === 'font-style' ||
key === 'text-decoration' ||
key === 'text-align' ||
key === 'text-transform' ||
key === 'line-height' ||
key === 'letter-spacing' ||
key === 'word-spacing' ||
key === 'text-shadow' ||
key === 'text-overflow' ||
key === 'white-space' ||
key === 'word-wrap' ||
key === 'word-break' ||
key === 'overflow-wrap' ||
key === 'hyphens'
) {
if (isLabelStyle(key)) {
labelStyles.push(style.join(':') + ' !important');
} else {
nodeStyles.push(style.join(':') + ' !important');

View File

@ -101,6 +101,7 @@ export interface Edge {
arrowheadStyle?: string;
arrowTypeEnd?: string;
arrowTypeStart?: string;
cssCompiledStyles?: string[];
// Flowchart specific properties
defaultInterpolate?: string;
end?: string;

View File

@ -27,7 +27,28 @@ const getStyles = (
font-size: ${options.fontSize};
fill: ${options.textColor}
}
@keyframes edge-animation-frame {
from {
stroke-dashoffset: 0;
}
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
& .edge-animation-slow {
stroke-dasharray: 9,5 !important;
stroke-dashoffset: 900;
animation: dash 50s linear infinite;
stroke-linecap: round;
}
& .edge-animation-fast {
stroke-dasharray: 9,5 !important;
stroke-dashoffset: 900;
animation: dash 20s linear infinite;
stroke-linecap: round;
}
/* Classes common for multiple diagrams */
& .error-icon {

View File

@ -12,6 +12,11 @@ export interface NodeMetaData {
assigned?: string;
ticket?: string;
}
export interface EdgeMetaData {
animation?: 'fast' | 'slow';
animate?: boolean;
}
import type { MermaidConfig } from './config.type.js';
export interface Point {

View File

@ -937,8 +937,12 @@ export const getEdgeId = (
counter?: number;
prefix?: string;
suffix?: string;
}
},
id?: string
) => {
if (id) {
return id;
}
return `${prefix ? `${prefix}_` : ''}${from}_${to}_${counter}${suffix ? `_${suffix}` : ''}`;
};