mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-10-31 19:04:16 +01:00 
			
		
		
		
	added proper hierarchy from parsed data
This commit is contained in:
		| @@ -4,6 +4,7 @@ import { log } from '../../logger.js'; | ||||
| import { populateCommonDb } from '../common/populateCommonDb.js'; | ||||
| import { db } from './db.js'; | ||||
| import type { TreemapNode } from './types.js'; | ||||
| import { buildHierarchy } from './utils.js'; | ||||
|  | ||||
| /** | ||||
|  * Populates the database with data from the Treemap AST | ||||
| @@ -12,11 +13,8 @@ import type { TreemapNode } from './types.js'; | ||||
| const populate = (ast: any) => { | ||||
|   populateCommonDb(ast, db); | ||||
|  | ||||
|   // Process rows | ||||
|   let lastLevel = 0; | ||||
|   let lastNode: TreemapNode | undefined; | ||||
|  | ||||
|   // Process each row in the treemap, building the node hierarchy | ||||
|   const items = []; | ||||
|   // Extract data from each row in the treemap | ||||
|   for (const row of ast.TreemapRows || []) { | ||||
|     const item = row.item; | ||||
|     if (!item) { | ||||
| @@ -25,57 +23,26 @@ const populate = (ast: any) => { | ||||
|  | ||||
|     const level = row.indent ? parseInt(row.indent) : 0; | ||||
|     const name = getItemName(item); | ||||
|  | ||||
|     // Create the node | ||||
|     const node: TreemapNode = { | ||||
|       name, | ||||
|       children: [], | ||||
|     }; | ||||
|  | ||||
|     // If it's a leaf node, add the value | ||||
|     if (item.$type === 'Leaf') { | ||||
|       node.value = item.value; | ||||
|     } | ||||
|  | ||||
|     // Add to the right place in hierarchy | ||||
|     if (level === 0) { | ||||
|       // Root node | ||||
|       db.addNode(node, level); | ||||
|     } else if (level > lastLevel) { | ||||
|       // Child of the last node | ||||
|       if (lastNode) { | ||||
|         lastNode.children = lastNode.children || []; | ||||
|         lastNode.children.push(node); | ||||
|         node.parent = lastNode; | ||||
|       } | ||||
|       db.addNode(node, level); | ||||
|     } else if (level === lastLevel) { | ||||
|       // Sibling of the last node | ||||
|       if (lastNode?.parent) { | ||||
|         lastNode.parent.children = lastNode.parent.children || []; | ||||
|         lastNode.parent.children.push(node); | ||||
|         node.parent = lastNode.parent; | ||||
|       } | ||||
|       db.addNode(node, level); | ||||
|     } else if (level < lastLevel) { | ||||
|       // Go up in the hierarchy | ||||
|       let parent = lastNode ? lastNode.parent : undefined; | ||||
|       for (let i = lastLevel; i > level; i--) { | ||||
|         if (parent) { | ||||
|           parent = parent.parent; | ||||
|         } | ||||
|       } | ||||
|       if (parent) { | ||||
|         parent.children = parent.children || []; | ||||
|         parent.children.push(node); | ||||
|         node.parent = parent; | ||||
|       } | ||||
|       db.addNode(node, level); | ||||
|     } | ||||
|  | ||||
|     lastLevel = level; | ||||
|     lastNode = node; | ||||
|     const itemData = { level, name, type: item.$type, value: item.value }; | ||||
|     items.push(itemData); | ||||
|   } | ||||
|  | ||||
|   // Convert flat structure to hierarchical | ||||
|   const hierarchyNodes = buildHierarchy(items); | ||||
|  | ||||
|   // Add all nodes to the database | ||||
|   const addNodesRecursively = (nodes: TreemapNode[], level: number) => { | ||||
|     for (const node of nodes) { | ||||
|       db.addNode(node, level); | ||||
|       if (node.children && node.children.length > 0) { | ||||
|         addNodesRecursively(node.children, level + 1); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   addNodesRecursively(hierarchyNodes, 0); | ||||
|  | ||||
|   log.debug('Processed items:', items); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -3,10 +3,9 @@ import type { DiagramRenderer, DrawDefinition } from '../../diagram-api/types.js | ||||
| import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; | ||||
| import { configureSvgSize } from '../../setupGraphViewbox.js'; | ||||
| import type { TreemapDB, TreemapNode } from './types.js'; | ||||
| import { scaleOrdinal, treemap, hierarchy, format } from 'd3'; | ||||
|  | ||||
| const DEFAULT_PADDING = 10; | ||||
| const DEFAULT_NODE_WIDTH = 100; | ||||
| const DEFAULT_NODE_HEIGHT = 40; | ||||
| const DEFAULT_PADDING = 1; | ||||
|  | ||||
| /** | ||||
|  * Draws the treemap diagram | ||||
| @@ -23,136 +22,144 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { | ||||
|   } | ||||
|  | ||||
|   const svg = selectSvgElement(id); | ||||
|  | ||||
|   // Calculate the size of the treemap | ||||
|   const { width, height } = calculateTreemapSize(root, config); | ||||
|   // Use config dimensions or defaults | ||||
|   const width = config.nodeWidth ? config.nodeWidth * 10 : 960; | ||||
|   const height = config.nodeHeight ? config.nodeHeight * 10 : 500; | ||||
|   const titleHeight = title ? 30 : 0; | ||||
|   const svgWidth = width + padding * 2; | ||||
|   const svgHeight = height + padding * 2 + titleHeight; | ||||
|   const svgWidth = width; | ||||
|   const svgHeight = height + titleHeight; | ||||
|  | ||||
|   // Set the SVG size | ||||
|   svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`); | ||||
|   configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth); | ||||
|  | ||||
|   // Format for displaying values | ||||
|   const valueFormat = format(',d'); | ||||
|  | ||||
|   // Create color scale | ||||
|   const colorScale = scaleOrdinal<string>().range([ | ||||
|     '#8dd3c7', | ||||
|     '#ffffb3', | ||||
|     '#bebada', | ||||
|     '#fb8072', | ||||
|     '#80b1d3', | ||||
|     '#fdb462', | ||||
|     '#b3de69', | ||||
|     '#fccde5', | ||||
|     '#d9d9d9', | ||||
|     '#bc80bd', | ||||
|   ]); | ||||
|  | ||||
|   // Create a container group to hold all elements | ||||
|   const g = svg.append('g').attr('transform', `translate(${padding}, ${padding + titleHeight})`); | ||||
|   const g = svg.append('g').attr('transform', `translate(0, ${titleHeight})`); | ||||
|  | ||||
|   // Draw the title if it exists | ||||
|   if (title) { | ||||
|     svg | ||||
|       .append('text') | ||||
|       .attr('x', svgWidth / 2) | ||||
|       .attr('y', padding + titleHeight / 2) | ||||
|       .attr('y', titleHeight / 2) | ||||
|       .attr('class', 'treemapTitle') | ||||
|       .attr('text-anchor', 'middle') | ||||
|       .attr('dominant-baseline', 'middle') | ||||
|       .text(title); | ||||
|   } | ||||
|  | ||||
|   // Draw the treemap recursively | ||||
|   drawNode(g, root, 0, 0, width, height, config); | ||||
| }; | ||||
|   // Convert data to hierarchical structure | ||||
|   const hierarchyRoot = hierarchy<TreemapNode>(root) | ||||
|     .sum((d) => d.value || 0) | ||||
|     .sort((a, b) => (b.value || 0) - (a.value || 0)); | ||||
|  | ||||
| /** | ||||
|  * Calculates the size of the treemap | ||||
|  */ | ||||
| const calculateTreemapSize = ( | ||||
|   root: TreemapNode, | ||||
|   config: any | ||||
| ): { width: number; height: number } => { | ||||
|   // If we have a value, use it as the size | ||||
|   if (root.value) { | ||||
|     return { | ||||
|       width: config.nodeWidth || DEFAULT_NODE_WIDTH, | ||||
|       height: config.nodeHeight || DEFAULT_NODE_HEIGHT, | ||||
|     }; | ||||
|   } | ||||
|   // Create treemap layout | ||||
|   const treemapLayout = treemap<TreemapNode>().size([width, height]).padding(padding).round(true); | ||||
|  | ||||
|   // Otherwise, layout the children | ||||
|   if (!root.children || root.children.length === 0) { | ||||
|     return { | ||||
|       width: config.nodeWidth || DEFAULT_NODE_WIDTH, | ||||
|       height: config.nodeHeight || DEFAULT_NODE_HEIGHT, | ||||
|     }; | ||||
|   } | ||||
|   // Apply the treemap layout to the hierarchy | ||||
|   const treemapData = treemapLayout(hierarchyRoot); | ||||
|  | ||||
|   // Calculate based on children | ||||
|   let totalWidth = 0; | ||||
|   let maxHeight = 0; | ||||
|   // Draw ALL nodes, not just leaves | ||||
|   const allNodes = treemapData.descendants(); | ||||
|  | ||||
|   // Arrange in a simple tiled layout | ||||
|   for (const child of root.children) { | ||||
|     const { width, height } = calculateTreemapSize(child, config); | ||||
|     totalWidth += width + (config.padding || DEFAULT_PADDING); | ||||
|     maxHeight = Math.max(maxHeight, height); | ||||
|   } | ||||
|   // Draw section nodes (non-leaf nodes) | ||||
|   const sections = g | ||||
|     .selectAll('.treemapSection') | ||||
|     .data(allNodes.filter((d) => d.children && d.children.length > 0)) | ||||
|     .enter() | ||||
|     .append('g') | ||||
|     .attr('class', 'treemapSection') | ||||
|     .attr('transform', (d) => `translate(${d.x0},${d.y0})`); | ||||
|  | ||||
|   // Remove the last padding | ||||
|   totalWidth -= config.padding || DEFAULT_PADDING; | ||||
|  | ||||
|   return { | ||||
|     width: Math.max(totalWidth, config.nodeWidth || DEFAULT_NODE_WIDTH), | ||||
|     height: Math.max( | ||||
|       maxHeight + (config.padding || DEFAULT_PADDING) * 2, | ||||
|       config.nodeHeight || DEFAULT_NODE_HEIGHT | ||||
|     ), | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Recursively draws a node and its children in the treemap | ||||
|  */ | ||||
| const drawNode = ( | ||||
|   parent: any, | ||||
|   node: TreemapNode, | ||||
|   x: number, | ||||
|   y: number, | ||||
|   width: number, | ||||
|   height: number, | ||||
|   config: any | ||||
| ) => { | ||||
|   // Add rectangle | ||||
|   parent | ||||
|   // Add rectangles for the sections | ||||
|   sections | ||||
|     .append('rect') | ||||
|     .attr('x', x) | ||||
|     .attr('y', y) | ||||
|     .attr('width', width) | ||||
|     .attr('height', height) | ||||
|     .attr('class', `treemapNode ${node.value ? 'treemapLeaf' : 'treemapSection'}`); | ||||
|     .attr('width', (d) => d.x1 - d.x0) | ||||
|     .attr('height', (d) => d.y1 - d.y0) | ||||
|     .attr('class', 'treemapSectionRect') | ||||
|     .attr('fill', (d) => colorScale(d.data.name)) | ||||
|     .attr('fill-opacity', 0.2) | ||||
|     .attr('stroke', (d) => colorScale(d.data.name)) | ||||
|     .attr('stroke-width', 1); | ||||
|  | ||||
|   // Add the label | ||||
|   parent | ||||
|   // Add section labels | ||||
|   sections | ||||
|     .append('text') | ||||
|     .attr('x', x + width / 2) | ||||
|     .attr('y', y + 20) // Position the label at the top | ||||
|     .attr('class', 'treemapLabel') | ||||
|     .attr('text-anchor', 'middle') | ||||
|     .text(node.name); | ||||
|     .attr('class', 'treemapSectionLabel') | ||||
|     .attr('x', 4) | ||||
|     .attr('y', 14) | ||||
|     .text((d) => d.data.name) | ||||
|     .attr('font-weight', 'bold'); | ||||
|  | ||||
|   // Add the value if it exists and should be shown | ||||
|   if (node.value !== undefined && config.showValues !== false) { | ||||
|     parent | ||||
|   // Add section values if enabled | ||||
|   if (config.showValues !== false) { | ||||
|     sections | ||||
|       .append('text') | ||||
|       .attr('x', x + width / 2) | ||||
|       .attr('y', y + height - 10) // Position the value at the bottom | ||||
|       .attr('class', 'treemapValue') | ||||
|       .attr('text-anchor', 'middle') | ||||
|       .text(node.value); | ||||
|       .attr('class', 'treemapSectionValue') | ||||
|       .attr('x', 4) | ||||
|       .attr('y', 28) | ||||
|       .text((d) => (d.value ? valueFormat(d.value) : '')) | ||||
|       .attr('font-style', 'italic'); | ||||
|   } | ||||
|  | ||||
|   // If this is a section with children, layout and draw the children | ||||
|   if (!node.value && node.children && node.children.length > 0) { | ||||
|     // Simple tiled layout for children | ||||
|     const padding = config.padding || DEFAULT_PADDING; | ||||
|     let currentX = x + padding; | ||||
|     const innerY = y + 30; // Allow space for the label | ||||
|     const innerHeight = height - 40; // Allow space for label | ||||
|   // Draw the leaf nodes (nodes with no children) | ||||
|   const cell = g | ||||
|     .selectAll('.treemapLeaf') | ||||
|     .data(treemapData.leaves()) | ||||
|     .enter() | ||||
|     .append('g') | ||||
|     .attr('class', 'treemapNode') | ||||
|     .attr('transform', (d) => `translate(${d.x0},${d.y0})`); | ||||
|  | ||||
|     for (const child of node.children) { | ||||
|       const childWidth = width / node.children.length - padding; | ||||
|       drawNode(parent, child, currentX, innerY, childWidth, innerHeight, config); | ||||
|       currentX += childWidth + padding; | ||||
|     } | ||||
|   // Add rectangle for each leaf node | ||||
|   cell | ||||
|     .append('rect') | ||||
|     .attr('width', (d) => d.x1 - d.x0) | ||||
|     .attr('height', (d) => d.y1 - d.y0) | ||||
|     .attr('class', 'treemapLeaf') | ||||
|     .attr('fill', (d) => { | ||||
|       // Go up to parent for color | ||||
|       let current = d; | ||||
|       while (current.depth > 1 && current.parent) { | ||||
|         current = current.parent; | ||||
|       } | ||||
|       return colorScale(current.data.name); | ||||
|     }) | ||||
|     .attr('fill-opacity', 0.8); | ||||
|  | ||||
|   // Add node labels | ||||
|   cell | ||||
|     .append('text') | ||||
|     .attr('class', 'treemapLabel') | ||||
|     .attr('x', 4) | ||||
|     .attr('y', 14) | ||||
|     .text((d) => d.data.name); | ||||
|  | ||||
|   // Add node values if enabled | ||||
|   if (config.showValues !== false) { | ||||
|     cell | ||||
|       .append('text') | ||||
|       .attr('class', 'treemapValue') | ||||
|       .attr('x', 4) | ||||
|       .attr('y', 26) | ||||
|       .text((d) => (d.value ? valueFormat(d.value) : '')); | ||||
|   } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,11 @@ export const getStyles: DiagramStylesProvider = ({ | ||||
|   const options = cleanAndMerge(defaultPacketStyleOptions, packet); | ||||
|  | ||||
|   return ` | ||||
|   .treemapNode { | ||||
|     fill: pink; | ||||
|     stroke: black; | ||||
|     stroke-width: 1; | ||||
|   } | ||||
| 	.packetByte { | ||||
| 		font-size: ${options.byteFontSize}; | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										100
									
								
								packages/mermaid/src/diagrams/treemap/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								packages/mermaid/src/diagrams/treemap/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { buildHierarchy } from './utils.js'; | ||||
| import type { TreemapNode } from './types.js'; | ||||
|  | ||||
| describe('treemap utilities', () => { | ||||
|   describe('buildHierarchy', () => { | ||||
|     it('should convert a flat array into a hierarchical structure', () => { | ||||
|       // Input flat structure | ||||
|       const flatItems = [ | ||||
|         { level: 0, name: 'Root', type: 'Section' }, | ||||
|         { level: 4, name: 'Branch 1', type: 'Section' }, | ||||
|         { level: 8, name: 'Leaf 1.1', type: 'Leaf', value: 10 }, | ||||
|         { level: 8, name: 'Leaf 1.2', type: 'Leaf', value: 15 }, | ||||
|         { level: 4, name: 'Branch 2', type: 'Section' }, | ||||
|         { level: 8, name: 'Leaf 2.1', type: 'Leaf', value: 20 }, | ||||
|         { level: 8, name: 'Leaf 2.2', type: 'Leaf', value: 25 }, | ||||
|         { level: 8, name: 'Leaf 2.3', type: 'Leaf', value: 30 }, | ||||
|       ]; | ||||
|  | ||||
|       // Expected hierarchical structure | ||||
|       const expectedHierarchy: TreemapNode[] = [ | ||||
|         { | ||||
|           name: 'Root', | ||||
|           children: [ | ||||
|             { | ||||
|               name: 'Branch 1', | ||||
|               children: [ | ||||
|                 { name: 'Leaf 1.1', value: 10 }, | ||||
|                 { name: 'Leaf 1.2', value: 15 }, | ||||
|               ], | ||||
|             }, | ||||
|             { | ||||
|               name: 'Branch 2', | ||||
|               children: [ | ||||
|                 { name: 'Leaf 2.1', value: 20 }, | ||||
|                 { name: 'Leaf 2.2', value: 25 }, | ||||
|                 { name: 'Leaf 2.3', value: 30 }, | ||||
|               ], | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       ]; | ||||
|  | ||||
|       const result = buildHierarchy(flatItems); | ||||
|       expect(result).toEqual(expectedHierarchy); | ||||
|     }); | ||||
|  | ||||
|     it('should handle empty input', () => { | ||||
|       expect(buildHierarchy([])).toEqual([]); | ||||
|     }); | ||||
|  | ||||
|     it('should handle only root nodes', () => { | ||||
|       const flatItems = [ | ||||
|         { level: 0, name: 'Root 1', type: 'Section' }, | ||||
|         { level: 0, name: 'Root 2', type: 'Section' }, | ||||
|       ]; | ||||
|  | ||||
|       const expected = [ | ||||
|         { name: 'Root 1', children: [] }, | ||||
|         { name: 'Root 2', children: [] }, | ||||
|       ]; | ||||
|  | ||||
|       expect(buildHierarchy(flatItems)).toEqual(expected); | ||||
|     }); | ||||
|  | ||||
|     it('should handle complex nesting levels', () => { | ||||
|       const flatItems = [ | ||||
|         { level: 0, name: 'Root', type: 'Section' }, | ||||
|         { level: 2, name: 'Level 1', type: 'Section' }, | ||||
|         { level: 4, name: 'Level 2', type: 'Section' }, | ||||
|         { level: 6, name: 'Leaf 1', type: 'Leaf', value: 10 }, | ||||
|         { level: 4, name: 'Level 2 again', type: 'Section' }, | ||||
|         { level: 6, name: 'Leaf 2', type: 'Leaf', value: 20 }, | ||||
|       ]; | ||||
|  | ||||
|       const expected = [ | ||||
|         { | ||||
|           name: 'Root', | ||||
|           children: [ | ||||
|             { | ||||
|               name: 'Level 1', | ||||
|               children: [ | ||||
|                 { | ||||
|                   name: 'Level 2', | ||||
|                   children: [{ name: 'Leaf 1', value: 10 }], | ||||
|                 }, | ||||
|                 { | ||||
|                   name: 'Level 2 again', | ||||
|                   children: [{ name: 'Leaf 2', value: 20 }], | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       ]; | ||||
|  | ||||
|       expect(buildHierarchy(flatItems)).toEqual(expected); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										53
									
								
								packages/mermaid/src/diagrams/treemap/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								packages/mermaid/src/diagrams/treemap/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import type { TreemapNode } from './types.js'; | ||||
|  | ||||
| /** | ||||
|  * Converts a flat array of treemap items into a hierarchical structure | ||||
|  * @param items - Array of flat treemap items with level, name, type, and optional value | ||||
|  * @returns A hierarchical tree structure | ||||
|  */ | ||||
| export function buildHierarchy( | ||||
|   items: { level: number; name: string; type: string; value?: number }[] | ||||
| ): TreemapNode[] { | ||||
|   if (!items.length) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   const root: TreemapNode[] = []; | ||||
|   const stack: { node: TreemapNode; level: number }[] = []; | ||||
|  | ||||
|   items.forEach((item) => { | ||||
|     const node: TreemapNode = { | ||||
|       name: item.name, | ||||
|       children: item.type === 'Leaf' ? undefined : [], | ||||
|     }; | ||||
|  | ||||
|     if (item.type === 'Leaf' && item.value !== undefined) { | ||||
|       node.value = item.value; | ||||
|     } | ||||
|  | ||||
|     // Find the right parent for this node | ||||
|     while (stack.length > 0 && stack[stack.length - 1].level >= item.level) { | ||||
|       stack.pop(); | ||||
|     } | ||||
|  | ||||
|     if (stack.length === 0) { | ||||
|       // This is a root node | ||||
|       root.push(node); | ||||
|     } else { | ||||
|       // Add as child to the parent | ||||
|       const parent = stack[stack.length - 1].node; | ||||
|       if (parent.children) { | ||||
|         parent.children.push(node); | ||||
|       } else { | ||||
|         parent.children = [node]; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Only add to stack if it can have children | ||||
|     if (item.type !== 'Leaf') { | ||||
|       stack.push({ node, level: item.level }); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return root; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Knut Sveidqvist
					Knut Sveidqvist