Mobile

TouchChart

A line chart read by finger with a crosshair and value, pinched to a range, with buttons, keys and a table to the same places.

#mobile#interactive#chart#gesture#pinch#tablet

Demo

Preview

Worked by hand

D0 to D60

$100
$120
$140
$160
Low
$100
High
$160
Change
$60
Worked by hand, D0 to D60
PointValue
D0$100
D1$101
D2$102
D3$103
D4$104
D5$105
D6$106
D7$107
D8$108
D9$109
D10$110
D11$111
D12$112
D13$113
D14$114
D15$115
D16$116
D17$117
D18$118
D19$119
D20$120
D21$121
D22$122
D23$123
D24$124
D25$125
D26$126
D27$127
D28$128
D29$129
D30$130
D31$131
D32$132
D33$133
D34$134
D35$135
D36$136
D37$137
D38$138
D39$139
D40$140
D41$141
D42$142
D43$143
D44$144
D45$145
D46$146
D47$147
D48$148
D49$149
D50$150
D51$151
D52$152
D53$153
D54$154
D55$155
D56$156
D57$157
D58$158
D59$159
D60$160

Nothing chosen.

All.

Daily active people

1 May to 27 Oct

150
200
Low
113.3 k
High
210.7 k
Change
89.6 k
Daily active people, 1 May to 27 Oct
PointValue
1 May120 k
2 May128.3 k
3 May123.2 k
4 May121.5 k
5 May132.3 k
6 May136.3 k
7 May129.3 k
8 May131.7 k
9 May142.3 k
10 May141.1 k
11 May134.4 k
12 May140.4 k
13 May148.3 k
14 May142.4 k
15 May137.8 k
16 May146.1 k
17 May149.3 k
18 May140.4 k
19 May139.2 k
20 May147.7 k
21 May145.5 k
22 May136 k
23 May138.6 k
24 May145.1 k
25 May138.1 k
26 May130.4 k
27 May136.2 k
28 May139 k
29 May129 k
30 May125.2 k
31 May132.6 k
1 Jun130.9 k
2 Jun120.5 k
3 Jun121.6 k
4 Jun128.6 k
5 Jun122.9 k
6 Jun114.8 k
7 Jun120.4 k
8 Jun125.2 k
9 Jun117 k
10 Jun113.3 k
11 Jun121.9 k
12 Jun123.4 k
13 Jun114.8 k
14 Jun116.5 k
15 Jun126.1 k
16 Jun123.8 k
17 Jun117.2 k
18 Jun124.1 k
19 Jun132.2 k
20 Jun127 k
21 Jun124.3 k
22 Jun134.6 k
23 Jun139.4 k
24 Jun132.7 k
25 Jun134.8 k
26 Jun146.2 k
27 Jun146.5 k
28 Jun140.5 k
29 Jun147.1 k
30 Jun156.7 k
1 Jul152.8 k
2 Jul149.1 k
3 Jul158.7 k
4 Jul164.3 k
5 Jul157.4 k
6 Jul157.3 k
7 Jul167.6 k
8 Jul168 k
9 Jul160 k
10 Jul163.7 k
11 Jul172.2 k
12 Jul167.3 k
13 Jul160.6 k
14 Jul167.2 k
15 Jul171.8 k
16 Jul163 k
17 Jul159.4 k
18 Jul167.2 k
19 Jul166.7 k
20 Jul156.4 k
21 Jul156.8 k
22 Jul163.9 k
23 Jul158.4 k
24 Jul149.3 k
25 Jul153.6 k
26 Jul158 k
27 Jul149 k
28 Jul143.4 k
29 Jul150.4 k
30 Jul151 k
31 Jul140.9 k
1 Aug140.1 k
2 Aug148.1 k
3 Aug144.7 k
4 Aug136 k
5 Aug140.4 k
6 Aug147.2 k
7 Aug140.7 k
8 Aug135.8 k
9 Aug144.2 k
10 Aug148.2 k
11 Aug140.4 k
12 Aug140.6 k
13 Aug150.9 k
14 Aug151.1 k
15 Aug144.2 k
16 Aug149.7 k
17 Aug159.4 k
18 Aug156 k
19 Aug151.9 k
20 Aug161.5 k
21 Aug168.3 k
22 Aug162.3 k
23 Aug162.4 k
24 Aug173.7 k
25 Aug176.1 k
26 Aug169.4 k
27 Aug173.9 k
28 Aug184.2 k
29 Aug181.7 k
30 Aug176.3 k
31 Aug184.2 k
1 Sep191.1 k
2 Sep184.6 k
3 Sep182 k
4 Sep191.5 k
5 Sep193.4 k
6 Sep184.7 k
7 Sep185.8 k
8 Sep194.5 k
9 Sep190.9 k
10 Sep182.5 k
11 Sep187.1 k
12 Sep192.7 k
13 Sep184.7 k
14 Sep178.8 k
15 Sep185.7 k
16 Sep186.9 k
17 Sep176.5 k
18 Sep174.6 k
19 Sep182 k
20 Sep178.4 k
21 Sep168.4 k
22 Sep171 k
23 Sep176.9 k
24 Sep169.5 k
25 Sep162.3 k
26 Sep168.7 k
27 Sep171.6 k
28 Sep162.2 k
29 Sep159.9 k
30 Sep168.4 k
1 Oct167.5 k
2 Oct158.7 k
3 Oct161.7 k
4 Oct170.2 k
5 Oct165.9 k
6 Oct159.9 k
7 Oct167.7 k
8 Oct174 k
9 Oct167.4 k
10 Oct166 k
11 Oct176.7 k
12 Oct179.3 k
13 Oct172.4 k
14 Oct176.2 k
15 Oct187.1 k
16 Oct185.6 k
17 Oct180.4 k
18 Oct188.5 k
19 Oct197.1 k
20 Oct192.1 k
21 Oct190.1 k
22 Oct200.8 k
23 Oct205 k
24 Oct198 k
25 Oct200.1 k
26 Oct210.7 k
27 Oct209.6 k

