mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-22 08:50:13 +02:00
Repackaging cose-bilkins
This commit is contained in:
@@ -133,7 +133,7 @@
|
|||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: dagre
|
layout: cose-bilkent
|
||||||
---
|
---
|
||||||
mindmap
|
mindmap
|
||||||
root((mindmap))
|
root((mindmap))
|
||||||
@@ -154,13 +154,13 @@
|
|||||||
<pre id="diagram4" class="mermaid">
|
<pre id="diagram4" class="mermaid">
|
||||||
---
|
---
|
||||||
config:
|
config:
|
||||||
layout: elk
|
layout: cose-bilkent
|
||||||
---
|
---
|
||||||
flowchart LR
|
flowchart LR
|
||||||
root{mindmap} --- Origins --- Europe
|
root{mindmap} --- Origins --- Europe
|
||||||
root --- Origins --- Asia
|
Origins --> Asia
|
||||||
root --- Background --- Rich
|
root --- Background --- Rich
|
||||||
root --- Background --- Poor
|
Background --- Poor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -196,12 +196,34 @@ const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): v
|
|||||||
cssClasses += ` ${node.class}`;
|
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 = {
|
const processedNode: MindmapLayoutNode = {
|
||||||
id: 'node_' + node.id.toString(),
|
id: 'node_' + node.id.toString(),
|
||||||
domId: 'node_' + node.id.toString(),
|
domId: 'node_' + node.id.toString(),
|
||||||
label: node.descr,
|
label: node.descr,
|
||||||
isGroup: false,
|
isGroup: false,
|
||||||
shape: 'rect', // Default shape, can be customized based on node.type
|
shape: getShapeFromType(node.type),
|
||||||
width: node.width,
|
width: node.width,
|
||||||
height: node.height ?? 0,
|
height: node.height ?? 0,
|
||||||
padding: node.padding,
|
padding: node.padding,
|
||||||
@@ -298,6 +320,17 @@ const getData = (): LayoutData => {
|
|||||||
|
|
||||||
log.debug(`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`);
|
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 {
|
return {
|
||||||
nodes: processedNodes,
|
nodes: processedNodes,
|
||||||
edges: processedEdges,
|
edges: processedEdges,
|
||||||
@@ -309,6 +342,11 @@ const getData = (): LayoutData => {
|
|||||||
direction: 'TB', // Top-to-bottom direction for mindmaps
|
direction: 'TB', // Top-to-bottom direction for mindmaps
|
||||||
nodeSpacing: 50, // Default spacing between nodes
|
nodeSpacing: 50, // Default spacing between nodes
|
||||||
rankSpacing: 50, // Default spacing between ranks
|
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(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
import type { SVG } from '../../../diagram-api/types.js';
|
import { render as renderWithCoseBilkent } from './render.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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cose-Bilkent Layout Algorithm for Generic Diagrams
|
* Cose-Bilkent Layout Algorithm for Generic Diagrams
|
||||||
@@ -19,7 +14,7 @@ import { executeCoseBilkentLayout } from './layout.js';
|
|||||||
* Render function for the cose-bilkent layout algorithm
|
* Render function for the cose-bilkent layout algorithm
|
||||||
*
|
*
|
||||||
* This function follows the unified rendering pattern used by all layout algorithms.
|
* 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.
|
* and renders the positioned elements to the SVG.
|
||||||
*
|
*
|
||||||
* @param layoutData - Layout data containing nodes, edges, and configuration
|
* @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 helpers - Internal helper functions for rendering
|
||||||
* @param options - Rendering options
|
* @param options - Rendering options
|
||||||
*/
|
*/
|
||||||
export const render = async (
|
export const render = renderWithCoseBilkent;
|
||||||
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'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@@ -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');
|
||||||
|
};
|
40
test-unified-rendering.html
Normal file
40
test-unified-rendering.html
Normal 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>
|
Reference in New Issue
Block a user