mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-18 07:49:26 +02:00
Merge pull request #6651 from mermaid-js/6584-piechart-zero-negative-values
6584: prevent pie chart crash on zero or negative values
This commit is contained in:
9
.changeset/seven-papayas-film.md
Normal file
9
.changeset/seven-papayas-film.md
Normal file
@@ -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.
|
@@ -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
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
35
cypress/platform/darshan.html
Normal file
35
cypress/platform/darshan.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>Mermaid Quick Test Page</title>
|
||||||
|
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=" />
|
||||||
|
<style>
|
||||||
|
div.mermaid {
|
||||||
|
font-family: 'Courier New', Courier, monospace !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Pie chart demos</h1>
|
||||||
|
<pre class="mermaid">
|
||||||
|
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
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from '/mermaid.esm.mjs';
|
||||||
|
mermaid.initialize({
|
||||||
|
theme: 'forest',
|
||||||
|
logLevel: 3,
|
||||||
|
securityLevel: 'loose',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -37,6 +37,11 @@ Drawing a pie chart is really simple in mermaid.
|
|||||||
- Followed by `:` colon as separator
|
- Followed by `:` colon as separator
|
||||||
- Followed by `positive numeric value` (supported up to two decimal places)
|
- 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)
|
\[pie] \[showData] (OPTIONAL)
|
||||||
\[title] \[titlevalue] (OPTIONAL)
|
\[title] \[titlevalue] (OPTIONAL)
|
||||||
"\[datakey1]" : \[dataValue1]
|
"\[datakey1]" : \[dataValue1]
|
||||||
|
@@ -139,6 +139,32 @@ describe('pie', () => {
|
|||||||
}).rejects.toThrowError();
|
}).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 () => {
|
it('should handle unsafe properties', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
parser.parse(`pie title Unsafe props test
|
parser.parse(`pie title Unsafe props test
|
||||||
|
@@ -34,6 +34,11 @@ const clear = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addSection = ({ label, value }: D3Section): 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)) {
|
if (!sections.has(label)) {
|
||||||
sections.set(label, value);
|
sections.set(label, value);
|
||||||
log.debug(`added new section: ${label}, with value: ${value}`);
|
log.debug(`added new section: ${label}, with value: ${value}`);
|
||||||
|
@@ -10,20 +10,14 @@ import { cleanAndMerge, parseFontSize } from '../../utils.js';
|
|||||||
import type { D3Section, PieDB, Sections } from './pieTypes.js';
|
import type { D3Section, PieDB, Sections } from './pieTypes.js';
|
||||||
|
|
||||||
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {
|
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {
|
||||||
// Compute the position of each group on the pie:
|
const sum = [...sections.values()].reduce((acc, val) => acc + val, 0);
|
||||||
|
|
||||||
const pieData: D3Section[] = [...sections.entries()]
|
const pieData: D3Section[] = [...sections.entries()]
|
||||||
.map((element: [string, number]): D3Section => {
|
.map(([label, value]) => ({ label, value }))
|
||||||
return {
|
.filter((d) => (d.value / sum) * 100 >= 1) // Remove values < 1%
|
||||||
label: element[0],
|
.sort((a, b) => b.value - a.value);
|
||||||
value: element[1],
|
|
||||||
};
|
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value((d) => d.value);
|
||||||
})
|
|
||||||
.sort((a: D3Section, b: D3Section): number => {
|
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value(
|
|
||||||
(d3Section: D3Section): number => d3Section.value
|
|
||||||
);
|
|
||||||
return pie(pieData);
|
return pie(pieData);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,13 +83,21 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
|||||||
themeVariables.pie11,
|
themeVariables.pie11,
|
||||||
themeVariables.pie12,
|
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
|
// Set the color scale
|
||||||
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
|
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.
|
// Build the pie chart: each part of the pie is a path that we build using the arc function.
|
||||||
group
|
group
|
||||||
.selectAll('mySlices')
|
.selectAll('mySlices')
|
||||||
.data(arcs)
|
.data(filteredArcs)
|
||||||
.enter()
|
.enter()
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', arcGenerator)
|
.attr('d', arcGenerator)
|
||||||
@@ -104,15 +106,11 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
|||||||
})
|
})
|
||||||
.attr('class', 'pieCircle');
|
.attr('class', 'pieCircle');
|
||||||
|
|
||||||
let sum = 0;
|
|
||||||
sections.forEach((section) => {
|
|
||||||
sum += section;
|
|
||||||
});
|
|
||||||
// Now add the percentage.
|
// Now add the percentage.
|
||||||
// Use the centroid method to get the best coordinates.
|
// Use the centroid method to get the best coordinates.
|
||||||
group
|
group
|
||||||
.selectAll('mySlices')
|
.selectAll('mySlices')
|
||||||
.data(arcs)
|
.data(filteredArcs)
|
||||||
.enter()
|
.enter()
|
||||||
.append('text')
|
.append('text')
|
||||||
.text((datum: d3.PieArcDatum<D3Section>): string => {
|
.text((datum: d3.PieArcDatum<D3Section>): string => {
|
||||||
@@ -133,15 +131,20 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
|||||||
.attr('class', 'pieTitleText');
|
.attr('class', 'pieTitleText');
|
||||||
|
|
||||||
// Add the legends/annotations for each section
|
// Add the legends/annotations for each section
|
||||||
|
const allSectionData: D3Section[] = [...sections.entries()].map(([label, value]) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
|
||||||
const legend = group
|
const legend = group
|
||||||
.selectAll('.legend')
|
.selectAll('.legend')
|
||||||
.data(color.domain())
|
.data(allSectionData)
|
||||||
.enter()
|
.enter()
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', 'legend')
|
.attr('class', 'legend')
|
||||||
.attr('transform', (_datum, index: number): string => {
|
.attr('transform', (_datum, index: number): string => {
|
||||||
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
|
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 horizontal = 12 * LEGEND_RECT_SIZE;
|
||||||
const vertical = index * height - offset;
|
const vertical = index * height - offset;
|
||||||
return 'translate(' + horizontal + ',' + vertical + ')';
|
return 'translate(' + horizontal + ',' + vertical + ')';
|
||||||
@@ -151,20 +154,18 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
|
|||||||
.append('rect')
|
.append('rect')
|
||||||
.attr('width', LEGEND_RECT_SIZE)
|
.attr('width', LEGEND_RECT_SIZE)
|
||||||
.attr('height', LEGEND_RECT_SIZE)
|
.attr('height', LEGEND_RECT_SIZE)
|
||||||
.style('fill', color)
|
.style('fill', (d) => color(d.label))
|
||||||
.style('stroke', color);
|
.style('stroke', (d) => color(d.label));
|
||||||
|
|
||||||
legend
|
legend
|
||||||
.data(arcs)
|
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
|
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
|
||||||
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
|
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
|
||||||
.text((datum: d3.PieArcDatum<D3Section>): string => {
|
.text((d) => {
|
||||||
const { label, value } = datum.data;
|
|
||||||
if (db.getShowData()) {
|
if (db.getShowData()) {
|
||||||
return `${label} [${value}]`;
|
return `${d.label} [${d.value}]`;
|
||||||
}
|
}
|
||||||
return label;
|
return d.label;
|
||||||
});
|
});
|
||||||
|
|
||||||
const longestTextWidth = Math.max(
|
const longestTextWidth = Math.max(
|
||||||
|
@@ -24,6 +24,11 @@ Drawing a pie chart is really simple in mermaid.
|
|||||||
- Followed by `:` colon as separator
|
- Followed by `:` colon as separator
|
||||||
- Followed by `positive numeric value` (supported up to two decimal places)
|
- 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)
|
[pie] [showData] (OPTIONAL)
|
||||||
[title] [titlevalue] (OPTIONAL)
|
[title] [titlevalue] (OPTIONAL)
|
||||||
"[datakey1]" : [dataValue1]
|
"[datakey1]" : [dataValue1]
|
||||||
|
@@ -12,5 +12,9 @@ entry Pie:
|
|||||||
;
|
;
|
||||||
|
|
||||||
PieSection:
|
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;
|
Reference in New Issue
Block a user