With holes

1 to 8

6
8
Low
5
High
9
Change
4
With holes, 1 to 8
PointValue
15
26
3no value
4no value
57
6no value
78
89

Five thousand points and a spike

1 to 5000

0
50
100
Low
0
High
100
Change
1
Five thousand points and a spike, 1 to 5000, the first 400 points
PointValue
10
21
30
41
50
61
70
81
90
101
110
121
130
141
150
161
170
181
190
201
210
221
230
241
250
261
270
281
290
301
310
321
330
341
350
361
370
381
390
401
410
421
430
441
450
461
470
481
490
501
510
521
530
541
550
561
570
581
590
601
610
621
630
641
650
661
670
681
690
701
710
721
730
741
750
761
770
781
790
801
810
821
830
841
850
861
870
881
890
901
910
921
930
941
950
961
970
981
990
1001
1010
1021
1030
1041
1050
1061
1070
1081
1090
1101
1110
1121
1130
1141
1150
1161
1170
1181
1190
1201
1210
1221
1230
1241
1250
1261
1270
1281
1290
1301
1310
1321
1330
1341
1350
1361
1370
1381
1390
1401
1410
1421
1430
1441
1450
1461
1470
1481
1490
1501
1510
1521
1530
1541
1550
1561
1570
1581
1590
1601
1610
1621
1630
1641
1650
1661
1670
1681
1690
1701
1710
1721
1730
1741
1750
1761
1770
1781
1790
1801
1810
1821
1830
1841
1850
1861
1870
1881
1890
1901
1910
1921
1930
1941
1950
1961
1970
1981
1990
2001
2010
2021
2030
2041
2050
2061
2070
2081
2090
2101
2110
2121
2130
2141
2150
2161
2170
2181
2190
2201
2210
2221
2230
2241
2250
2261
2270
2281
2290
2301
2310
2321
2330
2341
2350
2361
2370
2381
2390
2401
2410
2421
2430
2441
2450
2461
2470
2481
2490
2501
2510
2521
2530
2541
2550
2561
2570
2581
2590
2601
2610
2621
2630
2641
2650
2661
2670
2681
2690
2701
2710
2721
2730
2741
2750
2761
2770
2781
2790
2801
2810
2821
2830
2841
2850
2861
2870
2881
2890
2901
2910
2921
2930
2941
2950
2961
2970
2981
2990
3001
3010
3021
3030
3041
3050
3061
3070
3081
3090
3101
3110
3121
3130
3141
3150
3161
3170
3181
3190
3201
3210
3221
3230
3241
3250
3261
3270
3281
3290
3301
3310
3321
3330
3341
3350
3361
3370
3381
3390
3401
3410
3421
3430
3441
3450
3461
3470
3481
3490
3501
3510
3521
3530
3541
3550
3561
3570
3581
3590
3601
3610
3621
3630
3641
3650
3661
3670
3681
3690
3701
3710
3721
3730
3741
3750
3761
3770
3781
3790
3801
3810
3821
3830
3841
3850
3861
3870
3881
3890
3901
3910
3921
3930
3941
3950
3961
3970
3981
3990
4001

