WaffleChart
Parts of a whole as a 10×10 grid of squares, one per percent, with a legend of values and shares; hovering picks a part out.
Demo
| Part | Value | Share |
|---|---|---|
| Wind | 81.2 TWh | 33.7% |
| Gas | 62.5 TWh | 25.9% |
| Solar | 44.9 TWh | 18.6% |
| Nuclear | 31.7 TWh | 13.2% |
| Coal | 12.4 TWh | 5.1% |
| Hydro | 8.3 TWh | 3.4% |
| Part | Value | Share |
|---|---|---|
| Members | 2,380 | 47.6% |
| Public | 1,740 | 34.8% |
| Rest | 880 | 17.6% |
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/waffle_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/waffle_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/waffle_chart.qwe
let waffle_chart_demo_grid = "display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 32px 40px"view WaffleChartDemo() = <div style={waffle_chart_demo_grid}> <WaffleChart label="Electricity by source" show_label={true} suffix=" TWh" parts={[ { label: "Wind", value: 81.2 }, { label: "Gas", value: 62.5 }, { label: "Solar", value: 44.9 }, { label: "Nuclear", value: 31.7 }, { label: "Coal", value: 12.4 }, { label: "Hydro", value: 8.3 }]}/> <WaffleChart label="Seats sold" show_label={true} size={150} total={5000} parts={[ { label: "Members", value: 2380 }, { label: "Public", value: 1740 }]}/> </div>Source
ui/waffle_chart.qwe
// WaffleChart: parts of a whole as a 10 by 10 grid of squares, one square for each// percent, coloured part by part from the top left, with a legend of each part's// value and share. CSS only: hovering a square or a legend row picks its part out,// and screen readers get the parts as a table.//// <WaffleChart label="Energy mix" parts={[// { label: "Wind", value: 38 }, { label: "Solar", value: 21 }, { label: "Gas", value: 29 }, { label: "Coal", value: 12 }]}/>// <WaffleChart label="Seats sold" parts={[{ label: "Sold", value: 4120 }]} total={5000}/>//// parts: a list of { label, value } (values of 0 or less are left out); past eight,// the eighth and the rest are counted together as "Other". total: what the parts are// shares of, when they are not the whole (the squares left over stay empty);// otherwise the parts' sum. Each part gets its share of the 100 squares rounded so// that they add up (the largest remainders round up). legend: the list beside it// (true). prefix and suffix: around every value shown ("$", " GWh"); decimals: digits// after the point, always, instead of up to two as needed. size: the grid's width in// pixels (200; it shrinks to fit). label: what the chart shows; it names it for// screen readers and captions their table. show_label also writes it above.import ui/theme.qwe use ui_clslet waffle_chart_css = css`.ui-wafflec { --ui-wafflec-1: light-dark(#2a78d6, #3987e5); --ui-wafflec-2: light-dark(#eb6834, #d95926); --ui-wafflec-3: light-dark(#1baf7a, #199e70); --ui-wafflec-4: light-dark(#eda100, #c98500); --ui-wafflec-5: light-dark(#e87ba4, #d55181); --ui-wafflec-6: light-dark(#008300, #008300); --ui-wafflec-7: light-dark(#6250d6, #9085e9); --ui-wafflec-8: light-dark(#e34948, #e66767); display: flex; flex-wrap: wrap; align-items: center; gap: 16px 28px; min-width: 0; margin: 0; font: 13px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-wafflec-title { flex-basis: 100%; margin: 0; font-size: 14px; font-weight: 600; }.ui-wafflec-s1 { --c: var(--ui-wafflec-1); } .ui-wafflec-s2 { --c: var(--ui-wafflec-2); } .ui-wafflec-s3 { --c: var(--ui-wafflec-3); }.ui-wafflec-s4 { --c: var(--ui-wafflec-4); } .ui-wafflec-s5 { --c: var(--ui-wafflec-5); } .ui-wafflec-s6 { --c: var(--ui-wafflec-6); }.ui-wafflec-s7 { --c: var(--ui-wafflec-7); } .ui-wafflec-s8 { --c: var(--ui-wafflec-8); }.ui-wafflec-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 3px; flex: none; max-width: 100%; aspect-ratio: 1; }.ui-wafflec-cell { border-radius: 2px; background: var(--c, color-mix(in srgb, var(--ui-border) 75%, transparent)); transition: opacity .15s ease; }.ui-wafflec-legend { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: 6px 10px; align-items: center; flex: 1 1 200px; min-width: 0; max-width: 340px; margin: 0; padding: 0; list-style: none; }.ui-wafflec-legend li { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; transition: opacity .15s ease; }.ui-wafflec-key { width: 10px; height: 10px; border-radius: 2px; background: var(--c, color-mix(in srgb, var(--ui-border) 75%, transparent)); }.ui-wafflec-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-wafflec-val { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-wafflec-pct { min-width: 3.2em; color: var(--ui-muted); text-align: right; font-variant-numeric: tabular-nums; }.ui-wafflec:has(.ui-wafflec-p1:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p1),.ui-wafflec:has(.ui-wafflec-p2:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p2),.ui-wafflec:has(.ui-wafflec-p3:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p3),.ui-wafflec:has(.ui-wafflec-p4:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p4),.ui-wafflec:has(.ui-wafflec-p5:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p5),.ui-wafflec:has(.ui-wafflec-p6:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p6),.ui-wafflec:has(.ui-wafflec-p7:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p7),.ui-wafflec:has(.ui-wafflec-p8:hover) :is(.ui-wafflec-cell, .ui-wafflec-legend li):not(.ui-wafflec-p8) { opacity: .25; }@media (prefers-reduced-motion: reduce) { .ui-wafflec-cell, .ui-wafflec-legend li { transition: none; } }`// n (a whole number, 0 or more) with at least `width` digits.fn waffle_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 waffle_chart_group(n: int) -> str = n < 1000 ? str(n) : waffle_chart_group(floor(n / 1000)) + "," + waffle_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 waffle_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)}` : `.${waffle_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${waffle_chart_group(floor(units / scale))}${tail}`}// A share out of 100, to one decimal: "42%", "12.5%", "0.4%".fn waffle_chart_share(p: num) -> str { let tenths = round(p * 10) `${floor(tenths / 10)}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}%`}// Whole numbers of squares for each share (out of `whole`, summing to `whole` when// the shares do): each rounded down, then one more for the largest remainders.fn waffle_chart_squares(shares: list, whole: int) -> list { let floors = for s in shares => floor(s) let left = whole - sum(floors) let by_rest = sort_by(range(len(shares)), (i) => -(shares[i] - floors[i])) let extra = take(by_rest, left < 0 ? 0 : left) for i in range(len(shares)) => floors[i] + (any(extra, (k) => k == i) ? 1 : 0)}view WaffleChart(parts: list, total: num? = null, legend: bool = true, prefix: str = "", suffix: str = "", decimals: int? = null, size: int = 200, label: str = "Waffle chart", show_label: bool = false, class: str? = null) { style global waffle_chart_css let positive = parts | filter((p) => p.value != null && p.value > 0) let shown = len(positive) > 8 ? take(positive, 7) + [{ label: "Other", value: sum(drop(positive, 7) | map((p) => p.value)) }] : positive let sum_parts = sum(shown | map((p) => p.value)) let whole = total != null && total > sum_parts ? total : sum_parts let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${waffle_chart_num(abs(v), decimals)}${suffix}` let shares = for p in shown => whole > 0 ? p.value * 100 / whole : 0 let filled = whole > 0 ? round(sum_parts * 100 / whole) : 0 let counts = waffle_chart_squares(shares, filled) let starts = reduce(counts, [0], (acc, c) => acc + [acc[-1] + c]) let owner = (cell) => find(range(len(shown)), (i) => cell >= starts[i] && cell < starts[i + 1]) <figure class={ui_cls(["ui-wafflec", class])} aria-label={label}> {show_label && <figcaption.ui-wafflec-title>{label}</figcaption>} <div.ui-wafflec-grid style={`width: ${size}px`} aria-hidden="true"> {for cell in range(100) => { let i = owner(cell) <span class={ui_cls(["ui-wafflec-cell", i != null && `ui-wafflec-p${i + 1} ui-wafflec-s${i % 8 + 1}`])} title={i != null ? `${shown[i].label}: ${fmt(shown[i].value)} (${waffle_chart_share(shares[i])})` : null}></span> }} </div> {legend && <ul.ui-wafflec-legend aria-hidden="true"> {for [i, p] in enumerate(shown) => <li class={`ui-wafflec-p${i + 1} ui-wafflec-s${i % 8 + 1}`}> <span.ui-wafflec-key></span> <span.ui-wafflec-name>{p.label}</span> <span.ui-wafflec-val>{fmt(p.value)}</span> <span.ui-wafflec-pct>{waffle_chart_share(shares[i])}</span> </li>} {whole > sum_parts && <li> <span.ui-wafflec-key></span> <span.ui-wafflec-name>Rest</span> <span.ui-wafflec-val>{fmt(whole - sum_parts)}</span> <span.ui-wafflec-pct>{waffle_chart_share((whole - sum_parts) * 100 / whole)}</span> </li>} </ul>} <table.ui-sr> <caption>{label}</caption> <thead><tr><th scope="col">Part</th><th scope="col">Value</th><th scope="col">Share</th></tr></thead> <tbody> {for [i, p] in enumerate(shown) => <tr><th scope="row">{p.label}</th><td>{fmt(p.value)}</td><td>{waffle_chart_share(shares[i])}</td></tr>} {whole > sum_parts && <tr><th scope="row">Rest</th><td>{fmt(whole - sum_parts)}</td><td>{waffle_chart_share((whole - sum_parts) * 100 / whole)}</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 downloadsBarChartBars up or across, one series or several grouped or stacked, on round ticks, with values on hover and a table for screen readers.7 views · 7 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBoxPlotQuartiles, median, whiskers and outliers for each group, worked out from the raw values, with the five numbers on hover.5 views · 7 downloadsBrushChartA large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.6 views · 5 downloadsBubbleChartPoints placed by two numbers and sized by a third, in coloured groups, labelled when big enough, with all three numbers on hover.7 views · 8 downloads
Thread
Thread 0 posts
On WaffleChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.