Charts

ParetoChart

Causes as bars sorted largest first with the running share as a line on a 0 to 100% axis, and the few that reach 80% picked out.

#chart#data#dashboard#no-js

Demo

Preview
Returns by reason
Returns by reason
ReturnsShareRunning share
Wrong size41250.7%50.7%
Damaged16019.7%70.4%
Not as shown9611.8%82.2%
Late749.1%91.3%
Changed mind384.7%95.9%
Other212.6%98.5%
Duplicate121.5%100%
Support hours by topic
Support hours by topic
HoursShareRunning share
Login36.5 h38.8%38.8%
Billing28 h29.8%68.6%
API14 h14.9%83.5%
Exports9.5 h10.1%93.6%
Mobile app6 h6.4%100%

Given in any order, drawn largest first; the strong bars reach the dashed line.

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

Example

The code of the preview above.

demos/pareto_chart.qwe
view ParetoChartDemo() =    <div.demo-stack>        <ParetoChart label="Returns by reason" show_label={true} value_name="Returns"                     labels={["Damaged", "Late", "Wrong size", "Not as shown", "Changed mind", "Duplicate", "Other"]}                     values={[160, 74, 412, 96, 38, 12, 21]}/>        <ParetoChart label="Support hours by topic" show_label={true} suffix=" h" threshold={70} value_name="Hours" height={180}                     labels={["Login", "Billing", "Exports", "API", "Mobile app"]} values={[36.5, 28, 9.5, 14, 6]}/>        <p.demo-note>Given in any order, drawn largest first; the strong bars reach the dashed line.</p>    </div>

Source

