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