#5237 Adding basic flow, WIP

This commit is contained in:
Knut Sveidqvist
2024-04-01 19:46:06 +02:00
parent fcda3dc8c6
commit 6b7e1225dd
12 changed files with 449 additions and 32 deletions

View File

@@ -372,10 +372,13 @@ const rect = async (parent, node) => {
// add the rect
const rect = shapeSvg.insert('rect', ':first-child');
// console.log('Rect node:', node, 'bbox:', bbox, 'halfPadding:', halfPadding, 'node.padding:', node.padding);
// const totalWidth = bbox.width + node.padding * 2;
// const totalHeight = bbox.height + node.padding * 2;
const totalWidth = bbox.width + node.padding;
const totalHeight = bbox.height + node.padding;
console.log('Rect node:', node, rect.node(), 'bbox:', bbox, 'halfPadding:', halfPadding, 'node.padding:', node.padding, 'totalWidth:', totalWidth, 'totalHeight:', totalHeight);
rect
.attr('class', 'basic label-container')
.attr('style', node.style)
@@ -1062,6 +1065,8 @@ export const insertNode = async (elem, node, dir) => {
let newEl;
let el;
console.log('insertNode element', elem, elem.node());
// debugger;
// Add link when appropriate
if (node.link) {
let target;

View File

@@ -14,6 +14,8 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
classes = _classes;
}
console.log('parentY', parent.node());
// Add outer g element
const shapeSvg = parent
.insert('g')
@@ -31,7 +33,9 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
labelText = typeof node.labelText === 'string' ? node.labelText : node.labelText[0];
}
const textNode = label.node();
console.log('parentX', parent, 'node',node,'labelText',labelText, textNode, node.labelType, 'label', label.node());
let text;
if (node.labelType === 'markdown') {
// text = textNode;

View File

@@ -29,6 +29,9 @@ export const setConf = function (cnf) {
*/
export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
const svg = root.select(`[id="${svgId}"]`);
console.log('SVG:', svg, svg.node(), 'root:', root, root.node());
const keys = Object.keys(vert);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition

View File

@@ -541,8 +541,43 @@ const setDirection = (dir) => {
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
const dataFetcher = (parentId, doc, nodes, edges) => {
doc.forEach((item) => {
switch (item.stmt) {
case STMT_STATE:
if(parentId) {
nodes.push({...item, labelText: item.id, labelType:'text', parentId});
} else {
nodes.push({...item, labelText: item.id, labelType:'text'});
}
if(item.doc) {
dataFetcher(item.id, item.doc, nodes, edges);
}
break;
case STMT_RELATION:
edges.push(item);
break;
}
});
}
export const getData = () => {
const nodes = [];
const edges = [];
// for (const key in currentDocument.states) {
// if (currentDocument.states.hasOwnProperty(key)) {
// nodes.push({...currentDocument.states[key]});
// }
// }
dataFetcher(undefined, rootDoc, nodes, edges);
return {nodes, edges, other: {}};
}
export default {
getConfig: () => getConfig().state,
getData,
addState,
clear,
getState,

View File

@@ -3,7 +3,8 @@ import type { DiagramDefinition } from '../../diagram-api/types.js';
import parser from './parser/stateDiagram.jison';
import db from './stateDb.js';
import styles from './styles.js';
import renderer from './stateRenderer-v2.js';
// import renderer from './stateRenderer-v2.js';
import renderer from './stateRenderer-v3-unified.js';
export const diagram: DiagramDefinition = {
parser,

View File

@@ -1,28 +1,12 @@
import { log } from '../../logger.js';
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
import type { LayoutData, LayoutMethod } from '../../rendering-util/types';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import doLayout from '../../rendering-util/doLayout';
import performRender from '../../rendering-util/performRender';
import insertElementsForSize, { getDiagramElements} from '../../rendering-util/inserElementsForSize.js';
interface LayoutData {}
interface RenderData {}
type LayoutMethod =
| 'dagre'
| 'dagre-wrapper'
| 'elk'
| 'neato'
| 'dot'
| 'circo'
| 'fdp'
| 'osage'
| 'grid';
const performLayout = (
layoutData: LayoutData,
id: string,
_version: string,
layoutMethod: LayoutMethod
): RenderData => {
return {};
};
const performRender = (data: RenderData) => {};
// Configuration
const conf: Record<string, any> = {};
@@ -44,6 +28,7 @@ export const getClasses = function (
export const draw = async function (text: string, id: string, _version: string, diag: any) {
log.info('Drawing state diagram (v2)', id);
const { securityLevel, state: conf } = getConfig();
// Extracting the data from the parsed structure into a more usable form
// Not related to the refactoring, but this is the first step in the rendering process
@@ -51,10 +36,11 @@ export const draw = async function (text: string, id: string, _version: string,
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const data4Layout = diag.db.getData();
const data4Layout = diag.db.getData() as LayoutData;
const { svg, element } = getDiagramElements(id, securityLevel);
// For some diagrams this call is not needed, but in the state diagram it is
const data4Rendering = performLayout(data4Layout, id, _version, 'dagre-wrapper');
await insertElementsForSize(element, data4Layout);
const data4Rendering = doLayout(data4Layout, id, _version, 'dagre-wrapper');
// The performRender method provided in all supported diagrams is used to render the data
performRender(data4Rendering);

View File

@@ -0,0 +1,14 @@
import { LayoutData, LayoutMethod, RenderData } from './types';
const layoutAlgorithms = {} as Record<string, any>;
const performLayout = (
layoutData: LayoutData,
id: string,
_version: string,
layoutMethod: LayoutMethod
): RenderData => {
console.log('Performing layout', layoutData, id, _version, layoutMethod);
return { items: [], otherDetails:{} };
};
export default performLayout;

View File

@@ -0,0 +1,53 @@
// import type { LayoutData } from './types';
import { select } from 'd3';
import { insertNode } from '../dagre-wrapper/nodes.js'
// export const getDiagramElements = (id: string, securityLevel: any) => {
export const getDiagramElements = (id, securityLevel) => {
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? select(sandboxElement.nodes()[0].contentDocument.body)
: select('body');
const svg = root.select(`[id="${id}"]`);
console.log('SVG:', svg, svg.node(), 'id:',id,'root:', root, root.node());
// console.log('SVG:', svg, svg.node(), 'root:', root, 'sandboxElement:', sandboxElement, 'id:', id, 'securityLevel:', securityLevel);
// Run the renderer. This is what draws the final graph.
// @ts-ignore todo: fix this
const element = root.select('#' + id + ' g');
return { svg, element };
}
// export function insertElementsForSize(el: SVGElement, data: LayoutData): void {
export function insertElementsForSize(el, data) {
const nodesElem = el.insert('g').attr('class', 'nodes');
const edgesElem = el.insert('g').attr('class', 'edges');
console.log('Inserting elements for size:', data);
data.nodes.forEach(async item => {
item.shape = 'rect';
console.log('Inserting node id:', item.id, 'shape:', item.shape);
const e = await insertNode(nodesElem, {...item, class: 'default flowchart-label', labelStyle: '', x:0, y:0, width: 100,rx:0,ry:0, height: 100, shape: 'rect', padding:8});
console.log('Inserted node:', e, e.node());
// Create a new DOM element
// const element = document.createElement('div');
// // Set the content of the element to the name of the item
// element.textContent = item.name;
// // Set the size of the element to the size of the item
// element.style.width = `${item.size}px`;
// element.style.height = `${item.size}px`;
// Append the element to the body of the document
// document.body.appendChild(element);
});
console.log('Element', el, 'data:', data);
}
export default insertElementsForSize;

View File

@@ -0,0 +1,187 @@
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
import * as graphlibJson from 'dagre-d3-es/src/graphlib/json.js';
import insertMarkers from './markers.js';
import { updateNodeBounds } from './shapes/util.js';
import {
clear as clearGraphlib,
clusterDb,
adjustClustersAndEdges,
findNonClusterChild,
sortNodesByHierarchy,
} from './mermaid-graphlib.js';
import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './nodes.js';
import { insertCluster, clear as clearClusters } from './clusters.js';
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
import { log } from '../logger.js';
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
import { getConfig } from '../diagram-api/diagramAPI.js';
const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => {
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
const dir = graph.graph().rankdir;
log.trace('Dir in recursive render - dir:', dir);
const elem = _elem.insert('g').attr('class', 'root');
if (!graph.nodes()) {
log.info('No nodes found for', graph);
} else {
log.info('Recursive render XXX', graph.nodes());
}
if (graph.edges().length > 0) {
log.trace('Recursive edges', graph.edge(graph.edges()[0]));
}
const clusters = elem.insert('g').attr('class', 'clusters');
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
const nodes = elem.insert('g').attr('class', 'nodes');
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
// to the abstract node and is later used by dagre for the layout
await Promise.all(
graph.nodes().map(async function (v) {
const node = graph.node(v);
if (parentCluster !== undefined) {
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
// data.clusterPositioning = true;
log.info('Setting data for cluster XXX (', v, ') ', data, parentCluster);
graph.setNode(parentCluster.id, data);
if (!graph.parent(v)) {
log.trace('Setting parent', v, parentCluster.id);
graph.setParent(v, parentCluster.id, data);
}
}
log.info('(Insert) Node XXX' + v + ': ' + JSON.stringify(graph.node(v)));
if (node && node.clusterNode) {
// const children = graph.children(v);
log.info('Cluster identified', v, node.width, graph.node(v));
const o = await recursiveRender(
nodes,
node.graph,
diagramtype,
id,
graph.node(v),
siteConfig
);
const newEl = o.elem;
updateNodeBounds(node, newEl);
node.diff = o.diff || 0;
log.info('Node bounds (abc123)', v, node, node.width, node.x, node.y);
setNodeElem(newEl, node);
log.warn('Recursive render complete ', newEl, node);
} else {
if (graph.children(v).length > 0) {
// This is a cluster but not to be rendered recursively
// Render as before
log.info('Cluster - the non recursive path XXX', v, node.id, node, graph);
log.info(findNonClusterChild(node.id, graph));
clusterDb[node.id] = { id: findNonClusterChild(node.id, graph), node };
// insertCluster(clusters, graph.node(v));
} else {
log.info('Node - the non recursive path', v, node.id, node);
await insertNode(nodes, graph.node(v), dir);
}
}
})
);
// Insert labels, this will insert them into the dom so that the width can be calculated
// Also figure out which edges point to/from clusters and adjust them accordingly
// Edges from/to clusters really points to the first child in the cluster.
// TODO: pick optimal child in the cluster to us as link anchor
graph.edges().forEach(function (e) {
const edge = graph.edge(e.v, e.w, e.name);
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e)));
// Check if link is either from or to a cluster
log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
insertEdgeLabel(edgeLabels, edge);
});
graph.edges().forEach(function (e) {
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
});
log.info('#############################################');
log.info('### Layout ###');
log.info('#############################################');
log.info(graph);
dagreLayout(graph);
log.info('Graph after layout:', graphlibJson.write(graph));
// Move the nodes to the correct place
let diff = 0;
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
sortNodesByHierarchy(graph).forEach(function (v) {
const node = graph.node(v);
log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
log.info(
'Position ' + v + ': (' + node.x,
',' + node.y,
') width: ',
node.width,
' height: ',
node.height
);
if (node && node.clusterNode) {
// clusterDb[node.id].node = node;
node.y += subGraphTitleTotalMargin;
positionNode(node);
} else {
// Non cluster node
if (graph.children(v).length > 0) {
// A cluster in the non-recursive way
// positionCluster(node);
node.height += subGraphTitleTotalMargin;
insertCluster(clusters, node);
clusterDb[node.id].node = node;
} else {
node.y += subGraphTitleTotalMargin / 2;
positionNode(node);
}
}
});
// Move the edge labels to the correct place after layout
graph.edges().forEach(function (e) {
const edge = graph.edge(e);
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2));
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id);
positionEdgeLabel(edge, paths);
});
graph.nodes().forEach(function (v) {
const n = graph.node(v);
log.info(v, n.type, n.diff);
if (n.type === 'group') {
diff = n.diff;
}
});
return { elem, diff };
};
export const render = async (elem, graph, markers, diagramtype, id) => {
insertMarkers(elem, markers, diagramtype, id);
clearNodes();
clearEdges();
clearClusters();
clearGraphlib();
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
adjustClustersAndEdges(graph);
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
const siteConfig = getConfig();
await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig);
};
// const shapeDefinitions = {};
// export const addShape = ({ shapeType: fun }) => {
// shapeDefinitions[shapeType] = fun;
// };
// const arrowDefinitions = {};
// export const addArrow = ({ arrowType: fun }) => {
// arrowDefinitions[arrowType] = fun;
// };

View File

@@ -0,0 +1,3 @@
import { RenderData } from './types';
const performRender = (data: RenderData) => { };
export default performRender;

View File

@@ -6,3 +6,129 @@ export interface MarkdownWord {
export type MarkdownLine = MarkdownWord[];
/** Returns `true` if the line fits a constraint (e.g. it's under 𝑛 chars) */
export type CheckFitFunction = (text: MarkdownLine) => boolean;
// Common properties for any node in the system
interface Node {
id: string;
label?: string;
parentId?: string;
position?: string;
styles?: string;
classes?: string;
// Flowchart specific properties
labelType?: string;
domId: string;
// Rendering specific properties for both Flowchart and State Diagram nodes
dir?: string;
haveCallback?: boolean;
labelStyle?: string;
labelText?: string;
link?: string;
linkTarget?: string;
padding?: number;
props?: Record<string, unknown>;
rx?: number;
ry?: number;
shape?: string;
tooltip?: string;
type: string;
width?: number;
}
// Common properties for any edge in the system
interface Edge {
id: string;
label?: string;
classes?: string;
style?: string;
// Properties common to both Flowchart and State Diagram edges
arrowhead?: string;
arrowheadStyle?: string;
arrowTypeEnd?: string;
arrowTypeStart?: string;
// Flowchart specific properties
defaultInterpolate?: string;
end?: string;
interpolate?: string;
labelType?: string;
length?: number;
start?: string;
stroke?: string;
text?: string;
type: string;
// Rendering specific properties
curve?: string;
labelpos?: string;
labelStyle?: string;
minlen?: number;
pattern?: string;
thickness?: number;
}
// Extending the Node interface for specific types if needed
interface ClassDiagramNode extends Node {
memberData: any; // Specific property for class diagram nodes
}
// Specific interfaces for layout and render data
export interface LayoutData {
nodes: Node[];
edges: Edge[];
other: any; // Additional properties not yet defined
}
export interface RenderData {
items: (Node | Edge)[];
otherDetails: any; // Placeholder for additional, undefined properties
}
// This refactored approach ensures that common properties are included in the base `Node` and `Edge` interfaces, with specific types extending these bases with additional properties as needed. This maintains flexibility while ensuring type safety and reducing redundancy.
export type LayoutMethod =
| 'dagre'
| 'dagre-wrapper'
| 'elk'
| 'neato'
| 'dot'
| 'circo'
| 'fdp'
| 'osage'
| 'grid';
export function createDomElement(node: Node): Node {
// Create a new DOM element. Assuming we're creating a div as an example
const element = document.createElement('div');
// Check if node.domId is set, if not generate a unique identifier for it
if (!node.domId) {
// This is a simplistic approach to generate a unique ID
// In a real application, you might want to use a more robust method
node.domId = `node-${Math.random().toString(36).substr(2, 9)}`;
}
// Set the ID of the DOM element
element.id = node.domId;
// Optional: Apply styles and classes to the element
if (node.styles) {
element.style.cssText = node.styles;
}
if (node.classes) {
element.className = node.classes;
}
// Optional: Add content or additional attributes to the element
// This can be based on other properties of the node
if (node.label) {
element.textContent = node.label;
}
// Append the newly created element to the document body or a specific container
// This is just an example; in a real application, you might append it somewhere specific
document.body.appendChild(element);
// Return the updated node with its domId set
return node;
}