From cd1a22f715948a9be44d8ee23a9f609e02b04b7f Mon Sep 17 00:00:00 2001 From: delchev Date: Mon, 7 Sep 2026 00:29:09 +0300 Subject: [PATCH] Chart: a pie/doughnut slice label is a share, not the raw value renderPie drew the slice's raw value with a percent sign appended, so a doughnut of one 336-hour slice read "336%" where the documented behaviour (and the already-computed `pct`) says "100%". Slices that happened to sum to 100 hid it, and so did the tests. Label with the share instead, one decimal below 10% so small-but-labelled slices stay distinguishable. Signed-off-by: delchev --- CHANGELOG.md | 6 +++++- src/components/chart.js | 7 +++++-- tests/components/chart.test.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddc5d60..f0fdbdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## v3.1.0 -A release that makes an interactive list valid HTML. An interactive item used to become a button itself, which left its `ul` with no list items and cost the list its announcement. The row control is now a real button or link inside the item, written with the new `x-h-list-item-button`. It also fixes a listbox and combobox bug where a list nested inside an option turned its own rows into options, and moves the listbox into a plugin of its own. It also repairs card and dialog padding around a slot behind an `x-if`, where the template Alpine leaves in place counted as the slot that never rendered, and lets a table header draw a top and bottom rule instead of a full outline. It ships the `mt-auto` utility class along with the half steps of the padding and gap scales. There are breaking changes to interactive lists and to registering the listbox by hand. +A release that makes an interactive list valid HTML. An interactive item used to become a button itself, which left its `ul` with no list items and cost the list its announcement. The row control is now a real button or link inside the item, written with the new `x-h-list-item-button`. It also fixes a listbox and combobox bug where a list nested inside an option turned its own rows into options, and moves the listbox into a plugin of its own. It also repairs card and dialog padding around a slot behind an `x-if`, where the template Alpine leaves in place counted as the slot that never rendered, and lets a table header draw a top and bottom rule instead of a full outline. A pie or doughnut slice label is a share of the total again, not the raw value with a percent sign after it. It ships the `mt-auto` utility class along with the half steps of the padding and gap scales. There are breaking changes to interactive lists and to registering the listbox by hand. ### List @@ -27,6 +27,10 @@ A release that makes an interactive list valid HTML. An interactive item used to - **New: the header border can be horizontal only.** `data-bordered="horizontal"` on `x-h-table-header` draws a rule above and below the head row and nothing down its sides, for a header separated from the rest of the table rather than boxed in. `data-bordered="true"` still draws the full outline. +### Chart + +- **Fixed: a pie or doughnut slice label printed the raw value with a percent sign.** A ring of one 336-hour slice read `336%` instead of `100%`. The label is now the slice's share of the total, as the option always documented, with one decimal below 10%. + ### New utility classes - **`mt-auto`** pushes an element to the bottom of a flex column, the way `ml-auto` and `mr-auto` push along a row. The auto margins previously covered only the horizontal sides. diff --git a/src/components/chart.js b/src/components/chart.js index 0d15174..ce2b1ea 100644 --- a/src/components/chart.js +++ b/src/components/chart.js @@ -541,7 +541,8 @@ function renderPie(root, cfg, ctx, doughnut = false) { ); svg.appendChild(wedge); - // Percentage label for the slice (skip slivers too small to fit one). + // Percentage label for the slice - its SHARE of the total, never its raw value, so a 336-hour + // sum in a one-slice ring reads "100%" and not "336%" (slivers too small to fit one are skipped). const pct = (s.value / total) * 100; if (showLabels && pct >= 5) { const angle = ((start + end) / 2) * 2 * Math.PI - Math.PI / 2; @@ -549,7 +550,9 @@ function renderPie(root, cfg, ctx, doughnut = false) { const offset = outsideLabels ? radius * 1.16 : insideOffset; const lx = cx + Math.cos(angle) * offset; const ly = cy + Math.sin(angle) * offset; - svg.appendChild(outsideLabels ? dataText(`${s.value}%`, lx, ly) : onColorText(`${s.value}%`, lx, ly)); + // One decimal below 10% keeps small-but-labelled slices distinguishable. + const text = `${pct < 10 ? pct.toFixed(1) : Math.round(pct)}%`; + svg.appendChild(outsideLabels ? dataText(text, lx, ly) : onColorText(text, lx, ly)); } }); diff --git a/tests/components/chart.test.js b/tests/components/chart.test.js index 3e90bf5..879c9df 100644 --- a/tests/components/chart.test.js +++ b/tests/components/chart.test.js @@ -251,6 +251,39 @@ describe('chart directives', () => { expect(Array.from(slot(el, 'chart-label')).map((l) => l.textContent)).toEqual(['30%', '70%']); }); + it('labels each slice with its share of the total, not its raw value', () => { + mount( + 'h-chart-pie', + { + slices: [ + { label: 'Billable', value: 336 }, + { label: 'Non-billable', value: 64 }, + ], + }, + el + ); + expect(Array.from(slot(el, 'chart-label')).map((l) => l.textContent)).toEqual(['84%', '16%']); + }); + + it('labels a single slice 100%', () => { + mount('h-chart-pie', { slices: [{ label: 'Billable', value: 336 }] }, el); + expect(Array.from(slot(el, 'chart-label')).map((l) => l.textContent)).toEqual(['100%']); + }); + + it('keeps one decimal on a labelled slice under 10%', () => { + mount( + 'h-chart-pie', + { + slices: [ + { label: 'A', value: 938 }, + { label: 'B', value: 62 }, + ], + }, + el + ); + expect(Array.from(slot(el, 'chart-label')).map((l) => l.textContent)).toEqual(['94%', '6.2%']); + }); + it('omits slice labels when dataLabels is false', () => { mount('h-chart-pie', { dataLabels: false, slices }, el); expect(slot(el, 'chart-label').length).toBe(0);