diff --git a/.changeset/seven-papayas-film.md b/.changeset/seven-papayas-film.md new file mode 100644 index 000000000..109743117 --- /dev/null +++ b/.changeset/seven-papayas-film.md @@ -0,0 +1,9 @@ +--- +'mermaid': patch +--- + +Add validation for negative values in pie charts: + +Prevents crashes during parsing by validating values post-parsing. + +Provides clearer, user-friendly error messages for invalid negative inputs. diff --git a/cypress/integration/rendering/pie.spec.ts b/cypress/integration/rendering/pie.spec.ts index 171a83057..8f6ef7de3 100644 --- a/cypress/integration/rendering/pie.spec.ts +++ b/cypress/integration/rendering/pie.spec.ts @@ -82,4 +82,13 @@ describe('pie chart', () => { ` ); }); + it('should render pie slices only for non-zero values but shows all legends', () => { + imgSnapshotTest( + ` pie title Pets adopted by volunteers + "Dogs" : 386 + "Cats" : 85 + "Rats" : 1 + ` + ); + }); }); diff --git a/cypress/platform/darshan.html b/cypress/platform/darshan.html new file mode 100644 index 000000000..df5994d7d --- /dev/null +++ b/cypress/platform/darshan.html @@ -0,0 +1,35 @@ + + + + + + Mermaid Quick Test Page + + + + + +

Pie chart demos

+
+     pie title Default text position: Animal adoption
+        accTitle: simple pie char demo
+        accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
+         "dogs" : -60.67
+        "rats" : 40.12
+    
+ +
+ + + diff --git a/docs/syntax/pie.md b/docs/syntax/pie.md index b8f452b66..6a2eefb27 100644 --- a/docs/syntax/pie.md +++ b/docs/syntax/pie.md @@ -37,6 +37,11 @@ Drawing a pie chart is really simple in mermaid. - Followed by `:` colon as separator - Followed by `positive numeric value` (supported up to two decimal places) +**Note:** + +> Pie chart values must be **positive numbers greater than zero**.\ +> **Negative values are not allowed** and will result in an error. + \[pie] \[showData] (OPTIONAL) \[title] \[titlevalue] (OPTIONAL) "\[datakey1]" : \[dataValue1] diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts index f00906cc5..60fff01e1 100644 --- a/packages/mermaid/src/diagrams/pie/pie.spec.ts +++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts @@ -139,6 +139,32 @@ describe('pie', () => { }).rejects.toThrowError(); }); + it('should handle simple pie with zero slice value', async () => { + await parser.parse(`pie title Default text position: Animal adoption + accTitle: simple pie char demo + accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs. + "dogs" : 0 + "rats" : 40.12 + `); + + const sections = db.getSections(); + expect(sections.get('dogs')).toBe(0); + expect(sections.get('rats')).toBe(40.12); + }); + + it('should handle simple pie with negative slice value', async () => { + await expect(async () => { + await parser.parse(`pie title Default text position: Animal adoption + accTitle: simple pie char demo + accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs. + "dogs" : -60.67 + "rats" : 40.12 + `); + }).rejects.toThrowError( + '"dogs" has invalid value: -60.67. Negative values are not allowed in pie charts. All slice values must be >= 0.' + ); + }); + it('should handle unsafe properties', async () => { await expect( parser.parse(`pie title Unsafe props test diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts index 64831495c..083ee97d5 100644 --- a/packages/mermaid/src/diagrams/pie/pieDb.ts +++ b/packages/mermaid/src/diagrams/pie/pieDb.ts @@ -34,6 +34,11 @@ const clear = (): void => { }; const addSection = ({ label, value }: D3Section): void => { + if (value < 0) { + throw new Error( + `"${label}" has invalid value: ${value}. Negative values are not allowed in pie charts. All slice values must be >= 0.` + ); + } if (!sections.has(label)) { sections.set(label, value); log.debug(`added new section: ${label}, with value: ${value}`); diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts index a0cdce3df..5b87613ff 100644 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.ts +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -10,20 +10,14 @@ 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 sum = [...sections.values()].reduce((acc, val) => acc + val, 0); + const pieData: D3Section[] = [...sections.entries()] - .map((element: [string, number]): D3Section => { - return { - label: element[0], - value: element[1], - }; - }) - .sort((a: D3Section, b: D3Section): number => { - return b.value - a.value; - }); - const pie: d3.Pie = d3pie().value( - (d3Section: D3Section): number => d3Section.value - ); + .map(([label, value]) => ({ label, value })) + .filter((d) => (d.value / sum) * 100 >= 1) // Remove values < 1% + .sort((a, b) => b.value - a.value); + + const pie: d3.Pie = d3pie().value((d) => d.value); return pie(pieData); }; @@ -89,13 +83,21 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { themeVariables.pie11, themeVariables.pie12, ]; + let sum = 0; + sections.forEach((section) => { + sum += section; + }); + + // Filter out arcs that would render as 0% + const filteredArcs = arcs.filter((datum) => ((datum.data.value / sum) * 100).toFixed(0) !== '0'); + // 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) + .data(filteredArcs) .enter() .append('path') .attr('d', arcGenerator) @@ -104,15 +106,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { }) .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) + .data(filteredArcs) .enter() .append('text') .text((datum: d3.PieArcDatum): string => { @@ -133,15 +131,20 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { .attr('class', 'pieTitleText'); // Add the legends/annotations for each section + const allSectionData: D3Section[] = [...sections.entries()].map(([label, value]) => ({ + label, + value, + })); + const legend = group .selectAll('.legend') - .data(color.domain()) + .data(allSectionData) .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 offset = (height * allSectionData.length) / 2; const horizontal = 12 * LEGEND_RECT_SIZE; const vertical = index * height - offset; return 'translate(' + horizontal + ',' + vertical + ')'; @@ -151,20 +154,18 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => { .append('rect') .attr('width', LEGEND_RECT_SIZE) .attr('height', LEGEND_RECT_SIZE) - .style('fill', color) - .style('stroke', color); + .style('fill', (d) => color(d.label)) + .style('stroke', (d) => color(d.label)); 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; + .text((d) => { if (db.getShowData()) { - return `${label} [${value}]`; + return `${d.label} [${d.value}]`; } - return label; + return d.label; }); const longestTextWidth = Math.max( diff --git a/packages/mermaid/src/docs/syntax/pie.md b/packages/mermaid/src/docs/syntax/pie.md index 2e7a1799a..416119b5b 100644 --- a/packages/mermaid/src/docs/syntax/pie.md +++ b/packages/mermaid/src/docs/syntax/pie.md @@ -24,6 +24,11 @@ Drawing a pie chart is really simple in mermaid. - Followed by `:` colon as separator - Followed by `positive numeric value` (supported up to two decimal places) +**Note:** + +> Pie chart values must be **positive numbers greater than zero**. +> **Negative values are not allowed** and will result in an error. + [pie] [showData] (OPTIONAL) [title] [titlevalue] (OPTIONAL) "[datakey1]" : [dataValue1] diff --git a/packages/parser/src/language/pie/pie.langium b/packages/parser/src/language/pie/pie.langium index a80caa81f..f6802d718 100644 --- a/packages/parser/src/language/pie/pie.langium +++ b/packages/parser/src/language/pie/pie.langium @@ -12,5 +12,9 @@ entry Pie: ; PieSection: - label=STRING ":" value=NUMBER EOL + label=STRING ":" value=NUMBER_PIE EOL ; + +terminal FLOAT_PIE returns number: /-?[0-9]+\.[0-9]+(?!\.)/; +terminal INT_PIE returns number: /-?(0|[1-9][0-9]*)(?!\.)/; +terminal NUMBER_PIE returns number: FLOAT_PIE | INT_PIE; \ No newline at end of file