added proper hierarchy from parsed data

This commit is contained in:
Knut Sveidqvist
2025-05-08 11:19:19 +02:00
parent e0a075ecca
commit 1bd13b50f1
5 changed files with 288 additions and 156 deletions

View File

@@ -4,6 +4,7 @@ import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js'; import { populateCommonDb } from '../common/populateCommonDb.js';
import { db } from './db.js'; import { db } from './db.js';
import type { TreemapNode } from './types.js'; import type { TreemapNode } from './types.js';
import { buildHierarchy } from './utils.js';
/** /**
* Populates the database with data from the Treemap AST * Populates the database with data from the Treemap AST
@@ -12,11 +13,8 @@ import type { TreemapNode } from './types.js';
const populate = (ast: any) => { const populate = (ast: any) => {
populateCommonDb(ast, db); populateCommonDb(ast, db);
// Process rows const items = [];
let lastLevel = 0; // Extract data from each row in the treemap
let lastNode: TreemapNode | undefined;
// Process each row in the treemap, building the node hierarchy
for (const row of ast.TreemapRows || []) { for (const row of ast.TreemapRows || []) {
const item = row.item; const item = row.item;
if (!item) { if (!item) {
@@ -25,57 +23,26 @@ const populate = (ast: any) => {
const level = row.indent ? parseInt(row.indent) : 0; const level = row.indent ? parseInt(row.indent) : 0;
const name = getItemName(item); const name = getItemName(item);
const itemData = { level, name, type: item.$type, value: item.value };
// Create the node items.push(itemData);
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;
} }
// 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);
}; };
/** /**

View File

@@ -3,10 +3,9 @@ import type { DiagramRenderer, DrawDefinition } from '../../diagram-api/types.js
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { configureSvgSize } from '../../setupGraphViewbox.js'; import { configureSvgSize } from '../../setupGraphViewbox.js';
import type { TreemapDB, TreemapNode } from './types.js'; import type { TreemapDB, TreemapNode } from './types.js';
import { scaleOrdinal, treemap, hierarchy, format } from 'd3';
const DEFAULT_PADDING = 10; const DEFAULT_PADDING = 1;
const DEFAULT_NODE_WIDTH = 100;
const DEFAULT_NODE_HEIGHT = 40;
/** /**
* Draws the treemap diagram * Draws the treemap diagram
@@ -23,136 +22,144 @@ const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
} }
const svg = selectSvgElement(id); const svg = selectSvgElement(id);
// Use config dimensions or defaults
// Calculate the size of the treemap const width = config.nodeWidth ? config.nodeWidth * 10 : 960;
const { width, height } = calculateTreemapSize(root, config); const height = config.nodeHeight ? config.nodeHeight * 10 : 500;
const titleHeight = title ? 30 : 0; const titleHeight = title ? 30 : 0;
const svgWidth = width + padding * 2; const svgWidth = width;
const svgHeight = height + padding * 2 + titleHeight; const svgHeight = height + titleHeight;
// Set the SVG size // Set the SVG size
svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`); svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth); 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 // 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 // Draw the title if it exists
if (title) { if (title) {
svg svg
.append('text') .append('text')
.attr('x', svgWidth / 2) .attr('x', svgWidth / 2)
.attr('y', padding + titleHeight / 2) .attr('y', titleHeight / 2)
.attr('class', 'treemapTitle') .attr('class', 'treemapTitle')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle') .attr('dominant-baseline', 'middle')
.text(title); .text(title);
} }
// Draw the treemap recursively // Convert data to hierarchical structure
drawNode(g, root, 0, 0, width, height, config); const hierarchyRoot = hierarchy<TreemapNode>(root)
}; .sum((d) => d.value || 0)
.sort((a, b) => (b.value || 0) - (a.value || 0));
/** // Create treemap layout
* Calculates the size of the treemap const treemapLayout = treemap<TreemapNode>().size([width, height]).padding(padding).round(true);
*/
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,
};
}
// Otherwise, layout the children // Apply the treemap layout to the hierarchy
if (!root.children || root.children.length === 0) { const treemapData = treemapLayout(hierarchyRoot);
return {
width: config.nodeWidth || DEFAULT_NODE_WIDTH,
height: config.nodeHeight || DEFAULT_NODE_HEIGHT,
};
}
// Calculate based on children // Draw ALL nodes, not just leaves
let totalWidth = 0; const allNodes = treemapData.descendants();
let maxHeight = 0;
// Arrange in a simple tiled layout // Draw section nodes (non-leaf nodes)
for (const child of root.children) { const sections = g
const { width, height } = calculateTreemapSize(child, config); .selectAll('.treemapSection')
totalWidth += width + (config.padding || DEFAULT_PADDING); .data(allNodes.filter((d) => d.children && d.children.length > 0))
maxHeight = Math.max(maxHeight, height); .enter()
} .append('g')
.attr('class', 'treemapSection')
.attr('transform', (d) => `translate(${d.x0},${d.y0})`);
// Remove the last padding // Add rectangles for the sections
totalWidth -= config.padding || DEFAULT_PADDING; sections
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
.append('rect') .append('rect')
.attr('x', x) .attr('width', (d) => d.x1 - d.x0)
.attr('y', y) .attr('height', (d) => d.y1 - d.y0)
.attr('width', width) .attr('class', 'treemapSectionRect')
.attr('height', height) .attr('fill', (d) => colorScale(d.data.name))
.attr('class', `treemapNode ${node.value ? 'treemapLeaf' : 'treemapSection'}`); .attr('fill-opacity', 0.2)
.attr('stroke', (d) => colorScale(d.data.name))
.attr('stroke-width', 1);
// Add the label // Add section labels
parent sections
.append('text') .append('text')
.attr('x', x + width / 2) .attr('class', 'treemapSectionLabel')
.attr('y', y + 20) // Position the label at the top .attr('x', 4)
.attr('class', 'treemapLabel') .attr('y', 14)
.attr('text-anchor', 'middle') .text((d) => d.data.name)
.text(node.name); .attr('font-weight', 'bold');
// Add the value if it exists and should be shown // Add section values if enabled
if (node.value !== undefined && config.showValues !== false) { if (config.showValues !== false) {
parent sections
.append('text') .append('text')
.attr('x', x + width / 2) .attr('class', 'treemapSectionValue')
.attr('y', y + height - 10) // Position the value at the bottom .attr('x', 4)
.attr('class', 'treemapValue') .attr('y', 28)
.attr('text-anchor', 'middle') .text((d) => (d.value ? valueFormat(d.value) : ''))
.text(node.value); .attr('font-style', 'italic');
} }
// If this is a section with children, layout and draw the children // Draw the leaf nodes (nodes with no children)
if (!node.value && node.children && node.children.length > 0) { const cell = g
// Simple tiled layout for children .selectAll('.treemapLeaf')
const padding = config.padding || DEFAULT_PADDING; .data(treemapData.leaves())
let currentX = x + padding; .enter()
const innerY = y + 30; // Allow space for the label .append('g')
const innerHeight = height - 40; // Allow space for label .attr('class', 'treemapNode')
.attr('transform', (d) => `translate(${d.x0},${d.y0})`);
for (const child of node.children) { // Add rectangle for each leaf node
const childWidth = width / node.children.length - padding; cell
drawNode(parent, child, currentX, innerY, childWidth, innerHeight, config); .append('rect')
currentX += childWidth + padding; .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) : ''));
} }
}; };

View File

@@ -21,6 +21,11 @@ export const getStyles: DiagramStylesProvider = ({
const options = cleanAndMerge(defaultPacketStyleOptions, packet); const options = cleanAndMerge(defaultPacketStyleOptions, packet);
return ` return `
.treemapNode {
fill: pink;
stroke: black;
stroke-width: 1;
}
.packetByte { .packetByte {
font-size: ${options.byteFontSize}; font-size: ${options.byteFontSize};
} }

View 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);
});
});
});

View 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;
}