Repackaging cose-bilkins

This commit is contained in:
Knut Sveidqvist
2025-06-06 20:03:34 +02:00
parent f2eef37599
commit 34027bc589
5 changed files with 269 additions and 69 deletions

View File

@@ -133,7 +133,7 @@
<pre id="diagram4" class="mermaid">
---
config:
layout: dagre
layout: cose-bilkent
---
mindmap
root((mindmap))
@@ -154,13 +154,13 @@
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
layout: cose-bilkent
---
flowchart LR
root{mindmap} --- Origins --- Europe
root --- Origins --- Asia
Origins --> Asia
root --- Background --- Rich
root --- Background --- Poor
Background --- Poor

View File

@@ -196,12 +196,34 @@ const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): v
cssClasses += ` ${node.class}`;
}
// Map mindmap node type to valid shape name
const getShapeFromType = (type: number) => {
switch (type) {
case nodeType.CIRCLE:
return 'circle';
case nodeType.RECT:
return 'rect';
case nodeType.ROUNDED_RECT:
return 'rounded';
case nodeType.CLOUD:
return 'rounded'; // Map cloud to rounded for now
case nodeType.BANG:
return 'circle'; // Map bang to circle for now
case nodeType.HEXAGON:
return 'hexagon';
case nodeType.DEFAULT:
case nodeType.NO_BORDER:
default:
return 'rect';
}
};
const processedNode: MindmapLayoutNode = {
id: 'node_' + node.id.toString(),
domId: 'node_' + node.id.toString(),
label: node.descr,
isGroup: false,
shape: 'rect', // Default shape, can be customized based on node.type
shape: getShapeFromType(node.type),
width: node.width,
height: node.height ?? 0,
padding: node.padding,
@@ -298,6 +320,17 @@ const getData = (): LayoutData => {
log.debug(`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`);
// Create shapes map for ELK compatibility
const shapes = new Map<string, any>();
processedNodes.forEach((node) => {
shapes.set(node.id, {
shape: node.shape,
width: node.width,
height: node.height,
padding: node.padding,
});
});
return {
nodes: processedNodes,
edges: processedEdges,
@@ -309,6 +342,11 @@ const getData = (): LayoutData => {
direction: 'TB', // Top-to-bottom direction for mindmaps
nodeSpacing: 50, // Default spacing between nodes
rankSpacing: 50, // Default spacing between ranks
// Add shapes for ELK compatibility
shapes: Object.fromEntries(shapes),
// Additional properties that layout algorithms might expect
type: 'mindmap',
diagramId: 'mindmap-' + Date.now(),
};
};

View File

@@ -1,9 +1,4 @@
import type { SVG } from '../../../diagram-api/types.js';
import type { InternalHelpers } from '../../../internals.js';
import { log } from '../../../logger.js';
import type { LayoutData } from '../../types.js';
import type { RenderOptions } from '../../render.js';
import { executeCoseBilkentLayout } from './layout.js';
import { render as renderWithCoseBilkent } from './render.js';
/**
* Cose-Bilkent Layout Algorithm for Generic Diagrams
@@ -19,7 +14,7 @@ import { executeCoseBilkentLayout } from './layout.js';
* Render function for the cose-bilkent layout algorithm
*
* This function follows the unified rendering pattern used by all layout algorithms.
* It takes LayoutData, positions the nodes using Cytoscape with cose-bilkent,
* It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
* and renders the positioned elements to the SVG.
*
* @param layoutData - Layout data containing nodes, edges, and configuration
@@ -27,60 +22,4 @@ import { executeCoseBilkentLayout } from './layout.js';
* @param helpers - Internal helper functions for rendering
* @param options - Rendering options
*/
export const render = async (
layoutData: LayoutData,
_svg: SVG,
_helpers: InternalHelpers,
_options?: RenderOptions
): Promise<void> => {
log.debug('Cose-bilkent layout algorithm starting');
log.debug('LayoutData keys:', Object.keys(layoutData));
try {
// Validate input data
if (!layoutData.nodes || !Array.isArray(layoutData.nodes)) {
throw new Error('No nodes found in layout data');
}
if (!layoutData.edges || !Array.isArray(layoutData.edges)) {
throw new Error('No edges found in layout data');
}
log.debug(`Processing ${layoutData.nodes.length} nodes and ${layoutData.edges.length} edges`);
// Execute the layout algorithm directly with the provided data
const result = await executeCoseBilkentLayout(layoutData, layoutData.config);
// Update the original layout data with the positioned nodes and edges
layoutData.nodes.forEach((node) => {
const positionedNode = result.nodes.find((n) => n.id === node.id);
if (positionedNode) {
node.x = positionedNode.x;
node.y = positionedNode.y;
log.debug('Updated node position:', node.id, 'at', positionedNode.x, positionedNode.y);
}
});
layoutData.edges.forEach((edge) => {
const positionedEdge = result.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
// Update edge with positioning information if needed
const edgeWithPosition = edge as unknown as Record<string, unknown>;
edgeWithPosition.startX = positionedEdge.startX;
edgeWithPosition.startY = positionedEdge.startY;
edgeWithPosition.midX = positionedEdge.midX;
edgeWithPosition.midY = positionedEdge.midY;
edgeWithPosition.endX = positionedEdge.endX;
edgeWithPosition.endY = positionedEdge.endY;
}
});
log.debug('Cose-bilkent layout algorithm completed successfully');
log.debug(`Positioned ${result.nodes.length} nodes and ${result.edges.length} edges`);
} catch (error) {
log.error('Cose-bilkent layout algorithm failed:', error);
throw new Error(
`Layout algorithm failed: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
};
export const render = renderWithCoseBilkent;

View File

@@ -0,0 +1,183 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { executeCoseBilkentLayout } from './layout.js';
type Node = LayoutData['nodes'][number];
interface NodeWithPosition extends Node {
x?: number;
y?: number;
domId?: SVGGroup;
}
/**
* Render function for cose-bilkent layout algorithm
*
* This follows the same pattern as ELK and dagre renderers:
* 1. Insert nodes into DOM to get their actual dimensions
* 2. Run the layout algorithm to calculate positions
* 3. Position the nodes and edges based on layout results
*/
export const render = async (
data4Layout: LayoutData,
svg: SVG,
{
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm }: RenderOptions
) => {
const nodeDb: Record<string, NodeWithPosition> = {};
const clusterDb: Record<string, any> = {};
// Insert markers for edges
const element = svg.select('g');
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
// Create container groups
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
const edgePaths = element.insert('g').attr('class', 'edgePaths');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodes = element.insert('g').attr('class', 'nodes');
// Step 1: Insert nodes into DOM to get their actual dimensions
log.debug('Inserting nodes into DOM for dimension calculation');
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
// Handle subgraphs/clusters
const clusterNode: NodeWithPosition = { ...node };
clusterDb[node.id] = clusterNode;
nodeDb[node.id] = clusterNode;
// Insert cluster to get dimensions
await insertCluster(subGraphsEl, node);
} else {
// Handle regular nodes
const nodeWithPosition: NodeWithPosition = { ...node };
nodeDb[node.id] = nodeWithPosition;
// Insert node to get actual dimensions
const nodeEl = await insertNode(nodes, node, {
config: data4Layout.config,
dir: data4Layout.direction || 'TB',
});
// Get the actual bounding box after insertion
const boundingBox = nodeEl.node()!.getBBox();
nodeWithPosition.width = boundingBox.width;
nodeWithPosition.height = boundingBox.height;
nodeWithPosition.domId = nodeEl;
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
}
})
);
// Step 2: Run the cose-bilkent layout algorithm
log.debug('Running cose-bilkent layout algorithm');
// Update the layout data with actual dimensions
const updatedLayoutData = {
...data4Layout,
nodes: data4Layout.nodes.map((node) => {
const nodeWithDimensions = nodeDb[node.id];
return {
...node,
width: nodeWithDimensions.width,
height: nodeWithDimensions.height,
};
}),
};
const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
// Step 3: Position the nodes based on layout results
log.debug('Positioning nodes based on layout results');
layoutResult.nodes.forEach((positionedNode) => {
const node = nodeDb[positionedNode.id];
if (node && node.domId) {
// Position the node at the calculated coordinates
// The positionedNode.x/y represents the center of the node, so use directly
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
// Store the final position
node.x = positionedNode.x;
node.y = positionedNode.y;
log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
}
});
// Step 4: Insert and position edges
log.debug('Inserting and positioning edges');
await Promise.all(
data4Layout.edges.map(async (edge) => {
// Insert edge label first
const edgeLabel = await insertEdgeLabel(edgeLabels, edge);
// Get start and end nodes
const startNode = nodeDb[edge.start];
const endNode = nodeDb[edge.end];
if (startNode && endNode) {
// Find the positioned edge data
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
// Create edge path with positioned coordinates
const edgeWithPath = {
...edge,
points: [
{ x: positionedEdge.startX, y: positionedEdge.startY },
{ x: positionedEdge.endX, y: positionedEdge.endY },
],
};
// Insert the edge path
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
// Position the edge label
positionEdgeLabel(edgeWithPath, paths);
} else {
// Fallback: create a simple straight line between nodes
const edgeWithPath = {
...edge,
points: [
{ x: startNode.x || 0, y: startNode.y || 0 },
{ x: endNode.x || 0, y: endNode.y || 0 },
],
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
}
}
})
);
log.debug('Cose-bilkent rendering completed');
};

View File

@@ -0,0 +1,40 @@
<!doctype html>
<html>
<head>
<title>Test Unified Rendering - Mindmap with Different Layout Algorithms</title>
<script src="http://localhost:9000/mermaid.js"></script>
</head>
<body>
<h1>Test Unified Rendering System</h1>
<h2>Mindmap with Cose-Bilkent (Default)</h2>
<div class="mermaid">
mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular
psychology author Tony Buzan Research On effectiveness<br />and features On Automatic creation
Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid
</div>
<h2>Mindmap with Dagre Layout</h2>
<div class="mermaid">
--- config: layout: dagre --- mindmap root((mindmap)) Origins Long history Popularisation
British popular psychology author Tony Buzan Research On effectiveness<br />and features On
Automatic creation Uses Creative techniques Strategic planning Tools Pen and paper Mermaid
</div>
<h2>ER Diagram with Cose-Bilkent Layout</h2>
<div class="mermaid">
--- config: layout: cose-bilkent --- erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{
LINE-ITEM : contains CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
</div>
<script>
mermaid.initialize({
startOnLoad: true,
logLevel: 'debug',
mindmap: {
layoutAlgorithm: 'cose-bilkent',
},
});
</script>
</body>
</html>