mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Adding treemap
This commit is contained in:
@@ -258,6 +258,16 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
radar: {
|
||||
...defaultConfigJson.radar,
|
||||
},
|
||||
treemap: {
|
||||
useMaxWidth: true,
|
||||
padding: 10,
|
||||
showValues: true,
|
||||
nodeWidth: 100,
|
||||
nodeHeight: 40,
|
||||
borderWidth: 1,
|
||||
valueFontSize: 12,
|
||||
labelFontSize: 14,
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@@ -27,6 +27,7 @@ import block from '../diagrams/block/blockDetector.js';
|
||||
import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
import { treemap } from '../diagrams/treemap/detector.js';
|
||||
|
||||
let hasLoadedDiagrams = false;
|
||||
export const addDiagrams = () => {
|
||||
@@ -96,6 +97,7 @@ export const addDiagrams = () => {
|
||||
xychart,
|
||||
block,
|
||||
architecture,
|
||||
radar
|
||||
radar,
|
||||
treemap
|
||||
);
|
||||
};
|
||||
|
60
packages/mermaid/src/diagrams/treemap/db.ts
Normal file
60
packages/mermaid/src/diagrams/treemap/db.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
getAccTitle,
|
||||
getDiagramTitle,
|
||||
setAccDescription,
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { TreemapDB, TreemapData, TreemapNode } from './types.js';
|
||||
|
||||
const defaultTreemapData: TreemapData = {
|
||||
nodes: [],
|
||||
levels: new Map(),
|
||||
};
|
||||
|
||||
let data: TreemapData = structuredClone(defaultTreemapData);
|
||||
|
||||
const getConfig = () => {
|
||||
return cleanAndMerge({
|
||||
...DEFAULT_CONFIG.treemap,
|
||||
...commonGetConfig().treemap,
|
||||
});
|
||||
};
|
||||
|
||||
const getNodes = (): TreemapNode[] => data.nodes;
|
||||
|
||||
const addNode = (node: TreemapNode, level: number) => {
|
||||
data.nodes.push(node);
|
||||
data.levels.set(node, level);
|
||||
|
||||
// Set the root node if this is a level 0 node and we don't have a root yet
|
||||
if (level === 0 && !data.root) {
|
||||
data.root = node;
|
||||
}
|
||||
};
|
||||
|
||||
const getRoot = (): TreemapNode | undefined => data.root;
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
data = structuredClone(defaultTreemapData);
|
||||
};
|
||||
|
||||
export const db: TreemapDB = {
|
||||
getNodes,
|
||||
addNode,
|
||||
getRoot,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
};
|
23
packages/mermaid/src/diagrams/treemap/detector.ts
Normal file
23
packages/mermaid/src/diagrams/treemap/detector.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'treemap';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
console.log('treemap detector', txt);
|
||||
return /^\s*treemap/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./diagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const treemap: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
12
packages/mermaid/src/diagrams/treemap/diagram.ts
Normal file
12
packages/mermaid/src/diagrams/treemap/diagram.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
};
|
103
packages/mermaid/src/diagrams/treemap/parser.ts
Normal file
103
packages/mermaid/src/diagrams/treemap/parser.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import type { TreemapNode } from './types.js';
|
||||
|
||||
/**
|
||||
* Populates the database with data from the Treemap AST
|
||||
* @param ast - The Treemap AST
|
||||
*/
|
||||
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
|
||||
for (const row of ast.TreemapRows || []) {
|
||||
const item = row.item;
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the name of a treemap item
|
||||
* @param item - The treemap item
|
||||
* @returns The name of the item
|
||||
*/
|
||||
const getItemName = (item: any): string => {
|
||||
return item.name ? String(item.name) : '';
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: async (text: string): Promise<void> => {
|
||||
try {
|
||||
// Use a generic parse that accepts any diagram type
|
||||
const parseFunc = parse as (diagramType: string, text: string) => Promise<any>;
|
||||
const ast = await parseFunc('treemap', text);
|
||||
log.debug('Treemap AST:', ast);
|
||||
populate(ast);
|
||||
} catch (error) {
|
||||
log.error('Error parsing treemap:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
159
packages/mermaid/src/diagrams/treemap/renderer.ts
Normal file
159
packages/mermaid/src/diagrams/treemap/renderer.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
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';
|
||||
|
||||
const DEFAULT_PADDING = 10;
|
||||
const DEFAULT_NODE_WIDTH = 100;
|
||||
const DEFAULT_NODE_HEIGHT = 40;
|
||||
|
||||
/**
|
||||
* Draws the treemap diagram
|
||||
*/
|
||||
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
|
||||
const treemapDb = diagram.db as TreemapDB;
|
||||
const config = treemapDb.getConfig();
|
||||
const padding = config.padding || DEFAULT_PADDING;
|
||||
const title = treemapDb.getDiagramTitle();
|
||||
const root = treemapDb.getRoot();
|
||||
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = selectSvgElement(id);
|
||||
|
||||
// Calculate the size of the treemap
|
||||
const { width, height } = calculateTreemapSize(root, config);
|
||||
const titleHeight = title ? 30 : 0;
|
||||
const svgWidth = width + padding * 2;
|
||||
const svgHeight = height + padding * 2 + titleHeight;
|
||||
|
||||
// Set the SVG size
|
||||
svg.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
||||
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
|
||||
|
||||
// Create a container group to hold all elements
|
||||
const g = svg.append('g').attr('transform', `translate(${padding}, ${padding + titleHeight})`);
|
||||
|
||||
// Draw the title if it exists
|
||||
if (title) {
|
||||
svg
|
||||
.append('text')
|
||||
.attr('x', svgWidth / 2)
|
||||
.attr('y', padding + 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, layout the children
|
||||
if (!root.children || root.children.length === 0) {
|
||||
return {
|
||||
width: config.nodeWidth || DEFAULT_NODE_WIDTH,
|
||||
height: config.nodeHeight || DEFAULT_NODE_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate based on children
|
||||
let totalWidth = 0;
|
||||
let maxHeight = 0;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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
|
||||
.append('rect')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('class', `treemapNode ${node.value ? 'treemapLeaf' : 'treemapSection'}`);
|
||||
|
||||
// Add the label
|
||||
parent
|
||||
.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);
|
||||
|
||||
// Add the value if it exists and should be shown
|
||||
if (node.value !== undefined && config.showValues !== false) {
|
||||
parent
|
||||
.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);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
for (const child of node.children) {
|
||||
const childWidth = width / node.children.length - padding;
|
||||
drawNode(parent, child, currentX, innerY, childWidth, innerHeight, config);
|
||||
currentX += childWidth + padding;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const renderer: DiagramRenderer = { draw };
|
49
packages/mermaid/src/diagrams/treemap/styles.ts
Normal file
49
packages/mermaid/src/diagrams/treemap/styles.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import type { PacketStyleOptions } from './types.js';
|
||||
|
||||
const defaultPacketStyleOptions: PacketStyleOptions = {
|
||||
byteFontSize: '10px',
|
||||
startByteColor: 'black',
|
||||
endByteColor: 'black',
|
||||
labelColor: 'black',
|
||||
labelFontSize: '12px',
|
||||
titleColor: 'black',
|
||||
titleFontSize: '14px',
|
||||
blockStrokeColor: 'black',
|
||||
blockStrokeWidth: '1',
|
||||
blockFillColor: '#efefef',
|
||||
};
|
||||
|
||||
export const getStyles: DiagramStylesProvider = ({
|
||||
packet,
|
||||
}: { packet?: PacketStyleOptions } = {}) => {
|
||||
const options = cleanAndMerge(defaultPacketStyleOptions, packet);
|
||||
|
||||
return `
|
||||
.packetByte {
|
||||
font-size: ${options.byteFontSize};
|
||||
}
|
||||
.packetByte.start {
|
||||
fill: ${options.startByteColor};
|
||||
}
|
||||
.packetByte.end {
|
||||
fill: ${options.endByteColor};
|
||||
}
|
||||
.packetLabel {
|
||||
fill: ${options.labelColor};
|
||||
font-size: ${options.labelFontSize};
|
||||
}
|
||||
.packetTitle {
|
||||
fill: ${options.titleColor};
|
||||
font-size: ${options.titleFontSize};
|
||||
}
|
||||
.packetBlock {
|
||||
stroke: ${options.blockStrokeColor};
|
||||
stroke-width: ${options.blockStrokeWidth};
|
||||
fill: ${options.blockFillColor};
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export default getStyles;
|
47
packages/mermaid/src/diagrams/treemap/types.ts
Normal file
47
packages/mermaid/src/diagrams/treemap/types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||
import type { BaseDiagramConfig } from '../../config.type.js';
|
||||
|
||||
export interface TreemapNode {
|
||||
name: string;
|
||||
children?: TreemapNode[];
|
||||
value?: number;
|
||||
parent?: TreemapNode;
|
||||
}
|
||||
|
||||
export interface TreemapDB extends DiagramDBBase<TreemapDiagramConfig> {
|
||||
getNodes: () => TreemapNode[];
|
||||
addNode: (node: TreemapNode, level: number) => void;
|
||||
getRoot: () => TreemapNode | undefined;
|
||||
}
|
||||
|
||||
export interface TreemapStyleOptions {
|
||||
sectionStrokeColor?: string;
|
||||
sectionStrokeWidth?: string;
|
||||
sectionFillColor?: string;
|
||||
leafStrokeColor?: string;
|
||||
leafStrokeWidth?: string;
|
||||
leafFillColor?: string;
|
||||
labelColor?: string;
|
||||
labelFontSize?: string;
|
||||
valueFontSize?: string;
|
||||
valueColor?: string;
|
||||
titleColor?: string;
|
||||
titleFontSize?: string;
|
||||
}
|
||||
|
||||
export interface TreemapData {
|
||||
nodes: TreemapNode[];
|
||||
levels: Map<TreemapNode, number>;
|
||||
root?: TreemapNode;
|
||||
}
|
||||
|
||||
// Define the TreemapDiagramConfig interface
|
||||
export interface TreemapDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
showValues?: boolean;
|
||||
nodeWidth?: number;
|
||||
nodeHeight?: number;
|
||||
borderWidth?: number;
|
||||
valueFontSize?: number;
|
||||
labelFontSize?: number;
|
||||
}
|
@@ -8,6 +8,7 @@ export {
|
||||
Architecture,
|
||||
GitGraph,
|
||||
Radar,
|
||||
TreemapDoc,
|
||||
Branch,
|
||||
Commit,
|
||||
Merge,
|
||||
@@ -19,6 +20,7 @@ export {
|
||||
isPieSection,
|
||||
isArchitecture,
|
||||
isGitGraph,
|
||||
isTreemapDoc,
|
||||
isBranch,
|
||||
isCommit,
|
||||
isMerge,
|
||||
@@ -32,6 +34,7 @@ export {
|
||||
ArchitectureGeneratedModule,
|
||||
GitGraphGeneratedModule,
|
||||
RadarGeneratedModule,
|
||||
TreemapGeneratedModule,
|
||||
} from './generated/module.js';
|
||||
|
||||
export * from './gitGraph/index.js';
|
||||
@@ -41,3 +44,4 @@ export * from './packet/index.js';
|
||||
export * from './pie/index.js';
|
||||
export * from './architecture/index.js';
|
||||
export * from './radar/index.js';
|
||||
export * from './treemap/index.js';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
|
||||
import type { Info, Packet, Pie, Architecture, GitGraph, Radar } from './index.js';
|
||||
import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js';
|
||||
|
||||
export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar;
|
||||
|
||||
@@ -36,6 +36,11 @@ const initializers = {
|
||||
const parser = createRadarServices().Radar.parser.LangiumParser;
|
||||
parsers.radar = parser;
|
||||
},
|
||||
treemap: async () => {
|
||||
const { createTreemapServices } = await import('./language/treemap/index.js');
|
||||
const parser = createTreemapServices().Treemap.parser.LangiumParser;
|
||||
parsers.treemap = parser;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export async function parse(diagramType: 'info', text: string): Promise<Info>;
|
||||
@@ -44,6 +49,7 @@ export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
|
||||
export async function parse(diagramType: 'architecture', text: string): Promise<Architecture>;
|
||||
export async function parse(diagramType: 'gitGraph', text: string): Promise<GitGraph>;
|
||||
export async function parse(diagramType: 'radar', text: string): Promise<Radar>;
|
||||
export async function parse(diagramType: 'treemap', text: string): Promise<Treemap>;
|
||||
|
||||
export async function parse<T extends DiagramAST>(
|
||||
diagramType: keyof typeof initializers,
|
||||
|
Reference in New Issue
Block a user