diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 8f5aa428a..347b50418 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -133,7 +133,7 @@
--- config: - layout: dagre + layout: cose-bilkent --- mindmap root((mindmap)) @@ -154,13 +154,13 @@--- 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 diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts index f401881d4..75fbfd27b 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts +++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts @@ -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(); + 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(), }; }; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts index 78a82f02b..9e12d38a7 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts @@ -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 => { - 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 ; - 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; diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts new file mode 100644 index 000000000..1616fb781 --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts @@ -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 = {}; + const clusterDb: Record = {}; + + // 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'); +}; diff --git a/test-unified-rendering.html b/test-unified-rendering.html new file mode 100644 index 000000000..4fdb0efe3 --- /dev/null +++ b/test-unified-rendering.html @@ -0,0 +1,40 @@ + + + + Test Unified Rendering - Mindmap with Different Layout Algorithms + + + +Test Unified Rendering System
+ +Mindmap with Cose-Bilkent (Default)
++ mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular + psychology author Tony Buzan Research On effectiveness+ +
and features On Automatic creation + Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid +Mindmap with Dagre Layout
++ --- config: layout: dagre --- mindmap root((mindmap)) Origins Long history Popularisation + British popular psychology author Tony Buzan Research On effectiveness+ +
and features On + Automatic creation Uses Creative techniques Strategic planning Tools Pen and paper Mermaid +ER Diagram with Cose-Bilkent Layout
++ --- config: layout: cose-bilkent --- erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{ + LINE-ITEM : contains CUSTOMER }|..|{ DELIVERY-ADDRESS : uses ++ + + +