Charts

PolarAreaChart

Slices of equal angle whose length shows each value on rings of round ticks; hovering a slice or its legend picks it out.

#chart#data#no-js

Demo

Preview
Orders by weekday
Orders by weekday
Value
Mon132
Tue158
Wed141
Thu187
Fri224
Sat96
Sun78
Incidents by kind
Incidents by kind
Value
Outage4
Slow11
Data6
Security2
Billing8

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/polar_area_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/polar_area_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/polar_area_chart.qwe
let polar_area_chart_demo_grid = "display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 32px 40px"view PolarAreaChartDemo() =    <div style={polar_area_chart_demo_grid}>        <PolarAreaChart label="Orders by weekday" show_label={true} parts={[            { label: "Mon", value: 132 }, { label: "Tue", value: 158 }, { label: "Wed", value: 141 }, { label: "Thu", value: 187 },            { label: "Fri", value: 224 }, { label: "Sat", value: 96 }, { label: "Sun", value: 78 }]}/>        <PolarAreaChart label="Incidents by kind" show_label={true} labels={false} legend={true} size={220} parts={[            { label: "Outage", value: 4 }, { label: "Slow", value: 11 }, { label: "Data", value: 6 },            { label: "Security", value: 2 }, { label: "Billing", value: 8 }]}/>    </div>

Source

