When labels are long, when the rank order is the headline, or when you have more than 8 categories. Sort longest-to-shortest — always.
5–10 categories with long names ("Cancer Care Clinic" doesn't fit under a vertical bar). The rank order tells the story, so sort descending and let the reader scan top-down. Default colors are rank-aware: top 2 in coral (warning), middle in blue (neutral), bottom 2 in teal (good) — flip the meaning by overriding per-row color if "high is good" instead.
Each fill grows width: 0 → (value / max) * 100% over 1s with a cubic-bezier ease. Stagger 100ms top-to-bottom. Numeric value fades in once the bar settles, on a 0.55s lag from the bar's transition start.
waitTimeByClinic: {
unit: 'm', // suffix per value
rows: [
{ label: 'Cancer Care Clinic', value: 28 },
{ label: 'Orthopaedics Clinic', value: 22 },
// chart sorts longest → shortest automatically
],
}
rows.sort((a, b) => b.value - a.value); const max = rows[0].value; rows.forEach((row, i) => { const w = (row.value / max) * 100; // % is fine here // width % works because the track has a definite // width (grid-template-columns: 220px 1fr 64px) fill.style.setProperty('--w', w + '%'); });
Width percentages work here even though height percentages failed in component 02 — because the parent track's width is definite (the grid template 1fr resolves to a known px value), while the bar-col's height in tile 2 is indefinite (auto from flex-end content sizing). The lesson: percentages on a child only work when the relevant parent dimension is known. When in doubt, set an explicit dimension on the parent or compute the value in px.