mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-24 02:37:05 +02: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 { 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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) : ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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};
|
||||||
}
|
}
|
||||||
|
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