mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-22 00:40:22 +02:00
Repackaging cose-bilkins
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
|
@@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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