From 9563b221327e4ac50f87d9e14626c974e4852a13 Mon Sep 17 00:00:00 2001 From: Reda Al Sulais Date: Sun, 6 Aug 2023 21:20:33 +0300 Subject: [PATCH] refactor `pieRenderer` --- .../mermaid/src/diagrams/pie/pieRenderer.ts | 321 +++++++++--------- packages/mermaid/src/diagrams/pie/pieTypes.ts | 1 - 2 files changed, 155 insertions(+), 167 deletions(-) diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index 7b686970e..e5696a793 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -1,191 +1,180 @@ -// @ts-nocheck - placeholder to be handled -import d3, { select, scaleOrdinal, pie as d3pie, arc } from 'd3'; +import d3, { scaleOrdinal, pie as d3pie, arc } from 'd3'; + import { log } from '../../logger.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; import { getConfig } from '../../config.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 { 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[] => { + // 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 = d3pie().value( + (d3Section: D3Sections): number => d3Section.value + ); + return pie(pieData); +}; /** * Draws a Pie Chart with the data given in text. * * @param text - pie chart code * @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) => { - try { - log.debug('rendering pie chart\n' + txt); - const db: PieDB = diagramObject.db as PieDB; - const globalConfig: MermaidConfig = getConfig(); - const config: Required = db.getConfig(); +export const draw: DrawDefinition = (text, id, _version, diagObj) => { + log.debug('rendering pie chart\n' + text); - const height = 450; - const { securityLevel } = globalConfig; - // handle root and document for when rendering in sandbox mode - 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 db: PieDB = diagObj.db as PieDB; + const globalConfig: MermaidConfig = getConfig(); + const pieConfig: Required = db.getConfig(); - const diagram = root.select('#' + id); - // TODO: use global `useMaxWidth` until making setConfig update pie setConfig - configureSvgSize(diagram, height, width, globalConfig?.pie?.useMaxWidth ?? true); + const height = 450; + // TODO: remove document width + 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 - elem?.setAttribute('viewBox', '0 0 ' + width + ' ' + height); + const MARGIN = 40; + const LEGEND_RECT_SIZE = 18; + const LEGEND_SPACING = 4; - const margin = 40; - const legendRectSize = 18; - const legendSpacing = 4; + const group: Group = svg.append('g'); + group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); - 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 - .append('g') - .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); + const radius: number = Math.min(width, height) / 2 - MARGIN; + // Shape helper to build arcs: + const arcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(0) + .outerRadius(radius); + const labelArcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(radius * textPosition) + .outerRadius(radius * textPosition); - const sections: Sections = db.getSections(); - let sum = 1; - Object.keys(sections).forEach((key: string): void => { - sum += sections[key]; + group + .append('circle') + .attr('cx', 0) + .attr('cy', 0) + .attr('r', radius + outerStrokeWidth / 2) + .attr('class', 'pieOuterCircle'); + + const sections: Sections = db.getSections(); + const arcs: d3.PieArcDatum[] = 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 = 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): 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; - 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, - ]; + legend + .append('rect') + .attr('width', LEGEND_RECT_SIZE) + .attr('height', LEGEND_RECT_SIZE) + .style('fill', color) + .style('stroke', color); - const textPosition: number = config.textPosition; - let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); - outerStrokeWidth ??= 2; - - // Set the color scale - const color: d3.ScaleOrdinal = scaleOrdinal().range(myGeneratedColors); - - // Compute the position of each group on the pie: - const pieData: D3Sections[] = Object.entries(sections) - .map((element: [string, number], index: number): D3Sections => { - return { - 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); - } + legend + .data(arcs) + .append('text') + .attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING) + .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING) + .text((datum: d3.PieArcDatum): string => { + const { label, value } = datum.data; + if (db.getShowData()) { + return `${label} [${value}]`; + } else { + return label; + } + }); }; export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/pie/pieTypes.ts b/packages/mermaid/src/diagrams/pie/pieTypes.ts index 97c18fe66..2206207f1 100644 --- a/packages/mermaid/src/diagrams/pie/pieTypes.ts +++ b/packages/mermaid/src/diagrams/pie/pieTypes.ts @@ -41,7 +41,6 @@ export interface PieStyleOptions { export type Sections = Record; export interface D3Sections { - order: number; label: string; value: number; }