BoxPlot
Quartiles, median, whiskers and outliers for each group, worked out from the raw values, with the five numbers on hover.
Demo
| Values | Low | Q1 | Median | Q3 | High | Outliers | |
|---|---|---|---|---|---|---|---|
| Auth | 62 | 60 ms | 106 ms | 122 ms | 137 ms | 181 ms | 43 ms, 187 ms, 194 ms, 196 ms, 330 ms, 362 ms |
| Search | 60 | 94 ms | 162 ms | 216.5 ms | 256 ms | 321 ms | None |
| Checkout | 62 | 121 ms | 153.75 ms | 171 ms | 187.75 ms | 224 ms | 12 ms, 405 ms |
| Images | 60 | 35 ms | 81.5 ms | 96.5 ms | 115.25 ms | 162 ms | None |
| Reports | 61 | 166 ms | 236 ms | 272 ms | 314 ms | 378 ms | 530 ms |
| Values | Low | Q1 | Median | Q3 | High | Mean | Outliers | |
|---|---|---|---|---|---|---|---|---|
| Class A | 11 | 62 | 76 | 81 | 86.5 | 94 | 80.55 | None |
| Class B | 12 | 45 | 63.25 | 71 | 76 | 82 | 70.33 | 99 |
| Class C | 10 | 70 | 75.5 | 79 | 81.75 | 86 | 78.5 | None |
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/box_plot.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/box_plot.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/box_plot.qwe
// n numbers around `mid`, `spread` either side, from a seeded generator (the same on// every run): each the mean of two draws, so most fall near the middle.fn box_plot_demo_values(seed: int, n: int, mid: num, spread: num) -> list { let s = seed let out = [] while len(out) < n { let total = 0 for k in range(2) { s = (s * 1103515245 + 12345) % 2147483648 total += s / 2147483648 } out = push(out, round(mid + (total / 2 - 0.5) * 2 * spread)) } out}view BoxPlotDemo() = <div.demo-stack> <BoxPlot label="Response time by service" show_label={true} suffix=" ms" groups={[{ name: "Auth", values: box_plot_demo_values(7, 60, 120, 80) + [330, 362] }, { name: "Search", values: box_plot_demo_values(11, 60, 220, 140) }, { name: "Checkout", values: box_plot_demo_values(23, 60, 170, 60) + [12, 405] }, { name: "Images", values: box_plot_demo_values(31, 60, 100, 70) }, { name: "Reports", values: box_plot_demo_values(43, 60, 270, 120) + [530] }]}/> <BoxPlot label="Exam scores by class" show_label={true} points={true} mean={true} zero={true} height={200} groups={[{ name: "Class A", values: [62, 71, 74, 78, 80, 81, 83, 85, 88, 90, 94] }, { name: "Class B", values: [45, 58, 61, 64, 66, 70, 72, 73, 75, 79, 82, 99] }, { name: "Class C", values: [70, 72, 75, 77, 78, 80, 81, 82, 84, 86] }]}/> </div>Source
ui/box_plot.qwe
// BoxPlot: how each group's values spread, from the raw values: a box from the first// to the third quartile with the median across it, whiskers out to the furthest values// within 1.5 times the box's height, and the values beyond those as outliers. CSS// only: hovering a group shows its five numbers, and screen readers get them as a table.//// <BoxPlot label="Response time" suffix=" ms" groups={[// { name: "API", values: api_times }, { name: "Web", values: web_times }]}/>// <BoxPlot label="Scores" groups={classes} points={true} mean={true} zero={true}/>//// groups: a list of { name, values }, each values a list of numbers (nulls are left// out); a group with none shows nothing. Quartiles are worked out as numpy's// percentile does (linear between values). points: every value drawn as a dot over// its box, spread sideways so they can be seen. mean: the mean marked with a// diamond. zero: the axis starts at 0; otherwise it spans the values. prefix and// suffix: around every value shown ("$", " ms"; the axis takes the prefix);// decimals: digits after the point, always, instead of up to two as needed. 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.import ui/theme.qwe use ui_clslet box_plot_css = css`.ui-boxplot { --ui-boxplot-1: light-dark(#2a78d6, #3987e5); --ui-boxplot-2: light-dark(#eb6834, #d95926); --ui-boxplot-3: light-dark(#1baf7a, #199e70); --ui-boxplot-4: light-dark(#eda100, #c98500); --ui-boxplot-5: light-dark(#e87ba4, #d55181); --ui-boxplot-6: light-dark(#008300, #008300); --ui-boxplot-7: light-dark(#6250d6, #9085e9); --ui-boxplot-8: light-dark(#e34948, #e66767); display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-boxplot-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-boxplot-s1 { --c: var(--ui-boxplot-1); } .ui-boxplot-s2 { --c: var(--ui-boxplot-2); } .ui-boxplot-s3 { --c: var(--ui-boxplot-3); }.ui-boxplot-s4 { --c: var(--ui-boxplot-4); } .ui-boxplot-s5 { --c: var(--ui-boxplot-5); } .ui-boxplot-s6 { --c: var(--ui-boxplot-6); }.ui-boxplot-s7 { --c: var(--ui-boxplot-7); } .ui-boxplot-s8 { --c: var(--ui-boxplot-8); }.ui-boxplot-frame { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 8px; padding-top: 8px; }.ui-boxplot-y { position: relative; min-width: 24px; }.ui-boxplot-y span { position: absolute; right: 0; transform: translateY(50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-boxplot-plot { position: relative; }.ui-boxplot-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-boxplot-cols { position: absolute; inset: 0; display: flex; }.ui-boxplot-col { position: relative; flex: 1; min-width: 0; border-radius: 4px; }.ui-boxplot-col:hover { background: color-mix(in srgb, var(--ui-subtle) 70%, transparent); }.ui-boxplot-whisker { position: absolute; left: 50%; bottom: var(--a); height: var(--b); width: 0; border-left: 1.5px solid var(--c); }.ui-boxplot-cap { position: absolute; left: 50%; bottom: var(--a); width: min(20%, 20px); height: 0; border-top: 1.5px solid var(--c); transform: translate(-50%, 50%); }.ui-boxplot-box { position: absolute; left: 50%; bottom: var(--a); height: var(--b); width: min(44%, 56px); min-height: 2px; border: 1.5px solid var(--c); border-radius: 3px; background: color-mix(in srgb, var(--c) 18%, var(--ui-bg)); transform: translateX(-50%); }.ui-boxplot-median { position: absolute; left: 50%; bottom: var(--a); width: min(44%, 56px); height: 0; border-top: 2.5px solid var(--c); transform: translate(-50%, 50%); }.ui-boxplot-mean { position: absolute; left: 50%; bottom: var(--a); width: 8px; height: 8px; margin: 0 0 -4px -4px; background: var(--ui-bg); border: 1.5px solid var(--ui-fg); transform: rotate(45deg); }.ui-boxplot-out { position: absolute; left: 50%; bottom: var(--a); width: 7px; height: 7px; margin: 0 0 -3.5px -3.5px; border: 1.5px solid var(--c); border-radius: 50%; background: var(--ui-bg); }.ui-boxplot-pt { position: absolute; left: calc(50% + var(--j)); bottom: var(--a); width: 4px; height: 4px; margin: 0 0 -2px -2px; border-radius: 50%; background: var(--ui-fg); opacity: .45; }.ui-boxplot-x { display: flex; margin-top: 6px; }.ui-boxplot-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; }.ui-boxplot-tip { position: absolute; z-index: 20; display: none; min-width: 120px; 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; pointer-events: none; }.ui-boxplot-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-boxplot-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-boxplot-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-boxplot-tip { top: 0; left: calc(50% + 34px); }.ui-boxplot-flip .ui-boxplot-tip { left: auto; right: calc(50% + 34px); }.ui-boxplot-col:hover .ui-boxplot-tip { display: block; }`// n (a whole number, 0 or more) with at least `width` digits.fn box_plot_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 box_plot_group(n: int) -> str = n < 1000 ? str(n) : box_plot_group(floor(n / 1000)) + "," + box_plot_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 box_plot_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)}` : `.${box_plot_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${box_plot_group(floor(units / scale))}${tail}`}// A coordinate or a percentage for CSS and SVG, to two decimals.fn box_plot_pct(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// An axis tick, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn box_plot_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 ? box_plot_num(v) : `${v < 0 ? "-" : ""}${box_plot_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// The axis from lo to hi, widened to round ticks: about `steps` steps of 1, 2, 2.5 or 5 x 10^n.fn box_plot_scale(lo: num, hi: num, steps: int = 4) -> map { let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo)) : hi - lo let raw = span / steps 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 numbers a box is drawn from, of a list of numbers (one at least): { n, q1, median,// q3, low, high (the whiskers' ends), outliers, mean, min, max }.fn box_plot_summary(xs: list) -> map { let qs = stats.percentile(xs, [25, 50, 75]) let iqr = qs[2] - qs[0] let inside = xs | filter((v) => v >= qs[0] - 1.5 * iqr && v <= qs[2] + 1.5 * iqr) { n: len(xs), q1: qs[0], median: qs[1], q3: qs[2], low: min(inside), high: max(inside), outliers: sort(xs | filter((v) => v < qs[0] - 1.5 * iqr || v > qs[2] + 1.5 * iqr)), mean: stats.fmean(xs), min: min(xs), max: max(xs), }}view BoxPlot(groups: list, points: bool = false, mean: bool = false, zero: bool = false, prefix: str = "", suffix: str = "", decimals: int? = null, height: int = 240, label: str = "Box plot", show_label: bool = false, class: str? = null) { style global box_plot_css let fmt = (v) => `${v < 0 ? "-" : ""}${prefix}${box_plot_num(abs(v), decimals)}${suffix}` let n = len(groups) let sets = for g in groups => (g.values ?? []) | filter((v) => v != null) let sums = for xs in sets => len(xs) > 0 ? box_plot_summary(xs) : null let known = flatten(sets) let lo0 = len(known) == 0 ? 0 : min(known) let hi0 = len(known) == 0 ? 1 : max(known) let sc = box_plot_scale(zero && lo0 > 0 ? 0 : lo0, zero && hi0 < 0 ? 0 : hi0) let pos = (v) => (v - sc.lo) * 100 / (sc.hi - sc.lo) let at = (v) => `--a: ${box_plot_pct(pos(v))}%` let span = (a, b) => `--a: ${box_plot_pct(pos(a))}%; --b: ${box_plot_pct(pos(b) - pos(a))}%` <figure class={ui_cls(["ui-boxplot", class])} aria-label={label}> {show_label && <figcaption.ui-boxplot-title>{label}</figcaption>} <div.ui-boxplot-frame aria-hidden="true"> <div.ui-boxplot-y style={`height: ${height}px`}> {for t in sc.ticks => <span style={`bottom: ${box_plot_pct(pos(t))}%`}>{t < 0 ? "-" : ""}{prefix}{box_plot_short(abs(t))}</span>} </div> <div.ui-boxplot-plot style={`height: ${height}px`}> {for t in sc.ticks => <span.ui-boxplot-grid style={`bottom: ${box_plot_pct(pos(t))}%`}></span>} <div.ui-boxplot-cols> {for i in range(n) => { let s = sums[i] <div class={ui_cls(["ui-boxplot-col", `ui-boxplot-s${i % 8 + 1}`, n > 1 && i > (n - 1) / 2 && "ui-boxplot-flip"])}> {s != null && [ <span.ui-boxplot-whisker style={span(s.low, s.high)}></span>, <span.ui-boxplot-cap style={at(s.low)}></span>, <span.ui-boxplot-cap style={at(s.high)}></span>, <span.ui-boxplot-box style={span(s.q1, s.q3)}></span>, <span.ui-boxplot-median style={at(s.median)}></span>, for v in s.outliers => <span.ui-boxplot-out style={at(v)}></span>, points && (for [k, v] in enumerate(sets[i]) => <span.ui-boxplot-pt style={`${at(v)}; --j: ${(k * 13 + 7) % 37 - 18}px`}></span>), mean && <span.ui-boxplot-mean style={at(s.mean)}></span>, <span.ui-boxplot-tip> <strong>{groups[i].name}</strong> <p>High<b>{fmt(s.high)}</b></p> <p>Q3<b>{fmt(s.q3)}</b></p> <p>Median<b>{fmt(s.median)}</b></p> <p>Q1<b>{fmt(s.q1)}</b></p> <p>Low<b>{fmt(s.low)}</b></p> {mean && <p>Mean<b>{fmt(s.mean)}</b></p>} <p>Values<b>{s.n}</b></p> {len(s.outliers) > 0 && <p>Outliers<b>{join(map(s.outliers, fmt), ", ")}</b></p>} </span> ]} </div> }} </div> </div> <span></span> <div.ui-boxplot-x>{for g in groups => <span title={str(g.name)}>{g.name}</span>}</div> </div> <table.ui-sr> <caption>{label}</caption> <thead><tr> <th scope="col"></th><th scope="col">Values</th><th scope="col">Low</th><th scope="col">Q1</th> <th scope="col">Median</th><th scope="col">Q3</th><th scope="col">High</th> {mean && <th scope="col">Mean</th>}<th scope="col">Outliers</th> </tr></thead> <tbody> {for i in range(n) => { let s = sums[i] <tr> <th scope="row">{groups[i].name}</th> {s == null ? <td>0</td> : [ <td>{s.n}</td>, <td>{fmt(s.low)}</td>, <td>{fmt(s.q1)}</td>, <td>{fmt(s.median)}</td>, <td>{fmt(s.q3)}</td>, <td>{fmt(s.high)}</td>, mean && <td>{fmt(s.mean)}</td>, <td>{len(s.outliers) > 0 ? join(map(s.outliers, fmt), ", ") : "None"}</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 downloadsBarChartBars up or across, one series or several grouped or stacked, on round ticks, with values on hover and a table for screen readers.7 views · 7 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBrushChartA large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.6 views · 5 downloadsBubbleChartPoints placed by two numbers and sized by a third, in coloured groups, labelled when big enough, with all three numbers on hover.7 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 downloads
Thread
Thread 0 posts
On BoxPlot: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.