Nothing

Nothing to show yet.

Nothing, no data
PointValue

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

Example

The code of the preview above.

demos/touch_chart.qwe
// Sixty-one points that rise by one, so every reading below can be worked out by hand.let touch_chart_demo_exact = for i in range(0, 61) => { label: `D${i}`, value: 100 + i }let touch_chart_demo_year = for i in range(0, 180) => {    label: date.format(date.add_days("2026-05-01", i), "D MMM"),    value: round(120 + i * 0.4 + 18 * math.sin(i / 9) + 6 * math.sin(i * 1.7), 1),}let touch_chart_demo_long = for i in range(0, 5000) => i == 2345 ? 100 : i % 2component TouchChartDemoLive() {    state said = "Nothing chosen."    state range_said = "All."    state stir = 0    <div.demo-stack>        <div id="touch-chart-exact"><TouchChart title="Worked by hand" points={touch_chart_demo_exact | map((p) => { ...p, stir })} prefix="$" digits={2}            ranges={[{ label: "Last 10", count: 10 }, { label: "All", count: 0 }]}            on_select={(i) => { said = i == null ? "Cleared." : `${i.index} ${i.label} ${i.value}` }} on_range={(a, b) => { range_said = `${a} to ${b}` }}/></div>        <p.demo-note id="touch-chart-said">{said}</p>        <p.demo-note id="touch-chart-range">{range_said}</p>        <div.demo-row><Button id="touch-chart-stir" size="lg" variant="soft" on_click={() => { stir += 1 }}>New data arrives</Button></div>    </div>}view TouchChartDemo() =    <div.demo-stack>        <TouchChartDemoLive/>        <div id="touch-chart-season"><TouchChart title="Daily active people" unit="k" points={touch_chart_demo_year}            ranges={[{ label: "30 days", count: 30 }, { label: "90 days", count: 90 }, { label: "All", count: 0 }]}/></div>        <div id="touch-chart-holes"><TouchChart title="With holes" points={[5, 6, "broken", null, 7, null, 8, 9]} least={2}/></div>        <div id="touch-chart-long"><TouchChart title="Five thousand points and a spike" points={touch_chart_demo_long}/></div>        <div id="touch-chart-none"><TouchChart title="Nothing" points="none"/></div>    </div>

Source

