Charts

RadialBarChart

Bars bent into rings around a centre, each as long as its value against a limit, with a legend that picks a ring out on hover.

#chart#data#dashboard#no-js

Demo

Preview
Goals reached today
  • Steps82%
  • Exercise64%
  • Stand100%
  • Water45%
  • Sleep91%
Sales by region
  • Europe$48,200
  • Americas$39,700
  • Asia$27,150
  • Africa$8,400

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/radial_bar_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/radial_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/radial_bar_chart.qwe
view RadialBarChartDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 32px 40px">        <RadialBarChart label="Goals reached today" show_label={true} suffix="%" limit={100} size={210}                        items={[{ label: "Steps", value: 82 }, { label: "Exercise", value: 64 }, { label: "Stand", value: 100 },                                { label: "Water", value: 45 }, { label: "Sleep", value: 91 }]}/>        <RadialBarChart label="Sales by region" show_label={true} prefix="$" turn={360} size={200}                        items={[{ label: "Europe", value: 48200 }, { label: "Americas", value: 39700 },                                { label: "Asia", value: 27150 }, { label: "Africa", value: 8400 }]}/>    </div>

Source

ui/radial_bar_chart.qwe
// RadialBarChart: a bar per item bent into a ring around a centre, rings inside// rings, each as long around as its value, with a legend of the values beside it// (goals reached, scores, shares of a target). CSS only: hovering a ring or its line// in the legend picks it out; screen readers get the values as a list.////   <RadialBarChart label="Goals reached" suffix="%" limit={100} items={[//       { label: "Steps", value: 82 }, { label: "Sleep", value: 64 }, { label: "Water", value: 95 }]}/>//   <RadialBarChart label="Sales by region" prefix="$" items={regions} turn={360} size={200}/>//// items: a list of { label, value }, the first on the outside ring; values below 0// count as 0. limit: the value a whole track stands for (by default the highest// value, which then fills its track); a value past it fills the track. turn: how far// round a full track goes, in degrees (270; 360 for whole circles). Each ring has// its label at its start, at the top, when the tracks leave room for it (300 or less). size: its width in pixels (240; it shrinks to// fit). legend: the list beside it, with each value (true; false keeps it for// screen readers only). prefix and suffix: around every value shown ("$", "%");// decimals: digits after the point, always, instead of up to two as needed. label:// what it shows, for screen readers; show_label also writes it above. Up to eight// items are picked out on hover; past that the colours repeat.import ui/theme.qwe use ui_clslet radial_bar_chart_css = css`.ui-radial {  --ui-radial-1: light-dark(#2a78d6, #3987e5); --ui-radial-2: light-dark(#eb6834, #d95926);  --ui-radial-3: light-dark(#1baf7a, #199e70); --ui-radial-4: light-dark(#eda100, #c98500);  --ui-radial-5: light-dark(#e87ba4, #d55181); --ui-radial-6: light-dark(#008300, #008300);  --ui-radial-7: light-dark(#6250d6, #9085e9); --ui-radial-8: light-dark(#e34948, #e66767);  display: flex; flex-wrap: wrap; align-items: center; align-content: start; gap: 16px 28px; min-width: 0; margin: 0;  font: 13px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-radial-title { flex-basis: 100%; margin: 0; font-size: 14px; font-weight: 600; }.ui-radial-s1 { --c: var(--ui-radial-1); } .ui-radial-s2 { --c: var(--ui-radial-2); } .ui-radial-s3 { --c: var(--ui-radial-3); }.ui-radial-s4 { --c: var(--ui-radial-4); } .ui-radial-s5 { --c: var(--ui-radial-5); } .ui-radial-s6 { --c: var(--ui-radial-6); }.ui-radial-s7 { --c: var(--ui-radial-7); } .ui-radial-s8 { --c: var(--ui-radial-8); }.ui-radial-ring { flex: none; max-width: 100%; aspect-ratio: 1; }.ui-radial-ring svg { display: block; width: 100%; height: 100%; overflow: visible; }.ui-radial-track { fill: none; stroke: var(--ui-subtle); stroke-linecap: round; }.ui-radial-arc { fill: none; stroke: var(--c); stroke-linecap: round; transition: opacity .15s ease, stroke-width .15s ease; }.ui-radial-name { fill: var(--ui-muted); font-family: var(--ui-font); text-anchor: end; dominant-baseline: central; }.ui-radial-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: 340px; margin: 0; padding: 0; list-style: none; }.ui-radial-legend li { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; transition: opacity .15s ease; }.ui-radial-key { width: 10px; height: 10px; border-radius: 3px; background: var(--c); }.ui-radial-lname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-radial-val { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-radial:has(.ui-radial-p1:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p1),.ui-radial:has(.ui-radial-p2:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p2),.ui-radial:has(.ui-radial-p3:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p3),.ui-radial:has(.ui-radial-p4:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p4),.ui-radial:has(.ui-radial-p5:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p5),.ui-radial:has(.ui-radial-p6:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p6),.ui-radial:has(.ui-radial-p7:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p7),.ui-radial:has(.ui-radial-p8:hover) :is(.ui-radial-arc, .ui-radial-legend li):not(.ui-radial-p8) { opacity: .25; }@media (prefers-reduced-motion: reduce) { .ui-radial-arc, .ui-radial-legend li { transition: none; } }`// n (a whole number, 0 or more) with at least `width` digits.fn radial_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 radial_bar_chart_group(n: int) -> str =    n < 1000 ? str(n) : radial_bar_chart_group(floor(n / 1000)) + "," + radial_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 radial_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)}` : `.${radial_bar_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${radial_bar_chart_group(floor(units / scale))}${tail}`}// A coordinate or a percentage for CSS and SVG, to two decimals.fn radial_bar_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}view RadialBarChart(items: list, limit: num? = null, turn: num = 270, size: int = 240, legend: bool = true,                    prefix: str = "", suffix: str = "", decimals: int? = null, label: str = "Radial bar chart",                    show_label: bool = false, class: str? = null) {    style global radial_bar_chart_css    let n = len(items)    let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${radial_bar_chart_num(abs(v), decimals)}${suffix}`    let val = (it) => it.value != null && it.value > 0 ? it.value : 0    let top0 = max([0] + (items | map(val)))    let full = limit != null && limit > 0 ? limit : (top0 > 0 ? top0 : 1)    let sweep = (turn < 30 ? 30 : (turn > 360 ? 360 : turn)) / 360    let outer = 92    let hole = 22    let band = n == 0 ? 0 : (outer - hole) / n    let width = band * 0.7 > 16 ? 16 : band * 0.7    let r = (i) => outer - band * i - band / 2    let share = (it) => (val(it) > full ? 1 : val(it) / full) * sweep * 100    let text = band * 0.62 > 10 ? 10 : band * 0.62    <figure class={ui_cls(["ui-radial", class])} aria-label={label}>        {show_label && <figcaption.ui-radial-title>{label}</figcaption>}        <div.ui-radial-ring style={`width: ${size}px`} aria-hidden="true">            <svg viewBox="0 0 200 200">                <g transform="rotate(-90 100 100)">                    {for [i, it] in enumerate(items) => [                        <circle.ui-radial-track cx="100" cy="100" r={radial_bar_chart_pct(r(i))} pathLength="100"                            stroke-width={radial_bar_chart_pct(width)}                            stroke-dasharray={sweep >= 1 ? null : `${radial_bar_chart_pct(sweep * 100)} 100`}/>,                        val(it) > 0 && <circle class={`ui-radial-arc ui-radial-p${i + 1} ui-radial-s${i % 8 + 1}`} cx="100" cy="100"                            r={radial_bar_chart_pct(r(i))} pathLength="100" stroke-width={radial_bar_chart_pct(width)}                            stroke-dasharray={`${radial_bar_chart_pct(share(it))} 100`}>                            <title>{it.label}: {fmt(it.value ?? 0)}</title>                        </circle>                    ]}                </g>                {sweep <= 300 / 360 && (for [i, it] in enumerate(items) => <text.ui-radial-name x="94" y={radial_bar_chart_pct(100 - r(i))}                    style={`font-size: ${radial_bar_chart_pct(text)}px`}>{it.label}</text>)}            </svg>        </div>        <ul class={ui_cls(["ui-radial-legend", !legend && "ui-sr"])}>            {for [i, it] in enumerate(items) => <li class={`ui-radial-p${i + 1} ui-radial-s${i % 8 + 1}`}>                <span.ui-radial-key aria-hidden="true"></span>                <span.ui-radial-lname>{it.label}</span>                <span.ui-radial-val>{fmt(it.value ?? 0)}</span>            </li>}        </ul>    </figure>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.