Skip to main content

ui/
chart.rs

1// chart -- small charts for dashboards: sparkline, bar chart (grouped or stacked), donut,
2// legend, and line charts of time series (line_chart over SeriesBuf rings).  Colours: 0 =
3// the style's chart palette (StyleData::chart_palette) by index.
4//
5//   sparkline(id, values, color)          area + line, natural 120 x 32 DIP (next_size)
6//   bar_chart(id, labels, series, stacked) -> hovered column or -1
7//                                         value grid with labels, one column per label,
8//                                         natural 360 x 180 DIP; hovering a column shows a
9//                                         tooltip with its values.  Labels never overlap:
10//                                         where they do not fit side by side every k-th one
11//                                         is shown (the first and the last kept; a text
12//                                         record each), a low plot gets fewer value steps
13//   donut(id, slices, center, sub)        ring of slices with a centre text, natural 160 DIP
14//                                         (texts kept inside the hole)
15//   legend(items)                         colour dot + label per item, wrapping row (an
16//                                         item wider than the row: its label cut with "…")
17//   line_chart(id, &LineChart) -> Option<(series, x)>
18//        series of (x, value) points (LcSeries over a SeriesBuf: label, colour or palette
19//        index, line or area, LcUnit) over time -- LX_SECS seconds or LX_NANOS nanoseconds,
20//        ticks 0:05 / 1:00 / 1:30 (h:mm:ss from an hour on, decimals of the second below
21//        one) -- or plain numbers (LX_NUM).  Natural 480 x 200 DIP for the plot with its
22//        axes, the legend (legend(), the series' labels) below it; next_size sizes both.
23//        The y axis: "nice" ticks (1 / 2 / 2.5 / 5 x 10^k) labelled in the first series'
24//        unit; y_range fixed or automatic per end (NaN: from the visible values and the
25//        thresholds, out to the next tick).  The x range: x_range, else the data's --
26//        follow live with x_span (the newest x_span; LineChart::view() is what it shows).
27//        Grid lines at the ticks; markers (LcMarker: a line or a shaded range with a short
28//        label in a tag at the top: fault injections) and thresholds (LcThreshold: a dashed
29//        line with its label in a tag at its right end).  Tags never overlap: a marker's tag
30//        takes the first of up to three rows with room, else it is left out (the hover card
31//        still names the markers under the pointer).  Hovering the plot: a crosshair at the
32//        nearest point, a dot on every series and a card beside it at once (the style's
33//        tooltip surface) with the x and every series' value there; the return value is the
34//        series nearest to the pointer and that x.  Keys (the plot takes the focus: Tab, a
35//        click puts the cursor on the point under the pointer): Left / Right the previous /
36//        next point of the cursor's series, Up / Down another series, Home / End, Esc ends
37//        it; crosshair and card show the cursor, the record's label starts with its point.
38//        Up to two points per pixel column the points themselves are drawn, more as one
39//        band per series (per column the range of its values, SeriesBuf::columns), so
40//        100 000 points draw like 500.  No points at all: LineChart::empty in the middle
41//        ("Keine Daten" in the UI language).  High
42//        contrast: the scheme's highlight, text and link colours with a dash pattern per
43//        series (the legend shows line samples), no translucent fills.  The plot keeps its
44//        left-to-right order in mirrored windows (ltr_begin); the legend is mirrored.
45//        Records: K_CANVAS id, its label a summary (the name, then per series its last
46//        value, min. and max. in the view; value: the number of series); K_TEXT per axis
47//        label, tag and hover card row ("p95: 48 ms").  Flags LC_NO_LEGEND, LC_NO_GRID,
48//        LC_NO_HOVER.
49//   SeriesBuf::new(capacity)              the ring a series draws from: push(t, v) (times
50//                                         never decrease; a value that is not finite is a
51//                                         gap), clear, len / get / first / last / iter,
52//                                         lower_bound(t), nearest(t), min_max /
53//                                         min_max_in(t0, t1), columns(t0, t1, n) (per pixel
54//                                         column min / max / first / last), ver()
55//   LcUnit::Num(sfx) / Si(unit) / Secs    how values read: text(v, sep) ("48,2 ms", "1,25k
56//                                         req/s", "1,5 MB"), ticks(first, step, n, sep)
57//   lc_nice(lo, hi, steps), lc_time_step(min), lc_time_text(t, dec, hours, sep)
58//                                         the axis maths: tick steps, clock labels
59//
60// Area, line and ring are vector paths filled at render time (cmd_custom); their drawing
61// object is kept per id and replaced only when the data or the size change, so an unchanged
62// chart does not redraw (dirty.rs hashes the object's identity and version).  A line chart
63// keeps its statistics, summary and plot per id, keyed by the series' versions and the
64// view; its crosshair, dots and card are ordinary commands over the plot.
65
66use alloc::collections::{BTreeMap, VecDeque};
67use alloc::format;
68use alloc::rc::Rc;
69use alloc::string::String;
70use alloc::vec::Vec;
71use core::sync::atomic::{AtomicU32, Ordering};
72
73use crate::cmd::{Draw, DrawRef, Gfx};
74use crate::consts::*;
75use crate::i18n::tr;
76use crate::keys::*;
77use crate::stock::{card_opaque, sincos, text_at};
78use crate::style::Sp;
79use crate::theme::color_mix;
80use crate::value::fmt_f64_sep;
81use crate::Ui;
82use gfx::dash::{dash_pattern, dash_stroke, DStyle, DASH_DASH, DASH_DASHDOT, DASH_DOT, DASH_SOLID, LCAP_FLAT};
83use gfx::path::Path;
84
85/// one series of a bar chart
86#[derive(Clone, Copy, Debug)]
87pub struct BarSeries<'a> {
88    pub label: &'a str,
89    pub values: &'a [f32],
90    /// 0xAARRGGBB, 0 = palette colour of the series index
91    pub color: u32,
92}
93
94/// one slice of a donut
95#[derive(Clone, Copy, Debug)]
96pub struct DonutSlice<'a> {
97    pub label: &'a str,
98    pub value: f32,
99    /// 0xAARRGGBB, 0 = palette colour of the slice index
100    pub color: u32,
101}
102
103/// shared state: the drawing objects of the charts by id (hash, object), the line charts'
104/// states by id
105#[derive(Default)]
106pub(crate) struct S {
107    cache: BTreeMap<u32, (u64, DrawRef)>,
108    lines: BTreeMap<u32, LcState>,
109}
110
111struct Fnv(u64);
112impl Fnv {
113    fn new() -> Fnv {
114        Fnv(0xCBF2_9CE4_8422_2325)
115    }
116    fn u32(&mut self, v: u32) {
117        for b in v.to_le_bytes() {
118            self.0 ^= b as u64;
119            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01B3);
120        }
121    }
122    fn f32(&mut self, v: f32) {
123        self.u32(v.to_bits());
124    }
125    fn u64(&mut self, v: u64) {
126        self.u32(v as u32);
127        self.u32((v >> 32) as u32);
128    }
129    fn f64(&mut self, v: f64) {
130        self.u64(v.to_bits());
131    }
132    fn str(&mut self, s: &str) {
133        for b in s.bytes() {
134            self.0 ^= b as u64;
135            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01B3);
136        }
137        self.u32(s.len() as u32);
138    }
139}
140
141// the line + area of a sparkline (points in 0..1 of the rectangle)
142struct Spark {
143    pts: Vec<(f32, f32)>,
144    line: u32,
145    area: u32,
146    width: i32,
147}
148
149impl Draw for Spark {
150    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
151        if self.pts.len() < 2 {
152            return;
153        }
154        let (x0, y0, w, h) = (r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32);
155        let m = self.width as f32 / 256.0;
156        let px = |p: (f32, f32)| (x0 + p.0 * w, y0 + m + (1.0 - p.1) * (h - m * 2.0));
157        // area down to the bottom
158        let path = &mut g.text.path;
159        path.reset();
160        path.set_origin(0, 0);
161        let (sx, _) = px(self.pts[0]);
162        path.move_f(sx, y0 + h);
163        for &p in &self.pts {
164            let (x, y) = px(p);
165            path.line_f(x, y);
166        }
167        let (ex, _) = px(self.pts[self.pts.len() - 1]);
168        path.line_f(ex, y0 + h);
169        path.close();
170        g.dc.set_color(self.area);
171        path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
172        // the line
173        let path = &mut g.text.path;
174        path.reset();
175        path.set_origin(0, 0);
176        for (i, &p) in self.pts.iter().enumerate() {
177            let (x, y) = px(p);
178            if i == 0 {
179                path.move_f(x, y);
180            } else {
181                path.line_f(x, y);
182            }
183        }
184        g.dc.set_color(self.line);
185        path.stroke(&mut g.text.ras, g.dc, g.sf, self.width, gfx::JOIN_ROUND | gfx::CAP_ROUND << 4);
186    }
187}
188
189// the slices of a donut: (start fraction, end fraction, colour), ring thickness 0..1 of the
190// radius, the track colour
191struct Ring {
192    slices: Vec<(f32, f32, u32)>,
193    thick: f32,
194    track: u32,
195    gap: f32,
196}
197
198fn ring_path(g: &mut Gfx, cx: f32, cy: f32, ro: f32, ri: f32, a0: f32, a1: f32) {
199    let path = &mut g.text.path;
200    path.reset();
201    path.set_origin(0, 0);
202    let span = a1 - a0;
203    let n = ((span * 48.0) as i32).clamp(2, 96);
204    let tau = core::f32::consts::TAU;
205    for k in 0..=n {
206        let a = (a0 + span * k as f32 / n as f32) * tau - tau / 4.0;
207        let (s, c) = sincos(a);
208        if k == 0 {
209            path.move_f(cx + c * ro, cy + s * ro);
210        } else {
211            path.line_f(cx + c * ro, cy + s * ro);
212        }
213    }
214    for k in (0..=n).rev() {
215        let a = (a0 + span * k as f32 / n as f32) * tau - tau / 4.0;
216        let (s, c) = sincos(a);
217        path.line_f(cx + c * ri, cy + s * ri);
218    }
219    path.close();
220}
221
222impl Draw for Ring {
223    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
224        let d = r[2].min(r[3]) as f32;
225        let cx = r[0] as f32 + r[2] as f32 / 2.0;
226        let cy = r[1] as f32 + r[3] as f32 / 2.0;
227        let ro = d / 2.0 - 1.0;
228        let ri = ro * (1.0 - self.thick);
229        ring_path(g, cx, cy, ro, ri, 0.0, 1.0);
230        g.dc.set_color(self.track);
231        g.text.path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
232        for &(a0, a1, c) in &self.slices {
233            let (a0, a1) = if self.slices.len() > 1 { (a0 + self.gap / 2.0, a1 - self.gap / 2.0) } else { (a0, a1) };
234            if a1 <= a0 {
235                continue;
236            }
237            ring_path(g, cx, cy, ro, ri, a0, a1);
238            g.dc.set_color(c);
239            g.text.path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
240        }
241    }
242}
243
244/// a short number for axis labels: 0, 12, 1.5k, 2.3M
245fn short_num(v: f32) -> String {
246    let a = if v < 0.0 { -v } else { v };
247    if a >= 1.0e9 {
248        format!("{:.1}G", v / 1.0e9)
249    } else if a >= 1.0e6 {
250        format!("{:.1}M", v / 1.0e6)
251    } else if a >= 1.0e4 {
252        format!("{:.0}k", v / 1.0e3)
253    } else if a >= 1.0e3 {
254        format!("{:.1}k", v / 1.0e3)
255    } else if a == (a as i64) as f32 {
256        format!("{}", v as i64)
257    } else {
258        format!("{:.1}", v)
259    }
260}
261
262/// a "nice" axis maximum and step count (at most maxn, 1 ..= 5) for the largest value
263fn nice_max(m: f32, maxn: i32) -> (f32, i32) {
264    if m <= 0.0 {
265        return (4.0, 4.min(maxn));
266    }
267    // (an infinite value -- a division by zero in the app's data -- would never end the loop
268    // below, and one near f32::MAX would give an infinite step)
269    let m = if m > 1e30 { 1e30 } else { m };
270    let mut p = 1.0f32;
271    while p * 10.0 <= m {
272        p *= 10.0;
273    }
274    while p > m {
275        p /= 10.0;
276    }
277    for f in [1.0f32, 2.0, 2.5, 5.0, 10.0] {
278        let step = p * f / 2.0;
279        let mut n = (m / step) as i32;
280        if (n as f32) * step < m {
281            n += 1;
282        }
283        if n <= maxn {
284            return (step * n.max(1) as f32, n.max(1));
285        }
286    }
287    (p * 10.0, 4.min(maxn))
288}
289
290/// the column labels shown (index, x) when every k-th one is: the first, every k-th after
291/// it and the last (in place of the k-th one before it), centred under their column (x0 +
292/// cw * i) but kept in lo .. hi; None when two of them come closer than sep
293fn axis_labels(ws: &[i32], x0: i32, cw: i32, lo: i32, hi: i32, k: usize, sep: i32) -> Option<Vec<(usize, i32)>> {
294    let n = ws.len();
295    let mut cols: Vec<usize> = (0..n).step_by(k).collect();
296    if n > 1 && cols.last() != Some(&(n - 1)) {
297        if cols.len() > 1 {
298            cols.pop();
299        }
300        cols.push(n - 1);
301    }
302    let mut out: Vec<(usize, i32)> = Vec::with_capacity(cols.len());
303    for i in cols {
304        let x = (x0 + cw * i as i32 + ((cw - ws[i]) >> 1)).min(hi - ws[i]).max(lo);
305        if let Some(&(j, px)) = out.last() {
306            if x < px + ws[j] + sep {
307                return None;
308            }
309        }
310        out.push((i, x));
311    }
312    Some(out)
313}
314
315// ------------------------------------------------------------------ time series
316
317static SERIES_VER: AtomicU32 = AtomicU32::new(1);
318
319// a version for a changed series: one counter for every SeriesBuf, so two buffers (or a
320// buffer and its clone after either changed) never share one
321fn series_ver() -> u32 {
322    SERIES_VER.fetch_add(1, Ordering::Relaxed)
323}
324
325/// a time series for line_chart: (t, value) points in a ring that keeps the newest
326/// `capacity` (a full buffer drops its oldest point on push).  Times never decrease (push
327/// refuses an earlier one, so every lookup is a binary search); a value that is not finite
328/// (NaN) is a gap where the line breaks.  ver() changes with every change: a chart redraws
329/// only then.
330#[derive(Clone, Debug)]
331pub struct SeriesBuf {
332    pts: VecDeque<(f64, f64)>,
333    cap: usize,
334    ver: u32,
335}
336
337/// one pixel column of a downsampled series (SeriesBuf::columns): the points with a finite
338/// value whose time falls into it
339#[derive(Clone, Copy, Debug, PartialEq)]
340pub struct SeriesCol {
341    /// the column (0 .. n)
342    pub col: u32,
343    /// its points with a value
344    pub n: u32,
345    pub min: f64,
346    pub max: f64,
347    /// the values of its first and its last point
348    pub first: f64,
349    pub last: f64,
350    /// a gap came before it (after the previous point with a value inside t0 .. t1): the
351    /// line breaks there; a gap inside a column starts a second entry for it
352    pub gap: bool,
353}
354
355// the smallest and the largest finite value of points (slices: plain loops, the fast path)
356fn min_max_of(parts: [&[(f64, f64)]; 2]) -> Option<(f64, f64)> {
357    let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
358    for part in parts {
359        for &(_, v) in part {
360            if v.is_finite() {
361                if v < lo {
362                    lo = v;
363                }
364                if v > hi {
365                    hi = v;
366                }
367            }
368        }
369    }
370    if lo <= hi { Some((lo, hi)) } else { None }
371}
372
373impl SeriesBuf {
374    /// an empty series that keeps the newest `capacity` points (at least one)
375    pub fn new(capacity: usize) -> SeriesBuf {
376        SeriesBuf { pts: VecDeque::new(), cap: capacity.max(1), ver: series_ver() }
377    }
378
379    /// append a point (t: the time, or any x that never decreases; v NaN: a gap); false
380    /// (nothing added) when t is not finite or earlier than the last point's
381    pub fn push(&mut self, t: f64, v: f64) -> bool {
382        if !t.is_finite() || self.pts.back().is_some_and(|p| t < p.0) {
383            return false;
384        }
385        let n = self.pts.len();
386        if n >= self.cap {
387            self.pts.pop_front();
388        } else if n == self.pts.capacity() {
389            // grow towards the capacity, never beyond it
390            self.pts.reserve_exact(n.max(16).min(self.cap - n));
391        }
392        self.pts.push_back((t, v));
393        self.ver = series_ver();
394        true
395    }
396
397    /// remove every point
398    pub fn clear(&mut self) {
399        self.pts.clear();
400        self.ver = series_ver();
401    }
402
403    /// the number of points
404    pub fn len(&self) -> usize {
405        self.pts.len()
406    }
407
408    /// no points
409    pub fn is_empty(&self) -> bool {
410        self.pts.is_empty()
411    }
412
413    /// the most points it keeps
414    pub fn capacity(&self) -> usize {
415        self.cap
416    }
417
418    /// changes with every push and clear (equal versions: equal points)
419    pub fn ver(&self) -> u32 {
420        self.ver
421    }
422
423    /// the i-th point, the oldest first
424    pub fn get(&self, i: usize) -> Option<(f64, f64)> {
425        self.pts.get(i).copied()
426    }
427
428    /// the oldest point
429    pub fn first(&self) -> Option<(f64, f64)> {
430        self.pts.front().copied()
431    }
432
433    /// the newest point
434    pub fn last(&self) -> Option<(f64, f64)> {
435        self.pts.back().copied()
436    }
437
438    /// the points, the oldest first
439    pub fn iter(&self) -> impl DoubleEndedIterator<Item = (f64, f64)> + ExactSizeIterator + '_ {
440        self.pts.iter().copied()
441    }
442
443    /// the index of the first point at t or later (len() when there is none)
444    pub fn lower_bound(&self, t: f64) -> usize {
445        self.pts.partition_point(|p| p.0 < t)
446    }
447
448    // the index after the last point at t or earlier
449    fn upper_bound(&self, t: f64) -> usize {
450        self.pts.partition_point(|p| p.0 <= t)
451    }
452
453    // the points a .. b as (at most) two slices of the ring
454    fn slices(&self, a: usize, b: usize) -> [&[(f64, f64)]; 2] {
455        let (s0, s1) = self.pts.as_slices();
456        let n0 = s0.len();
457        let b = b.min(self.pts.len()).max(a);
458        if b <= n0 {
459            [&s0[a..b], &[]]
460        } else if a >= n0 {
461            [&s1[a - n0..b - n0], &[]]
462        } else {
463            [&s0[a..], &s1[..b - n0]]
464        }
465    }
466
467    /// the index of the point with a finite value nearest to t (of two as near the earlier)
468    pub fn nearest(&self, t: f64) -> Option<usize> {
469        let i = self.lower_bound(t);
470        let after = (i..self.pts.len()).find(|&k| self.pts[k].1.is_finite());
471        let before = (0..i).rev().find(|&k| self.pts[k].1.is_finite());
472        match (before, after) {
473            (Some(b), Some(a)) => Some(if t - self.pts[b].0 <= self.pts[a].0 - t { b } else { a }),
474            (b, a) => b.or(a),
475        }
476    }
477
478    /// the smallest and the largest finite value (None without one); looks at every point
479    pub fn min_max(&self) -> Option<(f64, f64)> {
480        min_max_of(self.slices(0, self.pts.len()))
481    }
482
483    /// the smallest and the largest finite value of the points with t0 <= t <= t1
484    pub fn min_max_in(&self, t0: f64, t1: f64) -> Option<(f64, f64)> {
485        let (a, b) = (self.lower_bound(t0), self.upper_bound(t1));
486        if a >= b {
487            return None;
488        }
489        min_max_of(self.slices(a, b))
490    }
491
492    /// the points with t0 <= t <= t1 in n columns of equal width (t1 belongs to the last):
493    /// per column the range of its values, its first and its last value.  Columns without
494    /// a point with a value are left out; the work is one pass over the points, the result
495    /// at most n entries (plus one per gap)
496    pub fn columns(&self, t0: f64, t1: f64, n: usize) -> Vec<SeriesCol> {
497        let mut out: Vec<SeriesCol> = Vec::new();
498        if n == 0 || !(t1 > t0) {
499            return out;
500        }
501        let (a, b) = (self.lower_bound(t0), self.upper_bound(t1));
502        out.reserve(n.min(b.saturating_sub(a)));
503        let k = n as f64 / (t1 - t0);
504        let last = (n - 1).min(u32::MAX as usize - 1) as u32;
505        let lastf = last as f64;
506        // the column being filled in locals (c.col == u32::MAX: none yet)
507        let mut c = SeriesCol { col: u32::MAX, n: 0, min: 0.0, max: 0.0, first: 0.0, last: 0.0, gap: false };
508        let mut gap = false;
509        for part in self.slices(a, b) {
510            for &(t, v) in part {
511                if !v.is_finite() {
512                    gap = true;
513                    continue;
514                }
515                let f = (t - t0) * k;
516                let col = if f >= lastf { last } else if f > 0.0 { f as u32 } else { 0 };
517                if col == c.col && !gap {
518                    c.n += 1;
519                    if v < c.min {
520                        c.min = v;
521                    }
522                    if v > c.max {
523                        c.max = v;
524                    }
525                    c.last = v;
526                    continue;
527                }
528                if c.n > 0 {
529                    out.push(c);
530                }
531                c = SeriesCol { col, n: 1, min: v, max: v, first: v, last: v, gap };
532                gap = false;
533            }
534        }
535        if c.n > 0 {
536            out.push(c);
537        }
538        out
539    }
540}
541
542// ------------------------------------------------------------------ axis maths and labels
543
544/// how a line chart labels values (axis ticks, the hover card, the summary)
545#[derive(Clone, Copy, PartialEq, Eq, Debug)]
546#[non_exhaustive]
547pub enum LcUnit<'a> {
548    /// numbers, large ones with k / M / G / T (1,5k), then the suffix after a space
549    /// ("req/s", "%"; "" none)
550    Num(&'a str),
551    /// SI prefixes on a unit: Si("B") 950 B / 1,5 kB / 20 MB, Si("bit/s") 100 Mbit/s
552    Si(&'a str),
553    /// seconds shown as ns / µs / ms / s (latencies)
554    Secs,
555}
556
557// the scales of the units, ascending (PREFIX[0] and DURATION[3] are the unit itself)
558const PREFIX: [(f64, &str); 5] = [(1.0, ""), (1e3, "k"), (1e6, "M"), (1e9, "G"), (1e12, "T")];
559const DURATION: [(f64, &str); 4] = [(1e-9, "ns"), (1e-6, "µs"), (1e-3, "ms"), (1.0, "s")];
560static POW10F: [f64; 10] = [1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9];
561static POW10U: [u64; 10] = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000];
562
563fn abs(x: f64) -> f64 {
564    if x < 0.0 { -x } else { x }
565}
566
567fn floor_f(x: f64) -> f64 {
568    let f = x as i64 as f64;
569    if f > x { f - 1.0 } else { f }
570}
571
572fn ceil_f(x: f64) -> f64 {
573    -floor_f(-x)
574}
575
576// 10^e (exact up to 10^22)
577fn pow10(e: i32) -> f64 {
578    let mut p = 1.0f64;
579    for _ in 0..e.unsigned_abs().min(320) {
580        p *= 10.0;
581    }
582    if e < 0 { 1.0 / p } else { p }
583}
584
585// the decimals of three significant digits of x >= 0 (at most 6)
586fn sig_decimals(x: f64) -> u32 {
587    if !(x > 0.0) || x >= 100.0 {
588        0
589    } else if x >= 10.0 {
590        1
591    } else if x >= 1.0 {
592        2
593    } else {
594        let (mut d, mut s) = (2, x);
595        while s < 1.0 && d < 6 {
596            s *= 10.0;
597            d += 1;
598        }
599        d
600    }
601}
602
603// the decimals that show multiples of x (at most 9)
604fn step_decimals(x: f64) -> u32 {
605    let mut s = abs(x);
606    let mut d = 0;
607    while d < 9 && s > 0.0 && abs(s - floor_f(s + 0.5)) > s * 1e-9 {
608        s *= 10.0;
609        d += 1;
610    }
611    d
612}
613
614// a formatted number without the zeros at the end of its decimals
615fn trim_zeros(s: String, sep: u32) -> String {
616    let Some(c) = char::from_u32(sep) else { return s };
617    if !s.contains(c) {
618        return s;
619    }
620    String::from(s.trim_end_matches('0').trim_end_matches(c))
621}
622
623impl LcUnit<'_> {
624    fn scales(&self) -> &'static [(f64, &'static str)] {
625        if *self == LcUnit::Secs { &DURATION } else { &PREFIX }
626    }
627
628    // the scale of the unit itself
629    fn base(&self) -> usize {
630        if *self == LcUnit::Secs { 3 } else { 0 }
631    }
632
633    // the largest scale not above m (the smallest for less, the unit itself for 0)
634    fn scale_of(&self, m: f64) -> usize {
635        if !(m > 0.0) {
636            return self.base();
637        }
638        let s = self.scales();
639        let mut k = 0;
640        while k + 1 < s.len() && m >= s[k + 1].0 * 0.999_999_999 {
641            k += 1;
642        }
643        k
644    }
645
646    // a number (scaled already) with the unit at scale k
647    fn unit_text(&self, num: &str, k: usize) -> String {
648        let p = self.scales()[k].1;
649        match *self {
650            LcUnit::Num("") => format!("{num}{p}"),
651            LcUnit::Num(sfx) => format!("{num}{p} {sfx}"),
652            LcUnit::Si(u) if p.is_empty() && u.is_empty() => String::from(num),
653            LcUnit::Si(u) => format!("{num} {p}{u}"),
654            LcUnit::Secs => format!("{num} {p}"),
655        }
656    }
657
658    /// v as one value: three significant digits with the scale that fits ("48,2 ms",
659    /// "1,25k req/s", "12 %", "1,5 MB"); sep = the decimal separator (ui.decimal_sep());
660    /// "–" for a value that is not finite
661    pub fn text(&self, v: f64, sep: u32) -> String {
662        if !v.is_finite() {
663            return String::from("–");
664        }
665        let a = abs(v);
666        let s = self.scales();
667        let mut k = self.scale_of(a);
668        loop {
669            let x = a / s[k].0;
670            let d = sig_decimals(x);
671            let p = POW10F[d as usize];
672            let r = floor_f(x * p + 0.5) / p;
673            if r >= 1000.0 && k + 1 < s.len() {
674                k += 1;
675                continue;
676            }
677            let num = trim_zeros(fmt_f64_sep(if v < 0.0 { -r } else { r }, d, sep), sep);
678            return self.unit_text(&num, k);
679        }
680    }
681
682    /// the labels of the axis ticks first + k * step (k = 0 ..= n): one scale for all (the
683    /// largest tick's; plain numbers get k / M only from 10 000 on), the decimals the step
684    /// needs; a zero tick reads "0" with the unit ("0 ms", "0 req/s")
685    pub fn ticks(&self, first: f64, step: f64, n: u32, sep: u32) -> Vec<String> {
686        let m = abs(first).max(abs(first + step * n as f64));
687        let m = if m > 0.0 { m } else { abs(step) };
688        let k = self.scale_of(if matches!(self, LcUnit::Num(_)) { m / 10.0 } else { m });
689        let sc = self.scales()[k].0;
690        let d = step_decimals(step / sc).min(6);
691        (0..=n)
692            .map(|i| {
693                let v = first + step * i as f64;
694                if abs(v) <= abs(step) * 1e-6 {
695                    let z = if matches!(self, LcUnit::Num(_)) { self.base() } else { k };
696                    return self.unit_text("0", z);
697                }
698                self.unit_text(&fmt_f64_sep(v / sc, d, sep), k)
699            })
700            .collect()
701    }
702}
703
704/// "nice" ticks over lo ..= hi: (first, step, n) -- the step 1, 2, 2.5 or 5 x 10^k, first
705/// and first + n * step the multiples of it at or around lo and hi, n <= max(steps, 1) (at
706/// least 1; lo == hi spans |lo|, or 1)
707pub fn lc_nice(lo: f64, hi: f64, steps: u32) -> (f64, f64, u32) {
708    let m = steps.max(1) as f64;
709    let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
710    let span = hi - lo;
711    if !span.is_finite() || !lo.is_finite() {
712        return (0.0, 1.0, 1);
713    }
714    let span = if span > 0.0 { span } else if lo != 0.0 { abs(lo) } else { 1.0 };
715    let raw = span / m;
716    let mut e = 0i32;
717    while e < 300 && pow10(e + 1) <= raw {
718        e += 1;
719    }
720    while e > -300 && pow10(e) > raw {
721        e -= 1;
722    }
723    let p = pow10(e);
724    for f in [1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0, 50.0, 100.0] {
725        let step = f * p;
726        let a = floor_f(lo / step + 1e-9);
727        let b = ceil_f(hi / step - 1e-9);
728        let n = (b - a).max(1.0);
729        if n <= m {
730            return (a * step, step, n as u32);
731        }
732    }
733    let step = 100.0 * p;
734    let a = floor_f(lo / step + 1e-9);
735    (a * step, step, (ceil_f(hi / step - 1e-9) - a).max(1.0) as u32)
736}
737
738// the steps of a time axis from a second on (s): 1 2 5 10 15 30 s, 1 2 5 10 15 30 min, 1 2
739// 3 6 12 h, a day
740const CLOCK: [f64; 18] =
741    [1.0, 2.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 7200.0, 10800.0, 21600.0, 43200.0, 86400.0];
742
743/// the step of a time axis (s): the smallest of 1 / 2 / 5 x 10^k below a second (down to a
744/// nanosecond), 1 2 5 10 15 30 s, 1 2 5 10 15 30 min, 1 2 3 6 12 h, then days (1 / 2 / 5 x
745/// 10^k) that is at least `min`
746pub fn lc_time_step(min: f64) -> f64 {
747    if !(min > 1e-9) {
748        return 1e-9;
749    }
750    if min <= 1.0 {
751        for e in -9..0 {
752            for f in [1.0, 2.0, 5.0] {
753                let s = f * pow10(e);
754                if s >= min * (1.0 - 1e-12) {
755                    return s;
756                }
757            }
758        }
759        return 1.0;
760    }
761    for s in CLOCK {
762        if s >= min {
763            return s;
764        }
765    }
766    let d = min / 86400.0;
767    for e in 0..300 {
768        for f in [1.0, 2.0, 5.0] {
769            if f * pow10(e) >= d {
770                return f * pow10(e) * 86400.0;
771            }
772        }
773    }
774    min
775}
776
777/// a time t (s) as a clock: "0:05", "1:30", "12:00", with hours "1:00:00"; dec decimals of
778/// the second (up to 9) after sep (the decimal separator): "0:01,5"; negative with a minus
779pub fn lc_time_text(t: f64, dec: u32, hours: bool, sep: u32) -> String {
780    let dec = dec.min(9);
781    let p = POW10U[dec as usize];
782    let neg = t < 0.0;
783    let u = (abs(t) * p as f64 + 0.5) as u64;
784    let (whole, frac) = (u / p, u % p);
785    let mut s = String::new();
786    if neg && u != 0 {
787        s.push('-');
788    }
789    if hours {
790        s.push_str(&format!("{}:{:02}:{:02}", whole / 3600, whole / 60 % 60, whole % 60));
791    } else {
792        s.push_str(&format!("{}:{:02}", whole / 60, whole % 60));
793    }
794    if dec > 0 {
795        if let Some(c) = char::from_u32(sep) {
796            s.push(c);
797        }
798        s.push_str(&format!("{:0w$}", frac, w = dec as usize));
799    }
800    s
801}
802
803// ------------------------------------------------------------------ line chart
804
805/// x axes of a line chart (LineChart::x)
806pub const LX_SECS: u8 = 0; // time in seconds: 0:05, 1:00, 1:30
807pub const LX_NANOS: u8 = 1; // time in nanoseconds (a simulation clock), labelled the same
808pub const LX_NUM: u8 = 2; // plain numbers
809/// LineChart::flags
810pub const LC_NO_LEGEND: u32 = 1; // no legend below the plot
811pub const LC_NO_GRID: u32 = 2; // no grid lines
812pub const LC_NO_HOVER: u32 = 4; // no crosshair, card and keyboard cursor (no focus stop)
813
814/// one series of a line chart: a label, its points, colour, line or area, unit
815#[derive(Clone, Copy, Debug)]
816pub struct LcSeries<'a> {
817    pub label: &'a str,
818    pub data: &'a SeriesBuf,
819    /// 0xAARRGGBB; 0 = the chart palette's colour `pal`, without one the series' index
820    pub color: u32,
821    pub pal: Option<u8>,
822    /// a translucent fill under the line, down to zero (or to the edge of the plot)
823    pub area: bool,
824    /// how its values read (the y axis takes the first series')
825    pub unit: LcUnit<'a>,
826}
827
828impl<'a> LcSeries<'a> {
829    /// a line in the palette colour of its index, plain numbers
830    pub const fn new(label: &'a str, data: &'a SeriesBuf) -> LcSeries<'a> {
831        LcSeries { label, data, color: 0, pal: None, area: false, unit: LcUnit::Num("") }
832    }
833    /// the same series in a colour (0xAARRGGBB)
834    pub const fn color(mut self, c: u32) -> LcSeries<'a> {
835        self.color = c;
836        self
837    }
838    /// the same series in the chart palette's colour k (0 ..= 7)
839    pub const fn pal(mut self, k: u8) -> LcSeries<'a> {
840        self.pal = Some(k);
841        self
842    }
843    /// the same series as an area
844    pub const fn area(mut self) -> LcSeries<'a> {
845        self.area = true;
846        self
847    }
848    /// the same series with a unit
849    pub const fn unit(mut self, u: LcUnit<'a>) -> LcSeries<'a> {
850        self.unit = u;
851        self
852    }
853}
854
855/// a marker on the x axis: a line at t0, or the range t0 .. t1 shaded (fault injections)
856#[derive(Clone, Copy, Debug)]
857pub struct LcMarker<'a> {
858    pub t0: f64,
859    /// <= t0: a line
860    pub t1: f64,
861    /// a short label in a tag at the top of the plot ("" none)
862    pub label: &'a str,
863    /// 0xAARRGGBB, 0 = T_TEXT2
864    pub color: u32,
865}
866
867impl<'a> LcMarker<'a> {
868    /// a line at t
869    pub const fn line(t: f64, label: &'a str) -> LcMarker<'a> {
870        LcMarker { t0: t, t1: t, label, color: 0 }
871    }
872    /// the range t0 .. t1
873    pub const fn range(t0: f64, t1: f64, label: &'a str) -> LcMarker<'a> {
874        LcMarker { t0, t1, label, color: 0 }
875    }
876    /// the same marker in a colour
877    pub const fn color(mut self, c: u32) -> LcMarker<'a> {
878        self.color = c;
879        self
880    }
881}
882
883/// a threshold: a dashed horizontal line at a value with a label at its right end
884#[derive(Clone, Copy, Debug)]
885pub struct LcThreshold<'a> {
886    pub value: f64,
887    pub label: &'a str,
888    /// 0xAARRGGBB, 0 = T_TEXT2
889    pub color: u32,
890}
891
892impl<'a> LcThreshold<'a> {
893    /// a threshold at value with its label, in T_TEXT2
894    pub const fn new(value: f64, label: &'a str) -> LcThreshold<'a> {
895        LcThreshold { value, label, color: 0 }
896    }
897    /// the same threshold in a colour
898    pub const fn color(mut self, c: u32) -> LcThreshold<'a> {
899        self.color = c;
900        self
901    }
902}
903
904/// what ui.line_chart shows: the series and how (see the module header of chart.rs); build
905/// it with LineChart::new(&series) and the setters, or change the fields
906#[derive(Clone, Copy, Debug)]
907pub struct LineChart<'a> {
908    pub series: &'a [LcSeries<'a>],
909    /// the x axis: LX_SECS (default), LX_NANOS or LX_NUM
910    pub x: u8,
911    /// the visible x range (the points' units); empty (the default) = the data's
912    pub x_range: (f64, f64),
913    /// follow live: without x_range the newest x_span of the data (0 = all of it)
914    pub x_span: f64,
915    /// the y range; a NaN end (the default) is automatic: the visible values and the
916    /// thresholds, out to the next tick.  (0.0, f64::NAN): from zero (metrics)
917    pub y_range: (f64, f64),
918    pub markers: &'a [LcMarker<'a>],
919    pub thresholds: &'a [LcThreshold<'a>],
920    /// LC_*
921    pub flags: u32,
922    /// the chart's name: the start of its summary (screen readers)
923    pub name: &'a str,
924    /// the text without points ("" = "Keine Daten" in the UI language)
925    pub empty: &'a str,
926}
927
928impl<'a> LineChart<'a> {
929    /// the series over time in seconds, both ranges automatic
930    pub const fn new(series: &'a [LcSeries<'a>]) -> LineChart<'a> {
931        LineChart {
932            series,
933            x: LX_SECS,
934            x_range: (0.0, 0.0),
935            x_span: 0.0,
936            y_range: (f64::NAN, f64::NAN),
937            markers: &[],
938            thresholds: &[],
939            flags: 0,
940            name: "",
941            empty: "",
942        }
943    }
944    /// the x axis: LX_SECS, LX_NANOS or LX_NUM
945    pub const fn x_axis(mut self, x: u8) -> LineChart<'a> {
946        self.x = x;
947        self
948    }
949    /// a fixed x range (a paused view)
950    pub const fn x_range(mut self, lo: f64, hi: f64) -> LineChart<'a> {
951        self.x_range = (lo, hi);
952        self
953    }
954    /// follow live: the newest `span` of the data
955    pub const fn follow(mut self, span: f64) -> LineChart<'a> {
956        self.x_span = span;
957        self
958    }
959    /// the y range (a NaN end: automatic)
960    pub const fn y_range(mut self, lo: f64, hi: f64) -> LineChart<'a> {
961        self.y_range = (lo, hi);
962        self
963    }
964    /// markers on the x axis (lines and shaded ranges with tags)
965    pub const fn markers(mut self, m: &'a [LcMarker<'a>]) -> LineChart<'a> {
966        self.markers = m;
967        self
968    }
969    /// thresholds (dashed lines with tags)
970    pub const fn thresholds(mut self, t: &'a [LcThreshold<'a>]) -> LineChart<'a> {
971        self.thresholds = t;
972        self
973    }
974    /// LC_* flags
975    pub const fn flags(mut self, f: u32) -> LineChart<'a> {
976        self.flags = f;
977        self
978    }
979    /// the name its summary starts with
980    pub const fn name(mut self, n: &'a str) -> LineChart<'a> {
981        self.name = n;
982        self
983    }
984    /// the text of the empty chart
985    pub const fn empty(mut self, s: &'a str) -> LineChart<'a> {
986        self.empty = s;
987        self
988    }
989
990    /// the x range the chart shows (the points' units): x_range when it is set, else the
991    /// data's -- with x_span the newest x_span of it (from the first point on while the
992    /// data is shorter: it grows from the left); without points 0 .. x_span (or one second)
993    pub fn view(&self) -> (f64, f64) {
994        let (a, b) = self.x_range;
995        if a.is_finite() && b.is_finite() && b > a {
996            return (a, b);
997        }
998        let one = match self.x {
999            LX_NANOS => 1e9,
1000            _ => 1.0,
1001        };
1002        let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1003        for s in self.series {
1004            if let (Some(f), Some(l)) = (s.data.first(), s.data.last()) {
1005                lo = lo.min(f.0);
1006                hi = hi.max(l.0);
1007            }
1008        }
1009        let span = if self.x_span.is_finite() && self.x_span > 0.0 { self.x_span } else { 0.0 };
1010        let v = if lo > hi {
1011            (0.0, if span > 0.0 { span } else { one })
1012        } else if span > 0.0 {
1013            let end = hi.max(lo + span);
1014            (end - span, end)
1015        } else if hi > lo {
1016            (lo, hi)
1017        } else {
1018            (lo - one / 2.0, lo + one / 2.0)
1019        };
1020        if v.1 > v.0 { v } else { (v.0, v.0 + one) }
1021    }
1022}
1023
1024/// the state of a line chart between passes: what the view shows of each series, the
1025/// summary and the plot's drawing object, each with the key of what it was made from
1026#[derive(Default)]
1027pub(crate) struct LcState {
1028    stats_key: u64,
1029    stats: Vec<Stat>,
1030    sum_key: u64,
1031    summary: String,
1032    obj_key: u64,
1033    obj: Option<DrawRef>,
1034    // the last pass's hover (series, x bits)
1035    hover: Option<(usize, u64)>,
1036    // the keyboard's cursor: series, x of its point
1037    cursor: Option<(usize, f64)>,
1038    // the plot's rectangle in the last pass (px)
1039    rect: [i32; 4],
1040}
1041
1042// what a series shows in the view: the range of its values, its last value, its points
1043#[derive(Clone, Copy, Default)]
1044struct Stat {
1045    lo: f64,
1046    hi: f64,
1047    last: f64,
1048    has: bool,
1049    n: usize,
1050}
1051
1052fn stat_of(d: &SeriesBuf, v: (f64, f64)) -> Stat {
1053    let (a, b) = (d.lower_bound(v.0), d.upper_bound(v.1));
1054    let mut s = Stat { n: b.saturating_sub(a), ..Stat::default() };
1055    if let Some((lo, hi)) = d.min_max_in(v.0, v.1) {
1056        s.lo = lo;
1057        s.hi = hi;
1058        s.has = true;
1059        s.last = (a..b).rev().filter_map(|i| d.get(i)).map(|p| p.1).find(|x| x.is_finite()).unwrap_or(f64::NAN);
1060    }
1061    s
1062}
1063
1064// how a series is drawn: its colour, dash (high contrast), a fill under it
1065#[derive(Clone, Copy)]
1066struct Look {
1067    color: u32,
1068    dash: u32,
1069    area: bool,
1070}
1071
1072// the dash pattern of series i in high contrast (with the colour of i % 3 every series up
1073// to the 12th differs)
1074const HC_DASH: [u32; 4] = [DASH_SOLID, DASH_DASH, DASH_DOT, DASH_DASHDOT];
1075
1076// a drawn series: polylines to stroke, or bands to fill; colour, width (24.8), DASH_*
1077struct Trace {
1078    polys: Vec<Vec<(f32, f32)>>,
1079    band: bool,
1080    color: u32,
1081    width: i32,
1082    dash: u32,
1083}
1084
1085// the plot of a line chart (px relative to its top left, m inside the rectangle of its
1086// command; drawn at render time): grid, shaded ranges, areas, lines, thresholds, markers
1087struct Plot {
1088    m: i32,
1089    w: i32,
1090    h: i32,
1091    hair: i32,
1092    // grid lines: position, vertical
1093    grid: Vec<(i32, bool)>,
1094    grid_c: u32,
1095    dotted: bool,
1096    // the shaded ranges of markers: x0, x1, fill
1097    ranges: Vec<(i32, i32, u32)>,
1098    areas: Vec<(Vec<Vec<(f32, f32)>>, u32)>,
1099    traces: Vec<Trace>,
1100    // thresholds: y, colour; dashes (thickness, on, off px)
1101    limits: Vec<(i32, u32)>,
1102    lim: (i32, i32, i32),
1103    // marker lines and the edges of ranges: x, width, colour
1104    rules: Vec<(i32, i32, u32)>,
1105}
1106
1107// polylines (or polygons) moved by (ox, oy) into the path
1108fn trace_path(path: &mut Path, polys: &[Vec<(f32, f32)>], ox: f32, oy: f32, close: bool) {
1109    path.reset();
1110    path.set_origin(0, 0);
1111    for p in polys {
1112        for (k, &(x, y)) in p.iter().enumerate() {
1113            if k == 0 {
1114                path.move_f(ox + x, oy + y);
1115            } else {
1116                path.line_f(ox + x, oy + y);
1117            }
1118        }
1119        if close {
1120            path.close();
1121        }
1122    }
1123}
1124
1125impl Draw for Plot {
1126    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
1127        let (ox, oy) = (r[0] + self.m, r[1] + self.m);
1128        let (w, h, t) = (self.w, self.h, self.hair);
1129        g.dc.set_color(self.grid_c);
1130        for &(p, vert) in &self.grid {
1131            if self.dotted {
1132                // high contrast: dotted, so the grid stays behind the series
1133                let n = if vert { h } else { w };
1134                let mut k = 0;
1135                while k < n {
1136                    if vert {
1137                        g.dc.fill(g.sf, ox + p, oy + k, t, t);
1138                    } else {
1139                        g.dc.fill(g.sf, ox + k, oy + p, t, t);
1140                    }
1141                    k += t * 3;
1142                }
1143            } else if vert {
1144                g.dc.fill(g.sf, ox + p, oy, t, h);
1145            } else {
1146                g.dc.fill(g.sf, ox, oy + p, w, t);
1147            }
1148        }
1149        for &(x0, x1, c) in &self.ranges {
1150            g.dc.set_color(c);
1151            g.dc.fill(g.sf, ox + x0, oy, x1 - x0, h);
1152        }
1153        let (fx, fy) = (ox as f32, oy as f32);
1154        for (polys, c) in &self.areas {
1155            trace_path(&mut g.text.path, polys, fx, fy, true);
1156            g.dc.set_color(*c);
1157            g.text.path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
1158        }
1159        for tr in &self.traces {
1160            trace_path(&mut g.text.path, &tr.polys, fx, fy, tr.band);
1161            g.dc.set_color(tr.color);
1162            if tr.band {
1163                g.text.path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
1164            } else if tr.dash == DASH_SOLID {
1165                g.text.path.stroke(&mut g.text.ras, g.dc, g.sf, tr.width, gfx::JOIN_ROUND | gfx::CAP_ROUND << 4);
1166            } else {
1167                let st = DStyle {
1168                    width: tr.width as f64 / 256.0,
1169                    pat: dash_pattern(tr.dash),
1170                    join: gfx::JOIN_ROUND,
1171                    cap0: LCAP_FLAT,
1172                    cap1: LCAP_FLAT,
1173                    dcap: LCAP_FLAT,
1174                    offset: 0.0,
1175                    miter: 0.0,
1176                };
1177                dash_stroke(&mut g.text.path, &st);
1178                g.text.path.fill(&mut g.text.ras, g.dc, g.sf, gfx::RULE_NONZERO);
1179            }
1180        }
1181        let (lt, on, off) = self.lim;
1182        for &(y, c) in &self.limits {
1183            g.dc.set_color(c);
1184            let mut x = 0;
1185            while x < w {
1186                g.dc.fill(g.sf, ox + x, oy + y, on.min(w - x), lt);
1187                x += on + off;
1188            }
1189        }
1190        for &(x, lw, c) in &self.rules {
1191            g.dc.set_color(c);
1192            g.dc.fill(g.sf, ox + x, oy, lw, h);
1193        }
1194    }
1195}
1196
1197// a polyline of points (px, f64) into f32, its ends cut back to 64 px outside the plot
1198// (the points before and after the view may be far away)
1199fn polyline(run: &mut [(f64, f64)], w: f64) -> Vec<(f32, f32)> {
1200    const M: f64 = 64.0;
1201    let n = run.len();
1202    if n >= 2 {
1203        let (a, b) = (run[0], run[1]);
1204        if a.0 < -M && b.0 > a.0 {
1205            let f = (-M - a.0) / (b.0 - a.0);
1206            run[0] = (-M, a.1 + (b.1 - a.1) * f);
1207        }
1208        let (a, b) = (run[n - 2], run[n - 1]);
1209        if b.0 > w + M && b.0 > a.0 {
1210            let f = (w + M - a.0) / (b.0 - a.0);
1211            run[n - 1] = (w + M, a.1 + (b.1 - a.1) * f);
1212        }
1213    }
1214    if n == 1 {
1215        // a point alone: a dot (the round caps of a pixel long line)
1216        let (x, y) = run[0];
1217        return alloc::vec![((x - 0.5) as f32, y as f32), ((x + 0.5) as f32, y as f32)];
1218    }
1219    run.iter().map(|&(x, y)| (x as f32, y as f32)).collect()
1220}
1221
1222type Polys = Vec<Vec<(f32, f32)>>;
1223
1224// the lines (or bands) and the areas of a series over the view in px relative to a w x h
1225// plot whose values run from y.0 (bottom) to y.1: up to two points per pixel column the
1226// points themselves (with the one before and after the view), more as one band per run of
1227// columns (each column the range of its values, joined to the next one), lw thick; dashed
1228// bands are cut into the pattern's pieces along x.  -> (polylines or bands, band, areas)
1229#[allow(clippy::too_many_arguments)]
1230fn trace_of(d: &SeriesBuf, v: (f64, f64), (w, h): (i32, i32), y: (f64, f64), lw: f64, dense: bool, area: bool, dash: &[f64]) -> (Polys, bool, Polys) {
1231    let sx = w as f64 / (v.1 - v.0);
1232    let sy = h as f64 / (y.1 - y.0);
1233    let hf = h as f64;
1234    let fy = |val: f64| (hf - (val - y.0) * sy).clamp(-hf - 64.0, hf * 2.0 + 64.0);
1235    let base = fy(0.0f64.clamp(y.0, y.1));
1236    let mut lines: Vec<Vec<(f32, f32)>> = Vec::new();
1237    let mut areas: Vec<Vec<(f32, f32)>> = Vec::new();
1238    if !dense {
1239        let a = d.lower_bound(v.0).saturating_sub(1);
1240        let b = (d.upper_bound(v.1) + 1).min(d.len());
1241        let mut run: Vec<(f64, f64)> = Vec::new();
1242        let mut flush = |run: &mut Vec<(f64, f64)>| {
1243            if run.is_empty() {
1244                return;
1245            }
1246            let p = polyline(run, w as f64);
1247            if area && p.len() >= 2 {
1248                let mut q = p.clone();
1249                q.push((p[p.len() - 1].0, base as f32));
1250                q.push((p[0].0, base as f32));
1251                areas.push(q);
1252            }
1253            lines.push(p);
1254            run.clear();
1255        };
1256        for i in a..b {
1257            let Some((t, val)) = d.get(i) else { break };
1258            if !val.is_finite() {
1259                flush(&mut run);
1260                continue;
1261            }
1262            run.push(((t - v.0) * sx, fy(val)));
1263        }
1264        flush(&mut run);
1265        return (lines, false, areas);
1266    }
1267    let cols = d.columns(v.0, v.1, w.max(1) as usize);
1268    let half = lw / 2.0;
1269    // the dash pattern along x (px): its period and whether x is in a dash
1270    let pat: Vec<f64> = dash.iter().map(|k| k * lw).collect();
1271    let period: f64 = pat.iter().sum::<f64>() * if pat.len() % 2 == 1 { 2.0 } else { 1.0 };
1272    let on = |x: f64| -> bool {
1273        if pat.is_empty() || !(period > 0.0) {
1274            return true;
1275        }
1276        let mut p = x - floor_f(x / period) * period;
1277        let mut k = 0;
1278        loop {
1279            let len = pat[k % pat.len()];
1280            if p < len {
1281                return k % 2 == 0;
1282            }
1283            p -= len;
1284            k += 1;
1285            if k > 64 {
1286                return true;
1287            }
1288        }
1289    };
1290    let mut i = 0;
1291    while i < cols.len() {
1292        let mut j = i + 1;
1293        while j < cols.len() && !cols[j].gap {
1294            j += 1;
1295        }
1296        // columns i .. j are one run: top and bottom of each column joined to the next one
1297        let mut ext: Vec<(f64, f64, f64)> = Vec::with_capacity(j - i);
1298        for k in i..j {
1299            let c = cols[k];
1300            let (mut lo, mut hi) = (c.min, c.max);
1301            if k + 1 < j {
1302                lo = lo.min(cols[k + 1].first);
1303                hi = hi.max(cols[k + 1].first);
1304            }
1305            ext.push((c.col as f64 + 0.5, fy(hi), fy(lo)));
1306        }
1307        if area {
1308            let mut q: Vec<(f32, f32)> = Vec::with_capacity(ext.len() + 4);
1309            q.push(((ext[0].0 - 0.5) as f32, ext[0].1 as f32));
1310            q.extend(ext.iter().map(|e| (e.0 as f32, e.1 as f32)));
1311            let l = ext[ext.len() - 1];
1312            q.push(((l.0 + 0.5) as f32, l.1 as f32));
1313            q.push(((l.0 + 0.5) as f32, base as f32));
1314            q.push(((ext[0].0 - 0.5) as f32, base as f32));
1315            areas.push(q);
1316        }
1317        // the bands (one per dash piece)
1318        let mut k = 0;
1319        while k < ext.len() {
1320            if !on(ext[k].0) {
1321                k += 1;
1322                continue;
1323            }
1324            let mut e = k + 1;
1325            while e < ext.len() && on(ext[e].0) {
1326                e += 1;
1327            }
1328            let part = &ext[k..e];
1329            let mut q: Vec<(f32, f32)> = Vec::with_capacity(part.len() * 2 + 4);
1330            q.push(((part[0].0 - 0.5) as f32, (part[0].1 - half) as f32));
1331            q.extend(part.iter().map(|p| (p.0 as f32, (p.1 - half) as f32)));
1332            let l = part[part.len() - 1];
1333            q.push(((l.0 + 0.5) as f32, (l.1 - half) as f32));
1334            q.push(((l.0 + 0.5) as f32, (l.2 + half) as f32));
1335            q.extend(part.iter().rev().map(|p| (p.0 as f32, (p.2 + half) as f32)));
1336            q.push(((part[0].0 - 0.5) as f32, (part[0].2 + half) as f32));
1337            lines.push(q);
1338            k = e;
1339        }
1340        i = j;
1341    }
1342    (lines, true, areas)
1343}
1344
1345// two rectangles closer than sep
1346fn near(a: [i32; 4], b: [i32; 4], sep: i32) -> bool {
1347    a[0] < b[0] + b[2] + sep && b[0] < a[0] + a[2] + sep && a[1] < b[1] + b[3] && b[1] < a[1] + a[3]
1348}
1349
1350// an x as the hover card's title: the clock to the last decimal that is not 0 (x in s or
1351// ns), or the number
1352fn lc_x_label(x: u8, t: f64, hours: bool, sep: u32) -> String {
1353    if x == LX_NUM {
1354        return LcUnit::Num("").text(t, sep);
1355    }
1356    let s = if x == LX_NANOS { t * 1e-9 } else { t };
1357    let full = lc_time_text(s, 9, hours, sep);
1358    match char::from_u32(sep) {
1359        Some(c) => String::from(full.trim_end_matches('0').trim_end_matches(c)),
1360        None => full,
1361    }
1362}
1363
1364// where a marker ends (a line: where it starts)
1365fn mark_end(m: &LcMarker) -> f64 {
1366    if m.t1 > m.t0 { m.t1 } else { m.t0 }
1367}
1368
1369// the x of the first point with a value inside v from index i on (back: before i)
1370fn lc_step(d: &SeriesBuf, i: usize, back: bool, v: (f64, f64)) -> Option<f64> {
1371    let mut k = i;
1372    loop {
1373        if back {
1374            k = k.checked_sub(1)?;
1375        }
1376        let (t, val) = d.get(k)?;
1377        if (back && t < v.0) || (!back && t > v.1) {
1378            return None;
1379        }
1380        if val.is_finite() && t >= v.0 && t <= v.1 {
1381            return Some(t);
1382        }
1383        if !back {
1384            k += 1;
1385        }
1386    }
1387}
1388
1389impl Ui {
1390    fn chart_color(&self, c: u32, i: usize) -> u32 {
1391        if c != 0 {
1392            return c;
1393        }
1394        let p = self.style.data().chart_palette(self);
1395        p[i % 8]
1396    }
1397
1398    // the cached drawing object for id with this content hash (built by make otherwise)
1399    fn chart_obj(&mut self, id: u32, hash: u64, make: impl FnOnce() -> DrawRef) -> DrawRef {
1400        if let Some((h, d)) = self.m.chart.cache.get(&id) {
1401            if *h == hash {
1402                return d.clone();
1403            }
1404        }
1405        let d = make();
1406        if self.m.chart.cache.len() > 256 {
1407            self.m.chart.cache.clear();
1408        }
1409        self.m.chart.cache.insert(id, (hash, d.clone()));
1410        d
1411    }
1412
1413    /// ui_sparkline(id, values, color): area + line over the values (min .. max scaled)
1414    #[cfg_attr(jfgui_dev, track_caller)]
1415    pub fn sparkline(&mut self, id: u32, values: &[f32], color: u32) {
1416        let _src = self.src_scope();
1417        let r = self.place(self.dp(120), self.dp(32));
1418        let line = self.chart_color(color, 0);
1419        let area = (line & 0x00FF_FFFF) | 0x2E00_0000;
1420        let (mut lo, mut hi) = (f32::MAX, f32::MIN);
1421        for &v in values {
1422            lo = lo.min(v);
1423            hi = hi.max(v);
1424        }
1425        if hi <= lo {
1426            hi = lo + 1.0;
1427            lo -= 1.0;
1428        }
1429        let width = self.dp_fx(2 * 256).max(256);
1430        let mut h = Fnv::new();
1431        for &v in values {
1432            h.f32(v);
1433        }
1434        h.u32(line);
1435        h.u32(width as u32);
1436        let n = values.len().max(2) - 1;
1437        let hash = h.0;
1438        let d = self.chart_obj(id, hash, || {
1439            let pts = values.iter().enumerate().map(|(i, &v)| (i as f32 / n as f32, (v - lo) / (hi - lo))).collect();
1440            Rc::new(Spark { pts, line, area, width })
1441        });
1442        self.cmd_custom(r[0], r[1], r[2], r[3], Some(d), 0, hash as u32);
1443        self.wp.ctx.last_state = 0;
1444        self.sem(K_CANVAS, id, None, values.len() as i32);
1445    }
1446
1447    /// ui_bar_chart(id, labels, series, stacked) -> the hovered column (-1 = none)
1448    #[cfg_attr(jfgui_dev, track_caller)]
1449    pub fn bar_chart(&mut self, id: u32, labels: &[&str], series: &[BarSeries], stacked: bool) -> i32 {
1450        let _src = self.src_scope();
1451        let r = self.place(self.dp(360), self.dp(180));
1452        let n = labels.len();
1453        // the largest column (stacked: the sums)
1454        let mut m = 0.0f32;
1455        for i in 0..n {
1456            if stacked {
1457                let s: f32 = series.iter().map(|s| s.values.get(i).copied().unwrap_or(0.0).max(0.0)).sum();
1458                m = m.max(s);
1459            } else {
1460                for s in series {
1461                    m = m.max(s.values.get(i).copied().unwrap_or(0.0));
1462                }
1463            }
1464        }
1465        // value labels a line apart at least: fewer grid steps in a low plot (large text)
1466        let lh = self.ts_line(TS_CAPTION);
1467        let ph = r[3] - lh - self.dp(6) - (lh >> 1);
1468        let (top, steps) = nice_max(m, (ph / lh.max(1)).clamp(1, 5));
1469        let vals: Vec<String> = (0..=steps).map(|k| short_num(top * k as f32 / steps as f32)).collect();
1470        // axis label width: the widest value label
1471        self.ts_font(TS_CAPTION);
1472        let vw = vals.iter().map(|s| self.text.width(s)).max().unwrap_or(0);
1473        let lw = vw.max(self.dp(16)) + self.dp(8);
1474        let plot = [r[0] + lw, r[1] + (lh >> 1), r[2] - lw, ph];
1475        // grid lines and value labels
1476        let hair = self.hair();
1477        let grid = self.color(T_DIVIDER);
1478        let t3 = self.color(T_TEXT3);
1479        for (k, s) in vals.iter().enumerate() {
1480            let y = plot[1] + plot[3] - (plot[3] as i64 * k as i64 / steps as i64) as i32;
1481            self.draw_color(grid);
1482            self.draw_rect(plot[0], y, plot[2], hair);
1483            self.ts_font(TS_CAPTION);
1484            let tw = self.text.width(s);
1485            text_at(self, TS_CAPTION, plot[0] - tw - self.dp(6), [r[0], y - (lh >> 1), lw, lh], s, t3, 0);
1486        }
1487        if n == 0 {
1488            return -1;
1489        }
1490        // columns
1491        let cw = plot[2] / n as i32;
1492        // column labels that do not fit side by side: every k-th one (the smallest k that
1493        // keeps them apart), the first and the last kept; one alone is cut with "…"
1494        self.ts_font(TS_CAPTION);
1495        let lws: Vec<i32> = labels.iter().map(|l| self.text.width(crate::basic::cstr(l))).collect();
1496        let (lo, hi) = (plot[0], r[0] + r[2]);
1497        let sep = self.dp(4);
1498        let shown = (1..=n)
1499            .find_map(|k| axis_labels(&lws, plot[0], cw, lo, hi, k, sep))
1500            .unwrap_or_else(|| alloc::vec![(0, lo)]);
1501        let ly = plot[1] + plot[3] + self.dp(6);
1502        let gw = ((cw * 3) / 5).max(2);
1503        let ns = series.len().max(1) as i32;
1504        let mut hovered = -1;
1505        let rad = self.dp(2);
1506        let mut next = 0; // the next of `shown`
1507        for (i, label) in labels.iter().enumerate() {
1508            let cx = plot[0] + cw * i as i32;
1509            // hover area of the column
1510            let ev = self.hit_rect(id.wrapping_add(1 + i as u32), cx, plot[1], cw, plot[3] + lh + self.dp(6), 0);
1511            if ev & EV_HOVER != 0 {
1512                hovered = i as i32;
1513                let c = self.color(T_SUBTLE_HOVER);
1514                if c & 0xFF00_0000 != 0 {
1515                    self.cmd_rrect(cx + self.dp(2), plot[1], cw - self.dp(4), plot[3], self.dp(4), 0, c, 0, 0);
1516                }
1517            }
1518            let x0 = cx + ((cw - gw) >> 1);
1519            let mut base = plot[1] + plot[3];
1520            for (si, s) in series.iter().enumerate() {
1521                // (at most the axis: a value beyond what nice_max takes fills the plot)
1522                let v = s.values.get(i).copied().unwrap_or(0.0).max(0.0).min(top);
1523                let h = ((v / top) * plot[3] as f32) as i32;
1524                let c = self.chart_color(s.color, si);
1525                if stacked {
1526                    if h > 0 {
1527                        self.cmd_rrect(x0, base - h, gw, h, if si as i32 == ns - 1 { rad } else { 0 }, 0, c, 0, 0);
1528                    }
1529                    base -= h;
1530                } else {
1531                    let bw = (gw / ns).max(1);
1532                    let bx = x0 + bw * si as i32;
1533                    if h > 0 {
1534                        self.cmd_rrect(bx, base - h, (bw - hair).max(1), h, rad.min(bw / 2), 0, c, 0, 0);
1535                    }
1536                }
1537            }
1538            // column label (a text record each, value 1 = cut)
1539            let lab = crate::basic::cstr(label);
1540            if let Some(&(_, x)) = shown.get(next).filter(|s| s.0 == i) {
1541                next += 1;
1542                let room = hi - x;
1543                self.ts_font(TS_CAPTION);
1544                text_at(self, TS_CAPTION, x, [x, ly, room, lh], lab, t3, room.max(1));
1545                self.wp.ctx.last_rect = [x, ly, lws[i].min(room), lh];
1546                self.wp.ctx.last_state = 0;
1547                self.sem(K_TEXT, 0, Some(lab), (lws[i] > room) as i32);
1548            }
1549            if ev & EV_HOVER != 0 {
1550                let mut tip = String::from(lab);
1551                for s in series {
1552                    let v = s.values.get(i).copied().unwrap_or(0.0);
1553                    tip.push_str(&format!("\n{}: {}", s.label, short_num(v)));
1554                }
1555                self.tooltip(&tip);
1556            }
1557        }
1558        self.wp.ctx.last_rect = r;
1559        self.wp.ctx.last_state = 0;
1560        self.sem(K_CANVAS, id, None, n as i32);
1561        hovered
1562    }
1563
1564    /// ui_donut(id, slices, center, sub): ring of slices (fractions of their sum) with a
1565    /// centre text (TS_SUBTITLE; TS_BODY_STRONG where that is wider than the hole) and a
1566    /// caption below it (left out where the hole is too low for both)
1567    #[cfg_attr(jfgui_dev, track_caller)]
1568    pub fn donut(&mut self, id: u32, slices: &[DonutSlice], center: &str, sub: &str) {
1569        let _src = self.src_scope();
1570        let s = self.dp(160);
1571        let r = self.place(s, s);
1572        let d = r[2].min(r[3]);
1573        let sq = [r[0] + ((r[2] - d) >> 1), r[1] + ((r[3] - d) >> 1), d, d];
1574        let total: f32 = slices.iter().map(|s| s.value.max(0.0)).sum();
1575        let track = self.color(T_TRACK);
1576        let mut h = Fnv::new();
1577        let mut parts = Vec::new();
1578        let mut a = 0.0f32;
1579        for (i, sl) in slices.iter().enumerate() {
1580            let f = if total > 0.0 { sl.value.max(0.0) / total } else { 0.0 };
1581            let c = self.chart_color(sl.color, i);
1582            parts.push((a, a + f, c));
1583            h.f32(f);
1584            h.u32(c);
1585            a += f;
1586        }
1587        h.u32(track);
1588        h.u32(d as u32);
1589        let hash = h.0;
1590        let gap = if slices.len() > 1 { 0.006 } else { 0.0 };
1591        let obj = self.chart_obj(id, hash, || Rc::new(Ring { slices: parts, thick: 0.22, track, gap }));
1592        self.cmd_custom(sq[0], sq[1], sq[2], sq[3], Some(obj), 0, hash as u32);
1593        // centre texts inside the hole (large text: the centre in a smaller style, the caption
1594        // left out, else cut with "…")
1595        let t = self.color(T_TEXT);
1596        let t2 = self.color(T_TEXT2);
1597        let hole = (d as f32 * 0.78) as i32 - self.dp(8);
1598        let mut ts1 = TS_SUBTITLE;
1599        self.ts_font(ts1);
1600        if self.text.width(center) > hole {
1601            ts1 = TS_BODY_STRONG;
1602        }
1603        let l1 = self.ts_line(ts1);
1604        let cap = self.ts_line(TS_CAPTION);
1605        let sub = if sub.is_empty() || l1 + cap > hole { "" } else { sub };
1606        let l2 = if sub.is_empty() { 0 } else { cap };
1607        let y = sq[1] + ((d - l1 - l2) >> 1);
1608        self.ts_font(ts1);
1609        let w1 = self.text.width(center).min(hole);
1610        text_at(self, ts1, sq[0] + ((d - w1) >> 1), [sq[0], y, d, l1], center, t, hole.max(1));
1611        if !sub.is_empty() {
1612            self.ts_font(TS_CAPTION);
1613            let w2 = self.text.width(sub).min(hole);
1614            text_at(self, TS_CAPTION, sq[0] + ((d - w2) >> 1), [sq[0], y + l1, d, l2], sub, t2, hole.max(1));
1615        }
1616        // hover: the slice under the pointer as a tooltip
1617        let ev = self.hit_rect(id, sq[0], sq[1], sq[2], sq[3], 0);
1618        if ev & EV_HOVER != 0 && total > 0.0 {
1619            let (mx, my) = self.mouse();
1620            let dx = (mx - (sq[0] + d / 2)) as f32;
1621            let dy = (my - (sq[1] + d / 2)) as f32;
1622            let dist = rt::fx::sqrt(dx * dx + dy * dy);
1623            let ro = d as f32 / 2.0;
1624            if dist <= ro && dist >= ro * 0.78 {
1625                let ang = rt::x87::atan2(dx as f64, -dy as f64) as f32 / core::f32::consts::TAU;
1626                let ang = if ang < 0.0 { ang + 1.0 } else { ang };
1627                let mut acc = 0.0f32;
1628                for sl in slices {
1629                    let f = sl.value.max(0.0) / total;
1630                    if ang >= acc && ang < acc + f {
1631                        let tip = format!("{}: {} ({:.0} %)", sl.label, short_num(sl.value), f * 100.0);
1632                        self.tooltip(&tip);
1633                        break;
1634                    }
1635                    acc += f;
1636                }
1637            }
1638        }
1639        self.wp.ctx.last_rect = r;
1640        self.wp.ctx.last_state = 0;
1641        self.sem(K_CANVAS, id, Some(center), slices.len() as i32);
1642    }
1643
1644    /// ui_legend(items): a colour dot and a label per item (colour 0 = palette by index),
1645    /// laid out in a wrapping row; an item wider than the row has its label cut with "…"
1646    #[cfg_attr(jfgui_dev, track_caller)]
1647    pub fn legend(&mut self, items: &[(&str, u32)]) {
1648        let _src = self.src_scope();
1649        self.legend_marks(items, &[], 0);
1650    }
1651
1652    // the legend; the items with an entry in `dashes` get a line sample in that DASH_*
1653    // pattern (lw thick, 24.8) instead of the dot (line charts in high contrast)
1654    fn legend_marks(&mut self, items: &[(&str, u32)], dashes: &[u32], lw: i32) {
1655        let s = self.dp(10);
1656        let sl = self.dp(20);
1657        let (gap, igap) = (self.dp(12), self.dp(6));
1658        // the width the row gets (from the items' own widths, so a cut item does not narrow
1659        // the row it is measured against)
1660        self.ts_font(TS_CAPTION);
1661        let ws: Vec<i32> = items
1662            .iter()
1663            .enumerate()
1664            .map(|(i, &(label, _))| if i < dashes.len() { sl } else { s } + igap + self.text.width(crate::basic::cstr(label)))
1665            .collect();
1666        let one_row = ws.iter().sum::<i32>() + gap * (ws.len() as i32 - 1).max(0);
1667        let (room, _) = self.avail_width(one_row);
1668        self.box_begin(0, BOX_H | BOX_WRAP | BOX_XCENTER, 12);
1669        for (i, &(label, c)) in items.iter().enumerate() {
1670            let col = self.chart_color(c, i);
1671            let cut = room > 0 && ws[i] > room;
1672            if cut {
1673                self.next_size(self.px2dip(room), 0);
1674            }
1675            self.box_begin(0, BOX_H | BOX_XCENTER, 6);
1676            if let Some(&dash) = dashes.get(i) {
1677                let r = self.place(sl, s);
1678                self.lc_sample(r, col, dash, lw);
1679            } else {
1680                let r = self.place(s, s);
1681                self.draw_color(col);
1682                self.draw_circle((r[0] << 8) + (s << 7), (r[1] << 8) + (r[3] << 7), s << 7, 0);
1683            }
1684            if cut {
1685                self.next_size(-1, 0);
1686            }
1687            self.text(TS_CAPTION | TX_SECONDARY | TX_ELLIPSIS, label);
1688            self.box_end();
1689        }
1690        self.box_end();
1691    }
1692
1693    // a line sample across the middle of r: lw thick (24.8), in the pieces of a DASH_* pattern
1694    fn lc_sample(&mut self, r: [i32; 4], c: u32, dash: u32, lw: i32) {
1695        let t = ((lw + 128) >> 8).max(1);
1696        let y = r[1] + ((r[3] - t) >> 1);
1697        self.draw_color(c);
1698        let pat = dash_pattern(dash);
1699        if pat.is_empty() {
1700            self.draw_rect(r[0], y, r[2], t);
1701            return;
1702        }
1703        let (mut x, mut k) = (0, 0);
1704        while x < r[2] {
1705            let len = ((pat[k % pat.len()] * t as f64) as i32).max(1);
1706            if k % 2 == 0 {
1707                self.draw_rect(r[0] + x, y, len.min(r[2] - x), t);
1708            }
1709            x += len;
1710            k += 1;
1711        }
1712    }
1713
1714    // the ticks of the x axis over pw px: (x in the points' units, label); labels at least
1715    // 72 DIP apart and never closer than their width and 12 DIP
1716    fn lc_x_ticks(&mut self, x: u8, v: (f64, f64), pw: i32, sep: u32) -> Vec<(f64, String)> {
1717        self.ts_font(TS_CAPTION);
1718        let room = self.dp(12);
1719        if x == LX_NUM {
1720            let pps = pw as f64 / (v.1 - v.0);
1721            let mut steps = (pw / self.dp(72).max(1)).clamp(1, 12) as u32;
1722            loop {
1723                let (_, step, _) = lc_nice(v.0, v.1, steps);
1724                let (k0, k1) = (ceil_f(v.0 / step - 1e-9), floor_f(v.1 / step + 1e-9));
1725                if k1 < k0 || k1 - k0 > 64.0 {
1726                    return Vec::new();
1727                }
1728                let n = (k1 - k0) as u32;
1729                let labels = LcUnit::Num("").ticks(k0 * step, step, n, sep);
1730                let wmax = labels.iter().map(|s| self.text.width(s)).max().unwrap_or(0);
1731                if step * pps >= (wmax + room) as f64 || steps == 1 {
1732                    return labels.into_iter().enumerate().map(|(i, s)| ((k0 + i as f64) * step, s)).collect();
1733                }
1734                steps -= 1;
1735            }
1736        }
1737        let xs = if x == LX_NANOS { 1e-9 } else { 1.0 };
1738        let (a, b) = (v.0 * xs, v.1 * xs);
1739        let pps = pw as f64 / (b - a);
1740        let hours = abs(a).max(abs(b)) >= 3600.0;
1741        let mut step = lc_time_step(self.dp(72) as f64 / pps);
1742        for _ in 0..12 {
1743            let dec = if step >= 1.0 { 0 } else { step_decimals(step) };
1744            let (k0, k1) = (ceil_f(a / step - 1e-9), floor_f(b / step + 1e-9));
1745            if k1 < k0 || k1 - k0 > 64.0 {
1746                break;
1747            }
1748            let ticks: Vec<(f64, String)> = (0..=(k1 - k0) as i64)
1749                .map(|i| {
1750                    let t = (k0 + i as f64) * step;
1751                    (t / xs, lc_time_text(t, dec, hours, sep))
1752                })
1753                .collect();
1754            let wmax = ticks.iter().map(|t| self.text.width(&t.1)).max().unwrap_or(0);
1755            if step * pps >= (wmax + room) as f64 {
1756                return ticks;
1757            }
1758            step = lc_time_step(step * 1.0001);
1759        }
1760        Vec::new()
1761    }
1762
1763    // the summary of a line chart for screen readers: "Latenz – p50: aktuell 12 ms, min.
1764    // 9,8 ms, max. 41 ms; p95: ..."
1765    fn lc_summary(c: &LineChart, stats: &[Stat], empty: bool, sep: u32) -> String {
1766        let mut s = String::from(c.name);
1767        if !s.is_empty() {
1768            s.push_str(" – ");
1769        }
1770        let none = if c.empty.is_empty() { tr("Keine Daten") } else { c.empty };
1771        if empty || c.series.is_empty() {
1772            s.push_str(none);
1773            return s;
1774        }
1775        for (i, (ser, st)) in c.series.iter().zip(stats).enumerate() {
1776            if i > 0 {
1777                s.push_str("; ");
1778            }
1779            s.push_str(crate::basic::cstr(ser.label));
1780            s.push_str(": ");
1781            if st.has {
1782                let u = ser.unit;
1783                s.push_str(&format!(
1784                    "{} {}, {} {}, {} {}",
1785                    tr("aktuell"),
1786                    u.text(st.last, sep),
1787                    tr("min."),
1788                    u.text(st.lo, sep),
1789                    tr("max."),
1790                    u.text(st.hi, sep)
1791                ));
1792            } else {
1793                s.push_str(none);
1794            }
1795        }
1796        s
1797    }
1798
1799    // a tag with a label: the colour's tint (high contrast: the background, framed) under
1800    // the text, a text record
1801    fn lc_tag(&mut self, rect: [i32; 4], label: &str, c: u32, lx: impl Fn(i32, i32) -> i32) {
1802        let hc = self.high_contrast;
1803        let rad = self.dp(3);
1804        let (fill, stroke, t) = if hc {
1805            (self.color(T_BG), self.color(T_TEXT), self.hair())
1806        } else {
1807            (color_mix(card_opaque(self), c | 0xFF00_0000, 52), 0, 0)
1808        };
1809        self.cmd_rrect(rect[0], rect[1], rect[2], rect[3], rad, t, fill, stroke, stroke);
1810        let tc = self.color(T_TEXT);
1811        let pad = self.dp(5);
1812        text_at(self, TS_CAPTION, rect[0] + pad, rect, label, tc, rect[2] - pad * 2);
1813        self.wp.ctx.last_rect = [lx(rect[0], rect[2]), rect[1], rect[2], rect[3]];
1814        self.wp.ctx.last_state = 0;
1815        self.sem(K_TEXT, 0, Some(label), 0);
1816    }
1817
1818    // the keys of a focused line chart -> its cursor (series, x of a point in the view v):
1819    // Left / Right the previous / next point of its series (as drawn: the plot stays left to
1820    // right in mirrored windows), Up / Down the series before / after, Home / End the first /
1821    // last point, Esc ends it; the first key starts it at the first series' last point
1822    fn lc_keys(&mut self, c: &LineChart, v: (f64, f64), cur: Option<(usize, f64)>, rtl: bool) -> Option<(usize, f64)> {
1823        let swap = rtl && self.mods & (MOD_CTRL | MOD_ALT) == 0;
1824        let (kp, kn) = if swap { (VK_RIGHT, VK_LEFT) } else { (VK_LEFT, VK_RIGHT) };
1825        let n = c.series.len();
1826        let last = |s: usize| {
1827            let d = c.series[s].data;
1828            lc_step(d, d.upper_bound(v.1), true, v).map(|t| (s, t))
1829        };
1830        let mut q = cur;
1831        let mut moved = false;
1832        for (key, back) in [(kp, true), (kn, false)] {
1833            if self.key(key) {
1834                moved = true;
1835                q = match q {
1836                    Some((s, t)) => {
1837                        let d = c.series[s].data;
1838                        let i = if back { d.lower_bound(t) } else { d.upper_bound(t) };
1839                        lc_step(d, i, back, v).map(|t| (s, t)).or(q)
1840                    }
1841                    None => last(0),
1842                };
1843            }
1844        }
1845        if self.key(VK_HOME) {
1846            moved = true;
1847            let s = q.map_or(0, |x| x.0);
1848            let d = c.series[s].data;
1849            q = lc_step(d, d.lower_bound(v.0), false, v).map(|t| (s, t)).or(q);
1850        }
1851        if self.key(VK_END) {
1852            moved = true;
1853            q = last(q.map_or(0, |x| x.0)).or(q);
1854        }
1855        for (key, up) in [(VK_UP, true), (VK_DOWN, false)] {
1856            if self.key(key) {
1857                moved = true;
1858                let (s, t) = q.unwrap_or((0, v.1));
1859                let s2 = if up { s.saturating_sub(1) } else { (s + 1).min(n - 1) };
1860                let d = c.series[s2].data;
1861                let p = d.nearest(t).and_then(|i| d.get(i)).filter(|p| p.0 >= v.0 && p.0 <= v.1);
1862                q = p.map(|p| (s2, p.0)).or(q);
1863            }
1864        }
1865        if q.is_some() && self.key(VK_ESCAPE) {
1866            q = None;
1867        }
1868        if moved {
1869            self.wp.ctx.focus_visible = true;
1870        }
1871        q
1872    }
1873
1874    /// ui_line_chart(id, chart) -> the hovered series and the x it snapped to: series over
1875    /// time (or plain numbers) with axes, grid, legend, markers, thresholds and a hover card
1876    /// (see the module header of chart.rs)
1877    #[cfg_attr(jfgui_dev, track_caller)]
1878    pub fn line_chart(&mut self, id: u32, c: &LineChart) -> Option<(usize, f64)> {
1879        let _src = self.src_scope();
1880        let hc = self.high_contrast;
1881        let sep = self.m.value.decimal_sep;
1882        let lh = self.ts_line(TS_CAPTION);
1883        let hair = self.hair();
1884        let n = c.series.len();
1885        let empty = c.series.iter().all(|s| s.data.is_empty());
1886        let xs = if c.x == LX_NANOS { 1e-9 } else { 1.0 }; // seconds per x (time axes)
1887        // the box of the plot and the legend below it
1888        self.box_begin(0, BOX_V, 6);
1889        self.next_size(-1, -1);
1890        let r = self.place(self.dp(480), self.dp(200));
1891        // how each series looks: high contrast takes the scheme's colours with dashes
1892        let pal = self.style.data().chart_palette(self);
1893        let hcc = [self.color(T_ACCENT), self.color(T_TEXT), self.color(T_ACCENT_TEXT)];
1894        let looks: Vec<Look> = c
1895            .series
1896            .iter()
1897            .enumerate()
1898            .map(|(i, s)| {
1899                if hc {
1900                    Look { color: hcc[i % 3], dash: HC_DASH[i % 4], area: false }
1901                } else {
1902                    let color = if s.color != 0 { s.color } else { pal[s.pal.map_or(i, |k| k as usize) % 8] };
1903                    Look { color, dash: DASH_SOLID, area: s.area }
1904                }
1905            })
1906            .collect();
1907        let lw = (if hc { self.dp_fx(512) } else { self.dp_fx(384) }).max(256);
1908        let (t2, tx) = (self.color(T_TEXT2), self.color(T_TEXT));
1909        let mut st = self.m.chart.lines.remove(&id).unwrap_or_default();
1910        // the view and what each series shows of it
1911        let view = c.view();
1912        let mut h = Fnv::new();
1913        for s in c.series {
1914            h.u32(s.data.ver());
1915        }
1916        h.f64(view.0);
1917        h.f64(view.1);
1918        let skey = h.0;
1919        if st.stats_key != skey || st.stats.len() != n {
1920            st.stats = c.series.iter().map(|s| stat_of(s.data, view)).collect();
1921            st.stats_key = skey;
1922        }
1923        // the summary for screen readers (kept while the statistics and texts stay)
1924        let mut h = Fnv::new();
1925        h.u64(skey);
1926        h.u32(sep);
1927        h.u32(crate::i18n::language());
1928        h.str(c.name);
1929        h.str(c.empty);
1930        h.u32(empty as u32);
1931        for s in c.series {
1932            h.str(s.label);
1933            match s.unit {
1934                LcUnit::Num(u) => h.str(u),
1935                LcUnit::Si(u) => {
1936                    h.u32(1);
1937                    h.str(u);
1938                }
1939                LcUnit::Secs => h.u32(2),
1940            }
1941        }
1942        if st.sum_key != h.0 {
1943            st.summary = Ui::lc_summary(c, &st.stats, empty, sep);
1944            st.sum_key = h.0;
1945        }
1946        // the y range: the visible values and the thresholds, fixed ends as they are
1947        let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1948        for s in st.stats.iter().filter(|s| s.has) {
1949            lo = lo.min(s.lo);
1950            hi = hi.max(s.hi);
1951        }
1952        for t in c.thresholds.iter().filter(|t| t.value.is_finite()) {
1953            lo = lo.min(t.value);
1954            hi = hi.max(t.value);
1955        }
1956        let (flo, fhi) = (c.y_range.0.is_finite(), c.y_range.1.is_finite());
1957        if flo {
1958            lo = c.y_range.0;
1959        }
1960        if fhi {
1961            hi = c.y_range.1;
1962        }
1963        if !(lo <= hi) {
1964            (lo, hi) = if flo {
1965                (lo, lo + 1.0)
1966            } else if fhi {
1967                (hi - 1.0, hi)
1968            } else {
1969                (0.0, 1.0)
1970            };
1971        }
1972        if hi == lo {
1973            let d = abs(lo).max(1.0) * if flo || fhi { 1.0 } else { 0.1 };
1974            if !fhi {
1975                hi = lo + d;
1976            } else if !flo {
1977                lo = hi - d;
1978            } else {
1979                hi = lo + 1.0;
1980            }
1981        }
1982        // the plot: the y labels at its left, half a line above it (the top label), the x
1983        // labels below; at most a y step per two lines
1984        let top = lh / 2;
1985        let ph = (r[3] - top - lh - self.dp(4)).max(1);
1986        let steps = (ph / (lh * 2).max(self.dp(28)).max(1)).clamp(1, 8) as u32;
1987        let (first, step, nst) = lc_nice(lo, hi, steps);
1988        let ylo = if flo { lo } else { first };
1989        let yhi = if fhi { hi } else { first + step * nst as f64 };
1990        let (ylo, yhi) = if yhi > ylo { (ylo, yhi) } else { (ylo, ylo + 1.0) };
1991        let (k0, k1) = (ceil_f(ylo / step - 1e-9), floor_f(yhi / step + 1e-9));
1992        let unit = c.series.first().map_or(LcUnit::Num(""), |s| s.unit);
1993        let ylab = if empty || k1 < k0 || k1 - k0 > 64.0 { Vec::new() } else { unit.ticks(k0 * step, step, (k1 - k0) as u32, sep) };
1994        self.ts_font(TS_CAPTION);
1995        let yw = ylab.iter().map(|s| self.text.width(s)).max().unwrap_or(0);
1996        let ylw = if yw > 0 { yw + self.dp(8) } else { 0 };
1997        let px0 = r[0] + ylw;
1998        let pw = (r[2] - ylw - self.dp(4)).max(1);
1999        let py0 = r[1] + top;
2000        let xt = if empty { Vec::new() } else { self.lc_x_ticks(c.x, view, pw, sep) };
2001        let sx = pw as f64 / (view.1 - view.0);
2002        let sy = ph as f64 / (yhi - ylo);
2003        let xpx = |x: f64| ((x - view.0) * sx).clamp(-1e6, 1e6);
2004        let ypx = |v: f64| (ph as f64 - (v - ylo) * sy).clamp(-1e6, 1e6);
2005        // the parts of the plot: grid, markers, thresholds (px relative to the plot)
2006        let mut grid: Vec<(i32, bool)> = Vec::new();
2007        let gy: Vec<i32> =
2008            (0..ylab.len()).map(|k| (floor_f(ypx((k0 + k as f64) * step) + 0.5) as i32).min(ph - hair).max(0)).collect();
2009        let gx: Vec<i32> = xt.iter().map(|t| (floor_f(xpx(t.0) + 0.5) as i32).min(pw - hair).max(0)).collect();
2010        if c.flags & LC_NO_GRID == 0 {
2011            grid.extend(gy.iter().map(|&y| (y, false)));
2012            grid.extend(gx.iter().map(|&x| (x, true)));
2013        }
2014        let mlw = self.dp(1).max(1);
2015        let mcolor = |m: &LcMarker| if hc { tx } else if m.color != 0 { m.color } else { t2 };
2016        let mut ranges: Vec<(i32, i32, u32)> = Vec::new();
2017        let mut rules: Vec<(i32, i32, u32)> = Vec::new();
2018        let mut tags: Vec<(i32, &str, u32)> = Vec::new(); // marker tags: x, label, colour
2019        for m in c.markers.iter().filter(|m| !empty && m.t0.is_finite()) {
2020            let (a, b) = (m.t0, mark_end(m));
2021            if b < view.0 || a > view.1 {
2022                continue;
2023            }
2024            let col = mcolor(m);
2025            let xa = floor_f(xpx(a) + 0.5) as i32;
2026            let rule = |x: i32| (x - mlw / 2).min(pw - mlw).max(0);
2027            if b > a {
2028                let xb = floor_f(xpx(b) + 0.5) as i32;
2029                let (ca, cb) = (xa.clamp(0, pw), xb.clamp(0, pw));
2030                if !hc && cb > ca {
2031                    ranges.push((ca, cb, (col & 0x00FF_FFFF) | 0x1C00_0000));
2032                }
2033                let edge = if hc { col } else { (col & 0x00FF_FFFF) | 0x9000_0000 };
2034                for x in [xa, xb] {
2035                    if x >= 0 && x <= pw {
2036                        rules.push((rule(x), mlw, edge));
2037                    }
2038                }
2039                tags.push((ca, m.label, col));
2040            } else {
2041                rules.push((rule(xa), mlw, col));
2042                tags.push((xa, m.label, col));
2043            }
2044        }
2045        let lt = self.dp(1).max(1);
2046        let mut limits: Vec<(i32, u32)> = Vec::new();
2047        let mut ltags: Vec<(i32, &str, u32)> = Vec::new(); // threshold tags: y, label, colour
2048        for t in c.thresholds.iter().filter(|t| !empty && t.value.is_finite() && t.value >= ylo && t.value <= yhi) {
2049            let col = if hc { tx } else if t.color != 0 { t.color } else { t2 };
2050            let y = (floor_f(ypx(t.value) + 0.5) as i32 - lt / 2).min(ph - lt).max(0);
2051            limits.push((y, col));
2052            ltags.push((y + lt / 2, t.label, col));
2053        }
2054        // the plot's drawing object: rebuilt when anything it shows changed
2055        let m = ((lw + 255) >> 8) + 1;
2056        let grid_c = self.color(T_DIVIDER);
2057        let mut h = Fnv::new();
2058        h.u64(skey);
2059        h.f64(ylo);
2060        h.f64(yhi);
2061        for v in [pw, ph, lw, hair, m, hc as i32, grid_c as i32, lt, mlw] {
2062            h.u32(v as u32);
2063        }
2064        for l in &looks {
2065            h.u32(l.color);
2066            h.u32(l.dash);
2067            h.u32(l.area as u32);
2068        }
2069        for &(p, v) in &grid {
2070            h.u32(p as u32 | (v as u32) << 31);
2071        }
2072        for &(a, b, col) in ranges.iter().chain(rules.iter()) {
2073            h.u32(a as u32);
2074            h.u32(b as u32);
2075            h.u32(col);
2076        }
2077        for &(y, col) in &limits {
2078            h.u32(y as u32);
2079            h.u32(col);
2080        }
2081        let okey = h.0;
2082        if st.obj_key != okey || st.obj.is_none() {
2083            let lwf = lw as f64 / 256.0;
2084            let mut areas = Vec::new();
2085            let mut traces = Vec::new();
2086            if !empty {
2087                for (i, s) in c.series.iter().enumerate() {
2088                    let l = looks[i];
2089                    let dense = st.stats[i].n > pw as usize * 2;
2090                    let (polys, band, area) = trace_of(s.data, view, (pw, ph), (ylo, yhi), lwf, dense, l.area, dash_pattern(l.dash));
2091                    if l.area && !area.is_empty() {
2092                        areas.push((area, (l.color & 0x00FF_FFFF) | 0x2E00_0000));
2093                    }
2094                    traces.push(Trace { polys, band, color: l.color, width: lw, dash: l.dash });
2095                }
2096            }
2097            let lim = (lt, self.dp(4), self.dp(3));
2098            let plot = Plot { m, w: pw, h: ph, hair, grid, grid_c, dotted: hc, ranges, areas, traces, limits, lim, rules };
2099            st.obj = Some(Rc::new(plot));
2100            st.obj_key = okey;
2101        }
2102        // ---- commands: the plot keeps its left-to-right order in mirrored windows
2103        self.ltr_begin(r[0], r[2]);
2104        let rtl = self.rtl;
2105        let lx = move |x: i32, w: i32| if rtl { r[0] + r[0] + r[2] - x - w } else { x };
2106        let ev = if c.flags & LC_NO_HOVER == 0 && !empty { self.hit_rect(id, lx(px0, pw), py0, pw, ph, HB_FOCUS | HB_MOVES) } else { 0 };
2107        // the keyboard's cursor on the points while the plot has the focus
2108        let focused = ev & EV_FOCUS != 0;
2109        let mut cursor = st.cursor.filter(|q| focused && q.0 < n && q.1 >= view.0 && q.1 <= view.1);
2110        if focused && n > 0 {
2111            cursor = self.lc_keys(c, view, cursor, rtl);
2112        }
2113        // the record first (a screen reader meets the summary before the details); with the
2114        // cursor its point before it ("p95, 1:45: 88,1 ms. Latenz – ...")
2115        let hours = abs(view.0 * xs).max(abs(view.1 * xs)) >= 3600.0;
2116        let point = cursor.and_then(|(s, t)| {
2117            let ser = &c.series[s];
2118            let v = ser.data.nearest(t).and_then(|i| ser.data.get(i))?;
2119            Some(format!("{}, {}: {}. ", crate::basic::cstr(ser.label), lc_x_label(c.x, t, hours, sep), ser.unit.text(v.1, sep)))
2120        });
2121        let label = match &point {
2122            Some(p) => format!("{}{}", p, st.summary),
2123            None => st.summary.clone(),
2124        };
2125        self.wp.ctx.last_rect = r;
2126        let mut ss = if ev & EV_HOVER != 0 { SS_HOVER as u32 } else { 0 };
2127        if focused {
2128            ss |= SS_FOCUSED as u32;
2129        }
2130        self.wp.ctx.last_state = ss;
2131        self.sem(K_CANVAS, id, Some(&label), n as i32);
2132        self.cmd_custom(px0 - m, py0 - m, pw + m * 2, ph + m * 2, st.obj.clone(), 0, okey as u32);
2133        // the y labels before the plot, centred on their grid lines; the x labels below,
2134        // centred under their ticks inside the chart, never closer than 4 DIP
2135        for (k, s) in ylab.iter().enumerate() {
2136            self.ts_font(TS_CAPTION);
2137            let w = self.text.width(s);
2138            let y = py0 + gy[k] + hair / 2 - lh / 2;
2139            let x = px0 - self.dp(6) - w;
2140            text_at(self, TS_CAPTION, x, [x, y, w, lh], s, t2, 0);
2141            self.wp.ctx.last_rect = [lx(x, w), y, w, lh];
2142            self.wp.ctx.last_state = 0;
2143            self.sem(K_TEXT, 0, Some(s), 0);
2144        }
2145        let ly = py0 + ph + self.dp(4);
2146        let mut end = i32::MIN;
2147        for (k, (_, s)) in xt.iter().enumerate() {
2148            self.ts_font(TS_CAPTION);
2149            let w = self.text.width(s);
2150            let x = (px0 + gx[k] - w / 2).min(r[0] + r[2] - w).max(px0);
2151            if x < end + self.dp(4) {
2152                continue;
2153            }
2154            end = x + w;
2155            text_at(self, TS_CAPTION, x, [x, ly, w, lh], s, t2, 0);
2156            self.wp.ctx.last_rect = [lx(x, w), ly, w, lh];
2157            self.wp.ctx.last_state = 0;
2158            self.sem(K_TEXT, 0, Some(s), 0);
2159        }
2160        // the tags: thresholds at the right end of their line, markers in rows from the top
2161        // (right of their line, left of it where there is no room); never over each other
2162        let pad = self.dp(5);
2163        let tsep = self.dp(4);
2164        let mut placed: Vec<[i32; 4]> = Vec::new();
2165        for &(y, label, col) in &ltags {
2166            if label.is_empty() {
2167                continue;
2168            }
2169            self.ts_font(TS_CAPTION);
2170            let w = (self.text.width(label) + pad * 2).min(pw);
2171            let ty = (py0 + y - lh / 2).min(py0 + ph - lh).max(py0);
2172            let rect = [px0 + pw - w - self.dp(4), ty, w, lh];
2173            if rect[0] < px0 || placed.iter().any(|&p| near(p, rect, tsep)) {
2174                continue;
2175            }
2176            placed.push(rect);
2177            self.lc_tag(rect, label, col, lx);
2178        }
2179        let rows = ((ph / 3) / (lh + self.dp(2)).max(1)).clamp(1, 3);
2180        for &(x, label, col) in &tags {
2181            if label.is_empty() {
2182                continue;
2183            }
2184            self.ts_font(TS_CAPTION);
2185            let w = (self.text.width(label) + pad * 2).min(pw);
2186            let xm = px0 + x;
2187            let x0 = if xm + self.dp(4) + w <= px0 + pw { xm + self.dp(4) } else { (xm - self.dp(4) - w).max(px0) };
2188            for row in 0..rows {
2189                let rect = [x0, py0 + self.dp(2) + row * (lh + self.dp(2)), w, lh];
2190                if !placed.iter().any(|&p| near(p, rect, tsep)) {
2191                    placed.push(rect);
2192                    self.lc_tag(rect, label, col, lx);
2193                    break;
2194                }
2195            }
2196        }
2197        // no points: the empty text in the middle
2198        if empty {
2199            let s = if c.empty.is_empty() { tr("Keine Daten") } else { c.empty };
2200            self.ts_font(TS_BODY);
2201            let lb = self.ts_line(TS_BODY);
2202            let w = self.text.width(s).min(r[2]);
2203            let (x, y) = (r[0] + (r[2] - w) / 2, r[1] + (r[3] - lb) / 2);
2204            text_at(self, TS_BODY, x, [x, y, w, lb], s, t2, r[2]);
2205            self.wp.ctx.last_rect = [lx(x, w), y, w, lb];
2206            self.wp.ctx.last_state = 0;
2207            self.sem(K_TEXT, 0, Some(s), 0);
2208        }
2209        // hover (else the keyboard's cursor): the crosshair at the nearest point, a dot on
2210        // every series, the card
2211        let mut out = None;
2212        let probe = if ev & EV_HOVER != 0 {
2213            let mx = self.ltr_x(self.wp.ctx.mx);
2214            Some((view.0 + ((mx - px0) as f64 + 0.5) / sx, None, Some(self.wp.ctx.my)))
2215        } else {
2216            cursor.map(|(s, t)| (t, Some(s), None))
2217        };
2218        if let Some((tp, fixed, my)) = probe {
2219            let inside = |p: &(f64, f64)| p.0 >= view.0 && p.0 <= view.1;
2220            let snap = if fixed.is_some() {
2221                Some(tp)
2222            } else {
2223                c.series
2224                    .iter()
2225                    .filter_map(|s| s.data.nearest(tp).and_then(|i| s.data.get(i)).filter(inside))
2226                    .map(|p| p.0)
2227                    .min_by(|a, b| abs(a - tp).total_cmp(&abs(b - tp)))
2228            };
2229            if let Some(ts) = snap {
2230                // each series' point there (its nearest one, within 24 DIP)
2231                let tol = self.dp(24) as f64 / sx;
2232                let at: Vec<Option<(f64, f64)>> = c
2233                    .series
2234                    .iter()
2235                    .map(|s| s.data.nearest(ts).and_then(|i| s.data.get(i)).filter(|p| inside(p) && abs(p.0 - ts) <= tol))
2236                    .collect();
2237                // the series nearest to the pointer (the cursor's own); a click puts the
2238                // cursor there
2239                let best = fixed.or_else(|| {
2240                    let my = my.unwrap_or(0) as f64;
2241                    at.iter()
2242                        .enumerate()
2243                        .filter_map(|(i, p)| p.map(|p| (i, abs(py0 as f64 + ypx(p.1) - my))))
2244                        .min_by(|a, b| a.1.total_cmp(&b.1))
2245                        .map(|b| b.0)
2246                });
2247                if let Some(bi) = best {
2248                    out = Some((bi, ts));
2249                    if ev & EV_ACT != 0 && fixed.is_none() {
2250                        cursor = Some((bi, ts));
2251                    }
2252                }
2253                // the card at the pointer's height, else at the cursor's point
2254                let my = my.unwrap_or_else(|| {
2255                    let v = best.and_then(|b| at[b]).map_or(0.0, |p| ypx(p.1).clamp(0.0, ph as f64));
2256                    py0 + v as i32
2257                });
2258                let cx = px0 + floor_f(xpx(ts) + 0.5) as i32;
2259                let cross = self.color(T_TEXT3);
2260                self.draw_color(cross);
2261                self.draw_rect(cx, py0, hair, ph);
2262                let ring = if hc { self.color(T_BG) } else { card_opaque(self) };
2263                let (ro, ri, grow) = (self.dp_fx(1152), self.dp_fx(768), self.dp_fx(256));
2264                for (i, p) in at.iter().enumerate() {
2265                    let Some(p) = p else { continue };
2266                    let y = ypx(p.1);
2267                    if !(y >= 0.0 && y <= ph as f64) {
2268                        continue;
2269                    }
2270                    let (fx, fy) = (((px0 as f64 + xpx(p.0)) * 256.0) as i32, ((py0 as f64 + y) * 256.0) as i32);
2271                    let g = if Some(i) == best { grow } else { 0 };
2272                    self.draw_color(ring);
2273                    self.draw_circle(fx, fy, ro + g, 0);
2274                    self.draw_color(looks[i].color);
2275                    self.draw_circle(fx, fy, ri + g, 0);
2276                }
2277                // the card: the x, every series' value there, the markers there
2278                let head = lc_x_label(c.x, ts, hours, sep);
2279                let mut rows: Vec<(u32, bool, &str, String)> = c
2280                    .series
2281                    .iter()
2282                    .enumerate()
2283                    .map(|(i, s)| (looks[i].color, false, crate::basic::cstr(s.label), at[i].map_or(String::from("–"), |p| s.unit.text(p.1, sep))))
2284                    .collect();
2285                // the markers there with their time ("1:10 – 1:22")
2286                let half = self.dp(3) as f64 / sx;
2287                for mk in c.markers.iter().filter(|m| !m.label.is_empty() && m.t0.is_finite() && ts >= m.t0 - half && ts <= mark_end(m) + half) {
2288                    let mut when = lc_x_label(c.x, mk.t0, hours, sep);
2289                    if mk.t1 > mk.t0 {
2290                        when = format!("{} – {}", when, lc_x_label(c.x, mk.t1, hours, sep));
2291                    }
2292                    rows.push((mcolor(mk), true, crate::basic::cstr(mk.label), when));
2293                }
2294                self.lc_card(r, cx, my, &head, &rows);
2295            }
2296        }
2297        if self.focus_ring(ev) {
2298            let s = self.style;
2299            s.focus_ring(self, [px0, py0, pw, ph], 4);
2300        }
2301        self.ltr_end();
2302        // a new hover or cursor: once more, so what the app shows of it before the chart
2303        // (and the record's text) is current -- not while the layout still moves the plot
2304        // (that pass comes anyway, and the pointer means another point in each of them)
2305        let hk = out.map(|(i, t): (usize, f64)| (i, t.to_bits()));
2306        let moved = cursor.map(|q| (q.0, q.1.to_bits())) != st.cursor.map(|q| (q.0, q.1.to_bits()));
2307        let rect = [px0, py0, pw, ph];
2308        if (st.hover != hk || moved) && st.rect == rect {
2309            self.repass();
2310        }
2311        st.hover = hk;
2312        st.cursor = cursor;
2313        st.rect = rect;
2314        if self.m.chart.lines.len() > 64 {
2315            self.m.chart.lines.clear();
2316        }
2317        self.m.chart.lines.insert(id, st);
2318        // the legend: the series' labels (high contrast: line samples with their dashes)
2319        if c.flags & LC_NO_LEGEND == 0 && n > 0 {
2320            let items: Vec<(&str, u32)> = c.series.iter().enumerate().map(|(i, s)| (s.label, looks[i].color)).collect();
2321            let dashes: Vec<u32> = if hc { looks.iter().map(|l| l.dash).collect() } else { Vec::new() };
2322            self.legend_marks(&items, &dashes, lw);
2323        }
2324        self.box_end();
2325        out
2326    }
2327
2328    // the hover card of a line chart beside the crosshair at cx (px, left to right inside
2329    // the chart's region r), at the pointer's height: the x, then rows (colour, marker,
2330    // label, value) -- a series' value after a dot, a marker's time after a bar; on the
2331    // tooltip layer, inside the window
2332    fn lc_card(&mut self, r: [i32; 4], cx: i32, my: i32, head: &str, rows: &[(u32, bool, &str, String)]) {
2333        let prev = self.layer_set(L_TIP);
2334        self.ltr_begin(r[0], r[2]);
2335        let rtl = self.rtl;
2336        let lx = move |x: i32, w: i32| if rtl { r[0] + r[0] + r[2] - x - w } else { x };
2337        let lh = self.ts_line(TS_CAPTION);
2338        let (padx, pady) = (self.dp(10), self.dp(6));
2339        let (dot, dgap, cgap) = (self.dp(8), self.dp(6), self.dp(16));
2340        self.ts_font(TS_CAPTION);
2341        let hw = self.text.width(head);
2342        let lws: Vec<i32> = rows.iter().map(|q| self.text.width(q.2)).collect();
2343        let vws: Vec<i32> = rows.iter().map(|q| self.text.width(&q.3)).collect();
2344        let (lmax, vmax) = (lws.iter().copied().max().unwrap_or(0), vws.iter().copied().max().unwrap_or(0));
2345        let w = (padx * 2 + hw.max(dot + dgap + lmax + cgap + vmax)).min(self.dp(360));
2346        let h = pady * 2 + lh * (1 + rows.len()) as i32;
2347        // beside the crosshair where there is room, inside the window (its edges as the
2348        // region left to right sees them)
2349        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
2350        let d = if rtl { fw - r[0] - r[0] - r[2] } else { 0 };
2351        let (lo, hi) = (-d, fw - d);
2352        let mut x = cx + self.dp(14);
2353        if x + w > hi {
2354            x = cx - self.dp(14) - w;
2355        }
2356        let x = x.min(hi - w).max(lo);
2357        let y = (my - h / 2).min(fh - h).max(0);
2358        // the style's tooltip surface
2359        let st = self.style;
2360        let mut sp = Sp { label: Some(""), ..Sp::default() };
2361        sp.place([x, y, w, h]);
2362        st.tooltip(self, &sp);
2363        let (tx, t2) = (self.color(T_TEXT), self.color(T_TEXT2));
2364        let inner = w - padx * 2;
2365        let mut ty = y + pady;
2366        text_at(self, TS_CAPTION, x + padx, [x + padx, ty, inner, lh], head, t2, inner);
2367        self.wp.ctx.last_rect = [lx(x + padx, hw.min(inner)), ty, hw.min(inner), lh];
2368        self.wp.ctx.last_state = 0;
2369        self.sem(K_TEXT, 0, Some(head), 0);
2370        ty += lh;
2371        let dy = (lh - dot) / 2;
2372        for (k, (col, mark, label, value)) in rows.iter().enumerate() {
2373            self.draw_color(*col);
2374            if *mark {
2375                self.draw_rect(x + padx + dot / 2 - self.dp(2), ty + dy, self.dp(4), dot);
2376            } else {
2377                self.draw_circle(((x + padx) << 8) + (dot << 7), ((ty + dy) << 8) + (dot << 7), dot << 7, 0);
2378            }
2379            let lx0 = x + padx + dot + dgap;
2380            let vw = vws[k].min(inner);
2381            let room = (inner - dot - dgap - vw - cgap).max(1);
2382            text_at(self, TS_CAPTION, lx0, [lx0, ty, room, lh], label, tx, room);
2383            let vx = x + w - padx - vw;
2384            text_at(self, TS_CAPTION, vx, [vx, ty, vw, lh], value, if *mark { t2 } else { tx }, inner);
2385            let rec = format!("{}: {}", label, value);
2386            self.wp.ctx.last_rect = [lx(lx0, inner - dot - dgap), ty, inner - dot - dgap, lh];
2387            self.wp.ctx.last_state = 0;
2388            self.sem(K_TEXT, 0, Some(&rec), 0);
2389            ty += lh;
2390        }
2391        self.ltr_end();
2392        self.layer_set(prev);
2393    }
2394}