mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 23:39:50 +02:00
#5342 Initial commit
This commit is contained in:
23
packages/mermaid/src/diagrams/kanban/detector.ts
Normal file
23
packages/mermaid/src/diagrams/kanban/detector.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
const id = 'kanban';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*kanban/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./kanban-definition.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
13
packages/mermaid/src/diagrams/kanban/kanban-definition.ts
Normal file
13
packages/mermaid/src/diagrams/kanban/kanban-definition.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/mindmap.jison';
|
||||
import db from './kanbanDb.js';
|
||||
import renderer from './kanbanRenderer.js';
|
||||
import styles from './styles.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
365
packages/mermaid/src/diagrams/kanban/kanban.spec.ts
Normal file
365
packages/mermaid/src/diagrams/kanban/kanban.spec.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
// @ts-expect-error No types available for JISON
|
||||
import { parser as kanban } from './parser/kanban.jison';
|
||||
import kanbanDB from './kanbanDb.js';
|
||||
// Todo fix utils functions for tests
|
||||
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
describe('when parsing a kanban ', function () {
|
||||
beforeEach(function () {
|
||||
kanban.yy = kanbanDB;
|
||||
kanban.yy.clear();
|
||||
setLogLevel('trace');
|
||||
});
|
||||
describe('hiearchy', function () {
|
||||
it('KNBN-1 should handle a simple root definition abc122', function () {
|
||||
const str = `kanban
|
||||
root`;
|
||||
|
||||
kanban.parse(str);
|
||||
// console.log('Time for checks', kanban.yy.getMindmap().descr);
|
||||
expect(kanban.yy.getMindmap().descr).toEqual('root');
|
||||
});
|
||||
it('KNBN-2 should handle a hierachial kanban definition', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
child1
|
||||
child2
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
expect(mm.children[0].descr).toEqual('child1');
|
||||
expect(mm.children[1].descr).toEqual('child2');
|
||||
});
|
||||
|
||||
it('3 should handle a simple root definition with a shape and without an id abc123', function () {
|
||||
const str = `kanban
|
||||
(root)`;
|
||||
|
||||
kanban.parse(str);
|
||||
// console.log('Time for checks', kanban.yy.getMindmap().descr);
|
||||
expect(kanban.yy.getMindmap().descr).toEqual('root');
|
||||
});
|
||||
|
||||
it('KNBN-4 should handle a deeper hierachial kanban definition', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
child1
|
||||
leaf1
|
||||
child2`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
expect(mm.children[0].descr).toEqual('child1');
|
||||
expect(mm.children[0].children[0].descr).toEqual('leaf1');
|
||||
expect(mm.children[1].descr).toEqual('child2');
|
||||
});
|
||||
it('5 Multiple roots are illegal', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
fakeRoot`;
|
||||
|
||||
expect(() => kanban.parse(str)).toThrow(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
});
|
||||
it('KNBN-6 real root in wrong place', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
fakeRoot
|
||||
realRootWrongPlace`;
|
||||
expect(() => kanban.parse(str)).toThrow(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('nodes', function () {
|
||||
it('KNBN-7 should handle an id and type for a node definition', function () {
|
||||
const str = `kanban
|
||||
root[The root]
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('The root');
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||
});
|
||||
it('KNBN-8 should handle an id and type for a node definition', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
theId(child1)`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
const child = mm.children[0];
|
||||
expect(child.descr).toEqual('child1');
|
||||
expect(child.nodeId).toEqual('theId');
|
||||
expect(child.type).toEqual(kanban.yy.nodeType.ROUNDED_RECT);
|
||||
});
|
||||
it('KNBN-9 should handle an id and type for a node definition', function () {
|
||||
const str = `kanban
|
||||
root
|
||||
theId(child1)`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
const child = mm.children[0];
|
||||
expect(child.descr).toEqual('child1');
|
||||
expect(child.nodeId).toEqual('theId');
|
||||
expect(child.type).toEqual(kanban.yy.nodeType.ROUNDED_RECT);
|
||||
});
|
||||
it('KNBN-10 multiple types (circle)', function () {
|
||||
const str = `kanban
|
||||
root((the root))
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('the root');
|
||||
expect(mm.children.length).toEqual(0);
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.CIRCLE);
|
||||
});
|
||||
|
||||
it('KNBN-11 multiple types (cloud)', function () {
|
||||
const str = `kanban
|
||||
root)the root(
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('the root');
|
||||
expect(mm.children.length).toEqual(0);
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.CLOUD);
|
||||
});
|
||||
it('KNBN-12 multiple types (bang)', function () {
|
||||
const str = `kanban
|
||||
root))the root((
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.descr).toEqual('the root');
|
||||
expect(mm.children.length).toEqual(0);
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.BANG);
|
||||
});
|
||||
|
||||
it('KNBN-12-a multiple types (hexagon)', function () {
|
||||
const str = `kanban
|
||||
root{{the root}}
|
||||
`;
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.HEXAGON);
|
||||
expect(mm.descr).toEqual('the root');
|
||||
expect(mm.children.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('decorations', function () {
|
||||
it('KNBN-13 should be possible to set an icon for the node', function () {
|
||||
const str = `kanban
|
||||
root[The root]
|
||||
::icon(bomb)
|
||||
`;
|
||||
// ::class1 class2
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('The root');
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||
expect(mm.icon).toEqual('bomb');
|
||||
});
|
||||
it('KNBN-14 should be possible to set classes for the node', function () {
|
||||
const str = `kanban
|
||||
root[The root]
|
||||
:::m-4 p-8
|
||||
`;
|
||||
// ::class1 class2
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('The root');
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||
expect(mm.class).toEqual('m-4 p-8');
|
||||
});
|
||||
it('KNBN-15 should be possible to set both classes and icon for the node', function () {
|
||||
const str = `kanban
|
||||
root[The root]
|
||||
:::m-4 p-8
|
||||
::icon(bomb)
|
||||
`;
|
||||
// ::class1 class2
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('The root');
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||
expect(mm.class).toEqual('m-4 p-8');
|
||||
expect(mm.icon).toEqual('bomb');
|
||||
});
|
||||
it('KNBN-16 should be possible to set both classes and icon for the node', function () {
|
||||
const str = `kanban
|
||||
root[The root]
|
||||
::icon(bomb)
|
||||
:::m-4 p-8
|
||||
`;
|
||||
// ::class1 class2
|
||||
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('The root');
|
||||
expect(mm.type).toEqual(kanban.yy.nodeType.RECT);
|
||||
expect(mm.class).toEqual('m-4 p-8');
|
||||
expect(mm.icon).toEqual('bomb');
|
||||
});
|
||||
});
|
||||
describe('descriptions', function () {
|
||||
it('KNBN-17 should be possible to use node syntax in the descriptions', function () {
|
||||
const str = `kanban
|
||||
root["String containing []"]
|
||||
`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('String containing []');
|
||||
});
|
||||
it('KNBN-18 should be possible to use node syntax in the descriptions in children', function () {
|
||||
const str = `kanban
|
||||
root["String containing []"]
|
||||
child1["String containing ()"]
|
||||
`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('String containing []');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
expect(mm.children[0].descr).toEqual('String containing ()');
|
||||
});
|
||||
it('KNBN-19 should be possible to have a child after a class assignment', function () {
|
||||
const str = `kanban
|
||||
root(Root)
|
||||
Child(Child)
|
||||
:::hot
|
||||
a(a)
|
||||
b[New Stuff]`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('Root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('Child');
|
||||
expect(child.children[0].nodeId).toEqual('a');
|
||||
expect(child.children.length).toEqual(2);
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
});
|
||||
it('KNBN-20 should be possible to have meaningless empty rows in a kanban abc124', function () {
|
||||
const str = `kanban
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a)
|
||||
|
||||
b[New Stuff]`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('Root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('Child');
|
||||
expect(child.children[0].nodeId).toEqual('a');
|
||||
expect(child.children.length).toEqual(2);
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
it('KNBN-21 should be possible to have comments in a kanban', function () {
|
||||
const str = `kanban
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a)
|
||||
|
||||
%% This is a comment
|
||||
b[New Stuff]`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('Root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('Child');
|
||||
expect(child.children[0].nodeId).toEqual('a');
|
||||
expect(child.children.length).toEqual(2);
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
|
||||
it('KNBN-22 should be possible to have comments at the end of a line', function () {
|
||||
const str = `kanban
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a) %% This is a comment
|
||||
b[New Stuff]`;
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.descr).toEqual('Root');
|
||||
expect(mm.children.length).toEqual(1);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('Child');
|
||||
expect(child.children[0].nodeId).toEqual('a');
|
||||
expect(child.children.length).toEqual(2);
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
it('KNBN-23 Rows with only spaces should not interfere', function () {
|
||||
const str = 'kanban\nroot\n A\n \n\n B';
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('KNBN-24 Handle rows above the kanban declarations', function () {
|
||||
const str = '\n \nkanban\nroot\n A\n \n\n B';
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('KNBN-25 Handle rows above the kanban declarations, no space', function () {
|
||||
const str = '\n\n\nkanban\nroot\n A\n \n\n B';
|
||||
kanban.parse(str);
|
||||
const mm = kanban.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
expect(mm.children.length).toEqual(2);
|
||||
|
||||
const child = mm.children[0];
|
||||
expect(child.nodeId).toEqual('A');
|
||||
const child2 = mm.children[1];
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
});
|
159
packages/mermaid/src/diagrams/kanban/kanbanDb.ts
Normal file
159
packages/mermaid/src/diagrams/kanban/kanbanDb.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './kanbanTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
let nodes: MindmapNode[] = [];
|
||||
let cnt = 0;
|
||||
let elements: Record<number, D3Element> = {};
|
||||
|
||||
const clear = () => {
|
||||
nodes = [];
|
||||
cnt = 0;
|
||||
elements = {};
|
||||
};
|
||||
|
||||
const getParent = function (level: number) {
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
if (nodes[i].level < level) {
|
||||
return nodes[i];
|
||||
}
|
||||
}
|
||||
// No parent found
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMindmap = () => {
|
||||
return nodes.length > 0 ? nodes[0] : null;
|
||||
};
|
||||
|
||||
const addNode = (level: number, id: string, descr: string, type: number) => {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
const conf = getConfig();
|
||||
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
switch (type) {
|
||||
case nodeType.ROUNDED_RECT:
|
||||
case nodeType.RECT:
|
||||
case nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: cnt++,
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
} satisfies MindmapNode;
|
||||
|
||||
const parent = getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
// Keep all nodes in the list
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length === 0) {
|
||||
// First node, the root
|
||||
nodes.push(node);
|
||||
} else {
|
||||
// Syntax error ... there can only bee one root
|
||||
throw new Error(
|
||||
'There can be only one root. No parent could be found for ("' + node.descr + '")'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nodeType = {
|
||||
DEFAULT: 0,
|
||||
NO_BORDER: 0,
|
||||
ROUNDED_RECT: 1,
|
||||
RECT: 2,
|
||||
CIRCLE: 3,
|
||||
CLOUD: 4,
|
||||
BANG: 5,
|
||||
HEXAGON: 6,
|
||||
};
|
||||
|
||||
const getType = (startStr: string, endStr: string): number => {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
return nodeType.RECT;
|
||||
case '(':
|
||||
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD;
|
||||
case '((':
|
||||
return nodeType.CIRCLE;
|
||||
case ')':
|
||||
return nodeType.CLOUD;
|
||||
case '))':
|
||||
return nodeType.BANG;
|
||||
case '{{':
|
||||
return nodeType.HEXAGON;
|
||||
default:
|
||||
return nodeType.DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
const setElementForId = (id: number, element: D3Element) => {
|
||||
elements[id] = element;
|
||||
};
|
||||
|
||||
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
const config = getConfig();
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
};
|
||||
|
||||
const type2Str = (type: number) => {
|
||||
switch (type) {
|
||||
case nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
case nodeType.RECT:
|
||||
return 'rect';
|
||||
case nodeType.ROUNDED_RECT:
|
||||
return 'rounded-rect';
|
||||
case nodeType.CIRCLE:
|
||||
return 'circle';
|
||||
case nodeType.CLOUD:
|
||||
return 'cloud';
|
||||
case nodeType.BANG:
|
||||
return 'bang';
|
||||
case nodeType.HEXAGON:
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
};
|
||||
|
||||
// Expose logger to grammar
|
||||
const getLogger = () => log;
|
||||
const getElementById = (id: number) => elements[id];
|
||||
|
||||
const db = {
|
||||
clear,
|
||||
addNode,
|
||||
getMindmap,
|
||||
nodeType,
|
||||
getType,
|
||||
setElementForId,
|
||||
decorateNode,
|
||||
type2Str,
|
||||
getLogger,
|
||||
getElementById,
|
||||
} as const;
|
||||
|
||||
export default db;
|
203
packages/mermaid/src/diagrams/kanban/kanbanRenderer.ts
Normal file
203
packages/mermaid/src/diagrams/kanban/kanbanRenderer.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import cytoscape from 'cytoscape';
|
||||
// @ts-expect-error No types available
|
||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||
import { select } from 'd3';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './kanbanTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
async function drawNodes(
|
||||
db: MindmapDB,
|
||||
svg: D3Element,
|
||||
mindmap: FilledMindMapNode,
|
||||
section: number,
|
||||
conf: MermaidConfig
|
||||
) {
|
||||
await drawNode(db, svg, mindmap, section, conf);
|
||||
if (mindmap.children) {
|
||||
await Promise.all(
|
||||
mindmap.children.map((child, index) =>
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cytoscape' {
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
rscratch: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
cy.edges().map((edge, id) => {
|
||||
const data = edge.data();
|
||||
if (edge[0]._private.bodyBounds) {
|
||||
const bounds = edge[0]._private.rscratch;
|
||||
log.trace('Edge: ', id, data);
|
||||
edgesEl
|
||||
.insert('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
|
||||
)
|
||||
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: mindmap.id.toString(),
|
||||
labelText: mindmap.descr,
|
||||
height: mindmap.height,
|
||||
width: mindmap.width,
|
||||
level: level,
|
||||
nodeId: mindmap.id,
|
||||
padding: mindmap.padding,
|
||||
type: mindmap.type,
|
||||
},
|
||||
position: {
|
||||
x: mindmap.x!,
|
||||
y: mindmap.y!,
|
||||
},
|
||||
});
|
||||
if (mindmap.children) {
|
||||
mindmap.children.forEach((child) => {
|
||||
addNodes(child, cy, conf, level + 1);
|
||||
cy.add({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${mindmap.id}_${child.id}`,
|
||||
source: mindmap.id,
|
||||
target: child.id,
|
||||
depth: level,
|
||||
section: child.section,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
// Add temporary render element
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
const cy = cytoscape({
|
||||
container: document.getElementById('cy'), // container to render in
|
||||
style: [
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Remove element after layout
|
||||
renderEl.remove();
|
||||
addNodes(node, cy, conf, 0);
|
||||
|
||||
// Make cytoscape care about the dimensions of the nodes
|
||||
cy.nodes().forEach(function (n) {
|
||||
n.layoutDimensions = () => {
|
||||
const data = n.data();
|
||||
return { w: data.width, h: data.height };
|
||||
};
|
||||
});
|
||||
|
||||
cy.layout({
|
||||
name: 'cose-bilkent',
|
||||
// @ts-ignore Types for cose-bilkent are not correct?
|
||||
quality: 'proof',
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
}).run();
|
||||
cy.ready((e) => {
|
||||
log.info('Ready', e);
|
||||
resolve(cy);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
||||
cy.nodes().map((node, id) => {
|
||||
const data = node.data();
|
||||
data.x = node.position().x;
|
||||
data.y = node.position().y;
|
||||
positionNode(db, data);
|
||||
const el = db.getElementById(data.nodeId);
|
||||
log.info('Id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
||||
el.attr(
|
||||
'transform',
|
||||
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
|
||||
);
|
||||
el.attr('attr', `apa-${id})`);
|
||||
});
|
||||
}
|
||||
|
||||
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||
log.debug('Rendering mindmap diagram\n' + text);
|
||||
|
||||
const db = diagObj.db as MindmapDB;
|
||||
const mm = db.getMindmap();
|
||||
if (!mm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conf = getConfig();
|
||||
conf.htmlLabels = false;
|
||||
|
||||
const svg = selectSvgElement(id);
|
||||
|
||||
// Draw the graph and start with drawing the nodes without proper position
|
||||
// this gives us the size of the nodes and we can set the positions later
|
||||
|
||||
const edgesElem = svg.append('g');
|
||||
edgesElem.attr('class', 'mindmap-edges');
|
||||
const nodesElem = svg.append('g');
|
||||
nodesElem.attr('class', 'mindmap-nodes');
|
||||
await drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
|
||||
// Next step is to layout the mindmap, giving each node a position
|
||||
|
||||
const cy = await layoutMindmap(mm, conf);
|
||||
|
||||
// After this we can draw, first the edges and the then nodes with the correct position
|
||||
drawEdges(edgesElem, cy);
|
||||
positionNodes(db, cy);
|
||||
|
||||
// Setup the view box and size of the svg element
|
||||
setupGraphViewbox(
|
||||
undefined,
|
||||
svg,
|
||||
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
22
packages/mermaid/src/diagrams/kanban/kanbanTypes.ts
Normal file
22
packages/mermaid/src/diagrams/kanban/kanbanTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type mindmapDb from './kanbanDb.js';
|
||||
|
||||
export interface MindmapNode {
|
||||
id: number;
|
||||
nodeId: string;
|
||||
level: number;
|
||||
descr: string;
|
||||
type: number;
|
||||
children: MindmapNode[];
|
||||
width: number;
|
||||
padding: number;
|
||||
section?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
icon?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
export type MindmapDB = typeof mindmapDb;
|
127
packages/mermaid/src/diagrams/kanban/parser/kanban.jison
Normal file
127
packages/mermaid/src/diagrams/kanban/parser/kanban.jison
Normal file
@@ -0,0 +1,127 @@
|
||||
/** mermaid
|
||||
* https://knsv.github.io/mermaid
|
||||
* (c) 2015 Knut Sveidqvist
|
||||
* MIT license.
|
||||
*/
|
||||
%lex
|
||||
|
||||
%options case-insensitive
|
||||
|
||||
%{
|
||||
// Pre-lexer code can go here
|
||||
%}
|
||||
%x NODE
|
||||
%x NSTR
|
||||
%x NSTR2
|
||||
%x ICON
|
||||
%x CLASS
|
||||
|
||||
%%
|
||||
|
||||
\s*\%\%.* {yy.getLogger().trace('Found comment',yytext); return 'SPACELINE';}
|
||||
// \%\%[^\n]*\n /* skip comments */
|
||||
"kanban" return 'MINDMAP';
|
||||
":::" { this.begin('CLASS'); }
|
||||
<CLASS>.+ { this.popState();return 'CLASS'; }
|
||||
<CLASS>\n { this.popState();}
|
||||
// [\s]*"::icon(" { this.begin('ICON'); }
|
||||
"::icon(" { yy.getLogger().trace('Begin icon');this.begin('ICON'); }
|
||||
[\s]+[\n] {yy.getLogger().trace('SPACELINE');return 'SPACELINE' /* skip all whitespace */ ;}
|
||||
[\n]+ return 'NL';
|
||||
<ICON>[^\)]+ { return 'ICON'; }
|
||||
<ICON>\) {yy.getLogger().trace('end icon');this.popState();}
|
||||
"-)" { yy.getLogger().trace('Exploding node'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"(-" { yy.getLogger().trace('Cloud'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"))" { yy.getLogger().trace('Explosion Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||
")" { yy.getLogger().trace('Cloud Bang'); this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"((" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"{{" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"(" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||
"[" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
|
||||
// !(-\() return 'NODE_ID';
|
||||
[^\(\[\n\)\{\}]+ return 'NODE_ID';
|
||||
<<EOF>> return 'EOF';
|
||||
<NODE>["][`] { this.begin("NSTR2");}
|
||||
<NSTR2>[^`"]+ { return "NODE_DESCR";}
|
||||
<NSTR2>[`]["] { this.popState();}
|
||||
<NODE>["] { yy.getLogger().trace('Starting NSTR');this.begin("NSTR");}
|
||||
<NSTR>[^"]+ { yy.getLogger().trace('description:', yytext); return "NODE_DESCR";}
|
||||
<NSTR>["] {this.popState();}
|
||||
<NODE>[\)]\) {this.popState();yy.getLogger().trace('node end ))');return "NODE_DEND";}
|
||||
<NODE>[\)] {this.popState();yy.getLogger().trace('node end )');return "NODE_DEND";}
|
||||
<NODE>[\]] {this.popState();yy.getLogger().trace('node end ...',yytext);return "NODE_DEND";}
|
||||
<NODE>"}}" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||
<NODE>"(-" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||
<NODE>"-)" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
|
||||
<NODE>"((" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||
<NODE>"(" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
|
||||
<NODE>[^\)\]\(\}]+ { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||
<NODE>.+(?!\(\() { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
|
||||
// [\[] return 'NODE_START';
|
||||
// .+ return 'TXT' ;
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
// %{ : info document 'EOF' { return yy; } }
|
||||
: mindMap
|
||||
| spaceLines mindMap
|
||||
;
|
||||
|
||||
spaceLines
|
||||
: SPACELINE
|
||||
| spaceLines SPACELINE
|
||||
| spaceLines NL
|
||||
;
|
||||
|
||||
mindMap
|
||||
: MINDMAP document { return yy; }
|
||||
| MINDMAP NL document { return yy; }
|
||||
;
|
||||
|
||||
stop
|
||||
: NL {yy.getLogger().trace('Stop NL ');}
|
||||
| EOF {yy.getLogger().trace('Stop EOF ');}
|
||||
| SPACELINE
|
||||
| stop NL {yy.getLogger().trace('Stop NL2 ');}
|
||||
| stop EOF {yy.getLogger().trace('Stop EOF2 ');}
|
||||
;
|
||||
document
|
||||
: document statement stop
|
||||
| statement stop
|
||||
;
|
||||
|
||||
statement
|
||||
: SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
|
||||
| SPACELIST ICON { yy.getLogger().trace('Icon: ',$2);yy.decorateNode({icon: $2}); }
|
||||
| SPACELIST CLASS { yy.decorateNode({class: $2}); }
|
||||
| SPACELINE { yy.getLogger().trace('SPACELIST');}
|
||||
| node { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
|
||||
| ICON { yy.decorateNode({icon: $1}); }
|
||||
| CLASS { yy.decorateNode({class: $1}); }
|
||||
| SPACELIST
|
||||
;
|
||||
|
||||
|
||||
|
||||
node
|
||||
:nodeWithId
|
||||
|nodeWithoutId
|
||||
;
|
||||
|
||||
nodeWithoutId
|
||||
: NODE_DSTART NODE_DESCR NODE_DEND
|
||||
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $2, descr: $2, type: yy.getType($1, $3) }; }
|
||||
;
|
||||
|
||||
nodeWithId
|
||||
: NODE_ID { $$ = { id: $1, descr: $1, type: yy.nodeType.DEFAULT }; }
|
||||
| NODE_ID NODE_DSTART NODE_DESCR NODE_DEND
|
||||
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $1, descr: $3, type: yy.getType($2, $4) }; }
|
||||
;
|
||||
%%
|
84
packages/mermaid/src/diagrams/kanban/styles.ts
Normal file
84
packages/mermaid/src/diagrams/kanban/styles.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// @ts-expect-error Incorrect khroma types
|
||||
import { darken, lighten, isDark } from 'khroma';
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
|
||||
const genSections: DiagramStylesProvider = (options) => {
|
||||
let sections = '';
|
||||
|
||||
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||
options['lineColor' + i] = options['lineColor' + i] || options['cScaleInv' + i];
|
||||
if (isDark(options['lineColor' + i])) {
|
||||
options['lineColor' + i] = lighten(options['lineColor' + i], 20);
|
||||
} else {
|
||||
options['lineColor' + i] = darken(options['lineColor' + i], 20);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||
const sw = '' + (17 - 3 * i);
|
||||
sections += `
|
||||
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${
|
||||
i - 1
|
||||
} polygon, .section-${i - 1} path {
|
||||
fill: ${options['cScale' + i]};
|
||||
}
|
||||
.section-${i - 1} text {
|
||||
fill: ${options['cScaleLabel' + i]};
|
||||
}
|
||||
.node-icon-${i - 1} {
|
||||
font-size: 40px;
|
||||
color: ${options['cScaleLabel' + i]};
|
||||
}
|
||||
.section-edge-${i - 1}{
|
||||
stroke: ${options['cScale' + i]};
|
||||
}
|
||||
.edge-depth-${i - 1}{
|
||||
stroke-width: ${sw};
|
||||
}
|
||||
.section-${i - 1} line {
|
||||
stroke: ${options['cScaleInv' + i]} ;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.disabled, .disabled circle, .disabled text {
|
||||
fill: lightgray;
|
||||
}
|
||||
.disabled text {
|
||||
fill: #efefef;
|
||||
}
|
||||
`;
|
||||
}
|
||||
return sections;
|
||||
};
|
||||
|
||||
// TODO: These options seem incorrect.
|
||||
const getStyles: DiagramStylesProvider = (options) =>
|
||||
`
|
||||
.edge {
|
||||
stroke-width: 3;
|
||||
}
|
||||
${genSections(options)}
|
||||
.section-root rect, .section-root path, .section-root circle, .section-root polygon {
|
||||
fill: ${options.git0};
|
||||
}
|
||||
.section-root text {
|
||||
fill: ${options.gitBranchLabel0};
|
||||
}
|
||||
.icon-container {
|
||||
height:100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.edge {
|
||||
fill: none;
|
||||
}
|
||||
.mindmap-node-label {
|
||||
dy: 1em;
|
||||
alignment-baseline: middle;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
export default getStyles;
|
308
packages/mermaid/src/diagrams/kanban/svgDraw.ts
Normal file
308
packages/mermaid/src/diagrams/kanban/svgDraw.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import type { FilledMindMapNode, MindmapDB } from './kanbanTypes.js';
|
||||
import type { Point, D3Element } from '../../types.js';
|
||||
import { parseFontSize } from '../../utils.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
type ShapeFunction = (
|
||||
db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode,
|
||||
section?: number
|
||||
) => void;
|
||||
|
||||
const defaultBkg: ShapeFunction = function (db, elem, node, section) {
|
||||
const rd = 5;
|
||||
elem
|
||||
.append('path')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr(
|
||||
'd',
|
||||
`M0 ${node.height - rd} v${-node.height + 2 * rd} q0,-5 5,-5 h${
|
||||
node.width - 2 * rd
|
||||
} q5,0 5,5 v${node.height - rd} H0 Z`
|
||||
);
|
||||
|
||||
elem
|
||||
.append('line')
|
||||
.attr('class', 'node-line-' + section)
|
||||
.attr('x1', 0)
|
||||
.attr('y1', node.height)
|
||||
.attr('x2', node.width)
|
||||
.attr('y2', node.height);
|
||||
};
|
||||
|
||||
const rectBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('rect')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr('height', node.height)
|
||||
.attr('width', node.width);
|
||||
};
|
||||
|
||||
const cloudBkg: ShapeFunction = function (db, elem, node) {
|
||||
const w = node.width;
|
||||
const h = node.height;
|
||||
const r1 = 0.15 * w;
|
||||
const r2 = 0.25 * w;
|
||||
const r3 = 0.35 * w;
|
||||
const r4 = 0.2 * w;
|
||||
elem
|
||||
.append('path')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr(
|
||||
'd',
|
||||
`M0 0 a${r1},${r1} 0 0,1 ${w * 0.25},${-1 * w * 0.1}
|
||||
a${r3},${r3} 1 0,1 ${w * 0.4},${-1 * w * 0.1}
|
||||
a${r2},${r2} 1 0,1 ${w * 0.35},${1 * w * 0.2}
|
||||
|
||||
a${r1},${r1} 1 0,1 ${w * 0.15},${1 * h * 0.35}
|
||||
a${r4},${r4} 1 0,1 ${-1 * w * 0.15},${1 * h * 0.65}
|
||||
|
||||
a${r2},${r1} 1 0,1 ${-1 * w * 0.25},${w * 0.15}
|
||||
a${r3},${r3} 1 0,1 ${-1 * w * 0.5},${0}
|
||||
a${r1},${r1} 1 0,1 ${-1 * w * 0.25},${-1 * w * 0.15}
|
||||
|
||||
a${r1},${r1} 1 0,1 ${-1 * w * 0.1},${-1 * h * 0.35}
|
||||
a${r4},${r4} 1 0,1 ${w * 0.1},${-1 * h * 0.65}
|
||||
|
||||
H0 V0 Z`
|
||||
);
|
||||
};
|
||||
|
||||
const bangBkg: ShapeFunction = function (db, elem, node) {
|
||||
const w = node.width;
|
||||
const h = node.height;
|
||||
const r = 0.15 * w;
|
||||
elem
|
||||
.append('path')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr(
|
||||
'd',
|
||||
`M0 0 a${r},${r} 1 0,0 ${w * 0.25},${-1 * h * 0.1}
|
||||
a${r},${r} 1 0,0 ${w * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${w * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${w * 0.25},${1 * h * 0.1}
|
||||
|
||||
a${r},${r} 1 0,0 ${w * 0.15},${1 * h * 0.33}
|
||||
a${r * 0.8},${r * 0.8} 1 0,0 ${0},${1 * h * 0.34}
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.15},${1 * h * 0.33}
|
||||
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.25},${h * 0.15}
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.25},${0}
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.25},${-1 * h * 0.15}
|
||||
|
||||
a${r},${r} 1 0,0 ${-1 * w * 0.1},${-1 * h * 0.33}
|
||||
a${r * 0.8},${r * 0.8} 1 0,0 ${0},${-1 * h * 0.34}
|
||||
a${r},${r} 1 0,0 ${w * 0.1},${-1 * h * 0.33}
|
||||
|
||||
H0 V0 Z`
|
||||
);
|
||||
};
|
||||
|
||||
const circleBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('circle')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr('r', node.width / 2);
|
||||
};
|
||||
|
||||
function insertPolygonShape(
|
||||
parent: D3Element,
|
||||
w: number,
|
||||
h: number,
|
||||
points: Point[],
|
||||
node: FilledMindMapNode
|
||||
) {
|
||||
return parent
|
||||
.insert('polygon', ':first-child')
|
||||
.attr(
|
||||
'points',
|
||||
points
|
||||
.map(function (d) {
|
||||
return d.x + ',' + d.y;
|
||||
})
|
||||
.join(' ')
|
||||
)
|
||||
.attr('transform', 'translate(' + (node.width - w) / 2 + ', ' + h + ')');
|
||||
}
|
||||
|
||||
const hexagonBkg: ShapeFunction = function (
|
||||
_db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode
|
||||
) {
|
||||
const h = node.height;
|
||||
const f = 4;
|
||||
const m = h / f;
|
||||
const w = node.width - node.padding + 2 * m;
|
||||
const points: Point[] = [
|
||||
{ x: m, y: 0 },
|
||||
{ x: w - m, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
{ x: w - m, y: -h },
|
||||
{ x: m, y: -h },
|
||||
{ x: 0, y: -h / 2 },
|
||||
];
|
||||
insertPolygonShape(elem, w, h, points, node);
|
||||
};
|
||||
|
||||
const roundedRectBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('rect')
|
||||
.attr('id', 'node-' + node.id)
|
||||
.attr('class', 'node-bkg node-' + db.type2Str(node.type))
|
||||
.attr('height', node.height)
|
||||
.attr('rx', node.padding)
|
||||
.attr('ry', node.padding)
|
||||
.attr('width', node.width);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param db - The database
|
||||
* @param elem - The D3 dom element in which the node is to be added
|
||||
* @param node - The node to be added
|
||||
* @param fullSection - ?
|
||||
* @param conf - The configuration object
|
||||
* @returns The height nodes dom element
|
||||
*/
|
||||
export const drawNode = async function (
|
||||
db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode,
|
||||
fullSection: number,
|
||||
conf: MermaidConfig
|
||||
): Promise<number> {
|
||||
const htmlLabels = conf.htmlLabels;
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const nodeElem = elem.append('g');
|
||||
node.section = section;
|
||||
let sectionClass = 'section-' + section;
|
||||
if (section < 0) {
|
||||
sectionClass += ' section-root';
|
||||
}
|
||||
nodeElem.attr('class', (node.class ? node.class + ' ' : '') + 'mindmap-node ' + sectionClass);
|
||||
const bkgElem = nodeElem.append('g');
|
||||
|
||||
// Create the wrapped text element
|
||||
const textElem = nodeElem.append('g');
|
||||
const description = node.descr.replace(/(<br\/*>)/g, '\n');
|
||||
await createText(
|
||||
textElem,
|
||||
description,
|
||||
{
|
||||
useHtmlLabels: htmlLabels,
|
||||
width: node.width,
|
||||
classes: 'mindmap-node-label',
|
||||
},
|
||||
conf
|
||||
);
|
||||
|
||||
if (!htmlLabels) {
|
||||
textElem
|
||||
.attr('dy', '1em')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle');
|
||||
}
|
||||
const bbox = textElem.node().getBBox();
|
||||
const [fontSize] = parseFontSize(conf.fontSize);
|
||||
node.height = bbox.height + fontSize! * 1.1 * 0.5 + node.padding;
|
||||
node.width = bbox.width + 2 * node.padding;
|
||||
if (node.icon) {
|
||||
if (node.type === db.nodeType.CIRCLE) {
|
||||
node.height += 50;
|
||||
node.width += 50;
|
||||
const icon = nodeElem
|
||||
.append('foreignObject')
|
||||
.attr('height', '50px')
|
||||
.attr('width', node.width)
|
||||
.attr('style', 'text-align: center;');
|
||||
icon
|
||||
.append('div')
|
||||
.attr('class', 'icon-container')
|
||||
.append('i')
|
||||
.attr('class', 'node-icon-' + section + ' ' + node.icon);
|
||||
textElem.attr(
|
||||
'transform',
|
||||
'translate(' + node.width / 2 + ', ' + (node.height / 2 - 1.5 * node.padding) + ')'
|
||||
);
|
||||
} else {
|
||||
node.width += 50;
|
||||
const orgHeight = node.height;
|
||||
node.height = Math.max(orgHeight, 60);
|
||||
const heightDiff = Math.abs(node.height - orgHeight);
|
||||
const icon = nodeElem
|
||||
.append('foreignObject')
|
||||
.attr('width', '60px')
|
||||
.attr('height', node.height)
|
||||
.attr('style', 'text-align: center;margin-top:' + heightDiff / 2 + 'px;');
|
||||
|
||||
icon
|
||||
.append('div')
|
||||
.attr('class', 'icon-container')
|
||||
.append('i')
|
||||
.attr('class', 'node-icon-' + section + ' ' + node.icon);
|
||||
textElem.attr(
|
||||
'transform',
|
||||
'translate(' + (25 + node.width / 2) + ', ' + (heightDiff / 2 + node.padding / 2) + ')'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!htmlLabels) {
|
||||
const dx = node.width / 2;
|
||||
const dy = node.padding / 2;
|
||||
textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')');
|
||||
// textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')');
|
||||
} else {
|
||||
const dx = (node.width - bbox.width) / 2;
|
||||
const dy = (node.height - bbox.height) / 2;
|
||||
textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')');
|
||||
}
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case db.nodeType.DEFAULT:
|
||||
defaultBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.ROUNDED_RECT:
|
||||
roundedRectBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.RECT:
|
||||
rectBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.CIRCLE:
|
||||
bkgElem.attr('transform', 'translate(' + node.width / 2 + ', ' + +node.height / 2 + ')');
|
||||
circleBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.CLOUD:
|
||||
cloudBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.BANG:
|
||||
bangBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.HEXAGON:
|
||||
hexagonBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
}
|
||||
|
||||
db.setElementForId(node.id, nodeElem);
|
||||
return node.height;
|
||||
};
|
||||
|
||||
export const positionNode = function (db: MindmapDB, node: FilledMindMapNode) {
|
||||
const nodeElem = db.getElementById(node.id);
|
||||
|
||||
const x = node.x || 0;
|
||||
const y = node.y || 0;
|
||||
// Position the node to its coordinate
|
||||
nodeElem.attr('transform', 'translate(' + x + ',' + y + ')');
|
||||
};
|
Reference in New Issue
Block a user