diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 885868c167..c27b9decd6 120000 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -./packages/mermaid/src/docs/community/contributing.md \ No newline at end of file +./packages/mermaid/src/docs/community/contributing.md diff --git a/demos/dev/example.html b/demos/dev/example.html index cc49ddffb7..b4b3b2ad1c 100644 --- a/demos/dev/example.html +++ b/demos/dev/example.html @@ -1,4 +1,4 @@ - +OB @@ -28,6 +28,14 @@ b --> d c --> d +

Pie Chart Example

+
+    pie title Types of industry trends in the last 12 months
+    "Technology" : 50
+    "Healthcare" : 25
+    "Retail" : 15
+    "Finance" : 10
+    

Type code to view diagram: diff --git a/package.json b/package.json index c4c692d859..2e1cb9baca 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "markdown-table": "^3.0.3", "nyc": "^15.1.0", "path-browserify": "^1.0.1", - "prettier": "^3.2.5", + "prettier": "^3.3.3", "prettier-plugin-jsdoc": "^1.3.0", "rimraf": "^5.0.5", "rollup-plugin-visualizer": "^5.12.0", diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts index 0f02efa0d6..e2751ca3a6 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v2.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v2.ts @@ -219,10 +219,18 @@ export const addRelations = function (relations: ClassRelation[], g: graphlib.Gr const conf = getConfig().flowchart; let cnt = 0; + // A set to keep track of rendered edges to avoid duplicates + const renderedEdges = new Set(); + relations.forEach(function (edge) { cnt++; + const isSelfReferencing = edge.id1 === edge.id2; // Check if the edge is self-referencing + + const edgeKey = `${edge.id1}->${edge.id2}`; // Unique key for each edge + + // Edge data setup const edgeData: EdgeData = { - //Set relationship style and line type + // Set relationship style and line type classes: 'relation', pattern: edge.relation.lineType == 1 ? 'dashed' : 'solid', id: getEdgeId(edge.id1, edge.id2, { @@ -231,18 +239,23 @@ export const addRelations = function (relations: ClassRelation[], g: graphlib.Gr }), // Set link type for rendering arrowhead: edge.type === 'arrow_open' ? 'none' : 'normal', - //Set edge extra labels + // Set edge extra labels startLabelRight: edge.relationTitle1 === 'none' ? '' : edge.relationTitle1, endLabelLeft: edge.relationTitle2 === 'none' ? '' : edge.relationTitle2, - //Set relation arrow types + // Set relation arrow types arrowTypeStart: getArrowMarker(edge.relation.type1), arrowTypeEnd: getArrowMarker(edge.relation.type2), style: 'fill:none', labelStyle: '', - curve: interpolateToCurve(conf?.curve, curveLinear), + curve: isSelfReferencing + ? curveLinear // Apply a specific curve for self-referencing relations + : interpolateToCurve(conf?.curve, curveLinear), }; - log.info(edgeData, edge); + // Style adjustments + if (!edgeData.style) { + edgeData.style = 'stroke: #999; fill: none;'; + } if (edge.style !== undefined) { const styles = getStylesFromArray(edge.style); @@ -259,7 +272,7 @@ export const addRelations = function (relations: ClassRelation[], g: graphlib.Gr edgeData.arrowheadStyle = 'fill: #333'; edgeData.labelpos = 'c'; - // TODO V10: Flowchart ? Keeping flowchart for backwards compatibility. Remove in next major release + // Handle HTML labels for flowchart compatibility if (getConfig().flowchart?.htmlLabels ?? getConfig().htmlLabels) { edgeData.labelType = 'html'; edgeData.label = '' + edge.text + ''; @@ -274,8 +287,22 @@ export const addRelations = function (relations: ClassRelation[], g: graphlib.Gr edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); } } - // Add the edge to the graph - g.setEdge(edge.id1, edge.id2, edgeData, cnt); + + // Add specific adjustments for self-referencing edges + if (isSelfReferencing) { + edgeData.points = [ + { x: 50, y: 100 }, // Starting point + { x: 70, y: 70 }, // Control point 1 + { x: 50, y: 40 }, // Control point 2 + ]; + edgeData.arrowheadStyle = 'fill: #555'; // Darker arrow for visibility + } + + // Check and render edge if it hasn't already been rendered + if (!renderedEdges.has(edgeKey)) { + g.setEdge(edge.id1, edge.id2, edgeData, cnt); + renderedEdges.add(edgeKey); // Mark this edge as rendered + } }); }; @@ -386,14 +413,15 @@ export const draw = async function (text: string, id: string, _version: string, * @param type - The type to look for * @returns The arrow marker */ -function getArrowMarker(type: number) { +function getArrowMarker(type: number | string) { let marker; switch (type) { case 0: - marker = 'aggregation'; + case 'none': // Ensure "none" is explicitly handled + marker = 'none'; break; case 1: - marker = 'extension'; + marker = 'aggregation'; break; case 2: marker = 'composition'; @@ -405,7 +433,7 @@ function getArrowMarker(type: number) { marker = 'lollipop'; break; default: - marker = 'none'; + marker = 'none'; // Fallback } return marker; } diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index a0cdce3df7..6b1bd3b268 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -10,41 +10,32 @@ import { cleanAndMerge, parseFontSize } from '../../utils.js'; import type { D3Section, PieDB, Sections } from './pieTypes.js'; const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { - // Compute the position of each group on the pie: const pieData: D3Section[] = [...sections.entries()] - .map((element: [string, number]): D3Section => { - return { + .map( + (element: [string, number]): D3Section => ({ label: element[0], value: element[1], - }; - }) - .sort((a: D3Section, b: D3Section): number => { - return b.value - a.value; - }); + }) + ) + .sort((a: D3Section, b: D3Section): number => b.value - a.value); const pie: d3.Pie = d3pie().value( (d3Section: D3Section): 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 = (text, id, _version, diagObj) => { - log.debug('rendering pie chart\n' + text); + log.debug('Rendering pie chart\n' + text); const db = diagObj.db as PieDB; const globalConfig: MermaidConfig = getConfig(); const pieConfig: Required = cleanAndMerge(db.getConfig(), globalConfig.pie); + const MARGIN = 40; const LEGEND_RECT_SIZE = 18; const LEGEND_SPACING = 4; const height = 450; const pieWidth: number = height; + const svg: SVG = selectSvgElement(id); const group: SVGGroup = svg.append('g'); group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')'); @@ -55,7 +46,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { const textPosition: number = pieConfig.textPosition; const radius: number = Math.min(pieWidth, height) / 2 - MARGIN; - // Shape helper to build arcs: + const arcGenerator: d3.Arc> = arc>() .innerRadius(0) .outerRadius(radius); @@ -89,50 +80,63 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { 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: d3.PieArcDatum) => { - return color(datum.data.label); - }) + .attr('fill', (datum: d3.PieArcDatum) => color(datum.data.label)) .attr('class', 'pieCircle'); let sum = 0; sections.forEach((section) => { sum += section; }); - // Now add the percentage. - // Use the centroid method to get the best coordinates. + group .selectAll('mySlices') .data(arcs) .enter() .append('text') .text((datum: d3.PieArcDatum): string => { - return ((datum.data.value / sum) * 100).toFixed(0) + '%'; + return `${((datum.data.value / sum) * 100).toFixed(0)}%`; }) .attr('transform', (datum: d3.PieArcDatum): string => { - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - return 'translate(' + labelArcGenerator.centroid(datum) + ')'; + const [x, y] = labelArcGenerator.centroid(datum); + return `translate(${x}, ${y})`; }) .style('text-anchor', 'middle') .attr('class', 'slice'); - group + const titleGroup = group.append('g'); + const titleText = db.getDiagramTitle(); + + // Adjust title font size dynamically + let fontSize = 25; // Start with a larger font size + const minFontSize = 8; // Set a minimum font size + const maxAvailableWidth = pieWidth - MARGIN; + + const titleElement = titleGroup .append('text') - .text(db.getDiagramTitle()) + .text(titleText) .attr('x', 0) .attr('y', -(height - 50) / 2) - .attr('class', 'pieTitleText'); + .attr('class', 'pieTitleText') + .style('text-anchor', 'middle'); + + // Reduce font size dynamically until it fits + while ( + titleElement.node()?.getBBox()?.width > maxAvailableWidth && + fontSize > minFontSize + ) { + fontSize -= 1; + titleElement.style('font-size', `${fontSize}px`); + } - // Add the legends/annotations for each section const legend = group .selectAll('.legend') .data(color.domain()) @@ -161,10 +165,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING) .text((datum: d3.PieArcDatum): string => { const { label, value } = datum.data; - if (db.getShowData()) { - return `${label} [${value}]`; - } - return label; + return db.getShowData() ? `${label} [${value}]` : label; }); const longestTextWidth = Math.max( @@ -176,7 +177,6 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { const totalWidth = pieWidth + MARGIN + LEGEND_RECT_SIZE + LEGEND_SPACING + longestTextWidth; - // Set viewBox svg.attr('viewBox', `0 0 ${totalWidth} ${height}`); configureSvgSize(svg, height, totalWidth, pieConfig.useMaxWidth); }; diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index 5bd1b1dfcf..9064208d92 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -12,8 +12,12 @@ vi.mock('dagre-d3'); // mermaidAPI.spec.ts: import * as accessibility from './accessibility.js'; // Import it this way so we can use spyOn(accessibility,...) vi.mock('./accessibility.js', () => ({ - setA11yDiagramInfo: vi.fn(), - addSVGa11yTitleDescription: vi.fn(), + setA11yDiagramInfo: vi.fn(() => { + return 'setA11yDiagramInfo called'; + }), + addSVGa11yTitleDescription: vi.fn(() => { + return 'addSVGa11yTitleDescription called'; + }), })); // Mock the renderers specifically so we can test render(). Need to mock draw() for each renderer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f3f4fa5f9..3146c0a42b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,7 +63,7 @@ importers: version: 4.2.4 '@vitest/coverage-v8': specifier: ^1.4.0 - version: 1.6.0(vitest@1.6.0(@types/node@20.16.11)(@vitest/ui@1.6.0)(jsdom@24.1.3)(terser@5.34.1)) + version: 1.6.0(vitest@1.6.0) '@vitest/spy': specifier: ^1.4.0 version: 1.6.0 @@ -179,7 +179,7 @@ importers: specifier: ^1.0.1 version: 1.0.1 prettier: - specifier: ^3.2.5 + specifier: ^3.3.3 version: 3.3.3 prettier-plugin-jsdoc: specifier: ^1.3.0 @@ -12610,7 +12610,7 @@ snapshots: vite: 5.4.8(@types/node@20.16.11)(terser@5.34.1) vue: 3.5.11(typescript@5.6.2) - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.16.11)(@vitest/ui@1.6.0)(jsdom@24.1.3)(terser@5.34.1))': + '@vitest/coverage-v8@1.6.0(vitest@1.6.0)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -12927,17 +12927,17 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0))(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0))': + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0)(webpack@5.95.0)': dependencies: webpack: 5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0) - '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0))': + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: envinfo: 7.14.0 webpack-cli: 4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0) - '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0))(webpack-dev-server@4.15.2(webpack-cli@4.10.0)(webpack@5.95.0))': + '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)(webpack-dev-server@4.15.2)': dependencies: webpack-cli: 4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0) optionalDependencies: @@ -18949,25 +18949,25 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0)): + terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.95.0(esbuild@0.21.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.34.1 - webpack: 5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0) + webpack: 5.95.0(esbuild@0.21.5) optionalDependencies: esbuild: 0.21.5 - terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.95.0(esbuild@0.21.5)): + terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.95.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.34.1 - webpack: 5.95.0(esbuild@0.21.5) + webpack: 5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0) optionalDependencies: esbuild: 0.21.5 @@ -19732,9 +19732,9 @@ snapshots: webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0))(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0)) - '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0)) - '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0(webpack-dev-server@4.15.2)(webpack@5.95.0))(webpack-dev-server@4.15.2(webpack-cli@4.10.0)(webpack@5.95.0)) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.95.0) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0) + '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0)(webpack-dev-server@4.15.2) colorette: 2.0.20 commander: 7.2.0 cross-spawn: 7.0.3 @@ -19747,7 +19747,7 @@ snapshots: optionalDependencies: webpack-dev-server: 4.15.2(webpack-cli@4.10.0)(webpack@5.95.0) - webpack-dev-middleware@5.3.4(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0)): + webpack-dev-middleware@5.3.4(webpack@5.95.0): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -19786,7 +19786,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 5.3.4(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0)) + webpack-dev-middleware: 5.3.4(webpack@5.95.0) ws: 8.18.0 optionalDependencies: webpack: 5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0) @@ -19859,7 +19859,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.21.5)(webpack@5.95.0(esbuild@0.21.5)(webpack-cli@4.10.0)) + terser-webpack-plugin: 5.3.10(esbuild@0.21.5)(webpack@5.95.0) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: