HorizontalBarChart
Bars across with the labels down the side, sorted if asked, one picked out, values at their ends and 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/horizontal_bar_chart.qwemain.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>}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 HorizontalBarChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.