StackedBarChart
Bars of parts piled up to a total, or each scaled to 100% to compare shares, with the parts and total on hover.
Demo
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/stacked_bar_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/stacked_bar_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/stacked_bar_chart.qwe
let stacked_bar_chart_demo_grid = "display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px 40px"view StackedBarChartDemo() = <div style={stacked_bar_chart_demo_grid}> <StackedBarChart label="Revenue by channel" show_label={true} prefix="$" value_labels={true} height={200} labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]} series={[{ name: "Online", values: [42100, 45800, 51200, 49900, 56300, 61750] }, { name: "Stores", values: [30500, 29800, 31200, 28700, 27900, 30100] }, { name: "Partners", values: [8200, 9100, 7600, 10400, 11800, 12500] }]}/> <StackedBarChart label="Tickets by kind" show_label={true} percent={true} height={200} labels={["Support", "Billing", "Sales", "Ops"]} series={[{ name: "Question", values: [120, 45, 80, 12] }, { name: "Bug", values: [64, 8, 3, 40] }, { name: "Request", values: [30, 22, 41, 18] }]}/> </div>Source
ui/stacked_bar_chart.qwe
// StackedBarChart: one bar per label made of its parts, each series piled on the one// before, so the bar's height is the total; or, with percent, every bar full height// and each part its share of 100%. CSS only: hovering a bar lists its parts and the// total, and screen readers get the numbers as a table.//// <StackedBarChart label="Revenue by channel" labels={["Q1", "Q2", "Q3"]} prefix="$"// series={[{ name: "Online", values: [42000, 51000, 56000] }, { name: "Stores", values: [30500, 28000, 27100] }]}/>// <StackedBarChart label="Ticket mix" labels={teams} series={kinds} percent={true}/>//// labels: the categories, in order. series: a list of { name, values } (up to eight,// each in a colour of its own, with a legend), one value per label, stacked in that// order from the bottom; a null or a value below 0 counts as 0. percent: each bar// shows its parts as shares of its total (the axis runs 0% to 100%, and the tooltip// gives both the value and the share). value_labels: each bar's total above it (not// with percent). prefix and suffix: around every value shown ("$", " h"; the axis// takes the prefix); decimals: digits after the point, always (2 for money), instead// of up to two as needed. height: the plot's height in pixels (240). label: what the// chart shows; it names it for screen readers and captions their table. show_label// also writes it above the chart.import ui/theme.qwe use ui_clslet stacked_bar_chart_css = css`.ui-sbarc { --ui-sbarc-1: light-dark(#2a78d6, #3987e5); --ui-sbarc-2: light-dark(#eb6834, #d95926); --ui-sbarc-3: light-dark(#1baf7a, #199e70); --ui-sbarc-4: light-dark(#eda100, #c98500); --ui-sbarc-5: light-dark(#e87ba4, #d55181); --ui-sbarc-6: light-dark(#008300, #008300); --ui-sbarc-7: light-dark(#6250d6, #9085e9); --ui-sbarc-8: light-dark(#e34948, #e66767); display: grid; gap: 10px; align-content: start; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-sbarc-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-sbarc-s1 { --c: var(--ui-sbarc-1); } .ui-sbarc-s2 { --c: var(--ui-sbarc-2); } .ui-sbarc-s3 { --c: var(--ui-sbarc-3); }.ui-sbarc-s4 { --c: var(--ui-sbarc-4); } .ui-sbarc-s5 { --c: var(--ui-sbarc-5); } .ui-sbarc-s6 { --c: var(--ui-sbarc-6); }.ui-sbarc-s7 { --c: var(--ui-sbarc-7); } .ui-sbarc-s8 { --c: var(--ui-sbarc-8); }.ui-sbarc-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-sbarc-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-sbarc-key { width: 10px; height: 10px; border-radius: 3px; background: var(--c); flex: none; }.ui-sbarc-frame { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 8px; padding-top: 8px; }.ui-sbarc-y { position: relative; min-width: 24px; }.ui-sbarc-y span { position: absolute; right: 0; transform: translateY(50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-sbarc-plot { position: relative; }.ui-sbarc-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-sbarc-grid.ui-sbarc-zero { background: var(--ui-strong); }.ui-sbarc-cols { position: absolute; inset: 0; display: flex; }.ui-sbarc-col { position: relative; flex: 1; min-width: 0; border-radius: 4px; }.ui-sbarc-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-sbarc-bar { position: absolute; top: 0; bottom: 0; left: 50%; width: min(48px, 64%); transform: translateX(-50%); }.ui-sbarc-seg { position: absolute; left: 0; right: 0; bottom: var(--a); height: var(--b); min-height: 1px; box-sizing: border-box; background: var(--c); background-clip: padding-box; }.ui-sbarc-on { border-bottom: 2px solid transparent; }.ui-sbarc-top { border-radius: 4px 4px 0 0; }.ui-sbarc-val { position: absolute; left: 50%; bottom: calc(var(--a) + 3px); transform: translateX(-50%); color: var(--ui-muted); font-size: 11px; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-sbarc-x { display: flex; margin-top: 6px; }.ui-sbarc-x span { flex: 1; min-width: 0; padding: 0 2px; overflow: hidden; color: var(--ui-muted); text-align: center; text-overflow: ellipsis; white-space: nowrap; }.ui-sbarc-tip { position: absolute; z-index: 20; top: 0; left: 50%; display: none; min-width: 140px; padding: 7px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); box-shadow: var(--ui-shadow); color: var(--ui-fg); font-size: 12px; transform: translate(-50%, -100%); pointer-events: none; }.ui-sbarc-col:first-child .ui-sbarc-tip { left: 0; transform: translateY(-100%); }.ui-sbarc-col:last-child .ui-sbarc-tip { left: auto; right: 0; transform: translateY(-100%); }.ui-sbarc-col:hover .ui-sbarc-tip { display: block; }.ui-sbarc-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-sbarc-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-sbarc-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-sbarc-tip i { min-width: 3.4em; font-style: normal; text-align: right; font-variant-numeric: tabular-nums; }.ui-sbarc-tip .ui-sbarc-total { margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--ui-border); }`// n (a whole number, 0 or more) with at least `width` digits.fn stacked_bar_chart_pad(n: int, width: int) -> str { let s = str(n) len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn stacked_bar_chart_group(n: int) -> str = n < 1000 ? str(n) : stacked_bar_chart_group(floor(n / 1000)) + "," + stacked_bar_chart_pad(n % 1000, 3)// A value as it reads, with commas: `places` digits after the point, or up to two as// needed when that is null: 48210 is "48,210", 6350.5 is "6,350.5" ("6,350.50" with 2).// Worked out in whole numbers, so the server and the browser write the same text.fn stacked_bar_chart_num(v: num, places: int? = null) -> str { let digits = places == null ? 2 : (places < 0 ? 0 : (places > 4 ? 4 : places)) let scale = [1, 10, 100, 1000, 10000][digits] let units = round(abs(v) * scale) let frac = units % scale let tail = digits == 0 || (places == null && frac == 0) ? "" : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${stacked_bar_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${stacked_bar_chart_group(floor(units / scale))}${tail}`}// An axis tick, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn stacked_bar_chart_short(v: num) -> str { let a = abs(v) let unit = a >= 1000000000 ? 1000000000 : (a >= 1000000 ? 1000000 : (a >= 1000 ? 1000 : 1)) let tenths = round(a * 10 / unit) let mark = unit == 1000000000 ? "B" : (unit == 1000000 ? "M" : "k") unit == 1 ? stacked_bar_chart_num(v) : `${v < 0 ? "-" : ""}${stacked_bar_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A percentage for CSS (or a coordinate out of 100), to two decimals.fn stacked_bar_chart_pct(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// A share out of 100, to one decimal: "42%", "12.5%", "0.4%".fn stacked_bar_chart_share(p: num) -> str { let tenths = round(p * 10) `${floor(tenths / 10)}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}%`}// The axis from lo to hi, widened to round ticks: about four steps of 1, 2, 2.5 or 5 x 10^n.fn stacked_bar_chart_scale(lo: num, hi: num) -> map { let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo)) : hi - lo let raw = span / 4 let mag = 1 while mag * 10 <= raw { mag = mag * 10 } while mag > raw { mag = mag / 10 } let r = raw / mag let step = (r <= 1 ? 1 : (r <= 2 ? 2 : (r <= 2.5 ? 2.5 : (r <= 5 ? 5 : 10)))) * mag let a = floor(lo / step + 0.000001) let b0 = ceil(hi / step - 0.000001) let b = b0 == a ? a + 1 : b0 { lo: a * step, hi: b * step, ticks: for i in range(b - a + 1) => (a + i) * step }}view StackedBarChart(labels: list, series: list, percent: bool = false, value_labels: bool = false, prefix: str = "", suffix: str = "", decimals: int? = null, height: int = 240, label: str = "Stacked bar chart", show_label: bool = false, class: str? = null) { style global stacked_bar_chart_css let m = len(series) let n = len(labels) let part = (s, i) => s.values[i] != null && s.values[i] > 0 ? s.values[i] : 0 let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${stacked_bar_chart_num(abs(v), decimals)}${suffix}` // acc[i][j] is where series j starts in bar i, and acc[i][j + 1] where it ends. let acc = for i in range(n) => reduce(series, [0], (a, s) => a + [a[-1] + part(s, i)]) let totals = for i in range(n) => acc[i][m] let share = (v, i) => totals[i] > 0 ? v * 100 / totals[i] : 0 let sc = percent ? { lo: 0, hi: 100, ticks: [0, 25, 50, 75, 100] } : stacked_bar_chart_scale(0, n == 0 ? 0 : max(totals)) let pos = (v, i) => (percent ? share(v, i) : v) * 100 / sc.hi <figure class={ui_cls(["ui-sbarc", class])} aria-label={label}> {show_label && <figcaption.ui-sbarc-title>{label}</figcaption>} {m > 1 && <ul.ui-sbarc-legend aria-hidden="true"> {for [j, s] in enumerate(series) => <li><span class={`ui-sbarc-key ui-sbarc-s${j % 8 + 1}`}></span>{s.name}</li>} </ul>} <div.ui-sbarc-frame aria-hidden="true"> <div.ui-sbarc-y style={`height: ${height}px`}> {for t in sc.ticks => <span style={`bottom: ${stacked_bar_chart_pct(t * 100 / sc.hi)}%`}> {percent ? `${t}%` : `${prefix}${stacked_bar_chart_short(t)}`} </span>} </div> <div.ui-sbarc-plot style={`height: ${height}px`}> {for t in sc.ticks => <span class={ui_cls(["ui-sbarc-grid", t == 0 && "ui-sbarc-zero"])} style={`bottom: ${stacked_bar_chart_pct(t * 100 / sc.hi)}%`}></span>} <div.ui-sbarc-cols> {for i in range(n) => { let top_part = find(reverse(range(m)), (j) => part(series[j], i) > 0) <div.ui-sbarc-col> <span.ui-sbarc-bar> {for [j, s] in enumerate(series) => part(s, i) > 0 && <span class={ui_cls(["ui-sbarc-seg", `ui-sbarc-s${j % 8 + 1}`, acc[i][j] > 0 && "ui-sbarc-on", j == top_part && "ui-sbarc-top"])} style={`--a: ${stacked_bar_chart_pct(pos(acc[i][j], i))}%; --b: ${stacked_bar_chart_pct(pos(part(s, i), i))}%`} data-value={str(part(s, i))}></span>} {value_labels && !percent && <span.ui-sbarc-val style={`--a: ${stacked_bar_chart_pct(pos(totals[i], i))}%`}>{fmt(totals[i])}</span>} </span> <span.ui-sbarc-tip> <strong>{labels[i]}</strong> {for [j, s] in enumerate(reverse(series)) => <p> <span class={`ui-sbarc-key ui-sbarc-s${(m - 1 - j) % 8 + 1}`}></span> {s.name}<b>{fmt(part(s, i))}</b> {percent && <i>{stacked_bar_chart_share(share(part(s, i), i))}</i>} </p>} <p.ui-sbarc-total>Total<b>{fmt(totals[i])}</b>{percent && <i>100%</i>}</p> </span> </div> }} </div> </div> <span></span> <div.ui-sbarc-x>{for l in labels => <span title={str(l)}>{l}</span>}</div> </div> <table.ui-sr> <caption>{label}</caption> <thead><tr> <th scope="col"></th> {for s in series => <th scope="col">{s.name}</th>} <th scope="col">Total</th> </tr></thead> <tbody> {for i in range(n) => <tr> <th scope="row">{labels[i]}</th> {for s in series => <td>{fmt(part(s, i))}{percent ? ` (${stacked_bar_chart_share(share(part(s, i), i))})` : ""}</td>} <td>{fmt(totals[i])}</td> </tr>} </tbody> </table> </figure>}Related
AreaChartFilled areas for one or more series, overlapping or stacked into a total, with a hover crosshair listing every value.6 views · 8 downloadsBulletChartMeasures as thin bars against a target mark over grey bands from poor to good, a row each, with the band named on hover.5 views · 7 downloadsComboChartBars on a left axis and lines on a right axis over the same labels, for two measures of different size, with every value on hover.6 views · 6 downloadsFunnelChartStages of a process narrowing like a funnel, each with its count and share of the first, and the share that went on between them.5 views · 7 downloadsGaugeChartA half-circle dial of good, warning and bad bands with a needle at the value, and the value and its band's name under it.6 views · 7 downloadsGroupedBarChartA bar per series side by side for each label, below zero too, with the group's values on hover and the bar under the pointer picked out.6 views · 4 downloads
Thread
Thread 0 posts
On StackedBarChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.