Charts

HorizontalBarChart

Bars across with the labels down the side, sorted if asked, one picked out, values at their ends and on hover.

#chart#data#dashboard#no-js

Demo

Preview
Top pages by views
Top pages by views
Value
/pricing18,420
/docs/getting-started12,960
/blog/quillo-1-09,875
/components6,210
/about2,340
/careers1,180
Satisfaction by team
Satisfaction by team
Value
Sales78%
Support91%
Billing64%
Onboarding85%
Engineering72%
Net change in headcount
Net change in headcount
Value
Design3
Engineering12
Marketing-2
Sales5
Finance-1

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/horizontal_bar_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/horizontal_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/horizontal_bar_chart.qwe
let horizontal_bar_chart_demo_grid = "display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px 40px"view HorizontalBarChartDemo() =    <div style={horizontal_bar_chart_demo_grid}>        <HorizontalBarChart label="Top pages by views" show_label={true} sort="desc"                            labels={["/about", "/docs/getting-started", "/pricing", "/blog/quillo-1-0", "/components", "/careers"]}                            values={[2340, 12960, 18420, 9875, 6210, 1180]}/>        <HorizontalBarChart label="Satisfaction by team" show_label={true} suffix="%" highlight="Support" limit={100}                            labels={["Sales", "Support", "Billing", "Onboarding", "Engineering"]}                            values={[78, 91, 64, 85, 72]}/>        <HorizontalBarChart label="Net change in headcount" show_label={true} bar_height={12}                            labels={["Design", "Engineering", "Marketing", "Sales", "Finance"]}                            values={[3, 12, -2, 5, -1]}/>    </div>

Source