ui/touch_chart.qwe
// TouchChart: a line chart for a thumb. A finger along it stops on any point, with a// crosshair and the value in a label that keeps clear of the edges, and the page still// scrolls up and down under a passing finger. Two fingers pinch to a range and slide it, and// the scale fits what is in view. Range buttons, zoom buttons and keys do the same, and a// table holds what is in view for screen readers.////   <TouchChart title="Daily active people" unit="k" points={[//       { label: "1 Mar", value: 41.2 },//       { label: "2 Mar", value: 43.8 },//   ]} ranges={[{ label: "7 days", count: 7 }, { label: "30 days", count: 30 }, { label: "All", count: 0 }]}/>//// points: each { label, value }, or a plain number. A value that is not a number is a hole,// over which the line lifts. ranges: buttons, each { label, count }, showing the last count// points, 0 being all of them. least: the fewest points a pinch comes down to (5). prefix,// unit: around a value ("$", "k"), digits: decimals shown (1). title. on_select(info): inside// one of your components, when a finger lifts or a key moves, with { index, label, value }, or// null on clearing. on_range(from, to): when the range changes, with the first and last// index in view. class.//// Keys, on the focused chart: left and right move from point to point, Home and End go to// the ends of the range, + and - zoom, Page Up and Down slide the range, Escape clears.// An upright pinch reads as a scroll on some phones, as touch-action keeps the page's// up and down: a pinch across the chart is the one that always works.import ui/theme.qwe use ui_clslet touch_chart_css = css`.ui-tchart { position: relative; container-type: inline-size; color: var(--ui-fg); font: 15px/1.4 var(--ui-font); }.ui-tchart-title { margin: 0; font-size: 13px; font-weight: 600; color: var(--ui-muted); }.ui-tchart-span { margin: 0 0 8px; color: var(--ui-muted); font-size: 14px; font-variant-numeric: tabular-nums; }.ui-tchart-body { display: grid; gap: 10px; }.ui-tchart-main { min-width: 0; }.ui-tchart-plot { position: relative; height: 240px; overflow: hidden; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); outline: none; cursor: crosshair;  touch-action: pan-y; overscroll-behavior-x: contain; -webkit-tap-highlight-color: transparent; }.ui-tchart-plot:focus-visible { box-shadow: var(--ui-focus); }.ui-tchart-busy { user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }.ui-tchart-plot svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; transform-origin: 0 0; }.ui-tchart-line { fill: none; stroke: var(--ui-accent); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }.ui-tchart-grid { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid var(--ui-border); pointer-events: none; }.ui-tchart-grid span { position: absolute; left: 4px; top: 1px; color: var(--ui-muted); font-size: 11px; font-variant-numeric: tabular-nums; }/* As wide as the plot, the line at its left edge, so a percentage of its own width is a place on the plot at any size. */.ui-tchart-cross { position: absolute; inset: 0; pointer-events: none; }.ui-tchart-cross::before { content: ""; position: absolute; inset: 0 auto 0 -1px; width: 2px; background: var(--ui-fg); }.ui-tchart-pt { position: absolute; left: -7px; width: 14px; height: 14px; margin-top: -7px; border: 3px solid var(--ui-surface); border-radius: 50%; background: var(--ui-accent); }/* left: X% with translateX(-X%) keeps the label inside the plot at both ends, at any size. */.ui-tchart-tip { position: absolute; top: 6px; max-width: 70%; padding: 4px 8px; border-radius: var(--ui-radius-sm); background: var(--ui-fg); color: var(--ui-bg); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; pointer-events: none; overflow: hidden; text-overflow: ellipsis; }.ui-tchart-pinching .ui-tchart-cross, .ui-tchart-pinching .ui-tchart-tip { visibility: hidden; }.ui-tchart-axis { display: flex; justify-content: space-between; gap: 8px; margin-top: 2px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-tchart-keys { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }.ui-tchart-key { flex: 1 1 auto; min-width: 44px; min-height: 44px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg);  font: 500 14px/1 var(--ui-font); cursor: pointer; touch-action: manipulation; white-space: nowrap; }.ui-tchart-key[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent); }.ui-tchart-key:disabled { opacity: .45; cursor: default; }.ui-tchart-key:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-tchart-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; }.ui-tchart-stats div { min-width: 0; }.ui-tchart-stats dt { color: var(--ui-muted); font-size: 12px; }.ui-tchart-stats dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }.ui-tchart-none { margin: 0; color: var(--ui-muted); }@container (min-width: 620px) {  .ui-tchart-body { grid-template-columns: minmax(0, 1fr) 170px; gap: 24px; }  .ui-tchart-plot { height: 340px; }  .ui-tchart-stats { grid-template-columns: minmax(0, 1fr); align-content: start; gap: 18px; }  .ui-tchart-stats dd { font-size: 20px; }}@media (hover: hover) { .ui-tchart-key:not(:disabled):hover { background: var(--ui-subtle); } }`// The points to draw, at most 20000: a value is a real number or null, a label always text.fn touch_chart_clean(points) {    if type_of(points) != "list" { return [] }    for [i, p] in enumerate(take(points, 20000)) => {        let m = type_of(p) == "map"        let v = m ? get(p, "value") : p        let t = type_of(v)        let ok = (t == "int" || t == "float" || t == "dec") && float(v) == float(v) && abs(float(v)) < 1000000000000000        { i, label: m && get(p, "label") != null ? str(p.label) : `${i + 1}`, value: ok ? float(v) : null }    }}fn touch_chart_show(v, prefix: str, unit: str, digits: int) -> str {    if v == null { return "no value" }    let r = round(abs(v), digits)    let text = r == floor(r) ? str(floor(r)) : str(r)    `${v < 0 ? "-" : ""}${prefix}${text}${unit != "" ? " " + unit : ""}`}// The range a pinch comes to. scale is how much further apart the fingers are, share where// they began across the chart (0 to 1), and shift how far they slid, in widths: what was under// the fingers stays under them, and it stays inside the data and no closer than least.fn touch_chart_zoom(a0: num, b0: num, n: int, scale: num, share: num, shift: num, least: int) {    let top = n - 1    let floor_span = least < 1 ? 1 : (least > top ? top : least)    let span0 = b0 - a0    let wanted = span0 / (scale < 0.01 ? 0.01 : scale)    let span = wanted < floor_span ? floor_span : (wanted > top ? top : wanted)    let start = a0 + share * span0 - (share + shift) * span    let a = start < 0 ? 0.0 : (start > top - span ? top - span : start)    { from: a, to: a + span }}// Whole points, for what is kept once the fingers lift.fn touch_chart_whole(r) {    let a = round(r.from)    { from: a, to: a + round(r.to - r.from) }}// The last count points, or all.fn touch_chart_last(n: int, count: int) {    if count <= 0 || count >= n { return { from: 0, to: n - 1 } }    { from: n - count, to: n - 1 }}// The scale for what is in view: a little room around it, and something to show when flat.fn touch_chart_scale(vals) {    if len(vals) == 0 { return { lo: 0, hi: 1 } }    let a = min(vals)    let b = max(vals)    let wide = b - a    let room = wide > 0 ? wide * 0.1 : (a != 0 ? abs(a) * 0.1 : 1)    { lo: a - room, hi: b + room }}fn touch_chart_y(v: num, lo: num, hi: num) -> float = round((hi - v) * 300.0 / (hi - lo), 2)fn touch_chart_x(i: num, a: num, b: num) -> float = b > a ? round((i - a) * 1000.0 / (b - a), 2) : 500.0// A long range has more points than the plot has pixels: each stretch of it gives only// its lowest and highest, so a spike is never lost.fn touch_chart_thin(rows) {    let n = len(rows)    if n <= 600 { return rows }    let each = ceil(n / 300)    let chunks = for k in range(0, ceil(n / each)) => slice(rows, k * each, (k + 1) * each)    flatten(for c in chunks => {        let vals = c | filter((r) => r.value != null)        if len(vals) == 0 { [c[0]] } else {            let hi = reduce(vals, vals[0], (m, r) => r.value > m.value ? r : m)            let lo = reduce(vals, vals[0], (m, r) => r.value < m.value ? r : m)            lo.i == hi.i ? [lo] : (lo.i < hi.i ? [lo, hi] : [hi, lo])        }    })}// The line, lifted over a hole. A lone point between two holes gets a dot, or it would not show.fn touch_chart_path(rows, a: num, b: num, lo: num, hi: num) -> str {    let n = len(rows)    let parts = for [k, r] in enumerate(rows) => {        if r.value == null { "" } else {            let start = k == 0 || rows[k - 1].value == null            let alone = start && (k == n - 1 || rows[k + 1].value == null)            `${start ? "M" : "L"}${touch_chart_x(r.i, a, b)} ${touch_chart_y(r.value, lo, hi)}${alone ? "h.01" : ""}`        }    }    join(parts, "")}// Lines at round numbers.fn touch_chart_ticks(lo: num, hi: num) {    let wide = hi - lo    if wide <= 0 { return [] }    let rough = wide / 4    let power = pow(10, floor(math.log10(rough)))    let unit = rough / power    let step = (unit <= 1 ? 1 : (unit <= 2 ? 2 : (unit <= 5 ? 5 : 10))) * power    let first = ceil(lo / step)    let n = floor(hi / step) - first + 1    for i in range(0, n > 8 ? 8 : (n < 0 ? 0 : n)) => round((first + i) * step, 6)}// The nearest point to a share of the chart's width.fn touch_chart_at(share: num, a: int, b: int) -> int {    let s = share < 0 ? 0 : (share > 1 ? 1 : share)    a + round(s * (b - a))}// Low, high and how far it moved, in what is in view.fn touch_chart_stats(rows) {    let vals = rows | filter((r) => r.value != null) | map((r) => r.value)    if len(vals) == 0 { return null }    { low: min(vals), high: max(vals), change: last(vals) - first(vals) }}component TouchChart(points = [], title: str? = null, ranges = null, least: int = 5, prefix: str = "", unit: str = "", digits: int = 1, on_select = null, on_range = null, class: str? = null) {    style global touch_chart_css    let rows = touch_chart_clean(points)    let n = len(rows)    let chips = type_of(ranges) == "list" ? ranges | filter((r) => type_of(r) == "map" && filled(get(r, "label")) != null) : []    state from = 0    state to = n - 1    state pick = null        // the point chosen    state busy = false    state pinching = false    state g = null           // one finger    state pin = null         // two fingers: { a, b, x0, y0, x1, y1, ... }    state plot = null    state cross = null    state point = null    state tip = null    state svg = null    state span_el = null    let at_max = from == 0 && to == n - 1    let view_rows = n > 0 ? slice(rows, from, to + 1) : []    let vals = view_rows | filter((r) => r.value != null) | map((r) => r.value)    let sc = touch_chart_scale(vals)    let stats = touch_chart_stats(view_rows)    let say = (r) => `${r.label}: ${touch_chart_show(r.value, prefix, unit, digits)}`    let range_text = (a: int, b: int) => n > 0 ? `${rows[a].label} to ${rows[b].label}` : ""    // By hand, as these change with every move of a finger.    let show_it = (i) => {        if cross != null && i != null {            let r = rows[i]            let pct = touch_chart_x(i, from, to) / 10            cross.style.transform = `translateX(${pct}%)`            point.hidden = r.value == null            if r.value != null { point.style.top = `${touch_chart_y(r.value, sc.lo, sc.hi) / 3}%` }            tip.style.left = `${pct}%`            tip.style.transform = `translateX(${0 - pct}%)`            tip.textContent = say(r)        }    }    let share_of = (x: num) => {        let box = plot.getBoundingClientRect()        (x - box.left) / box.width    }    let choose = (i) => {        pick = i        if on_select != null {            on_select(i == null ? null : { index: i, label: rows[i].label, value: rows[i].value })        }    }    let settle = (a: int, b: int) => {        svg.style.transform = ""        span_el.textContent = range_text(a, b)        if a != from || b != to {            from = a            to = b            if pick != null && (pick < a || pick > b) { pick = null }            on_range && on_range(a, b)        }    }    let zoom_by = (scale: num, shift: num) => {        let middle = pick != null ? (pick - from) / (to - from) : 0.5        let r = touch_chart_whole(touch_chart_zoom(from, to, n, scale, middle, shift, least))        settle(r.from, r.to)    }    let pin_range = () => {        let dist = sqrt(pow(pin.x1 - pin.x0, 2) + pow(pin.y1 - pin.y0, 2))        let mid = (pin.x0 + pin.x1) / 2        let width = plot.getBoundingClientRect().width        touch_chart_zoom(pin.a0, pin.b0, n, dist / pin.dist, pin.share, (mid - pin.mid) / width, least)    }    // Follows the choice, the range and new data, but not under a finger, which has the line for now.    if cross != null && pick != null && from >= 0 && to >= 0 && !busy { show_it(pick) }    <section class={ui_cls(["ui-tchart", class])} aria-label={title ?? "Chart"}>        {filled(title) != null && <p.ui-tchart-title>{title}</p>}        {n > 0 && <p.ui-tchart-span @mount={(me) => { span_el = me }}>{range_text(from, to)}</p>}        {n == 0 && <p.ui-tchart-none>Nothing to show yet.</p>}        {n > 0 && <div.ui-tchart-body>            <div.ui-tchart-main>                <div class={ui_cls(["ui-tchart-plot", busy && "ui-tchart-busy", pinching && "ui-tchart-pinching"])} tabindex="0" role="slider" aria-label={`${title ?? "Chart"}: a point`}                     aria-valuemin={from + 1} aria-valuemax={to + 1} aria-valuenow={pick != null ? pick + 1 : from + 1}                     aria-valuetext={pick != null ? say(rows[pick]) : "Nothing chosen. Use the arrow keys."}                     @mount={(me) => { plot = me }}                     @contextmenu={(e) => { if busy { e.preventDefault() } }}                     @pointerdown={(e) => {                         if pin == null && g != null && e.pointerId != g.id && n > 2 {                             // The second finger: a pinch, and whatever the first was reading is put back.                             plot.setPointerCapture(g.id)                             plot.setPointerCapture(e.pointerId)                             pin = { id0: g.id, id1: e.pointerId, x0: g.lx, y0: g.ly, x1: e.clientX, y1: e.clientY, a0: from, b0: to,                                     dist: 1, mid: 0, share: 0 }                             let dist = sqrt(pow(pin.x1 - pin.x0, 2) + pow(pin.y1 - pin.y0, 2))                             let mid = (pin.x0 + pin.x1) / 2                             pin = { ...pin, dist: dist < 1 ? 1 : dist, mid, share: share_of(mid) }                             g = null                             busy = true                             pinching = true                             cross.hidden = pick == null                             tip.hidden = pick == null                             show_it(pick)                         } else if pin == null && g == null && e.isPrimary && e.button == 0 {                             g = { id: e.pointerId, x: e.clientX, y: e.clientY, lx: e.clientX, ly: e.clientY, on: false }                         }                     }}                     @pointermove={(e) => {                         if pin != null {                             if e.pointerId == pin.id0 { pin = { ...pin, x0: e.clientX, y0: e.clientY } }                             if e.pointerId == pin.id1 { pin = { ...pin, x1: e.clientX, y1: e.clientY } }                             if e.pointerId == pin.id0 || e.pointerId == pin.id1 {                                 let r = pin_range()                                 let sx = (pin.b0 - pin.a0) / (r.to - r.from)                                 let tx = (pin.a0 - r.from) * 100 * sx / (pin.b0 - pin.a0)                                 // What the fingers do shows at once on what is drawn; the true scale comes on lifting.                                 svg.style.transform = `translateX(${tx}%) scaleX(${sx})`                                 span_el.textContent = range_text(round(r.from), round(r.to))                             }                         } else if g != null && e.pointerId == g.id {                             g = { ...g, lx: e.clientX, ly: e.clientY }                             if !g.on {                                 let dx = abs(e.clientX - g.x)                                 let dy = abs(e.clientY - g.y)                                 // Up and down is the page's.                                 if dy > 6 && dy > dx { g = null }                                 if g != null && dx > 6 && dx >= dy {                                     plot.setPointerCapture(e.pointerId)                                     busy = true                                     g = { ...g, on: true }                                 }                             }                             if g != null && g.on {                                 cross.hidden = false                                 tip.hidden = false                                 show_it(touch_chart_at(share_of(e.clientX), from, to))                             }                         }                     }}                     @pointerup={(e) => {                         if pin != null && (e.pointerId == pin.id0 || e.pointerId == pin.id1) {                             let r = touch_chart_whole(pin_range())                             pin = null                             busy = false                             pinching = false                             settle(r.from, r.to)                         } else if g != null && e.pointerId == g.id {                             let x = e.clientX                             g = null                             busy = false                             choose(touch_chart_at(share_of(x), from, to))                         }                     }}                     @pointercancel={(e) => {                         if pin != null && (e.pointerId == pin.id0 || e.pointerId == pin.id1) {                             pin = null                             busy = false                             pinching = false                             settle(from, to)                         } else if g != null && e.pointerId == g.id {                             g = null                             busy = false                             cross.hidden = pick == null                             tip.hidden = pick == null                             show_it(pick)                         }                     }}                     @keydown={(e) => {                         let k = e.key                         if k == "Escape" && pick != null { choose(null) }                         let i = pick ?? from - 1                         let to_i = match k {                             "ArrowRight" => i + 1                             "ArrowLeft" => pick == null ? to : i - 1                             "Home" => from                             "End" => to                             _ => null                         }                         if to_i != null {                             e.preventDefault()                             choose(to_i < from ? from : (to_i > to ? to : to_i))                         }                         if k == "+" || k == "=" { e.preventDefault(); zoom_by(2, 0) }                         if k == "-" { e.preventDefault(); zoom_by(0.5, 0) }                         if k == "PageDown" { e.preventDefault(); zoom_by(1, 0 - 0.5) }                         if k == "PageUp" { e.preventDefault(); zoom_by(1, 0.5) }                     }}>                    {for t in touch_chart_ticks(sc.lo, sc.hi) => <div.ui-tchart-grid key={t} style={`top: ${touch_chart_y(t, sc.lo, sc.hi) / 3}%`}><span>{touch_chart_show(t, prefix, "", digits)}</span></div>}                    <svg viewBox="0 0 1000 300" preserveAspectRatio="none" aria-hidden="true" @mount={(me) => { svg = me }}>                        <path.ui-tchart-line d={touch_chart_path(touch_chart_thin(view_rows), from, to, sc.lo, sc.hi)}/>                    </svg>                    <div.ui-tchart-cross hidden={pick == null} @mount={(me) => { cross = me }}>                        <i.ui-tchart-pt @mount={(me) => { point = me }}></i>                    </div>                    <div.ui-tchart-tip hidden={pick == null} aria-hidden="true" @mount={(me) => { tip = me }}></div>                </div>                <div.ui-tchart-axis aria-hidden="true"><span>{rows[from].label}</span><span>{rows[to].label}</span></div>                <div.ui-tchart-keys role="group" aria-label="Range">                    {for [i, c] in enumerate(chips) => <button.ui-tchart-key key={i} type="button"                        aria-pressed={touch_chart_last(n, get(c, "count", 0)).from == from && to == n - 1 ? "true" : "false"}                        @click={() => {                            let r = touch_chart_last(n, get(c, "count", 0))                            settle(r.from, r.to)                        }}>{c.label}</button>}                    <button.ui-tchart-key.ui-tchart-in type="button" aria-label="Zoom in" disabled={to - from <= least} @click={() => { zoom_by(2, 0) }}>+</button>                    <button.ui-tchart-key.ui-tchart-out type="button" aria-label="Zoom out" disabled={at_max} @click={() => { zoom_by(0.5, 0) }}>-</button>                    <button.ui-tchart-key.ui-tchart-back type="button" aria-label="Earlier" disabled={from == 0} @click={() => { zoom_by(1, 0.5) }}>Earlier</button>                    <button.ui-tchart-key.ui-tchart-on type="button" aria-label="Later" disabled={to == n - 1} @click={() => { zoom_by(1, 0 - 0.5) }}>Later</button>                </div>            </div>            <dl.ui-tchart-stats>                <div><dt>Low</dt><dd>{stats != null ? touch_chart_show(stats.low, prefix, unit, digits) : "None"}</dd></div>                <div><dt>High</dt><dd>{stats != null ? touch_chart_show(stats.high, prefix, unit, digits) : "None"}</dd></div>                <div><dt>Change</dt><dd>{stats != null ? touch_chart_show(stats.change, prefix, unit, digits) : "None"}</dd></div>            </dl>        </div>}        <div.ui-sr><table>            <caption>{title ?? "Chart"}, {n > 0 ? range_text(from, to) : "no data"}{len(view_rows) > 400 ? ", the first 400 points" : ""}</caption>            <thead><tr><th scope="col">Point</th><th scope="col">Value</th></tr></thead>            <tbody>{for r in take(view_rows, 400) => <tr key={r.i}><td>{r.label}</td><td>{touch_chart_show(r.value, prefix, unit, digits)}</td></tr>}</tbody>        </table></div>    </section>}
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.