Skip to main content

datui_lib/widgets/
axes.rs

1//! Axis ticks, labels, titles and grid for every chart. ratatui would cut or run labels
2//! together and draw titles over the plot, so datui picks ticks and draws labels and
3//! marks itself. Ticks fall on nice values (1, 2 or 5 times a power of ten, or calendar
4//! boundaries), about one label per 15 columns or 4 rows, never closer than two cells: a
5//! crowded axis takes a coarser step, then shorter labels (`12.3k`, yearless dates),
6//! keeping a fixed axis's ends while anything fits. A title gets its own row, cut to
7//! fit, or is dropped.
8
9use ratatui::{
10    buffer::Buffer,
11    layout::Rect,
12    style::Style,
13    symbols::Marker,
14    text::Span,
15    widgets::{Axis, Chart, Widget},
16};
17use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
18
19use crate::chart::chart_data::{XAxisTemporalKind, x_axis_label_at};
20use crate::glyphs::Glyphs;
21use crate::widgets::axis_numbers::{AxisFormat, AxisNumbers};
22use crate::widgets::ticks;
23
24/// Rows the plot keeps before an axis title gives up its row.
25const MIN_PLOT_ROWS: u16 = 3;
26/// Forms a label steps through at most, so a label that never runs out stops.
27const MAX_LEVELS: usize = 8;
28/// Cells between two x labels: one would do, but two dates a space apart read as a
29/// range.
30const LABEL_GAP: u16 = 2;
31/// Columns between x labels the axis aims for, and the fewest it takes.
32const X_SPACING: f64 = 15.0;
33const X_LEAST: f64 = 6.0;
34/// Rows between y labels the axis aims for, and the fewest it takes.
35const Y_SPACING: f64 = 4.0;
36const Y_LEAST: f64 = 2.0;
37/// The fewest cells between minor ticks, across and down.
38const X_MINOR_GAP: f64 = 3.0;
39const Y_MINOR_GAP: f64 = 3.0;
40
41/// A tick's label at a level of detail, 0 the fullest; `None` past the shortest.
42pub type TickLabel<'a> = Box<dyn Fn(f64, usize) -> Option<String> + 'a>;
43
44/// How an axis chooses its ticks.
45enum Scale<'a> {
46    /// Nice numbers, written in one format; `widen` stretches the axis out to the
47    /// ticks either side of its ends, so a y axis starts and ends on a label.
48    Numbers { numbers: AxisNumbers, widen: bool },
49    /// Calendar boundaries on a time axis.
50    Calendar {
51        kind: XAxisTemporalKind,
52        numbers: AxisNumbers,
53    },
54    /// A log scale, where position `v` stands for the value `exp_m1(v)`, from zero
55    /// up: ticks at each power of ten, with 2 and 5 between when there is room.
56    Log { numbers: AxisNumbers },
57    /// Ticks given in advance: evenly spaced ones thin out keeping both ends.
58    Fixed {
59        ticks: Vec<f64>,
60        label: TickLabel<'a>,
61    },
62}
63
64/// One axis: its range, how its ticks are chosen and written, and its title.
65pub struct AxisSpec<'a> {
66    pub bounds: [f64; 2],
67    scale: Scale<'a>,
68    pub title: &'a str,
69    /// Labels right-aligned in at least this many cells.
70    pad: usize,
71}
72
73/// A way to tick an axis: where, the minor ticks between, and the labels at each
74/// level of detail.
75#[derive(Clone, Debug, Default)]
76pub struct TickSet {
77    pub ticks: Vec<f64>,
78    pub minor: Vec<f64>,
79    pub levels: Vec<Vec<String>>,
80    /// The axis's range under this set: widened to the outer ticks, or as given.
81    pub bounds: [f64; 2],
82}
83
84impl<'a> AxisSpec<'a> {
85    /// An axis ticked at `ticks`, written by `label`. Evenly spaced ticks thin out
86    /// keeping both ends.
87    pub fn fixed(bounds: [f64; 2], ticks: Vec<f64>, label: TickLabel<'a>, title: &'a str) -> Self {
88        Self {
89            bounds,
90            scale: Scale::Fixed { ticks, label },
91            title,
92            pad: 0,
93        }
94    }
95
96    /// Ticks at the ends of `bounds` and halfway between.
97    pub fn ends_and_middle(bounds: [f64; 2], label: TickLabel<'a>, title: &'a str) -> Self {
98        let [lo, hi] = bounds;
99        Self::fixed(bounds, vec![lo, (lo + hi) / 2.0, hi], label, title)
100    }
101
102    /// A numeric axis over `bounds` holding `numbers`, ticked at nice values (whole
103    /// ones when the numbers are whole), every tick in one format.
104    pub fn numbers(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
105        Self {
106            bounds,
107            scale: Scale::Numbers {
108                numbers: numbers.clone(),
109                widen: false,
110            },
111            title,
112            pad: 0,
113        }
114    }
115
116    /// A numeric y axis: widened out to the nice ticks either side of its range, so
117    /// it starts and ends on a label, short of a tick that would leave most of a
118    /// step empty ([`ticks::widen_snug`]).
119    pub fn y_numbers(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
120        Self {
121            scale: Scale::Numbers {
122                numbers: numbers.clone(),
123                widen: true,
124            },
125            ..Self::numbers(bounds, numbers, title)
126        }
127    }
128
129    /// A time axis of `kind` over `bounds`, ticked on calendar boundaries; `numbers`
130    /// writes a value past what a date can stand for.
131    pub fn calendar(
132        bounds: [f64; 2],
133        kind: XAxisTemporalKind,
134        numbers: &AxisNumbers,
135        title: &'a str,
136    ) -> Self {
137        if kind == XAxisTemporalKind::Numeric {
138            return Self::numbers(bounds, numbers, title);
139        }
140        Self {
141            bounds,
142            scale: Scale::Calendar {
143                kind,
144                numbers: numbers.clone(),
145            },
146            title,
147            pad: 0,
148        }
149    }
150
151    /// A numeric axis whose position `v` stands for the number `shown(v)`, as on a log
152    /// scale: ticked at its ends and middle, its format chosen from the numbers they
153    /// stand for.
154    pub fn numbers_as(
155        bounds: [f64; 2],
156        numbers: &AxisNumbers,
157        title: &'a str,
158        shown: impl Fn(f64) -> f64 + 'a,
159    ) -> Self {
160        let [lo, hi] = bounds;
161        let ticks = vec![lo, (lo + hi) / 2.0, hi];
162        let shown_ticks: Vec<f64> = ticks.iter().map(|&v| shown(v)).collect();
163        let format = AxisFormat::new(&shown_ticks, numbers);
164        let label = Box::new(move |v, level| format.label(shown(v), level));
165        Self::fixed(bounds, ticks, label, title)
166    }
167
168    /// A log-scale y axis (position `v` is `exp_m1(v)`): ticks at nice values (1, 10, 100,
169    /// with 2 and 5 between when there is room), widened to the ticks around its range, one
170    /// format throughout.
171    pub fn y_log(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
172        Self {
173            bounds,
174            scale: Scale::Log {
175                numbers: numbers.clone(),
176            },
177            title,
178            pad: 0,
179        }
180    }
181
182    /// The same axis, its labels right-aligned in at least `width` cells.
183    pub fn padded(self, width: usize) -> Self {
184        Self { pad: width, ..self }
185    }
186
187    /// The ways to tick this axis of `length`, preferred first: about one label per
188    /// `spacing`, then coarser, none closer than `least`, minor ticks at least `minor_gap`
189    /// apart (cells on screen, points in a file). A second group follows when the first may
190    /// be empty: a time axis's ends and middle.
191    pub fn tick_sets(
192        &self,
193        length: f64,
194        spacing: f64,
195        least: f64,
196        minor_gap: f64,
197    ) -> Vec<Vec<TickSet>> {
198        let [lo, hi] = self.bounds;
199        match &self.scale {
200            Scale::Fixed { ticks, label } => vec![
201                strides(ticks.len())
202                    .map(|subset| {
203                        let ticks: Vec<f64> = subset.iter().map(|&i| ticks[i]).collect();
204                        let levels = (0..MAX_LEVELS)
205                            .map_while(|level| ticks.iter().map(|&v| label(v, level)).collect())
206                            .collect();
207                        TickSet {
208                            ticks,
209                            minor: Vec::new(),
210                            levels,
211                            bounds: self.bounds,
212                        }
213                    })
214                    .collect(),
215            ],
216            Scale::Numbers { numbers, widen } => {
217                vec![number_sets(
218                    self.bounds,
219                    numbers,
220                    *widen,
221                    length,
222                    spacing,
223                    least,
224                    minor_gap,
225                )]
226            }
227            Scale::Log { numbers } => vec![log_sets(
228                self.bounds,
229                numbers,
230                length,
231                spacing,
232                least,
233                minor_gap,
234            )],
235            Scale::Calendar { kind, numbers } => {
236                let primary = calendar_sets(self.bounds, *kind, length, spacing, least, minor_gap);
237                let format = AxisFormat::ends_and_middle(self.bounds, numbers);
238                let kind = *kind;
239                let ends = AxisSpec::ends_and_middle(
240                    self.bounds,
241                    Box::new(move |v, level| x_axis_label_at(v, kind, (lo, hi), level, &format)),
242                    "",
243                );
244                let fallback = ends.tick_sets(length, spacing, least, minor_gap).remove(0);
245                vec![primary, fallback]
246            }
247        }
248    }
249}
250
251/// Whether `sets` hold more than one tick each where it counts.
252fn with_ticks(sets: Vec<TickSet>) -> Vec<TickSet> {
253    sets.into_iter().filter(|s| s.ticks.len() >= 2).collect()
254}
255
256/// One way to tick an axis, as [`preferred`] weighs it: the fewest cells between two
257/// of its ticks, and how many ticks it has.
258struct Candidate<T> {
259    set: T,
260    gap: f64,
261    ticks: usize,
262}
263
264/// Of the options (finest first), the one nearest `spacing` and every coarser one at
265/// least `least` apart. When the nearest has only two ticks and a finer one fits, the
266/// finer comes first (`0 2 4 6`, not `0 5`), the two-tick set as fallback.
267fn preferred<T>(options: Vec<Candidate<T>>, spacing: f64, least: f64) -> Vec<T> {
268    let closeness = |gap: f64| (gap / spacing).ln().abs();
269    let best = options
270        .iter()
271        .enumerate()
272        .filter(|(_, o)| o.gap >= least)
273        .min_by(|a, b| closeness(a.1.gap).total_cmp(&closeness(b.1.gap)))
274        .map(|(i, _)| i);
275    let best = best.map(|best| {
276        if options[best].ticks > 2 {
277            return best;
278        }
279        (0..best)
280            .rev()
281            .find(|&i| options[i].gap >= least)
282            .unwrap_or(best)
283    });
284    match best {
285        Some(best) => options.into_iter().skip(best).map(|o| o.set).collect(),
286        // Nothing is far enough apart: the coarsest, which may still have room.
287        None => options
288            .into_iter()
289            .last()
290            .map(|o| o.set)
291            .into_iter()
292            .collect(),
293    }
294}
295
296/// Nice-number tick sets over `bounds` for an axis `length` cells long.
297fn number_sets(
298    bounds: [f64; 2],
299    numbers: &AxisNumbers,
300    widen: bool,
301    length: f64,
302    spacing: f64,
303    least: f64,
304    minor_gap: f64,
305) -> Vec<TickSet> {
306    let [lo, hi] = bounds;
307    if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 {
308        // A point or nothing: the one value there is.
309        let format = AxisFormat::new(&[lo], numbers);
310        let levels = (0..2)
311            .map_while(|level| format.label(lo, level).map(|l| vec![l]))
312            .collect();
313        return vec![TickSet {
314            ticks: vec![lo],
315            minor: Vec::new(),
316            levels,
317            bounds,
318        }];
319    }
320    let finest = (hi - lo) * least.min(minor_gap) / length;
321    let options: Vec<Candidate<(f64, [f64; 2])>> = ticks::nice_steps(lo, hi, finest, numbers.whole)
322        .into_iter()
323        .map(|step| {
324            let range = if widen {
325                ticks::widen_snug(lo, hi, step, numbers.whole)
326            } else {
327                bounds
328            };
329            Candidate {
330                set: (step, range),
331                gap: step / (range[1] - range[0]) * length,
332                ticks: ticks::multiples(range[0], range[1], step).len(),
333            }
334        })
335        .filter(|o| o.ticks >= 2)
336        .collect();
337    let sets = preferred(options, spacing, least)
338        .into_iter()
339        .map(|(step, range)| {
340            let majors = ticks::multiples(range[0], range[1], step);
341            let format = AxisFormat::new(&majors, numbers);
342            let levels = (0..2)
343                .map_while(|level| majors.iter().map(|&v| format.label(v, level)).collect())
344                .collect();
345            let per_cell = length / (range[1] - range[0]);
346            let minor = ticks::minor_steps(step, numbers.whole)
347                .into_iter()
348                .find(|m| m * per_cell >= minor_gap)
349                .map(|m| {
350                    ticks::multiples(range[0], range[1], m)
351                        .into_iter()
352                        .filter(|v| !majors.iter().any(|t| (t - v).abs() < m * 1e-6))
353                        .collect()
354                })
355                .unwrap_or_default();
356            TickSet {
357                ticks: majors,
358                minor,
359                levels,
360                bounds: range,
361            }
362        })
363        .collect();
364    with_ticks(sets)
365}
366
367/// Log-scale tick sets over `bounds` (positions are `exp_m1`) for an axis of `length`
368/// cells: from one up, powers of ten (with 2 and 5 between, or every second or third
369/// power), 0 below where the axis starts there; under one, plain nice steps. Each set
370/// widens the axis to its ticks around the data.
371fn log_sets(
372    bounds: [f64; 2],
373    numbers: &AxisNumbers,
374    length: f64,
375    spacing: f64,
376    least: f64,
377    minor_gap: f64,
378) -> Vec<TickSet> {
379    let [lo, hi] = bounds;
380    let (low, high) = (lo.exp_m1().max(0.0), hi.exp_m1());
381    if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 || high <= 0.0 {
382        let format = AxisFormat::log(&[low], numbers);
383        return vec![TickSet {
384            ticks: vec![lo],
385            minor: Vec::new(),
386            levels: vec![vec![format.label(low, 0).unwrap_or_default()]],
387            bounds,
388        }];
389    }
390    // Each option: its values (before the log) and the minor ones between.
391    let options: Vec<(Vec<f64>, Vec<f64>)> = if high <= 1.0 {
392        // Under one a log scale is close to linear, never more than twice as steep.
393        let finest = (high - low) * least.min(minor_gap) / length / 2.0;
394        ticks::nice_steps(low, high, finest, false)
395            .into_iter()
396            .map(|step| {
397                let [a, b] = ticks::widen(low, high, step);
398                (ticks::multiples(a, b, step), Vec::new())
399            })
400            .collect()
401    } else {
402        let decade = |v: f64| (v.log10() + 1e-9).floor() as i32;
403        // Every `every` powers of ten from the one at or under the data's least (one
404        // at the least, or zero) to the one at or over its most.
405        let values = |mantissas: &[f64], every: i32| {
406            let first = if low >= 1.0 { decade(low) } else { 0 };
407            let first = first - first.rem_euclid(every);
408            let mut values: Vec<f64> = if low < 1.0 { vec![0.0] } else { Vec::new() };
409            let mut k = first;
410            'up: loop {
411                for &m in mantissas {
412                    // Parsed, so 2e21 is the double nearest it and not 2 times one.
413                    let v: f64 = format!("{m}e{k}").parse().unwrap_or(f64::INFINITY);
414                    values.push(v);
415                    if v >= high * (1.0 - 1e-9) {
416                        break 'up;
417                    }
418                }
419                k += every;
420                if k > 308 {
421                    break;
422                }
423            }
424            // A zero under them stands in for the one, which sits too close to it
425            // past a step of a whole power.
426            if every > 1 || mantissas.len() == 1 {
427                values.retain(|&v| v != 1.0 || low >= 1.0);
428            }
429            // From the tick at or under the data's least.
430            let start = values
431                .iter()
432                .rposition(|&v| v <= low * (1.0 + 1e-9))
433                .unwrap_or(0);
434            values.split_off(start)
435        };
436        let fine = values(&[1.0, 2.0, 5.0], 1);
437        let mut options = vec![(fine.clone(), Vec::new())];
438        for every in [1, 2, 3, 5, 10, 20, 50, 100] {
439            let majors = values(&[1.0], every);
440            let minor = if every == 1 {
441                fine.iter()
442                    .copied()
443                    .filter(|v| !majors.contains(v))
444                    .collect()
445            } else {
446                Vec::new()
447            };
448            options.push((majors, minor));
449        }
450        options
451    };
452    let candidates: Vec<Candidate<(Vec<f64>, Vec<f64>)>> = options
453        .into_iter()
454        .filter(|(values, _)| values.len() >= 2)
455        .map(|(values, minor)| {
456            let at: Vec<f64> = values.iter().map(|v| v.ln_1p()).collect();
457            let per_cell = length / (at[at.len() - 1] - at[0]);
458            let gap = at
459                .windows(2)
460                .map(|w| (w[1] - w[0]) * per_cell)
461                .fold(f64::INFINITY, f64::min);
462            Candidate {
463                ticks: values.len(),
464                set: (values, minor),
465                gap,
466            }
467        })
468        .collect();
469    preferred(candidates, spacing, least)
470        .into_iter()
471        .map(|(values, minor)| {
472            let format = AxisFormat::log(&values, numbers);
473            let levels = (0..2)
474                .map_while(|level| values.iter().map(|&v| format.label(v, level)).collect())
475                .collect();
476            let at: Vec<f64> = values.iter().map(|v| v.ln_1p()).collect();
477            let bounds = [at[0], at[at.len() - 1]];
478            let per_cell = length / (bounds[1] - bounds[0]);
479            // Minor ticks inside the axis, and only where every one keeps its
480            // distance from the next.
481            let minor: Vec<f64> = minor
482                .iter()
483                .filter(|v| **v > values[0] && **v < values[values.len() - 1])
484                .map(|v| v.ln_1p())
485                .collect();
486            let mut all: Vec<f64> = minor.iter().chain(&at).copied().collect();
487            all.sort_by(f64::total_cmp);
488            let roomy = all
489                .windows(2)
490                .all(|w| (w[1] - w[0]) * per_cell >= minor_gap);
491            let minor = if roomy { minor } else { Vec::new() };
492            TickSet {
493                ticks: at,
494                minor,
495                levels,
496                bounds,
497            }
498        })
499        .collect()
500}
501
502/// Calendar tick sets over `bounds` for a `kind` axis `length` cells long.
503fn calendar_sets(
504    bounds: [f64; 2],
505    kind: XAxisTemporalKind,
506    length: f64,
507    spacing: f64,
508    least: f64,
509    minor_gap: f64,
510) -> Vec<TickSet> {
511    let [lo, hi] = bounds;
512    let (Some(start), Some(end)) = (ticks::to_datetime(lo, kind), ticks::to_datetime(hi, kind))
513    else {
514        return Vec::new();
515    };
516    if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 {
517        return Vec::new();
518    }
519    let per_cell = length / (hi - lo);
520    // Every step with two ticks or more, finest first, each with the fewest cells
521    // between two of its ticks.
522    let all: Vec<(
523        ticks::CalendarStep,
524        Vec<chrono::NaiveDateTime>,
525        Vec<f64>,
526        f64,
527    )> = ticks::calendar_steps(kind)
528        .filter_map(|step| {
529            let at = ticks::calendar_ticks(start, end, step);
530            let values: Vec<f64> = at
531                .iter()
532                .map(|t| ticks::from_datetime(*t, kind))
533                .collect::<Option<_>>()?;
534            let gap = values
535                .windows(2)
536                .map(|w| (w[1] - w[0]) * per_cell)
537                .fold(f64::INFINITY, f64::min);
538            (values.len() >= 2).then_some((step, at, values, gap))
539        })
540        .collect();
541    let options = all
542        .iter()
543        .enumerate()
544        .map(|(i, (_, _, values, gap))| Candidate {
545            set: i,
546            gap: *gap,
547            ticks: values.len(),
548        })
549        .collect();
550    preferred(options, spacing, least)
551        .into_iter()
552        .map(|i| {
553            let (step, at, values, _) = &all[i];
554            // Minor ticks: the finest step that ticks at every one of these, and
555            // between, with room.
556            let minor = all[..i]
557                .iter()
558                .filter(|(.., gap)| *gap >= minor_gap)
559                .find(|(_, _, finer, _)| values.iter().all(|v| finer.contains(v)))
560                .map(|(_, _, finer, _)| {
561                    finer
562                        .iter()
563                        .copied()
564                        .filter(|v| !values.contains(v))
565                        .collect()
566                })
567                .unwrap_or_default();
568            TickSet {
569                ticks: values.clone(),
570                minor,
571                levels: vec![
572                    ticks::calendar_labels(at, step.unit, kind, true),
573                    ticks::calendar_labels(at, step.unit, kind, false),
574                ],
575                bounds,
576            }
577        })
578        .collect()
579}
580
581/// Where an axis's values land on screen: `cells` cells from `start`, each split in
582/// `sub` dots by the marker the plot draws with.
583#[derive(Clone, Copy, Debug)]
584pub struct Track {
585    pub start: u16,
586    pub cells: u16,
587    pub sub: u16,
588}
589
590impl Track {
591    /// The cell `f` of the way along, as ratatui's canvas rounds a point to its dot.
592    pub fn cell(&self, f: f64) -> u16 {
593        let dots = u32::from(self.cells) * u32::from(self.sub.max(1));
594        let dot = (f.clamp(0.0, 1.0) * f64::from(dots.saturating_sub(1))).round() as u32;
595        self.start + (dot / u32::from(self.sub.max(1))) as u16
596    }
597
598    /// Cells from the first value's to the last's.
599    fn length(&self) -> f64 {
600        let sub = f64::from(self.sub.max(1));
601        (f64::from(self.cells) * sub - 1.0).max(0.0) / sub
602    }
603}
604
605/// The dots per cell, across and down, of a canvas drawn with `marker`.
606pub fn resolution(marker: Marker) -> (u16, u16) {
607    match marker {
608        Marker::Braille | Marker::Octant => (2, 4),
609        Marker::Sextant => (2, 3),
610        Marker::Quadrant => (2, 2),
611        Marker::HalfBlock => (1, 2),
612        _ => (1, 1),
613    }
614}
615
616/// An axis's ticks as placed: each labeled tick's cell and label, and the cells of
617/// every major and minor tick.
618#[derive(Clone, Debug, Default)]
619pub struct Placed {
620    pub labels: Vec<(u16, String)>,
621    pub majors: Vec<u16>,
622    pub minors: Vec<u16>,
623    pub bounds: [f64; 2],
624}
625
626/// A chart's legend: a name per series, each in the style its series draws in.
627#[derive(Clone, Debug, Default)]
628pub struct Legend {
629    pub entries: Vec<(String, Style)>,
630}
631
632/// The widest a legend name is drawn before it is cut.
633const LEGEND_NAME_MAX: usize = 24;
634
635impl Legend {
636    /// The cells it covers for names `name_width` wide: a cell of air each side, the
637    /// swatch and a space, then the name; a row per series.
638    fn size(&self, name_width: usize) -> (u16, u16) {
639        (name_width as u16 + 4, self.entries.len() as u16)
640    }
641}
642
643/// A chart's two axes and how they are drawn.
644pub struct PlotAxes<'a> {
645    pub x: AxisSpec<'a>,
646    pub y: AxisSpec<'a>,
647    /// The axis lines and their tick marks.
648    pub line: Style,
649    pub labels: Style,
650    pub titles: Style,
651    /// The grid at the major ticks, in this style; `None` draws none.
652    pub grid: Option<Style>,
653    /// The marker the series draw with: ticks sit on the cells their values land on.
654    pub marker: Marker,
655    /// The legend, placed where it covers the fewest marks; `None` draws none.
656    pub legend: Option<Legend>,
657}
658
659/// Where a chart's parts sit in its area.
660pub struct PlotFrame {
661    pub y_title: Option<Rect>,
662    /// What ratatui's `Chart` is drawn in: the plot, its axes and y labels.
663    pub chart: Rect,
664    /// The plot inside `chart`, right of the y axis and above the x axis.
665    pub graph: Rect,
666    /// The x labels' row, under the x axis.
667    pub labels: Option<Rect>,
668    pub x_title: Option<Rect>,
669    /// The y axis's ticks, labels and range, rows counted on screen.
670    pub y: Placed,
671    y_width: u16,
672}
673
674impl<'a> PlotAxes<'a> {
675    /// Plain axes in `style`, as the chart view draws them: no grid, no legend, ticks
676    /// placed for `marker`.
677    pub fn new(x: AxisSpec<'a>, y: AxisSpec<'a>, style: Style, marker: Marker) -> Self {
678        Self {
679            x,
680            y,
681            line: style,
682            labels: style,
683            titles: style,
684            grid: None,
685            marker,
686            legend: None,
687        }
688    }
689
690    /// The frame for `area`: title rows while the plot keeps its rows, then the
691    /// plot as ratatui lays it out beside the y labels that fit.
692    pub fn frame(&self, area: Rect) -> PlotFrame {
693        // The x axis line and the label row under it.
694        let base = 2 + MIN_PLOT_ROWS;
695        let y_title = !self.y.title.is_empty() && area.height > base;
696        let x_title = !self.x.title.is_empty() && area.height > base + u16::from(y_title);
697        let mut chart = area;
698        let y_title = y_title.then(|| {
699            chart.y += 1;
700            chart.height -= 1;
701            Rect { height: 1, ..area }
702        });
703        let x_title = x_title.then(|| {
704            chart.height -= 1;
705            Rect {
706                y: chart.bottom(),
707                height: 1,
708                ..area
709            }
710        });
711        // The plot's rows do not depend on the labels' width; its columns do.
712        let rows = graph_area(chart, 0).0;
713        let track = Track {
714            start: rows.top(),
715            cells: rows.height,
716            sub: resolution(self.marker).1,
717        };
718        let y = fit_y_labels(&self.y, track, chart.width / 3);
719        let width = y.labels.iter().map(|(_, l)| l.width()).max().unwrap_or(0) as u16;
720        let (graph, labels) = graph_area(chart, width);
721        PlotFrame {
722            y_title,
723            chart,
724            graph,
725            labels,
726            x_title,
727            y,
728            y_width: width,
729        }
730    }
731
732    /// Draw `chart`'s datasets with these axes in `area`: the grid under them, the
733    /// legend over them, then the tick marks, labels and titles.
734    pub fn render(&self, chart: Chart<'_>, area: Rect, buf: &mut Buffer, g: &Glyphs) -> PlotFrame {
735        self.render_in(self.frame(area), chart, buf, g)
736    }
737
738    /// [`Self::render`] in a frame already worked out by [`Self::frame`].
739    pub fn render_in(
740        &self,
741        frame: PlotFrame,
742        chart: Chart<'_>,
743        buf: &mut Buffer,
744        g: &Glyphs,
745    ) -> PlotFrame {
746        let x_track = Track {
747            start: frame.graph.left(),
748            cells: frame.graph.width,
749            sub: resolution(self.marker).0,
750        };
751        let x = frame
752            .labels
753            .map(|row| fit_x_labels(&self.x, (row.left(), row.right()), x_track))
754            .unwrap_or_default();
755        // Two empty x labels have ratatui keep the label row and draw the x axis line;
756        // blank y labels as wide as datui's keep the space left of the y axis, where
757        // datui writes them on the rows of their ticks.
758        let blank = " ".repeat(frame.y_width as usize);
759        let y_labels = if frame.y_width > 0 {
760            vec![Span::raw(blank.as_str()), Span::raw(blank.as_str())]
761        } else {
762            Vec::new()
763        };
764        let chart = chart
765            .x_axis(
766                Axis::default()
767                    .bounds(self.x.bounds)
768                    .style(self.line)
769                    .labels(["", ""]),
770            )
771            .y_axis(
772                Axis::default()
773                    .bounds(frame.y.bounds)
774                    .style(self.line)
775                    .labels(y_labels),
776            )
777            .legend_position(None);
778        // The marks drawn once, on their own: the legend is placed by them, and they
779        // go over the grid, which their blank cells leave alone.
780        let mut marks = Buffer::empty(frame.chart);
781        chart.render(frame.chart, &mut marks);
782        let legend = self
783            .legend
784            .as_ref()
785            .and_then(|legend| place_legend(&marks, &frame, legend, g));
786        if let Some(style) = self.grid {
787            draw_grid(buf, frame.graph, &x.majors, &frame.y.majors, style, g);
788        }
789        let blank = ratatui::buffer::Cell::default();
790        for (i, cell) in marks.content().iter().enumerate() {
791            if *cell != blank {
792                let (x, y) = marks.pos_of(i);
793                buf[(x, y)] = cell.clone();
794            }
795        }
796        g.plot.redraw_axes(frame.chart, buf);
797        draw_tick_marks(buf, &frame, &x, self.line, g);
798        let label_x = frame.chart.left();
799        for (row, label) in &frame.y.labels {
800            let pad = (frame.y_width as usize).saturating_sub(label.width());
801            buf.set_string(label_x + pad as u16, *row, label, self.labels);
802        }
803        if let Some(row) = frame.labels {
804            for (x, label) in &x.labels {
805                buf.set_string(*x, row.y, label, self.labels);
806            }
807        }
808        if let Some(row) = frame.y_title {
809            let title = cut(self.y.title, row.width as usize, g);
810            buf.set_string(row.x, row.y, title, self.titles);
811        }
812        if let Some(row) = frame.x_title {
813            let title = cut(self.x.title, row.width as usize, g);
814            let x = row.right() - title.width() as u16;
815            buf.set_string(x, row.y, title, self.titles);
816        }
817        if let (Some(legend), Some(place)) = (&self.legend, legend) {
818            draw_legend(buf, legend, place, self.labels, g);
819        }
820        frame
821    }
822}
823
824/// Where a legend goes: its area, and how wide its names are drawn.
825#[derive(Clone, Copy, Debug)]
826struct LegendPlace {
827    area: Rect,
828    name_width: usize,
829}
830
831/// Where in `frame`'s plot the legend covers the fewest marks of `probe` (braille cells
832/// by dots): a corner or edge middle, corners first and top right first on ties. `None`
833/// when the plot cannot spare a quarter.
834fn place_legend(
835    probe: &Buffer,
836    frame: &PlotFrame,
837    legend: &Legend,
838    g: &Glyphs,
839) -> Option<LegendPlace> {
840    let graph = frame.graph;
841    let widest = legend
842        .entries
843        .iter()
844        .map(|(name, _)| name.width())
845        .max()
846        .unwrap_or(0);
847    let name_width = widest
848        .min(LEGEND_NAME_MAX)
849        .min((graph.width / 2).saturating_sub(4) as usize);
850    let (w, h) = legend.size(name_width);
851    if legend.entries.is_empty()
852        || name_width == 0
853        || name_width < widest.min(4)
854        || w > graph.width / 2
855        || h > graph.height / 2
856    {
857        return None;
858    }
859    let weight = |symbol: &str| -> usize {
860        let mut chars = symbol.chars();
861        match (chars.next(), chars.next()) {
862            (None | Some(' '), _) => 0,
863            (Some(c), None) if ('\u{2800}'..='\u{28ff}').contains(&c) => {
864                (c as u32 - 0x2800).count_ones() as usize
865            }
866            _ => 1,
867        }
868    };
869    // The axes ratatui drew are not marks.
870    let axis = [g.plot.axis.vertical, g.plot.axis.horizontal];
871    let marks = |x0: u16, y0: u16| -> usize {
872        (y0..y0 + h)
873            .flat_map(|y| (x0..x0 + w).map(move |x| (x, y)))
874            .map(|(x, y)| probe[(x, y)].symbol())
875            .filter(|symbol| !axis.contains(symbol))
876            .map(weight)
877            .sum()
878    };
879    let (left, right) = (graph.left(), graph.right() - w);
880    let (top, bottom) = (graph.top(), graph.bottom() - h);
881    let (center, middle) = (left + (right - left) / 2, top + (bottom - top) / 2);
882    [
883        (right, top),
884        (left, top),
885        (right, bottom),
886        (left, bottom),
887        (center, top),
888        (center, bottom),
889        (left, middle),
890        (right, middle),
891    ]
892    .into_iter()
893    .min_by_key(|&(x, y)| marks(x, y))
894    .map(|(x, y)| LegendPlace {
895        area: Rect::new(x, y, w, h),
896        name_width,
897    })
898}
899
900/// The legend in `place`: on the plot's background, cleared of the marks under it,
901/// a swatch in each series' color and its name beside it. No frame: the cleared
902/// patch sets it off.
903fn draw_legend(buf: &mut Buffer, legend: &Legend, place: LegendPlace, text: Style, g: &Glyphs) {
904    let area = place.area;
905    for y in area.top()..area.bottom() {
906        for x in area.left()..area.right() {
907            buf[(x, y)].reset();
908        }
909    }
910    for ((name, style), y) in legend.entries.iter().zip(area.top()..) {
911        buf.set_string(area.x + 1, y, g.bar_eighths[7], *style);
912        let name = cut(name, place.name_width, g);
913        buf.set_string(area.x + 3, y, name, text);
914    }
915}
916
917/// The grid: a dotted line across at each y tick and down at each x tick, but not
918/// beside the axes, where it would double them. Drawn before the series, whose marks
919/// replace it where they fall.
920fn draw_grid(
921    buf: &mut Buffer,
922    graph: Rect,
923    columns: &[u16],
924    rows: &[u16],
925    style: Style,
926    g: &Glyphs,
927) {
928    let rows: Vec<u16> = rows
929        .iter()
930        .copied()
931        .filter(|&y| y + 1 < graph.bottom())
932        .collect();
933    let columns: Vec<u16> = columns
934        .iter()
935        .copied()
936        .filter(|&x| x > graph.left())
937        .collect();
938    for &y in &rows {
939        for x in graph.left()..graph.right() {
940            buf[(x, y)].set_symbol(g.plot.grid_across).set_style(style);
941        }
942    }
943    for &x in &columns {
944        for y in graph.top()..graph.bottom() {
945            buf[(x, y)].set_symbol(g.plot.grid_down).set_style(style);
946        }
947    }
948}
949
950/// A mark on the axis line at every tick, major or minor.
951fn draw_tick_marks(buf: &mut Buffer, frame: &PlotFrame, x: &Placed, style: Style, g: &Glyphs) {
952    let graph = frame.graph;
953    if graph.left() > frame.chart.left() {
954        let column = graph.left() - 1;
955        for &row in frame.y.majors.iter().chain(&frame.y.minors) {
956            let cell = &mut buf[(column, row)];
957            if cell.symbol() == g.plot.axis.vertical {
958                cell.set_symbol(g.plot.tick_y).set_style(style);
959            }
960        }
961    }
962    let row = graph.bottom();
963    if row < frame.chart.bottom() {
964        for &column in x.majors.iter().chain(&x.minors) {
965            let cell = &mut buf[(column, row)];
966            if cell.symbol() == g.plot.axis.horizontal {
967                cell.set_symbol(g.plot.tick_x).set_style(style);
968            }
969        }
970    }
971}
972
973/// The plot and the x label row ratatui's `Chart` lays out in `chart` beside y
974/// labels `y_label_width` wide, when its x axis has labels. Kept in step with
975/// `Chart::layout`; `the_frame_matches_ratatuis_layout` checks it.
976fn graph_area(chart: Rect, y_label_width: u16) -> (Rect, Option<Rect>) {
977    if chart.is_empty() {
978        return (Rect::default(), None);
979    }
980    let mut x = chart.left();
981    let mut y = chart.bottom() - 1;
982    let mut labels = None;
983    if y > chart.top() {
984        labels = Some(Rect {
985            y,
986            height: 1,
987            ..chart
988        });
989        y -= 1;
990    }
991    x += y_label_width.min(chart.width / 3);
992    if y > chart.top() {
993        y -= 1;
994    }
995    if x + 1 < chart.right() {
996        x += 1;
997    }
998    let graph = Rect::new(
999        x,
1000        chart.top(),
1001        chart.right().saturating_sub(x),
1002        y - chart.top() + 1,
1003    );
1004    (graph, labels)
1005}
1006
1007/// Every subset of `n` evenly spaced ticks that keeps them evenly spaced and keeps
1008/// both ends, most ticks first.
1009fn strides(n: usize) -> impl Iterator<Item = Vec<usize>> {
1010    let last = n.saturating_sub(1);
1011    (1..=last.max(1))
1012        .filter(move |s| last.is_multiple_of(*s))
1013        .map(move |s| (0..n).step_by(s).collect())
1014}
1015
1016/// Where `v` falls along `bounds`, from 0 to 1.
1017fn fraction(v: f64, [lo, hi]: [f64; 2]) -> f64 {
1018    if hi > lo { (v - lo) / (hi - lo) } else { 0.0 }
1019}
1020
1021/// The y labels for a plot whose rows are `track`, with at most `width` cells left of
1022/// its axis: about one per four rows, each on its tick's row, in the fullest form
1023/// that fits the width and tells them apart.
1024pub fn fit_y_labels(axis: &AxisSpec<'_>, track: Track, width: u16) -> Placed {
1025    let groups = axis.tick_sets(track.length(), Y_SPACING, Y_LEAST, Y_MINOR_GAP);
1026    let place = |set: &TickSet, labels: Option<&Vec<String>>| {
1027        let row = |v: f64| track.cell(1.0 - fraction(v, set.bounds));
1028        Placed {
1029            labels: labels
1030                .map(|labels| {
1031                    set.ticks
1032                        .iter()
1033                        .zip(labels)
1034                        .map(|(&v, l)| (row(v), format!("{l:>w$}", w = axis.pad)))
1035                        .collect()
1036                })
1037                .unwrap_or_default(),
1038            majors: set.ticks.iter().map(|&v| row(v)).collect(),
1039            minors: set.minor.iter().map(|&v| row(v)).collect(),
1040            bounds: set.bounds,
1041        }
1042    };
1043    let fits = |set: &TickSet, labels: &Vec<String>| {
1044        let rows: Vec<u16> = set
1045            .ticks
1046            .iter()
1047            .map(|&v| track.cell(1.0 - fraction(v, set.bounds)))
1048            .collect();
1049        set.ticks.len() <= usize::from(track.cells.max(2))
1050            && rows.windows(2).all(|w| w[0] != w[1])
1051            && labels
1052                .iter()
1053                .all(|l| l.width().max(axis.pad) <= width as usize)
1054            && distinct(labels.iter())
1055    };
1056    for set in groups.iter().flatten() {
1057        if let Some(labels) = set.levels.iter().find(|labels| fits(set, labels)) {
1058            return place(set, Some(labels));
1059        }
1060    }
1061    // Nothing fits: the first set in its fullest form, cut short by the plot.
1062    match groups.iter().flatten().next() {
1063        Some(set) => place(set, set.levels.first()),
1064        None => Placed {
1065            bounds: axis.bounds,
1066            ..Placed::default()
1067        },
1068    }
1069}
1070
1071/// Neighbors differ; two alike say nothing about the space between them.
1072fn distinct<'a>(labels: impl Iterator<Item = &'a String>) -> bool {
1073    let labels: Vec<_> = labels.collect();
1074    labels.windows(2).all(|w| w[0] != w[1])
1075}
1076
1077/// The x labels fitting a row over columns `span` under a plot of `track`, each centered
1078/// under its tick, kept on the row, `LABEL_GAP` cells apart: about one per 15 columns,
1079/// coarser then shorter when crowded, none when even two do not fit.
1080pub fn fit_x_labels(axis: &AxisSpec<'_>, span: (u16, u16), track: Track) -> Placed {
1081    let (start, end) = span;
1082    let column = |v: f64| track.cell(fraction(v, axis.bounds));
1083    let groups = axis.tick_sets(track.length(), X_SPACING, X_LEAST, X_MINOR_GAP);
1084    for sets in &groups {
1085        for level in 0..MAX_LEVELS {
1086            for set in sets {
1087                let Some(labels) = set.levels.get(level) else {
1088                    continue;
1089                };
1090                let mut placed: Vec<(u16, String)> = Vec::with_capacity(labels.len());
1091                let mut next_free = start;
1092                let fits = set.ticks.iter().zip(labels).all(|(&v, label)| {
1093                    let w = label.width() as u16;
1094                    if w > end.saturating_sub(start) {
1095                        return false;
1096                    }
1097                    let x = column(v).saturating_sub(w / 2).clamp(start, end - w);
1098                    let clear =
1099                        x >= next_free && placed.last().is_none_or(|(_, prev)| prev != label);
1100                    next_free = x + w + LABEL_GAP;
1101                    placed.push((x, label.clone()));
1102                    clear
1103                });
1104                if fits {
1105                    return Placed {
1106                        labels: placed,
1107                        majors: set.ticks.iter().map(|&v| column(v)).collect(),
1108                        minors: set.minor.iter().map(|&v| column(v)).collect(),
1109                        bounds: axis.bounds,
1110                    };
1111                }
1112            }
1113        }
1114    }
1115    Placed {
1116        bounds: axis.bounds,
1117        ..Placed::default()
1118    }
1119}
1120
1121/// `text` in at most `width` cells, cut with the ellipsis when longer.
1122pub fn cut(text: &str, width: usize, g: &Glyphs) -> String {
1123    if text.width() <= width {
1124        return text.to_string();
1125    }
1126    let keep = width.saturating_sub(g.ellipsis.width());
1127    let mut used = 0;
1128    let mut out: String = text
1129        .chars()
1130        .take_while(|c| {
1131            used += c.width().unwrap_or(0);
1132            used <= keep
1133        })
1134        .collect();
1135    if keep + g.ellipsis.width() <= width {
1136        out.push_str(g.ellipsis);
1137    }
1138    out
1139}
1140
1141#[cfg(test)]
1142mod tests;