ui/horizontal_bar_chart.qwe
// HorizontalBarChart: a bar across for each label, the labels down the left side, so// long names and many categories stay readable; optionally sorted. CSS only:// hovering a row picks it out and shows its full label and value, and screen readers// get the numbers as a table.////   <HorizontalBarChart label="Top pages" labels={["/pricing", "/docs", "/blog"]} values={[18420, 12960, 9875]}/>//   <HorizontalBarChart label="Satisfaction by team" labels={teams} values={scores}//                       suffix="%" sort="desc" highlight="Support"/>//// labels: the categories; values: one number for each (a null counts as 0; below 0// a bar runs left of the zero line). sort: "desc" (largest at the top), "asc", or// "none" to keep the order given (the default). value_labels: each value at the end// of its bar (true). highlight: a label whose bar is drawn in a second colour, to pick// it out from the rest. limit: the axis' end, instead of the highest value rounded// up. prefix and suffix: around every value shown ("$", "%"; the axis takes the// prefix); decimals: digits after the point, always (2 for money), instead of up to// two as needed. bar_height: each bar's thickness in pixels (16). 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 horizontal_bar_chart_css = css`.ui-hbarc {  --ui-hbarc-1: light-dark(#2a78d6, #3987e5); --ui-hbarc-2: light-dark(#eb6834, #d95926);  display: grid; gap: 10px; align-content: start; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-hbarc-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-hbarc-s1 { --c: var(--ui-hbarc-1); } .ui-hbarc-s2 { --c: var(--ui-hbarc-2); }.ui-hbarc-body { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); column-gap: 10px; align-items: center; }.ui-hbarc-vals { padding-right: 52px; }.ui-hbarc-label { max-width: 16em; overflow: hidden; color: var(--ui-muted); font-size: 13px; text-align: right;  text-overflow: ellipsis; white-space: nowrap; }.ui-hbarc-hl .ui-hbarc-label { color: var(--ui-fg); font-weight: 600; }.ui-hbarc-grids { position: relative; grid-column: 2; align-self: stretch; }.ui-hbarc-grid { position: absolute; top: 0; bottom: 0; left: var(--a); width: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-hbarc-grid.ui-hbarc-zero { background: var(--ui-strong); }.ui-hbarc-row { display: contents; }.ui-hbarc-track { position: relative; grid-column: 2; height: calc(var(--h) + 10px); border-radius: 4px; }.ui-hbarc-row:hover .ui-hbarc-track { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-hbarc-row:hover .ui-hbarc-label { color: var(--ui-fg); }.ui-hbarc-bar { position: absolute; top: 5px; bottom: 5px; left: var(--a); width: var(--b); min-width: 1px;  background: var(--c); border-radius: 0 4px 4px 0; }.ui-hbarc-neg { border-radius: 4px 0 0 4px; }.ui-hbarc-val { position: absolute; top: 50%; left: calc(var(--a) + 6px); transform: translateY(-50%);  color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-hbarc-val.ui-hbarc-under { left: auto; right: calc(100% - var(--a) + 6px); }.ui-hbarc-tip { position: absolute; z-index: 20; bottom: calc(100% + 2px); right: 0; display: none; padding: 6px 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-muted); font-size: 12px; white-space: nowrap; pointer-events: none; }.ui-hbarc-tip b { padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-hbarc-row:hover .ui-hbarc-tip { display: block; }.ui-hbarc-x { position: relative; grid-column: 2; height: 16px; margin-top: 4px; }.ui-hbarc-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-hbarc-x span:first-child { transform: none; }.ui-hbarc-x span:last-child:not(:first-child) { transform: translateX(-100%); }`// n (a whole number, 0 or more) with at least `width` digits.fn horizontal_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 horizontal_bar_chart_group(n: int) -> str =    n < 1000 ? str(n) : horizontal_bar_chart_group(floor(n / 1000)) + "," + horizontal_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 horizontal_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)}` : `.${horizontal_bar_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${horizontal_bar_chart_group(floor(units / scale))}${tail}`}// An axis tick, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn horizontal_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 ? horizontal_bar_chart_num(v)        : `${v < 0 ? "-" : ""}${horizontal_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 horizontal_bar_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// The axis from lo to hi, widened to round ticks: about four steps of 1, 2, 2.5 or 5 x 10^n.fn horizontal_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 }}// The highest of a list of numbers.fn horizontal_bar_chart_top(xs: list) -> num = max(xs)view HorizontalBarChart(labels: list, values: list, sort: str = "none", value_labels: bool = true,                        highlight: str? = null, limit: num? = null, prefix: str = "", suffix: str = "",                        decimals: int? = null, bar_height: int = 16, label: str = "Horizontal bar chart",                        show_label: bool = false, class: str? = null) {    style global horizontal_bar_chart_css    let n = len(labels)    let at = (i) => values[i] ?? 0    let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${horizontal_bar_chart_num(abs(v), decimals)}${suffix}`    let order = sort == "desc" ? sort_by(range(n), (i) => -at(i))        : (sort == "asc" ? sort_by(range(n), (i) => at(i)) : range(n))    let all_values = [0] + (for i in range(n) => at(i))    let hi = horizontal_bar_chart_top(all_values)    let sc = horizontal_bar_chart_scale(min(all_values), limit != null && limit > hi ? limit : hi)    let pos = (v) => (v - sc.lo) * 100 / (sc.hi - sc.lo)    let zero = pos(0)    <figure class={ui_cls(["ui-hbarc", class])} aria-label={label}>        {show_label && <figcaption.ui-hbarc-title>{label}</figcaption>}        <div class={ui_cls(["ui-hbarc-body", value_labels && "ui-hbarc-vals"])} aria-hidden="true" style={`--h: ${bar_height}px`}>            <div.ui-hbarc-grids style={`grid-row: 1 / span ${n > 0 ? n : 1}`}>                {for t in sc.ticks => <span class={ui_cls(["ui-hbarc-grid", t == 0 && "ui-hbarc-zero"])}                                            style={`--a: ${horizontal_bar_chart_pct(pos(t))}%`}></span>}            </div>            {for [r, i] in enumerate(order) => {                let v = at(i)                let from = v < 0 ? pos(v) : zero                let to = v < 0 ? zero : pos(v)                let picked = highlight != null && str(labels[i]) == highlight                <div class={ui_cls(["ui-hbarc-row", picked && "ui-hbarc-hl"])}>                    <span.ui-hbarc-label style={`grid-row: ${r + 1}`} title={str(labels[i])}>{labels[i]}</span>                    <span.ui-hbarc-track style={`grid-row: ${r + 1}`}>                        {v != 0 && <span class={ui_cls(["ui-hbarc-bar", picked ? "ui-hbarc-s2" : "ui-hbarc-s1", v < 0 && "ui-hbarc-neg"])}                            style={`--a: ${horizontal_bar_chart_pct(from)}%; --b: ${horizontal_bar_chart_pct(to - from)}%`}                            data-value={str(v)}></span>}                        {value_labels && <span class={ui_cls(["ui-hbarc-val", v < 0 && "ui-hbarc-under"])}                            style={`--a: ${horizontal_bar_chart_pct(v < 0 ? from : to)}%`}>{fmt(v)}</span>}                        <span.ui-hbarc-tip>{labels[i]}<b>{fmt(v)}</b></span>                    </span>                </div>            }}            <div.ui-hbarc-x>                {for t in sc.ticks => <span style={`left: ${horizontal_bar_chart_pct(pos(t))}%`}>{t < 0 ? "-" : ""}{prefix}{horizontal_bar_chart_short(abs(t))}</span>}            </div>        </div>        <table.ui-sr>            <caption>{label}</caption>            <thead><tr><th scope="col"></th><th scope="col">Value</th></tr></thead>            <tbody>                {for i in order => <tr><th scope="row">{labels[i]}</th><td>{fmt(at(i))}</td></tr>}            </tbody>        </table>    </figure>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.