ui/pareto_chart.qwe
// ParetoChart: causes sorted from the largest, as bars on the left axis, and the// running share of the total as a line on a right axis of 0 to 100%, with a line at// 80%: the few bars before it account for most of the whole. CSS only: hovering a// bar shows its value, its share and the running share, and screen readers get the// numbers as a table.////   <ParetoChart label="Returns by reason" labels={["Size", "Damaged", "Late", "Other"]} values={[412, 160, 74, 30]}/>//   <ParetoChart label="Support time by topic" labels={topics} values={hours} suffix=" h" threshold={70}/>//// labels: the categories; values: one number for each (nulls and values below 0// count as 0). They are drawn largest first, whatever the order given. threshold: the// running share marked with a dashed line (80); the bars up to the one that crosses// it are drawn strong, the rest light. null leaves the line and the shading out.// prefix and suffix: around the values ("$", " h"; the left axis takes the prefix);// decimals: digits after the point, always, instead of up to two as needed.// value_name: what a value is called in the tooltip and table ("Count"). height: the// plot's height in pixels (240). 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 pareto_chart_css = css`.ui-paretoc {  --ui-paretoc-1: light-dark(#2a78d6, #3987e5); --ui-paretoc-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-paretoc-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-paretoc-s1 { --c: var(--ui-paretoc-1); } .ui-paretoc-s2 { --c: var(--ui-paretoc-2); }.ui-paretoc-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-paretoc-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-paretoc-key { width: 10px; height: 10px; border-radius: 3px; background: var(--c); flex: none; }.ui-paretoc-lkey { width: 14px; height: 3px; border-radius: 2px; }.ui-paretoc-tkey { width: 14px; height: 0; border-top: 1.5px dashed var(--ui-muted); }.ui-paretoc-frame { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: 8px; padding-top: 8px; }.ui-paretoc-y { position: relative; min-width: 24px; }.ui-paretoc-y span { position: absolute; transform: translateY(50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-paretoc-left span { right: 0; }.ui-paretoc-right span { left: 0; }.ui-paretoc-plot { position: relative; }.ui-paretoc-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-paretoc-grid.ui-paretoc-zero { background: var(--ui-strong); }.ui-paretoc-cut { position: absolute; left: 0; right: 0; border-top: 1.5px dashed var(--ui-muted); pointer-events: none; }.ui-paretoc-cut span { position: absolute; right: 4px; bottom: 3px; padding: 0 4px; border-radius: 4px; background: var(--ui-bg);  color: var(--ui-muted); font-size: 11px; font-weight: 600; }.ui-paretoc-cols { position: absolute; inset: 0; display: flex; }.ui-paretoc-col { position: relative; flex: 1; min-width: 0; border-radius: 4px; }.ui-paretoc-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-paretoc-bar { position: absolute; left: 50%; bottom: 0; width: min(44px, 70%); height: var(--b); min-height: 1px;  background: var(--ui-paretoc-1); border-radius: 4px 4px 0 0; transform: translateX(-50%); }.ui-paretoc-few { background: var(--ui-paretoc-1); }.ui-paretoc-many { background: color-mix(in srgb, var(--ui-paretoc-1) 38%, var(--ui-bg)); }.ui-paretoc-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-paretoc-line { fill: none; stroke: var(--ui-paretoc-2); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }.ui-paretoc-pt { position: absolute; width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%;  background: var(--ui-paretoc-2); box-shadow: 0 0 0 2px var(--ui-bg); pointer-events: none; }.ui-paretoc-tip { position: absolute; z-index: 20; top: 0; left: 50%; display: none; min-width: 140px; padding: 7px 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-fg); font-size: 12px; transform: translate(-50%, -100%); pointer-events: none; }.ui-paretoc-col:first-child .ui-paretoc-tip { left: 0; transform: translateY(-100%); }.ui-paretoc-col:last-child .ui-paretoc-tip { left: auto; right: 0; transform: translateY(-100%); }.ui-paretoc-col:hover .ui-paretoc-tip { display: block; }.ui-paretoc-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-paretoc-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-paretoc-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-paretoc-x { display: flex; margin-top: 6px; }.ui-paretoc-x span { flex: 1; min-width: 0; padding: 0 2px; overflow: hidden; color: var(--ui-muted); text-align: center;  text-overflow: ellipsis; white-space: nowrap; }`// n (a whole number, 0 or more) with at least `width` digits.fn pareto_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 pareto_chart_group(n: int) -> str =    n < 1000 ? str(n) : pareto_chart_group(floor(n / 1000)) + "," + pareto_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 pareto_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)}` : `.${pareto_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${pareto_chart_group(floor(units / scale))}${tail}`}// An axis tick, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn pareto_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 ? pareto_chart_num(v)        : `${v < 0 ? "-" : ""}${pareto_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 pareto_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// A share out of 100, to one decimal: "42%", "12.5%", "0.4%".fn pareto_chart_share(p: num) -> str {    let tenths = round(p * 10)    `${floor(tenths / 10)}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}%`}// The axis from lo to hi, widened to round ticks: about four steps of 1, 2, 2.5 or 5 x 10^n.fn pareto_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 }}view ParetoChart(labels: list, values: list, threshold: num? = 80, prefix: str = "", suffix: str = "",                 decimals: int? = null, value_name: str = "Count", height: int = 240,                 label: str = "Pareto chart", show_label: bool = false, class: str? = null) {    style global pareto_chart_css    let n = len(labels)    let at = (i) => values[i] != null && values[i] > 0 ? values[i] : 0    let order = sort_by(range(n), (i) => -at(i))    let vals = for i in order => at(i)    let total = sum(vals)    let cum = reduce(vals, [], (acc, v) => acc + [(len(acc) == 0 ? 0 : acc[-1]) + v])    let cpct = for c in cum => total > 0 ? c * 100 / total : 0    // The first bar whose running share reaches the threshold: it and those before are the few.    let cut = threshold == null ? n : (find(range(n), (k) => cpct[k] >= threshold - 0.000001) ?? n - 1)    let fmt = (v) => `${prefix}${pareto_chart_num(v, decimals)}${suffix}`    let sc = pareto_chart_scale(0, n == 0 ? 0 : vals[0])    let cx = (k) => pareto_chart_pct((k + 0.5) * 100 / n)    <figure class={ui_cls(["ui-paretoc", class])} aria-label={label}>        {show_label && <figcaption.ui-paretoc-title>{label}</figcaption>}        <ul.ui-paretoc-legend aria-hidden="true">            <li><span class="ui-paretoc-key ui-paretoc-s1"></span>{value_name}</li>            <li><span class="ui-paretoc-key ui-paretoc-lkey ui-paretoc-s2"></span>Running share</li>            {threshold != null && <li><span.ui-paretoc-tkey></span>{pareto_chart_share(threshold)}</li>}        </ul>        <div.ui-paretoc-frame aria-hidden="true">            <div class="ui-paretoc-y ui-paretoc-left" style={`height: ${height}px`}>                {for t in sc.ticks => <span style={`bottom: ${pareto_chart_pct(t * 100 / sc.hi)}%`}>{prefix}{pareto_chart_short(t)}</span>}            </div>            <div.ui-paretoc-plot style={`height: ${height}px`}>                {for t in sc.ticks => <span class={ui_cls(["ui-paretoc-grid", t == 0 && "ui-paretoc-zero"])}                                            style={`bottom: ${pareto_chart_pct(t * 100 / sc.hi)}%`}></span>}                <div.ui-paretoc-cols>                    {for [k, i] in enumerate(order) => <div.ui-paretoc-col>                        {vals[k] > 0 && <span class={ui_cls(["ui-paretoc-bar", threshold != null && (k <= cut ? "ui-paretoc-few" : "ui-paretoc-many")])}                            style={`--b: ${pareto_chart_pct(vals[k] * 100 / sc.hi)}%`}></span>}                        <span.ui-paretoc-tip>                            <strong>{labels[i]}</strong>                            <p>{value_name}<b>{fmt(vals[k])}</b></p>                            <p>Share<b>{pareto_chart_share(total > 0 ? vals[k] * 100 / total : 0)}</b></p>                            <p>Running share<b>{pareto_chart_share(cpct[k])}</b></p>                        </span>                    </div>}                </div>                {threshold != null && <span.ui-paretoc-cut style={`bottom: ${pareto_chart_pct(threshold)}%`}>                    <span>{pareto_chart_share(threshold)}</span>                </span>}                <svg.ui-paretoc-svg viewBox="0 0 100 100" preserveAspectRatio="none">                    {n > 0 && <path.ui-paretoc-line vector-effect="non-scaling-stroke"                        d={"M" + join(for k in range(n) => `${cx(k)},${pareto_chart_pct(100 - cpct[k])}`, " L")}/>}                </svg>                {for k in range(n) => <span.ui-paretoc-pt style={`left: ${cx(k)}%; top: ${pareto_chart_pct(100 - cpct[k])}%`}></span>}            </div>            <div class="ui-paretoc-y ui-paretoc-right" style={`height: ${height}px`}>                {for t in [0, 25, 50, 75, 100] => <span style={`bottom: ${t}%`}>{t}%</span>}            </div>            <span></span>            <div.ui-paretoc-x>{for i in order => <span title={str(labels[i])}>{labels[i]}</span>}</div>        </div>        <table.ui-sr>            <caption>{label}</caption>            <thead><tr><th scope="col"></th><th scope="col">{value_name}</th><th scope="col">Share</th><th scope="col">Running share</th></tr></thead>            <tbody>                {for [k, i] in enumerate(order) => <tr>                    <th scope="row">{labels[i]}</th>                    <td>{fmt(vals[k])}</td>                    <td>{pareto_chart_share(total > 0 ? vals[k] * 100 / total : 0)}</td>                    <td>{pareto_chart_share(cpct[k])}</td>                </tr>}            </tbody>        </table>    </figure>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.