ui/polar_area_chart.qwe
// PolarAreaChart: parts as slices of equal angle round a centre, each reaching out// as far as its value on rings of round ticks: like a pie, but the slice's length,// not its angle, carries the number, so parts that are not shares of a whole (sales// by month, incidents by kind) can be compared. CSS only: hovering a slice or its// legend row picks it out, and screen readers get the numbers as a table.////   <PolarAreaChart label="Orders by weekday" parts={[//       { label: "Mon", value: 132 }, { label: "Tue", value: 158 }, { label: "Wed", value: 141 }]}/>//// parts: a list of { label, value }, clockwise from the top (a null or a value below// 0 counts as 0); past eight, the eighth and the rest are counted together as// "Other". limit: the value at the rim, instead of the highest value rounded up.// labels: each slice's name outside the rim (true). legend: a list beside it with// each part's value (false). prefix and suffix: around every value shown;// decimals: digits after the point, always, instead of up to two as needed. size:// the chart's width in pixels (320; 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 polar_area_chart_css = css`.ui-polarc {  --ui-polarc-1: light-dark(#2a78d6, #3987e5); --ui-polarc-2: light-dark(#eb6834, #d95926);  --ui-polarc-3: light-dark(#1baf7a, #199e70); --ui-polarc-4: light-dark(#eda100, #c98500);  --ui-polarc-5: light-dark(#e87ba4, #d55181); --ui-polarc-6: light-dark(#008300, #008300);  --ui-polarc-7: light-dark(#6250d6, #9085e9); --ui-polarc-8: light-dark(#e34948, #e66767);  --ui-polarc-9: var(--ui-strong);  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 16px 28px; min-width: 0; margin: 0;  font: 13px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-polarc-title { flex-basis: 100%; margin: 0; font-size: 14px; font-weight: 600; }.ui-polarc-s1 { --c: var(--ui-polarc-1); } .ui-polarc-s2 { --c: var(--ui-polarc-2); } .ui-polarc-s3 { --c: var(--ui-polarc-3); }.ui-polarc-s4 { --c: var(--ui-polarc-4); } .ui-polarc-s5 { --c: var(--ui-polarc-5); } .ui-polarc-s6 { --c: var(--ui-polarc-6); }.ui-polarc-s7 { --c: var(--ui-polarc-7); } .ui-polarc-s8 { --c: var(--ui-polarc-8); }.ui-polarc-s9 { --c: var(--ui-polarc-9); }.ui-polarc-svg { display: block; max-width: 100%; height: auto; overflow: visible; }.ui-polarc-ring { fill: none; stroke: color-mix(in srgb, var(--ui-border) 80%, transparent); stroke-width: 1; }.ui-polarc-rim { stroke: var(--ui-strong); }.ui-polarc-slice { fill: var(--c); fill-opacity: .8; stroke: var(--ui-bg); stroke-width: 1.5; stroke-linejoin: round; }.ui-polarc-part { transition: opacity .15s ease; }.ui-polarc-part:hover .ui-polarc-slice { fill-opacity: 1; }.ui-polarc-name { fill: var(--ui-fg); font-size: 12px; }.ui-polarc-tick { fill: var(--ui-muted); font-size: 10px; font-variant-numeric: tabular-nums;  paint-order: stroke; stroke: var(--ui-bg); stroke-width: 3px; stroke-linejoin: round; pointer-events: none; }.ui-polarc-legend { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 6px 10px; align-items: center;  flex: 1 1 180px; min-width: 0; max-width: 300px; margin: 0; padding: 0; list-style: none; }.ui-polarc-legend li { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; transition: opacity .15s ease; }.ui-polarc-key { width: 10px; height: 10px; border-radius: 3px; background: var(--c); }.ui-polarc-lname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-polarc-val { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-polarc:has(.ui-polarc-p1:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p1),.ui-polarc:has(.ui-polarc-p2:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p2),.ui-polarc:has(.ui-polarc-p3:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p3),.ui-polarc:has(.ui-polarc-p4:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p4),.ui-polarc:has(.ui-polarc-p5:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p5),.ui-polarc:has(.ui-polarc-p6:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p6),.ui-polarc:has(.ui-polarc-p7:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p7),.ui-polarc:has(.ui-polarc-p8:hover) :is(.ui-polarc-part, .ui-polarc-legend li):not(.ui-polarc-p8) { opacity: .3; }@media (prefers-reduced-motion: reduce) { .ui-polarc-part, .ui-polarc-legend li { transition: none; } }`// n (a whole number, 0 or more) with at least `width` digits.fn polar_area_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 polar_area_chart_group(n: int) -> str =    n < 1000 ? str(n) : polar_area_chart_group(floor(n / 1000)) + "," + polar_area_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 polar_area_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)}` : `.${polar_area_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${polar_area_chart_group(floor(units / scale))}${tail}`}// An axis tick, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn polar_area_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 ? polar_area_chart_num(v)        : `${v < 0 ? "-" : ""}${polar_area_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// Rings of round values (1, 2, 2.5 or 5 x 10^n apart) out to just past `top`: of four// to six rings, the spacing whose outer ring comes closest to it.fn polar_area_chart_rings(top: num) -> list {    let tries = for k in [4, 5, 6] => {        let raw = top / k        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 ring_count = ceil(top / step - 0.000001)        for i in range(1, (ring_count < 1 ? 1 : ring_count) + 1) => i * step    }    sort_by(tries, (t) => t[-1])[0]}// A coordinate for SVG, to two decimals.fn polar_area_chart_len(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// The point at `turn` (0 to 1, clockwise from the top) on a circle of radius r.fn polar_area_chart_at(turn: num, r: num) -> list = [r * math.sin(turn * 2 * math.pi), -r * math.cos(turn * 2 * math.pi)]view PolarAreaChart(parts: list, limit: num? = null, labels: bool = true, legend: bool = false,                    prefix: str = "", suffix: str = "", decimals: int? = null, size: int = 320,                    label: str = "Polar area chart", show_label: bool = false, class: str? = null) {    style global polar_area_chart_css    let val = (p) => p.value != null && p.value > 0 ? p.value : 0    let shown = len(parts) > 8        ? take(parts, 7) + [{ label: "Other", value: sum(drop(parts, 7) | map(val)), other: true }]        : parts    let n = len(shown)    let fmt = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${polar_area_chart_num(abs(v), decimals)}${suffix}`    let top0 = max([0] + (shown | map(val)))    let rings0 = polar_area_chart_rings(limit != null && limit > 0 ? limit : (top0 == 0 ? 1 : top0))    let outer = limit != null && limit > 0 ? limit : rings0[-1]    let rings = rings0 | filter((t) => t <= outer)    let rim = 100    let r_of = (v) => rim * (v > outer ? outer : v) / outer    let color = (i, p) => p.other == true ? 9 : i % 8 + 1    let wedge = (i, r) => {        let [x1, y1] = polar_area_chart_at(i / n, r)        let [x2, y2] = polar_area_chart_at((i + 1) / n, r)        n == 1 ? `M0,${polar_area_chart_len(-r)} A${polar_area_chart_len(r)},${polar_area_chart_len(r)} 0 1 1 0,${polar_area_chart_len(r)} A${polar_area_chart_len(r)},${polar_area_chart_len(r)} 0 1 1 0,${polar_area_chart_len(-r)} Z`            : `M0,0 L${polar_area_chart_len(x1)},${polar_area_chart_len(y1)} A${polar_area_chart_len(r)},${polar_area_chart_len(r)} 0 ${n == 2 ? 1 : 0} 1 ${polar_area_chart_len(x2)},${polar_area_chart_len(y2)} Z`    }    let side = labels ? 70 : 6    let tall = labels ? 22 : 6    <figure class={ui_cls(["ui-polarc", class])} aria-label={label}>        {show_label && <figcaption.ui-polarc-title>{label}</figcaption>}        <svg.ui-polarc-svg width={size} height={polar_area_chart_len(size * (rim + tall) / (rim + side))} aria-hidden="true"             viewBox={`${-rim - side} ${-rim - tall} ${2 * (rim + side)} ${2 * (rim + tall)}`}>            {for [i, p] in enumerate(shown) => <g class={`ui-polarc-part ui-polarc-p${i + 1} ui-polarc-s${color(i, p)}`}>                {val(p) > 0 && <path.ui-polarc-slice d={wedge(i, r_of(val(p)))}>                    <title>{p.label}: {fmt(p.value)}</title>                </path>}                {labels && {                    let [x, y] = polar_area_chart_at((i + 0.5) / n, rim + 10)                    <text.ui-polarc-name x={polar_area_chart_len(x)} y={polar_area_chart_len(y)}                          dy={y < -rim * 0.9 ? "-0.1em" : (y > rim * 0.9 ? "0.8em" : "0.32em")}                          text-anchor={x > 5 ? "start" : (x < -5 ? "end" : "middle")}>{p.label}</text>                }}            </g>}            {for t in rings => <circle class={ui_cls(["ui-polarc-ring", t == rings[-1] && "ui-polarc-rim"])} cx="0" cy="0" r={polar_area_chart_len(r_of(t))}/>}            {for t in rings => <text.ui-polarc-tick x="3" y={polar_area_chart_len(-r_of(t))} dy="1em">{prefix}{polar_area_chart_short(t)}</text>}        </svg>        {legend && <ul.ui-polarc-legend aria-hidden="true">            {for [i, p] in enumerate(shown) => <li class={`ui-polarc-p${i + 1} ui-polarc-s${color(i, p)}`}>                <span.ui-polarc-key></span><span.ui-polarc-lname>{p.label}</span><span.ui-polarc-val>{fmt(p.value)}</span>            </li>}        </ul>}        <table.ui-sr>            <caption>{label}</caption>            <thead><tr><th scope="col"></th><th scope="col">Value</th></tr></thead>            <tbody>{for p in shown => <tr><th scope="row">{p.label}</th><td>{fmt(p.value)}</td></tr>}</tbody>        </table>    </figure>}
Thread
69Useful? Like it, or add to its thread below.

Thread 0 posts

On PolarAreaChart: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.