BrushChart
A large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.
Demo
| Value | |
|---|---|
| Jan 5 | $117.65 |
| Jan 6 | $119.75 |
| Jan 7 | $120.34 |
| Jan 8 | $121.87 |
| Jan 9 | $122.64 |
| Jan 10 | $120.66 |
| Jan 11 | $121.50 |
| Jan 12 | $120.96 |
| Jan 13 | $122.91 |
| Jan 14 | $121.38 |
| Jan 15 | $123.78 |
| Jan 16 | $122.17 |
| Jan 17 | $121.75 |
| Jan 18 | $123.77 |
| Jan 19 | $123.87 |
| Jan 20 | $124.56 |
| Jan 21 | $125.32 |
| Jan 22 | $127.61 |
| Jan 23 | $130.15 |
| Jan 24 | $130.90 |
| Jan 25 | $129.74 |
| Jan 26 | $130.76 |
| Jan 27 | $132.87 |
| Jan 28 | $131.24 |
| Jan 29 | $133.48 |
| Jan 30 | $133.94 |
| Jan 31 | $135.67 |
| Feb 1 | $135.39 |
| Feb 2 | $138.01 |
| Feb 3 | $136.60 |
| Feb 4 | $135.56 |
| Feb 5 | $135.94 |
| Feb 6 | $135.41 |
| Feb 7 | $136.36 |
| Feb 8 | $135.59 |
| Feb 9 | $136.66 |
| Feb 10 | $137.16 |
| Feb 11 | $139.88 |
| Feb 12 | $140.03 |
| Feb 13 | $137.46 |
| Feb 14 | $138.63 |
| Feb 15 | $140.10 |
| Feb 16 | $140.69 |
| Feb 17 | $139.59 |
| Feb 18 | $139.18 |
| Feb 19 | $139.24 |
| Feb 20 | $140.90 |
| Feb 21 | $138.76 |
| Feb 22 | $138.59 |
| Feb 23 | $139.12 |
| Feb 24 | $141.53 |
| Feb 25 | $144.39 |
| Feb 26 | $146.08 |
| Feb 27 | $145.38 |
| Feb 28 | $143.31 |
| Mar 1 | $143.11 |
| Mar 2 | $145.04 |
| Mar 3 | $145.28 |
| Mar 4 | $148.23 |
| Mar 5 | $150.75 |
| Mar 6 | $149.89 |
| Mar 7 | $151.77 |
| Mar 8 | $152.95 |
| Mar 9 | $154.13 |
| Mar 10 | $151.19 |
| Mar 11 | $153.30 |
| Mar 12 | $152.85 |
| Mar 13 | $155.48 |
| Mar 14 | $157.78 |
| Mar 15 | $157.28 |
| Mar 16 | $157.63 |
| Mar 17 | $155.43 |
| Mar 18 | $154.68 |
| Mar 19 | $154.22 |
| Mar 20 | $151.36 |
| Mar 21 | $152.84 |
| Mar 22 | $150.82 |
| Mar 23 | $149.60 |
| Mar 24 | $147.26 |
| Mar 25 | $149.20 |
| Mar 26 | $149.52 |
| Mar 27 | $151.97 |
| Mar 28 | $150.51 |
| Mar 29 | $151.03 |
| Mar 30 | $153.17 |
| Mar 31 | $152.62 |
| Apr 1 | $154.23 |
| Apr 2 | $154.08 |
| Apr 3 | $155.12 |
| Apr 4 | $157.08 |
| Apr 5 | $158.95 |
| Apr 6 | $159.17 |
| Apr 7 | $161.54 |
| Apr 8 | $164.77 |
| Apr 9 | $162.71 |
| Apr 10 | $165.86 |
| Apr 11 | $165.00 |
| Apr 12 | $162.13 |
| Apr 13 | $159.79 |
| Apr 14 | $162.78 |
| Apr 15 | $162.04 |
| Apr 16 | $163.17 |
| Apr 17 | $166.35 |
| Apr 18 | $166.00 |
| Apr 19 | $169.03 |
| Apr 20 | $168.89 |
| Apr 21 | $165.91 |
| Apr 22 | $163.23 |
| Apr 23 | $162.19 |
| Apr 24 | $159.79 |
| Apr 25 | $157.17 |
| Apr 26 | $156.91 |
| Apr 27 | $155.51 |
| Apr 28 | $154.24 |
| Apr 29 | $152.86 |
| Apr 30 | $153.84 |
| May 1 | $156.18 |
| May 2 | $157.36 |
| May 3 | $158.58 |
| May 4 | $157.45 |
| May 5 | $157.23 |
| May 6 | $159.87 |
| May 7 | $156.89 |
| May 8 | $155.49 |
| May 9 | $154.48 |
| May 10 | $151.91 |
| May 11 | $150.14 |
| May 12 | $148.90 |
| May 13 | $149.66 |
| May 14 | $149.78 |
| May 15 | $147.52 |
| May 16 | $149.69 |
| May 17 | $150.26 |
| May 18 | $151.06 |
| May 19 | $150.75 |
| May 20 | $152.13 |
| May 21 | $152.05 |
| May 22 | $149.30 |
| May 23 | $150.08 |
| May 24 | $151.57 |
| May 25 | $150.37 |
| May 26 | $148.04 |
| May 27 | $149.18 |
| May 28 | $149.90 |
| May 29 | $149.21 |
| May 30 | $147.78 |
| May 31 | $148.53 |
| Jun 1 | $146.56 |
| Jun 2 | $145.56 |
| Jun 3 | $145.88 |
| Jun 4 | $143.17 |
| Jun 5 | $142.72 |
| Jun 6 | $144.08 |
| Jun 7 | $145.91 |
| Jun 8 | $147.14 |
| Jun 9 | $147.84 |
| Jun 10 | $147.57 |
| Jun 11 | $150.19 |
| Jun 12 | $151.76 |
| Jun 13 | $150.84 |
| Jun 14 | $148.44 |
| Jun 15 | $149.20 |
| Jun 16 | $149.84 |
| Jun 17 | $151.82 |
| Jun 18 | $153.70 |
| Jun 19 | $155.74 |
| Jun 20 | $157.38 |
| Jun 21 | $157.83 |
| Jun 22 | $156.36 |
| Jun 23 | $157.28 |
| Jun 24 | $154.67 |
| Jun 25 | $152.58 |
| Jun 26 | $152.27 |
| Jun 27 | $154.67 |
| Jun 28 | $157.61 |
| Jun 29 | $159.16 |
| Jun 30 | $157.23 |
| Jul 1 | $158.11 |
| Jul 2 | $160.33 |
| Jul 3 | $160.36 |
| Jul 4 | $162.22 |
| Jul 5 | $163.95 |
| Jul 6 | $162.27 |
| Jul 7 | $159.65 |
| Jul 8 | $162.80 |
| Jul 9 | $162.79 |
| Jul 10 | $162.05 |
| Jul 11 | $162.40 |
| Jul 12 | $163.61 |
| Jul 13 | $162.36 |
| Jul 14 | $164.37 |
| Jul 15 | $165.55 |
| Jul 16 | $167.30 |
| Jul 17 | $170.23 |
| Jul 18 | $169.01 |
| Jul 19 | $170.91 |
| Jul 20 | $170.16 |
| Jul 21 | $170.12 |
| Jul 22 | $173.47 |
| Jul 23 | $173.33 |
| Jul 24 | $173.27 |
| Jul 25 | $175.49 |
| Jul 26 | $177.78 |
| Jul 27 | $176.43 |
| Jul 28 | $173.44 |
| Jul 29 | $170.56 |
| Jul 30 | $169.49 |
| Jul 31 | $168.88 |
| Aug 1 | $170.22 |
| Aug 2 | $167.81 |
| Aug 3 | $165.10 |
| Aug 4 | $168.07 |
| Aug 5 | $165.92 |
| Aug 6 | $168.88 |
| Aug 7 | $169.07 |
| Aug 8 | $167.34 |
| Aug 9 | $170.21 |
| Aug 10 | $171.03 |
| Aug 11 | $169.85 |
| Aug 12 | $168.83 |
| Aug 13 | $170.71 |
| Aug 14 | $168.11 |
| Aug 15 | $166.70 |
| Aug 16 | $163.54 |
| Aug 17 | $160.41 |
| Aug 18 | $162.61 |
| Aug 19 | $162.34 |
| Aug 20 | $163.81 |
| Aug 21 | $160.66 |
| Aug 22 | $158.55 |
| Aug 23 | $160.39 |
| Aug 24 | $160.81 |
| Aug 25 | $162.07 |
| Aug 26 | $161.25 |
| Aug 27 | $164.49 |
| Aug 28 | $166.67 |
| Aug 29 | $165.57 |
| Aug 30 | $167.06 |
| Aug 31 | $168.65 |
| Sep 1 | $166.60 |
Showing Jul 4 to Sep 1. Drag the window under the chart, or its edges.
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/brush_chart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/brush_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/brush_chart.qwe
// A year of daily closing prices: a seeded random walk with a drift, the same on every run// and in the browser too (the generator's products stay under 2^53, exact in JavaScript).fn brush_chart_demo_prices(seed: int, n: int) -> list { let s = seed let price = 120.0 let out = [] while len(out) < n { s = (s * 48271) % 2147483647 price = price * (1 + (s / 2147483647 - 0.49) * 0.04) out = push(out, round(price * 100) / 100) } out}let brush_chart_demo_days = for i in range(240) => date.format(date.add_days("2026-01-05", i), "MMM D")component BrushChartDemoLive() { state shown = `${brush_chart_demo_days[180]} to ${brush_chart_demo_days[239]}` <div.demo-stack> <BrushChart label="Closing price" show_label={true} labels={brush_chart_demo_days} values={brush_chart_demo_prices(11, 240)} prefix="$" decimals={2} start={180} end={239} on_change={(first, last) => { shown = `${first} to ${last}` }}/> <p.demo-note>Showing {shown}. Drag the window under the chart, or its edges.</p> </div>}view BrushChartDemo() = <BrushChartDemoLive/>Source
ui/brush_chart.qwe
// BrushChart: a large line chart of a range, with a small overview of everything// under it where a window picks that range: drag the window to move it, drag its// edges to widen or narrow it, or drag across the overview to draw a new one. The// keyboard does the same: the window and each edge are sliders (the arrow keys move// them a label, Page Up and Page Down a window's width, Home and End to the ends).// Hovering the large chart shows each value; screen readers hear the range and get// every number as a table.//// <BrushChart label="Price" labels={days} values={closes} prefix="$" start={200} end={260}/>// <BrushChart label="Traffic" labels={hours} series={[{ name: "In", values: rx }, { name: "Out", values: tx }]}// on_change={(first, last) => { range = `${first} to ${last}` }}/>//// labels: the x axis, in order. values: one series' numbers, one per label; or// series: a list of { name, values } (up to eight, each in a colour of its own, with// a legend). A null leaves a gap. start, end: the indexes of the first and last// labels in the window to begin with (by default the last quarter). zero: the large// chart's axis starts at 0; otherwise it spans the values in the window. height:// the large chart's height in pixels (220); overview: the small one's (56). 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. label:// what the chart shows; it names it for screen readers and captions their table.// show_label also writes it above. on_change(first, last): inside one of your// components, given the first and last labels in the window after each change.import ui/theme.qwe use ui_clslet brush_chart_css = css`.ui-brush { --ui-brush-1: light-dark(#2a78d6, #3987e5); --ui-brush-2: light-dark(#eb6834, #d95926); --ui-brush-3: light-dark(#1baf7a, #199e70); --ui-brush-4: light-dark(#eda100, #c98500); --ui-brush-5: light-dark(#e87ba4, #d55181); --ui-brush-6: light-dark(#008300, #008300); --ui-brush-7: light-dark(#6250d6, #9085e9); --ui-brush-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-brush-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-brush-s1 { --c: var(--ui-brush-1); } .ui-brush-s2 { --c: var(--ui-brush-2); } .ui-brush-s3 { --c: var(--ui-brush-3); }.ui-brush-s4 { --c: var(--ui-brush-4); } .ui-brush-s5 { --c: var(--ui-brush-5); } .ui-brush-s6 { --c: var(--ui-brush-6); }.ui-brush-s7 { --c: var(--ui-brush-7); } .ui-brush-s8 { --c: var(--ui-brush-8); }.ui-brush-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-brush-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-brush-key { width: 14px; height: 3px; border-radius: 2px; background: var(--c); flex: none; }.ui-brush-frame { display: grid; grid-template-columns: 44px minmax(0, 1fr); column-gap: 8px; padding-top: 8px; }.ui-brush-y { position: relative; }.ui-brush-y span { position: absolute; right: 0; transform: translateY(50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-brush-plot { position: relative; }.ui-brush-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 70%, transparent); }.ui-brush-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-brush-line { fill: none; stroke: var(--c); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }.ui-brush-area { fill: var(--c); opacity: .1; }.ui-brush-mini .ui-brush-line { stroke-width: 1.25; opacity: .8; }.ui-brush-cols { position: absolute; inset: 0; display: flex; }.ui-brush-col { position: relative; flex: 1; min-width: 0; --x: 50%; }.ui-brush-col:first-child:not(:last-child) { flex: .5; --x: 0%; }.ui-brush-col:last-child:not(:first-child) { flex: .5; --x: 100%; }.ui-brush-col::before { content: ""; position: absolute; top: 0; bottom: 0; left: var(--x); width: 1px; background: var(--ui-strong); opacity: 0; }.ui-brush-col:hover::before { opacity: 1; }.ui-brush-dot { position: absolute; left: var(--x); display: none; width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%; background: var(--c); box-shadow: 0 0 0 2px var(--ui-bg); }.ui-brush-col:hover .ui-brush-dot, .ui-brush-col:hover .ui-brush-tip { display: block; }.ui-brush-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-brush-tip strong { display: block; margin-bottom: 3px; font-weight: 600; white-space: nowrap; }.ui-brush-tip p { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; color: var(--ui-muted); white-space: nowrap; }.ui-brush-tip b { margin-left: auto; padding-left: 12px; color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-brush-tip { top: 0; left: calc(var(--x) + 10px); }.ui-brush-flip .ui-brush-tip { left: auto; right: calc(100% - var(--x) + 10px); }.ui-brush-tip .ui-brush-key { width: 8px; height: 8px; border-radius: 50%; }.ui-brush-x { position: relative; height: 16px; margin-top: 6px; }.ui-brush-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-brush-x .ui-brush-first { transform: none; }.ui-brush-x .ui-brush-last { transform: translateX(-100%); }.ui-brush-mini { position: relative; margin-top: 4px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-bg); cursor: crosshair; touch-action: none; user-select: none; }.ui-brush-shade { position: absolute; top: 0; bottom: 0; background: color-mix(in srgb, var(--ui-muted) 16%, transparent); pointer-events: none; }.ui-brush-win { position: absolute; top: -1px; bottom: -1px; border: 1.5px solid var(--ui-accent); border-radius: 4px; cursor: grab; }.ui-brush-win:focus-visible, .ui-brush-edge:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-brush-edge { position: absolute; top: 50%; width: 10px; height: 26px; margin-left: -5px; border: 1px solid var(--ui-accent); border-radius: 3px; background: var(--ui-surface); cursor: ew-resize; transform: translateY(-50%); z-index: 2; }.ui-brush-edge::before { content: ""; position: absolute; inset: 7px 3px; border-inline: 1px solid var(--ui-accent); }.ui-brush-ends { display: flex; justify-content: space-between; margin-top: 4px; color: var(--ui-muted); font-size: 11px; }`// n (a whole number, 0 or more) with at least `width` digits.fn brush_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 brush_chart_group(n: int) -> str = n < 1000 ? str(n) : brush_chart_group(floor(n / 1000)) + "," + brush_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 brush_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)}` : `.${brush_chart_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${brush_chart_group(floor(units / scale))}${tail}`}// A coordinate or a percentage for CSS and SVG, to two decimals.fn brush_chart_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 brush_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 ? brush_chart_num(v) : `${v < 0 ? "-" : ""}${brush_chart_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 brush_chart_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 index under the pointer in the overview, 0 to last. A function of its own, not the// component's: there, reading the page makes a handler wait, and the next pointer event// would run before this one's handler is done.fn brush_chart_at(e, last: int) -> int { let r = e.target.closest(".ui-brush-mini").getBoundingClientRect() let i = round((e.clientX - r.left) / r.width * last) i < 0 ? 0 : (i > last ? last : i)}component BrushChart(labels: list, values: list? = null, series: list? = null, start: int? = null, end: int? = null, zero: bool = false, height: int = 220, overview: int = 56, prefix: str = "", suffix: str = "", decimals: int? = null, label: str = "Brush chart", show_label: bool = false, on_change = null, class: str? = null) { style global brush_chart_css let n = len(labels) let sets = series ?? [{ name: label, values: values ?? [] }] let last = n > 0 ? n - 1 : 0 let fix = (i: int) => i < 0 ? 0 : (i > last ? last : i) let a0 = fix(start ?? (n - 1 - floor(n / 4))) let b0 = fix(end ?? last) state a = a0 < b0 ? a0 : (b0 > 0 ? b0 - 1 : 0) state b = b0 > a0 ? b0 : (a0 < last ? a0 + 1 : last) state mode = null state grab = 0 state from_a = 0 state from_b = 0 let fmt = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${brush_chart_num(abs(v), decimals)}${suffix}` let span = b - a let known = flatten(sets | map((s) => range(a, b + 1) | map((i) => s.values[i]))) | filter((v) => v != null) let lo0 = len(known) == 0 ? 0 : min(known) let hi0 = len(known) == 0 ? 1 : max(known) let sc = brush_chart_scale(zero && lo0 > 0 ? 0 : lo0, zero && hi0 < 0 ? 0 : hi0) let top = (v) => (sc.hi - v) * 100 / (sc.hi - sc.lo) let all = flatten(sets | map((s) => s.values)) | filter((v) => v != null) let mlo = len(all) == 0 ? 0 : min(all) let mhi = len(all) == 0 ? 1 : (max(all) > mlo ? max(all) : mlo + 1) let mtop = (v) => 8 + (mhi - v) * 84 / (mhi - mlo) let x = (i) => span == 0 ? 50 : (i - a) * 100 / span let mx = (i) => last == 0 ? 50 : i * 100 / last // The path through a series between first and to, placed by px and py. let path = (vals, first, to, px, py) => { let starts = range(first, to + 1) | filter((i) => vals[i] != null && (i == first || vals[i - 1] == null)) join(for st in starts => { let run = range(st, (find(range(st, to + 1), (k) => k == to || vals[k + 1] == null) ?? st) + 1) "M" + join(run | map((i) => `${brush_chart_pct(px(i))},${brush_chart_pct(py(vals[i]))}`), " L") + (len(run) == 1 ? " l0.01,0" : "") }, " ") } let tell = () => { on_change && on_change(labels[a], labels[b]) } let set_window = (p: int, q: int) => { let lo = fix(p < q ? p : q) let hi = fix(p < q ? q : p) if hi > lo { a = lo b = hi } } let every_nth = ceil((span + 1) / 8) let keys = (e, part: str) => { let size = b - a let step = match e.key { "ArrowLeft" | "ArrowDown" => -1 "ArrowRight" | "ArrowUp" => 1 "PageDown" => -size "PageUp" => size _ => 0 } if step != 0 { e.preventDefault() if part == "start" { set_window(a + step < b ? a + step : b - 1, b) } else if part == "end" { set_window(a, b + step > a ? b + step : a + 1) } else { let move = a + step < 0 ? -a : (b + step > last ? last - b : step) set_window(a + move, b + move) } tell() } else if e.key == "Home" || e.key == "End" { e.preventDefault() if part == "start" { set_window(e.key == "Home" ? 0 : b - 1, b) } else if part == "end" { set_window(a, e.key == "End" ? last : a + 1) } else { if e.key == "Home" { set_window(0, size) } else { set_window(last - size, last) } } tell() } } let said = `${labels[a] ?? ""} to ${labels[b] ?? ""}` <figure class={ui_cls(["ui-brush", class])} aria-label={label}> {show_label && <figcaption.ui-brush-title>{label}</figcaption>} {len(sets) > 1 && <ul.ui-brush-legend aria-hidden="true"> {for [j, s] in enumerate(sets) => <li><span class={`ui-brush-key ui-brush-s${j % 8 + 1}`}></span>{s.name}</li>} </ul>} <div.ui-brush-frame> <div.ui-brush-y style={`height: ${height}px`} aria-hidden="true"> {for t in sc.ticks => <span style={`bottom: ${brush_chart_pct(100 - top(t))}%`}>{t < 0 ? "-" : ""}{prefix}{brush_chart_short(abs(t))}</span>} </div> <div.ui-brush-plot style={`height: ${height}px`} aria-hidden="true"> {for t in sc.ticks => <span.ui-brush-grid style={`top: ${brush_chart_pct(top(t))}%`}></span>} <svg.ui-brush-svg viewBox="0 0 100 100" preserveAspectRatio="none"> {len(sets) == 1 && <path class="ui-brush-area ui-brush-s1" d={`${path(sets[0].values, a, b, x, top)} L100,100 L0,100 Z`}/>} {for [j, s] in enumerate(sets) => <path class={`ui-brush-line ui-brush-s${j % 8 + 1}`} d={path(s.values, a, b, x, top)} vector-effect="non-scaling-stroke"/>} </svg> <div.ui-brush-cols> {for i in range(a, b + 1) => <div class={ui_cls(["ui-brush-col", i - a > span / 2 && "ui-brush-flip"])}> {for [j, s] in enumerate(sets) => s.values[i] != null && <span class={`ui-brush-dot ui-brush-s${j % 8 + 1}`} style={`top: ${brush_chart_pct(top(s.values[i]))}%`}></span>} <span.ui-brush-tip> <strong>{labels[i]}</strong> {for [j, s] in enumerate(sets) => <p> {len(sets) > 1 && <span class={`ui-brush-key ui-brush-s${j % 8 + 1}`}></span>} {len(sets) > 1 ? s.name : label}<b>{fmt(s.values[i])}</b> </p>} </span> </div>} </div> </div> <span></span> <div.ui-brush-x aria-hidden="true"> {for i in range(a, b + 1) => (i - a) % every_nth == 0 && <span class={ui_cls([span > 0 && i == a && "ui-brush-first", span > 0 && i == b && "ui-brush-last"])} style={`left: ${brush_chart_pct(x(i))}%`}>{labels[i]}</span>} </div> <span></span> <div> <div.ui-brush-mini style={`height: ${overview}px`} @pointerdown={(e) => { if e.button == 0 { e.preventDefault() e.target.closest(".ui-brush-mini").setPointerCapture(e.pointerId) let part = e.target.dataset.part ?? "new" mode = part grab = brush_chart_at(e, last) from_a = a from_b = b } }} @pointermove={(e) => { if mode != null { let i = brush_chart_at(e, last) if mode == "start" { set_window(i < b ? i : b - 1, b) } else if mode == "end" { set_window(a, i > a ? i : a + 1) } else if mode == "move" { let d0 = i - grab let d = from_a + d0 < 0 ? -from_a : (from_b + d0 > last ? last - from_b : d0) set_window(from_a + d, from_b + d) } else if i != grab { set_window(grab, i) } } }} @pointerup={() => { if mode != null { mode = null tell() } }} @pointercancel={() => { mode = null }}> <svg.ui-brush-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"> {for [j, s] in enumerate(sets) => <path class={`ui-brush-line ui-brush-s${j % 8 + 1}`} d={path(s.values, 0, last, mx, mtop)} vector-effect="non-scaling-stroke"/>} </svg> <span.ui-brush-shade style={`left: 0; width: ${brush_chart_pct(mx(a))}%`}></span> <span.ui-brush-shade style={`left: ${brush_chart_pct(mx(b))}%; right: 0`}></span> <span.ui-brush-win data-part="move" tabindex="0" role="slider" aria-label={`${label}: the range shown`} aria-valuemin="0" aria-valuemax={last - span} aria-valuenow={a} aria-valuetext={said} style={`left: ${brush_chart_pct(mx(a))}%; width: ${brush_chart_pct(mx(b) - mx(a))}%`} @keydown={(e) => { keys(e, "move") }}></span> <span.ui-brush-edge data-part="start" tabindex="0" role="slider" aria-label="Start of the range" aria-valuemin="0" aria-valuemax={b - 1} aria-valuenow={a} aria-valuetext={str(labels[a] ?? "")} style={`left: ${brush_chart_pct(mx(a))}%`} @keydown={(e) => { keys(e, "start") }}></span> <span.ui-brush-edge data-part="end" tabindex="0" role="slider" aria-label="End of the range" aria-valuemin={a + 1} aria-valuemax={last} aria-valuenow={b} aria-valuetext={str(labels[b] ?? "")} style={`left: ${brush_chart_pct(mx(b))}%`} @keydown={(e) => { keys(e, "end") }}></span> </div> <div.ui-brush-ends aria-hidden="true"><span>{labels[0] ?? ""}</span><span>{labels[last] ?? ""}</span></div> </div> </div> <table.ui-sr> <caption>{label}</caption> <thead><tr><th scope="col"></th>{for s in sets => <th scope="col">{len(sets) > 1 ? s.name : "Value"}</th>}</tr></thead> <tbody>{for i in range(n) => <tr><th scope="row">{labels[i]}</th>{for s in sets => <td>{fmt(s.values[i])}</td>}</tr>}</tbody> </table> </figure>}Related
InteractiveLineChartLines with a crosshair and tooltip that follow the pointer or the keyboard, legend items that hide series, and drag to zoom.6 views · 6 downloadsStockChartA full trading chart: candles, line or area, volume, range buttons, a crosshair with the OHLC readout, wheel zoom and drag pan, indicators and RSI and MACD panes.6 views · 6 downloadsAreaChartFilled 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 downloadsBoxPlotQuartiles, median, whiskers and outliers for each group, worked out from the raw values, with the five numbers on hover.5 views · 7 downloads
Thread
Thread 0 posts
On BrushChart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.