mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 07:19:41 +02:00
refactor pieRenderer
This commit is contained in:
@@ -1,191 +1,180 @@
|
|||||||
// @ts-nocheck - placeholder to be handled
|
import d3, { scaleOrdinal, pie as d3pie, arc } from 'd3';
|
||||||
import d3, { select, scaleOrdinal, pie as d3pie, arc } from 'd3';
|
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||||
import { getConfig } from '../../config.js';
|
import { getConfig } from '../../config.js';
|
||||||
import { parseFontSize } from '../../utils.js';
|
import { parseFontSize } from '../../utils.js';
|
||||||
import type { DrawDefinition, HTML } from '../../diagram-api/types.js';
|
import type { DrawDefinition, Group, HTML, SVG } from '../../diagram-api/types.js';
|
||||||
import type { D3Sections, PieDB, PieDiagramConfig, Sections } from './pieTypes.js';
|
import type { D3Sections, PieDB, PieDiagramConfig, Sections } from './pieTypes.js';
|
||||||
import { MermaidConfig } from '../../config.type.js';
|
import type { MermaidConfig } from '../../config.type.js';
|
||||||
|
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||||
|
|
||||||
|
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
|
||||||
|
// Compute the position of each group on the pie:
|
||||||
|
const pieData: D3Sections[] = Object.entries(sections).map(
|
||||||
|
(element: [string, number]): D3Sections => {
|
||||||
|
return {
|
||||||
|
label: element[0],
|
||||||
|
value: element[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
|
||||||
|
(d3Section: D3Sections): number => d3Section.value
|
||||||
|
);
|
||||||
|
return pie(pieData);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a Pie Chart with the data given in text.
|
* Draws a Pie Chart with the data given in text.
|
||||||
*
|
*
|
||||||
* @param text - pie chart code
|
* @param text - pie chart code
|
||||||
* @param id - diagram id
|
* @param id - diagram id
|
||||||
|
* @param _version - MermaidJS version from package.json.
|
||||||
|
* @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram.
|
||||||
*/
|
*/
|
||||||
export const draw: DrawDefinition = (txt, id, _version, diagramObject) => {
|
export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
||||||
try {
|
log.debug('rendering pie chart\n' + text);
|
||||||
log.debug('rendering pie chart\n' + txt);
|
|
||||||
const db: PieDB = diagramObject.db as PieDB;
|
|
||||||
const globalConfig: MermaidConfig = getConfig();
|
|
||||||
const config: Required<PieDiagramConfig> = db.getConfig();
|
|
||||||
|
|
||||||
const height = 450;
|
const db: PieDB = diagObj.db as PieDB;
|
||||||
const { securityLevel } = globalConfig;
|
const globalConfig: MermaidConfig = getConfig();
|
||||||
// handle root and document for when rendering in sandbox mode
|
const pieConfig: Required<PieDiagramConfig> = db.getConfig();
|
||||||
let sandboxElement: HTML | undefined;
|
|
||||||
if (securityLevel === 'sandbox') {
|
|
||||||
sandboxElement = select('#i' + id);
|
|
||||||
}
|
|
||||||
const root =
|
|
||||||
securityLevel === 'sandbox'
|
|
||||||
? select(sandboxElement?.node()?.contentDocument?.body as HTMLIFrameElement)
|
|
||||||
: select('body');
|
|
||||||
const doc = securityLevel === 'sandbox' ? sandboxElement?.nodes()[0].contentDocument : document;
|
|
||||||
const elem = doc?.getElementById(id);
|
|
||||||
const width: number = elem?.parentElement?.offsetWidth ?? config.useWidth;
|
|
||||||
|
|
||||||
const diagram = root.select('#' + id);
|
const height = 450;
|
||||||
// TODO: use global `useMaxWidth` until making setConfig update pie setConfig
|
// TODO: remove document width
|
||||||
configureSvgSize(diagram, height, width, globalConfig?.pie?.useMaxWidth ?? true);
|
const width: number =
|
||||||
|
document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth;
|
||||||
|
const svg: SVG = selectSvgElement(id);
|
||||||
|
// Set viewBox
|
||||||
|
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||||
|
configureSvgSize(svg, height, width, globalConfig?.pie?.useMaxWidth ?? true);
|
||||||
|
|
||||||
// Set viewBox
|
const MARGIN = 40;
|
||||||
elem?.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
const LEGEND_RECT_SIZE = 18;
|
||||||
|
const LEGEND_SPACING = 4;
|
||||||
|
|
||||||
const margin = 40;
|
const group: Group = svg.append('g');
|
||||||
const legendRectSize = 18;
|
group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
|
||||||
const legendSpacing = 4;
|
|
||||||
|
|
||||||
const radius: number = Math.min(width, height) / 2 - margin;
|
const { themeVariables } = globalConfig;
|
||||||
|
const textPosition: number = pieConfig.textPosition;
|
||||||
|
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
|
||||||
|
outerStrokeWidth ??= 2;
|
||||||
|
|
||||||
const svg = diagram
|
const radius: number = Math.min(width, height) / 2 - MARGIN;
|
||||||
.append('g')
|
// Shape helper to build arcs:
|
||||||
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
|
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
|
||||||
|
d3.PieArcDatum<D3Sections>
|
||||||
|
>()
|
||||||
|
.innerRadius(0)
|
||||||
|
.outerRadius(radius);
|
||||||
|
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
|
||||||
|
d3.PieArcDatum<D3Sections>
|
||||||
|
>()
|
||||||
|
.innerRadius(radius * textPosition)
|
||||||
|
.outerRadius(radius * textPosition);
|
||||||
|
|
||||||
const sections: Sections = db.getSections();
|
group
|
||||||
let sum = 1;
|
.append('circle')
|
||||||
Object.keys(sections).forEach((key: string): void => {
|
.attr('cx', 0)
|
||||||
sum += sections[key];
|
.attr('cy', 0)
|
||||||
|
.attr('r', radius + outerStrokeWidth / 2)
|
||||||
|
.attr('class', 'pieOuterCircle');
|
||||||
|
|
||||||
|
const sections: Sections = db.getSections();
|
||||||
|
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
|
||||||
|
|
||||||
|
const myGeneratedColors = [
|
||||||
|
themeVariables.pie1,
|
||||||
|
themeVariables.pie2,
|
||||||
|
themeVariables.pie3,
|
||||||
|
themeVariables.pie4,
|
||||||
|
themeVariables.pie5,
|
||||||
|
themeVariables.pie6,
|
||||||
|
themeVariables.pie7,
|
||||||
|
themeVariables.pie8,
|
||||||
|
themeVariables.pie9,
|
||||||
|
themeVariables.pie10,
|
||||||
|
themeVariables.pie11,
|
||||||
|
themeVariables.pie12,
|
||||||
|
];
|
||||||
|
// Set the color scale
|
||||||
|
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
|
||||||
|
|
||||||
|
// Build the pie chart: each part of the pie is a path that we build using the arc function.
|
||||||
|
group
|
||||||
|
.selectAll('mySlices')
|
||||||
|
.data(arcs)
|
||||||
|
.enter()
|
||||||
|
.append('path')
|
||||||
|
.attr('d', arcGenerator)
|
||||||
|
.attr('fill', (datum) => {
|
||||||
|
return color(datum.data.label);
|
||||||
|
})
|
||||||
|
.attr('class', 'pieCircle');
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
Object.keys(sections).forEach((key: string): void => {
|
||||||
|
sum += sections[key];
|
||||||
|
});
|
||||||
|
// Now add the percentage.
|
||||||
|
// Use the centroid method to get the best coordinates.
|
||||||
|
group
|
||||||
|
.selectAll('mySlices')
|
||||||
|
.data(arcs)
|
||||||
|
.enter()
|
||||||
|
.append('text')
|
||||||
|
.text((datum: { data: D3Sections }): string => {
|
||||||
|
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
|
||||||
|
})
|
||||||
|
.attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
|
||||||
|
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
|
||||||
|
})
|
||||||
|
.style('text-anchor', 'middle')
|
||||||
|
.attr('class', 'slice');
|
||||||
|
|
||||||
|
group
|
||||||
|
.append('text')
|
||||||
|
.text(db.getDiagramTitle())
|
||||||
|
.attr('x', 0)
|
||||||
|
.attr('y', -(height - 50) / 2)
|
||||||
|
.attr('class', 'pieTitleText');
|
||||||
|
|
||||||
|
// Add the legends/annotations for each section
|
||||||
|
const legend = group
|
||||||
|
.selectAll('.legend')
|
||||||
|
.data(color.domain())
|
||||||
|
.enter()
|
||||||
|
.append('g')
|
||||||
|
.attr('class', 'legend')
|
||||||
|
.attr('transform', (_datum, index: number): string => {
|
||||||
|
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
|
||||||
|
const offset = (height * color.domain().length) / 2;
|
||||||
|
const horizontal = 12 * LEGEND_RECT_SIZE;
|
||||||
|
const vertical = index * height - offset;
|
||||||
|
return 'translate(' + horizontal + ',' + vertical + ')';
|
||||||
});
|
});
|
||||||
|
|
||||||
const { themeVariables } = globalConfig;
|
legend
|
||||||
const myGeneratedColors = [
|
.append('rect')
|
||||||
themeVariables.pie1,
|
.attr('width', LEGEND_RECT_SIZE)
|
||||||
themeVariables.pie2,
|
.attr('height', LEGEND_RECT_SIZE)
|
||||||
themeVariables.pie3,
|
.style('fill', color)
|
||||||
themeVariables.pie4,
|
.style('stroke', color);
|
||||||
themeVariables.pie5,
|
|
||||||
themeVariables.pie6,
|
|
||||||
themeVariables.pie7,
|
|
||||||
themeVariables.pie8,
|
|
||||||
themeVariables.pie9,
|
|
||||||
themeVariables.pie10,
|
|
||||||
themeVariables.pie11,
|
|
||||||
themeVariables.pie12,
|
|
||||||
];
|
|
||||||
|
|
||||||
const textPosition: number = config.textPosition;
|
legend
|
||||||
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
|
.data(arcs)
|
||||||
outerStrokeWidth ??= 2;
|
.append('text')
|
||||||
|
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
|
||||||
// Set the color scale
|
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
|
||||||
const color: d3.ScaleOrdinal<string, unknown, never> = scaleOrdinal().range(myGeneratedColors);
|
.text((datum: d3.PieArcDatum<D3Sections>): string => {
|
||||||
|
const { label, value } = datum.data;
|
||||||
// Compute the position of each group on the pie:
|
if (db.getShowData()) {
|
||||||
const pieData: D3Sections[] = Object.entries(sections)
|
return `${label} [${value}]`;
|
||||||
.map((element: [string, number], index: number): D3Sections => {
|
} else {
|
||||||
return {
|
return label;
|
||||||
order: index,
|
}
|
||||||
label: element[0],
|
});
|
||||||
value: element[1],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a: D3Sections, b: D3Sections): number => {
|
|
||||||
// Sort slices in clockwise direction
|
|
||||||
return a.order - b.order;
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const pie = d3pie().value((d: any): number => d.value);
|
|
||||||
// @ts-ignore - figure out how to assign D3Section[] to PieArcDatum
|
|
||||||
const dataReady = pie(pieData);
|
|
||||||
|
|
||||||
// Shape helper to build arcs:
|
|
||||||
const arcGenerator = arc().innerRadius(0).outerRadius(radius);
|
|
||||||
const labelArcGenerator = arc()
|
|
||||||
.innerRadius(radius * textPosition)
|
|
||||||
.outerRadius(radius * textPosition);
|
|
||||||
|
|
||||||
svg
|
|
||||||
.append('circle')
|
|
||||||
.attr('cx', 0)
|
|
||||||
.attr('cy', 0)
|
|
||||||
.attr('r', radius + outerStrokeWidth / 2)
|
|
||||||
.attr('class', 'pieOuterCircle');
|
|
||||||
|
|
||||||
// Build the pie chart: each part of the pie is a path that we build using the arc function.
|
|
||||||
svg
|
|
||||||
.selectAll('mySlices')
|
|
||||||
.data(dataReady)
|
|
||||||
.enter()
|
|
||||||
.append('path')
|
|
||||||
.attr('d', arcGenerator)
|
|
||||||
.attr('fill', (datum: { data: D3Sections }) => {
|
|
||||||
return color(datum.data.label);
|
|
||||||
})
|
|
||||||
.attr('class', 'pieCircle');
|
|
||||||
|
|
||||||
// Now add the percentage.
|
|
||||||
// Use the centroid method to get the best coordinates.
|
|
||||||
svg
|
|
||||||
.selectAll('mySlices')
|
|
||||||
.data(dataReady)
|
|
||||||
.enter()
|
|
||||||
.append('text')
|
|
||||||
.text((datum: { data: D3Sections }): string => {
|
|
||||||
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
.attr('transform', (datum: any): string => {
|
|
||||||
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
|
|
||||||
})
|
|
||||||
.style('text-anchor', 'middle')
|
|
||||||
.attr('class', 'slice');
|
|
||||||
|
|
||||||
svg
|
|
||||||
.append('text')
|
|
||||||
.text(db.getDiagramTitle())
|
|
||||||
.attr('x', 0)
|
|
||||||
.attr('y', -(height - 50) / 2)
|
|
||||||
.attr('class', 'pieTitleText');
|
|
||||||
|
|
||||||
// Add the legends/annotations for each section
|
|
||||||
const legend = svg
|
|
||||||
.selectAll('.legend')
|
|
||||||
.data(color.domain())
|
|
||||||
.enter()
|
|
||||||
.append('g')
|
|
||||||
.attr('class', 'legend')
|
|
||||||
.attr('transform', (_datum: D3Sections, index: number): string => {
|
|
||||||
const height = legendRectSize + legendSpacing;
|
|
||||||
const offset = (height * color.domain().length) / 2;
|
|
||||||
const horizontal = 12 * legendRectSize;
|
|
||||||
const vertical = index * height - offset;
|
|
||||||
return 'translate(' + horizontal + ',' + vertical + ')';
|
|
||||||
});
|
|
||||||
|
|
||||||
legend
|
|
||||||
.append('rect')
|
|
||||||
.attr('width', legendRectSize)
|
|
||||||
.attr('height', legendRectSize)
|
|
||||||
.style('fill', color)
|
|
||||||
.style('stroke', color);
|
|
||||||
|
|
||||||
legend
|
|
||||||
.data(dataReady)
|
|
||||||
.append('text')
|
|
||||||
.attr('x', legendRectSize + legendSpacing)
|
|
||||||
.attr('y', legendRectSize - legendSpacing)
|
|
||||||
.text((datum: { data: D3Sections }): string => {
|
|
||||||
if (db.getShowData()) {
|
|
||||||
return datum.data.label + ' [' + datum.data.value + ']';
|
|
||||||
} else {
|
|
||||||
return datum.data.label;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
log.error('error while rendering pie chart\n', e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderer = { draw };
|
export const renderer = { draw };
|
||||||
|
@@ -41,7 +41,6 @@ export interface PieStyleOptions {
|
|||||||
export type Sections = Record<string, number>;
|
export type Sections = Record<string, number>;
|
||||||
|
|
||||||
export interface D3Sections {
|
export interface D3Sections {
|
||||||
order: number;
|
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user