1use ratatui::{
15 buffer::Buffer,
16 layout::Rect,
17 style::Style,
18 symbols::Marker,
19 text::Span,
20 widgets::{Axis, Chart, Widget},
21};
22use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
23
24use crate::chart_data::{AxisFormat, AxisNumbers, XAxisTemporalKind, x_axis_label_at};
25use crate::glyphs::Glyphs;
26use crate::widgets::ticks;
27
28const MIN_PLOT_ROWS: u16 = 3;
30const MAX_LEVELS: usize = 8;
32const LABEL_GAP: u16 = 2;
35const X_SPACING: f64 = 15.0;
37const X_LEAST: f64 = 6.0;
38const Y_SPACING: f64 = 4.0;
40const Y_LEAST: f64 = 2.0;
41const X_MINOR_GAP: f64 = 3.0;
43const Y_MINOR_GAP: f64 = 3.0;
44
45pub type TickLabel<'a> = Box<dyn Fn(f64, usize) -> Option<String> + 'a>;
47
48enum Scale<'a> {
50 Numbers { numbers: AxisNumbers, widen: bool },
53 Calendar {
55 kind: XAxisTemporalKind,
56 numbers: AxisNumbers,
57 },
58 Log { numbers: AxisNumbers },
61 Fixed {
63 ticks: Vec<f64>,
64 label: TickLabel<'a>,
65 },
66}
67
68pub struct AxisSpec<'a> {
70 pub bounds: [f64; 2],
71 scale: Scale<'a>,
72 pub title: &'a str,
73 pad: usize,
75}
76
77#[derive(Clone, Debug, Default)]
80pub struct TickSet {
81 pub ticks: Vec<f64>,
82 pub minor: Vec<f64>,
83 pub levels: Vec<Vec<String>>,
84 pub bounds: [f64; 2],
86}
87
88impl<'a> AxisSpec<'a> {
89 pub fn fixed(bounds: [f64; 2], ticks: Vec<f64>, label: TickLabel<'a>, title: &'a str) -> Self {
92 Self {
93 bounds,
94 scale: Scale::Fixed { ticks, label },
95 title,
96 pad: 0,
97 }
98 }
99
100 pub fn ends_and_middle(bounds: [f64; 2], label: TickLabel<'a>, title: &'a str) -> Self {
102 let [lo, hi] = bounds;
103 Self::fixed(bounds, vec![lo, (lo + hi) / 2.0, hi], label, title)
104 }
105
106 pub fn numbers(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
109 Self {
110 bounds,
111 scale: Scale::Numbers {
112 numbers: numbers.clone(),
113 widen: false,
114 },
115 title,
116 pad: 0,
117 }
118 }
119
120 pub fn y_numbers(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
124 Self {
125 scale: Scale::Numbers {
126 numbers: numbers.clone(),
127 widen: true,
128 },
129 ..Self::numbers(bounds, numbers, title)
130 }
131 }
132
133 pub fn calendar(
136 bounds: [f64; 2],
137 kind: XAxisTemporalKind,
138 numbers: &AxisNumbers,
139 title: &'a str,
140 ) -> Self {
141 if kind == XAxisTemporalKind::Numeric {
142 return Self::numbers(bounds, numbers, title);
143 }
144 Self {
145 bounds,
146 scale: Scale::Calendar {
147 kind,
148 numbers: numbers.clone(),
149 },
150 title,
151 pad: 0,
152 }
153 }
154
155 pub fn numbers_as(
159 bounds: [f64; 2],
160 numbers: &AxisNumbers,
161 title: &'a str,
162 shown: impl Fn(f64) -> f64 + 'a,
163 ) -> Self {
164 let [lo, hi] = bounds;
165 let ticks = vec![lo, (lo + hi) / 2.0, hi];
166 let shown_ticks: Vec<f64> = ticks.iter().map(|&v| shown(v)).collect();
167 let format = AxisFormat::new(&shown_ticks, numbers);
168 let label = Box::new(move |v, level| format.label(shown(v), level));
169 Self::fixed(bounds, ticks, label, title)
170 }
171
172 pub fn y_log(bounds: [f64; 2], numbers: &AxisNumbers, title: &'a str) -> Self {
177 Self {
178 bounds,
179 scale: Scale::Log {
180 numbers: numbers.clone(),
181 },
182 title,
183 pad: 0,
184 }
185 }
186
187 pub fn padded(self, width: usize) -> Self {
189 Self { pad: width, ..self }
190 }
191
192 fn tick_sets(
196 &self,
197 track: Track,
198 spacing: f64,
199 least: f64,
200 minor_gap: f64,
201 ) -> Vec<Vec<TickSet>> {
202 let [lo, hi] = self.bounds;
203 let length = track.length();
204 match &self.scale {
205 Scale::Fixed { ticks, label } => vec![
206 strides(ticks.len())
207 .map(|subset| {
208 let ticks: Vec<f64> = subset.iter().map(|&i| ticks[i]).collect();
209 let levels = (0..MAX_LEVELS)
210 .map_while(|level| ticks.iter().map(|&v| label(v, level)).collect())
211 .collect();
212 TickSet {
213 ticks,
214 minor: Vec::new(),
215 levels,
216 bounds: self.bounds,
217 }
218 })
219 .collect(),
220 ],
221 Scale::Numbers { numbers, widen } => {
222 vec![number_sets(
223 self.bounds,
224 numbers,
225 *widen,
226 length,
227 spacing,
228 least,
229 minor_gap,
230 )]
231 }
232 Scale::Log { numbers } => vec![log_sets(
233 self.bounds,
234 numbers,
235 length,
236 spacing,
237 least,
238 minor_gap,
239 )],
240 Scale::Calendar { kind, numbers } => {
241 let primary = calendar_sets(self.bounds, *kind, length, spacing, least, minor_gap);
242 let format = AxisFormat::ends_and_middle(self.bounds, numbers);
243 let kind = *kind;
244 let ends = AxisSpec::ends_and_middle(
245 self.bounds,
246 Box::new(move |v, level| x_axis_label_at(v, kind, (lo, hi), level, &format)),
247 "",
248 );
249 let fallback = ends.tick_sets(track, spacing, least, minor_gap).remove(0);
250 vec![primary, fallback]
251 }
252 }
253 }
254}
255
256fn with_ticks(sets: Vec<TickSet>) -> Vec<TickSet> {
258 sets.into_iter().filter(|s| s.ticks.len() >= 2).collect()
259}
260
261struct Candidate<T> {
264 set: T,
265 gap: f64,
266 ticks: usize,
267}
268
269fn preferred<T>(options: Vec<Candidate<T>>, spacing: f64, least: f64) -> Vec<T> {
275 let closeness = |gap: f64| (gap / spacing).ln().abs();
276 let best = options
277 .iter()
278 .enumerate()
279 .filter(|(_, o)| o.gap >= least)
280 .min_by(|a, b| closeness(a.1.gap).total_cmp(&closeness(b.1.gap)))
281 .map(|(i, _)| i);
282 let best = best.map(|best| {
283 if options[best].ticks > 2 {
284 return best;
285 }
286 (0..best)
287 .rev()
288 .find(|&i| options[i].gap >= least)
289 .unwrap_or(best)
290 });
291 match best {
292 Some(best) => options.into_iter().skip(best).map(|o| o.set).collect(),
293 None => options
295 .into_iter()
296 .last()
297 .map(|o| o.set)
298 .into_iter()
299 .collect(),
300 }
301}
302
303fn number_sets(
305 bounds: [f64; 2],
306 numbers: &AxisNumbers,
307 widen: bool,
308 length: f64,
309 spacing: f64,
310 least: f64,
311 minor_gap: f64,
312) -> Vec<TickSet> {
313 let [lo, hi] = bounds;
314 if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 {
315 let format = AxisFormat::new(&[lo], numbers);
317 let levels = (0..2)
318 .map_while(|level| format.label(lo, level).map(|l| vec![l]))
319 .collect();
320 return vec![TickSet {
321 ticks: vec![lo],
322 minor: Vec::new(),
323 levels,
324 bounds,
325 }];
326 }
327 let finest = (hi - lo) * least.min(minor_gap) / length;
328 let options: Vec<Candidate<(f64, [f64; 2])>> = ticks::nice_steps(lo, hi, finest, numbers.whole)
329 .into_iter()
330 .map(|step| {
331 let range = if widen {
332 ticks::widen_snug(lo, hi, step, numbers.whole)
333 } else {
334 bounds
335 };
336 Candidate {
337 set: (step, range),
338 gap: step / (range[1] - range[0]) * length,
339 ticks: ticks::multiples(range[0], range[1], step).len(),
340 }
341 })
342 .filter(|o| o.ticks >= 2)
343 .collect();
344 let sets = preferred(options, spacing, least)
345 .into_iter()
346 .map(|(step, range)| {
347 let majors = ticks::multiples(range[0], range[1], step);
348 let format = AxisFormat::new(&majors, numbers);
349 let levels = (0..2)
350 .map_while(|level| majors.iter().map(|&v| format.label(v, level)).collect())
351 .collect();
352 let per_cell = length / (range[1] - range[0]);
353 let minor = ticks::minor_steps(step, numbers.whole)
354 .into_iter()
355 .find(|m| m * per_cell >= minor_gap)
356 .map(|m| {
357 ticks::multiples(range[0], range[1], m)
358 .into_iter()
359 .filter(|v| !majors.iter().any(|t| (t - v).abs() < m * 1e-6))
360 .collect()
361 })
362 .unwrap_or_default();
363 TickSet {
364 ticks: majors,
365 minor,
366 levels,
367 bounds: range,
368 }
369 })
370 .collect();
371 with_ticks(sets)
372}
373
374fn log_sets(
380 bounds: [f64; 2],
381 numbers: &AxisNumbers,
382 length: f64,
383 spacing: f64,
384 least: f64,
385 minor_gap: f64,
386) -> Vec<TickSet> {
387 let [lo, hi] = bounds;
388 let (low, high) = (lo.exp_m1().max(0.0), hi.exp_m1());
389 if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 || high <= 0.0 {
390 let format = AxisFormat::log(&[low], numbers);
391 return vec![TickSet {
392 ticks: vec![lo],
393 minor: Vec::new(),
394 levels: vec![vec![format.label(low, 0).unwrap_or_default()]],
395 bounds,
396 }];
397 }
398 let options: Vec<(Vec<f64>, Vec<f64>)> = if high <= 1.0 {
400 let finest = (high - low) * least.min(minor_gap) / length / 2.0;
402 ticks::nice_steps(low, high, finest, false)
403 .into_iter()
404 .map(|step| {
405 let [a, b] = ticks::widen(low, high, step);
406 (ticks::multiples(a, b, step), Vec::new())
407 })
408 .collect()
409 } else {
410 let decade = |v: f64| (v.log10() + 1e-9).floor() as i32;
411 let values = |mantissas: &[f64], every: i32| {
414 let first = if low >= 1.0 { decade(low) } else { 0 };
415 let first = first - first.rem_euclid(every);
416 let mut values: Vec<f64> = if low < 1.0 { vec![0.0] } else { Vec::new() };
417 let mut k = first;
418 'up: loop {
419 for &m in mantissas {
420 let v: f64 = format!("{m}e{k}").parse().unwrap_or(f64::INFINITY);
422 values.push(v);
423 if v >= high * (1.0 - 1e-9) {
424 break 'up;
425 }
426 }
427 k += every;
428 if k > 308 {
429 break;
430 }
431 }
432 if every > 1 || mantissas.len() == 1 {
435 values.retain(|&v| v != 1.0 || low >= 1.0);
436 }
437 let start = values
439 .iter()
440 .rposition(|&v| v <= low * (1.0 + 1e-9))
441 .unwrap_or(0);
442 values.split_off(start)
443 };
444 let fine = values(&[1.0, 2.0, 5.0], 1);
445 let mut options = vec![(fine.clone(), Vec::new())];
446 for every in [1, 2, 3, 5, 10, 20, 50, 100] {
447 let majors = values(&[1.0], every);
448 let minor = if every == 1 {
449 fine.iter()
450 .copied()
451 .filter(|v| !majors.contains(v))
452 .collect()
453 } else {
454 Vec::new()
455 };
456 options.push((majors, minor));
457 }
458 options
459 };
460 let candidates: Vec<Candidate<(Vec<f64>, Vec<f64>)>> = options
461 .into_iter()
462 .filter(|(values, _)| values.len() >= 2)
463 .map(|(values, minor)| {
464 let at: Vec<f64> = values.iter().map(|v| v.ln_1p()).collect();
465 let per_cell = length / (at[at.len() - 1] - at[0]);
466 let gap = at
467 .windows(2)
468 .map(|w| (w[1] - w[0]) * per_cell)
469 .fold(f64::INFINITY, f64::min);
470 Candidate {
471 ticks: values.len(),
472 set: (values, minor),
473 gap,
474 }
475 })
476 .collect();
477 preferred(candidates, spacing, least)
478 .into_iter()
479 .map(|(values, minor)| {
480 let format = AxisFormat::log(&values, numbers);
481 let levels = (0..2)
482 .map_while(|level| values.iter().map(|&v| format.label(v, level)).collect())
483 .collect();
484 let at: Vec<f64> = values.iter().map(|v| v.ln_1p()).collect();
485 let bounds = [at[0], at[at.len() - 1]];
486 let per_cell = length / (bounds[1] - bounds[0]);
487 let minor: Vec<f64> = minor
490 .iter()
491 .filter(|v| **v > values[0] && **v < values[values.len() - 1])
492 .map(|v| v.ln_1p())
493 .collect();
494 let mut all: Vec<f64> = minor.iter().chain(&at).copied().collect();
495 all.sort_by(f64::total_cmp);
496 let roomy = all
497 .windows(2)
498 .all(|w| (w[1] - w[0]) * per_cell >= minor_gap);
499 let minor = if roomy { minor } else { Vec::new() };
500 TickSet {
501 ticks: at,
502 minor,
503 levels,
504 bounds,
505 }
506 })
507 .collect()
508}
509
510fn calendar_sets(
512 bounds: [f64; 2],
513 kind: XAxisTemporalKind,
514 length: f64,
515 spacing: f64,
516 least: f64,
517 minor_gap: f64,
518) -> Vec<TickSet> {
519 let [lo, hi] = bounds;
520 let (Some(start), Some(end)) = (ticks::to_datetime(lo, kind), ticks::to_datetime(hi, kind))
521 else {
522 return Vec::new();
523 };
524 if hi.partial_cmp(&lo) != Some(std::cmp::Ordering::Greater) || length <= 0.0 {
525 return Vec::new();
526 }
527 let per_cell = length / (hi - lo);
528 let all: Vec<(
531 ticks::CalendarStep,
532 Vec<chrono::NaiveDateTime>,
533 Vec<f64>,
534 f64,
535 )> = ticks::calendar_steps(kind)
536 .filter_map(|step| {
537 let at = ticks::calendar_ticks(start, end, step);
538 let values: Vec<f64> = at
539 .iter()
540 .map(|t| ticks::from_datetime(*t, kind))
541 .collect::<Option<_>>()?;
542 let gap = values
543 .windows(2)
544 .map(|w| (w[1] - w[0]) * per_cell)
545 .fold(f64::INFINITY, f64::min);
546 (values.len() >= 2).then_some((step, at, values, gap))
547 })
548 .collect();
549 let options = all
550 .iter()
551 .enumerate()
552 .map(|(i, (_, _, values, gap))| Candidate {
553 set: i,
554 gap: *gap,
555 ticks: values.len(),
556 })
557 .collect();
558 preferred(options, spacing, least)
559 .into_iter()
560 .map(|i| {
561 let (step, at, values, _) = &all[i];
562 let minor = all[..i]
565 .iter()
566 .filter(|(.., gap)| *gap >= minor_gap)
567 .find(|(_, _, finer, _)| values.iter().all(|v| finer.contains(v)))
568 .map(|(_, _, finer, _)| {
569 finer
570 .iter()
571 .copied()
572 .filter(|v| !values.contains(v))
573 .collect()
574 })
575 .unwrap_or_default();
576 TickSet {
577 ticks: values.clone(),
578 minor,
579 levels: vec![
580 ticks::calendar_labels(at, step.unit, kind, true),
581 ticks::calendar_labels(at, step.unit, kind, false),
582 ],
583 bounds,
584 }
585 })
586 .collect()
587}
588
589#[derive(Clone, Copy, Debug)]
592pub struct Track {
593 pub start: u16,
594 pub cells: u16,
595 pub sub: u16,
596}
597
598impl Track {
599 pub fn cell(&self, f: f64) -> u16 {
601 let dots = u32::from(self.cells) * u32::from(self.sub.max(1));
602 let dot = (f.clamp(0.0, 1.0) * f64::from(dots.saturating_sub(1))).round() as u32;
603 self.start + (dot / u32::from(self.sub.max(1))) as u16
604 }
605
606 fn length(&self) -> f64 {
608 let sub = f64::from(self.sub.max(1));
609 (f64::from(self.cells) * sub - 1.0).max(0.0) / sub
610 }
611}
612
613pub fn resolution(marker: Marker) -> (u16, u16) {
615 match marker {
616 Marker::Braille | Marker::Octant => (2, 4),
617 Marker::Sextant => (2, 3),
618 Marker::Quadrant => (2, 2),
619 Marker::HalfBlock => (1, 2),
620 _ => (1, 1),
621 }
622}
623
624#[derive(Clone, Debug, Default)]
627pub struct Placed {
628 pub labels: Vec<(u16, String)>,
629 pub majors: Vec<u16>,
630 pub minors: Vec<u16>,
631 pub bounds: [f64; 2],
632}
633
634#[derive(Clone, Debug, Default)]
636pub struct Legend {
637 pub entries: Vec<(String, Style)>,
638}
639
640const LEGEND_NAME_MAX: usize = 24;
642
643impl Legend {
644 fn size(&self, name_width: usize) -> (u16, u16) {
647 (name_width as u16 + 4, self.entries.len() as u16)
648 }
649}
650
651pub struct PlotAxes<'a> {
653 pub x: AxisSpec<'a>,
654 pub y: AxisSpec<'a>,
655 pub line: Style,
657 pub labels: Style,
658 pub titles: Style,
659 pub grid: Option<Style>,
661 pub marker: Marker,
663 pub legend: Option<Legend>,
665}
666
667pub struct PlotFrame {
669 pub y_title: Option<Rect>,
670 pub chart: Rect,
672 pub graph: Rect,
674 pub labels: Option<Rect>,
676 pub x_title: Option<Rect>,
677 pub y: Placed,
679 y_width: u16,
680}
681
682impl<'a> PlotAxes<'a> {
683 pub fn new(x: AxisSpec<'a>, y: AxisSpec<'a>, style: Style, marker: Marker) -> Self {
686 Self {
687 x,
688 y,
689 line: style,
690 labels: style,
691 titles: style,
692 grid: None,
693 marker,
694 legend: None,
695 }
696 }
697
698 pub fn frame(&self, area: Rect) -> PlotFrame {
701 let base = 2 + MIN_PLOT_ROWS;
703 let y_title = !self.y.title.is_empty() && area.height > base;
704 let x_title = !self.x.title.is_empty() && area.height > base + u16::from(y_title);
705 let mut chart = area;
706 let y_title = y_title.then(|| {
707 chart.y += 1;
708 chart.height -= 1;
709 Rect { height: 1, ..area }
710 });
711 let x_title = x_title.then(|| {
712 chart.height -= 1;
713 Rect {
714 y: chart.bottom(),
715 height: 1,
716 ..area
717 }
718 });
719 let rows = graph_area(chart, 0).0;
721 let track = Track {
722 start: rows.top(),
723 cells: rows.height,
724 sub: resolution(self.marker).1,
725 };
726 let y = fit_y_labels(&self.y, track, chart.width / 3);
727 let width = y.labels.iter().map(|(_, l)| l.width()).max().unwrap_or(0) as u16;
728 let (graph, labels) = graph_area(chart, width);
729 PlotFrame {
730 y_title,
731 chart,
732 graph,
733 labels,
734 x_title,
735 y,
736 y_width: width,
737 }
738 }
739
740 pub fn render(&self, chart: Chart<'_>, area: Rect, buf: &mut Buffer, g: &Glyphs) -> PlotFrame {
743 let frame = self.frame(area);
744 let x_track = Track {
745 start: frame.graph.left(),
746 cells: frame.graph.width,
747 sub: resolution(self.marker).0,
748 };
749 let x = frame
750 .labels
751 .map(|row| fit_x_labels(&self.x, (row.left(), row.right()), x_track))
752 .unwrap_or_default();
753 let blank = " ".repeat(frame.y_width as usize);
757 let y_labels = if frame.y_width > 0 {
758 vec![Span::raw(blank.as_str()), Span::raw(blank.as_str())]
759 } else {
760 Vec::new()
761 };
762 let chart = chart
763 .x_axis(
764 Axis::default()
765 .bounds(self.x.bounds)
766 .style(self.line)
767 .labels(["", ""]),
768 )
769 .y_axis(
770 Axis::default()
771 .bounds(frame.y.bounds)
772 .style(self.line)
773 .labels(y_labels),
774 )
775 .legend_position(None);
776 let legend = self
778 .legend
779 .as_ref()
780 .and_then(|legend| place_legend(&chart, &frame, legend, g));
781 if let Some(style) = self.grid {
782 draw_grid(buf, frame.graph, &x.majors, &frame.y.majors, style, g);
783 }
784 chart.render(frame.chart, buf);
785 g.plot.redraw_axes(frame.chart, buf);
786 draw_tick_marks(buf, &frame, &x, self.line, g);
787 let label_x = frame.chart.left();
788 for (row, label) in &frame.y.labels {
789 let pad = (frame.y_width as usize).saturating_sub(label.width());
790 buf.set_string(label_x + pad as u16, *row, label, self.labels);
791 }
792 if let Some(row) = frame.labels {
793 for (x, label) in &x.labels {
794 buf.set_string(*x, row.y, label, self.labels);
795 }
796 }
797 if let Some(row) = frame.y_title {
798 let title = cut(self.y.title, row.width as usize, g);
799 buf.set_string(row.x, row.y, title, self.titles);
800 }
801 if let Some(row) = frame.x_title {
802 let title = cut(self.x.title, row.width as usize, g);
803 let x = row.right() - title.width() as u16;
804 buf.set_string(x, row.y, title, self.titles);
805 }
806 if let (Some(legend), Some(place)) = (&self.legend, legend) {
807 draw_legend(buf, legend, place, self.labels, g);
808 }
809 frame
810 }
811}
812
813#[derive(Clone, Copy, Debug)]
815struct LegendPlace {
816 area: Rect,
817 name_width: usize,
818}
819
820fn place_legend(
825 chart: &Chart<'_>,
826 frame: &PlotFrame,
827 legend: &Legend,
828 g: &Glyphs,
829) -> Option<LegendPlace> {
830 let graph = frame.graph;
831 let widest = legend
832 .entries
833 .iter()
834 .map(|(name, _)| name.width())
835 .max()
836 .unwrap_or(0);
837 let name_width = widest
838 .min(LEGEND_NAME_MAX)
839 .min((graph.width / 2).saturating_sub(4) as usize);
840 let (w, h) = legend.size(name_width);
841 if legend.entries.is_empty()
842 || name_width == 0
843 || name_width < widest.min(4)
844 || w > graph.width / 2
845 || h > graph.height / 2
846 {
847 return None;
848 }
849 let mut probe = Buffer::empty(frame.chart);
850 chart.clone().render(frame.chart, &mut probe);
851 let weight = |symbol: &str| -> usize {
852 let mut chars = symbol.chars();
853 match (chars.next(), chars.next()) {
854 (None | Some(' '), _) => 0,
855 (Some(c), None) if ('\u{2800}'..='\u{28ff}').contains(&c) => {
856 (c as u32 - 0x2800).count_ones() as usize
857 }
858 _ => 1,
859 }
860 };
861 let axis = [g.plot.axis.vertical, g.plot.axis.horizontal];
863 let marks = |x0: u16, y0: u16| -> usize {
864 (y0..y0 + h)
865 .flat_map(|y| (x0..x0 + w).map(move |x| (x, y)))
866 .map(|(x, y)| probe[(x, y)].symbol())
867 .filter(|symbol| !axis.contains(symbol))
868 .map(weight)
869 .sum()
870 };
871 let (left, right) = (graph.left(), graph.right() - w);
872 let (top, bottom) = (graph.top(), graph.bottom() - h);
873 let (center, middle) = (left + (right - left) / 2, top + (bottom - top) / 2);
874 [
875 (right, top),
876 (left, top),
877 (right, bottom),
878 (left, bottom),
879 (center, top),
880 (center, bottom),
881 (left, middle),
882 (right, middle),
883 ]
884 .into_iter()
885 .min_by_key(|&(x, y)| marks(x, y))
886 .map(|(x, y)| LegendPlace {
887 area: Rect::new(x, y, w, h),
888 name_width,
889 })
890}
891
892fn draw_legend(buf: &mut Buffer, legend: &Legend, place: LegendPlace, text: Style, g: &Glyphs) {
896 let area = place.area;
897 for y in area.top()..area.bottom() {
898 for x in area.left()..area.right() {
899 buf[(x, y)].reset();
900 }
901 }
902 for ((name, style), y) in legend.entries.iter().zip(area.top()..) {
903 buf.set_string(area.x + 1, y, g.bar_eighths[7], *style);
904 let name = cut(name, place.name_width, g);
905 buf.set_string(area.x + 3, y, name, text);
906 }
907}
908
909fn draw_grid(
913 buf: &mut Buffer,
914 graph: Rect,
915 columns: &[u16],
916 rows: &[u16],
917 style: Style,
918 g: &Glyphs,
919) {
920 let rows: Vec<u16> = rows
921 .iter()
922 .copied()
923 .filter(|&y| y + 1 < graph.bottom())
924 .collect();
925 let columns: Vec<u16> = columns
926 .iter()
927 .copied()
928 .filter(|&x| x > graph.left())
929 .collect();
930 for &y in &rows {
931 for x in graph.left()..graph.right() {
932 buf[(x, y)].set_symbol(g.plot.grid_across).set_style(style);
933 }
934 }
935 for &x in &columns {
936 for y in graph.top()..graph.bottom() {
937 buf[(x, y)].set_symbol(g.plot.grid_down).set_style(style);
938 }
939 }
940}
941
942fn draw_tick_marks(buf: &mut Buffer, frame: &PlotFrame, x: &Placed, style: Style, g: &Glyphs) {
944 let graph = frame.graph;
945 if graph.left() > frame.chart.left() {
946 let column = graph.left() - 1;
947 for &row in frame.y.majors.iter().chain(&frame.y.minors) {
948 let cell = &mut buf[(column, row)];
949 if cell.symbol() == g.plot.axis.vertical {
950 cell.set_symbol(g.plot.tick_y).set_style(style);
951 }
952 }
953 }
954 let row = graph.bottom();
955 if row < frame.chart.bottom() {
956 for &column in x.majors.iter().chain(&x.minors) {
957 let cell = &mut buf[(column, row)];
958 if cell.symbol() == g.plot.axis.horizontal {
959 cell.set_symbol(g.plot.tick_x).set_style(style);
960 }
961 }
962 }
963}
964
965fn graph_area(chart: Rect, y_label_width: u16) -> (Rect, Option<Rect>) {
969 if chart.is_empty() {
970 return (Rect::default(), None);
971 }
972 let mut x = chart.left();
973 let mut y = chart.bottom() - 1;
974 let mut labels = None;
975 if y > chart.top() {
976 labels = Some(Rect {
977 y,
978 height: 1,
979 ..chart
980 });
981 y -= 1;
982 }
983 x += y_label_width.min(chart.width / 3);
984 if y > chart.top() {
985 y -= 1;
986 }
987 if x + 1 < chart.right() {
988 x += 1;
989 }
990 let graph = Rect::new(
991 x,
992 chart.top(),
993 chart.right().saturating_sub(x),
994 y - chart.top() + 1,
995 );
996 (graph, labels)
997}
998
999fn strides(n: usize) -> impl Iterator<Item = Vec<usize>> {
1002 let last = n.saturating_sub(1);
1003 (1..=last.max(1))
1004 .filter(move |s| last.is_multiple_of(*s))
1005 .map(move |s| (0..n).step_by(s).collect())
1006}
1007
1008fn fraction(v: f64, [lo, hi]: [f64; 2]) -> f64 {
1010 if hi > lo { (v - lo) / (hi - lo) } else { 0.0 }
1011}
1012
1013pub fn fit_y_labels(axis: &AxisSpec<'_>, track: Track, width: u16) -> Placed {
1017 let groups = axis.tick_sets(track, Y_SPACING, Y_LEAST, Y_MINOR_GAP);
1018 let place = |set: &TickSet, labels: Option<&Vec<String>>| {
1019 let row = |v: f64| track.cell(1.0 - fraction(v, set.bounds));
1020 Placed {
1021 labels: labels
1022 .map(|labels| {
1023 set.ticks
1024 .iter()
1025 .zip(labels)
1026 .map(|(&v, l)| (row(v), format!("{l:>w$}", w = axis.pad)))
1027 .collect()
1028 })
1029 .unwrap_or_default(),
1030 majors: set.ticks.iter().map(|&v| row(v)).collect(),
1031 minors: set.minor.iter().map(|&v| row(v)).collect(),
1032 bounds: set.bounds,
1033 }
1034 };
1035 let fits = |set: &TickSet, labels: &Vec<String>| {
1036 let rows: Vec<u16> = set
1037 .ticks
1038 .iter()
1039 .map(|&v| track.cell(1.0 - fraction(v, set.bounds)))
1040 .collect();
1041 set.ticks.len() <= usize::from(track.cells.max(2))
1042 && rows.windows(2).all(|w| w[0] != w[1])
1043 && labels
1044 .iter()
1045 .all(|l| l.width().max(axis.pad) <= width as usize)
1046 && distinct(labels.iter())
1047 };
1048 for set in groups.iter().flatten() {
1049 if let Some(labels) = set.levels.iter().find(|labels| fits(set, labels)) {
1050 return place(set, Some(labels));
1051 }
1052 }
1053 match groups.iter().flatten().next() {
1055 Some(set) => place(set, set.levels.first()),
1056 None => Placed {
1057 bounds: axis.bounds,
1058 ..Placed::default()
1059 },
1060 }
1061}
1062
1063fn distinct<'a>(labels: impl Iterator<Item = &'a String>) -> bool {
1065 let labels: Vec<_> = labels.collect();
1066 labels.windows(2).all(|w| w[0] != w[1])
1067}
1068
1069pub fn fit_x_labels(axis: &AxisSpec<'_>, span: (u16, u16), track: Track) -> Placed {
1075 let (start, end) = span;
1076 let column = |v: f64| track.cell(fraction(v, axis.bounds));
1077 let groups = axis.tick_sets(track, X_SPACING, X_LEAST, X_MINOR_GAP);
1078 for sets in &groups {
1079 for level in 0..MAX_LEVELS {
1080 for set in sets {
1081 let Some(labels) = set.levels.get(level) else {
1082 continue;
1083 };
1084 let mut placed: Vec<(u16, String)> = Vec::with_capacity(labels.len());
1085 let mut next_free = start;
1086 let fits = set.ticks.iter().zip(labels).all(|(&v, label)| {
1087 let w = label.width() as u16;
1088 if w > end.saturating_sub(start) {
1089 return false;
1090 }
1091 let x = column(v).saturating_sub(w / 2).clamp(start, end - w);
1092 let clear =
1093 x >= next_free && placed.last().is_none_or(|(_, prev)| prev != label);
1094 next_free = x + w + LABEL_GAP;
1095 placed.push((x, label.clone()));
1096 clear
1097 });
1098 if fits {
1099 return Placed {
1100 labels: placed,
1101 majors: set.ticks.iter().map(|&v| column(v)).collect(),
1102 minors: set.minor.iter().map(|&v| column(v)).collect(),
1103 bounds: axis.bounds,
1104 };
1105 }
1106 }
1107 }
1108 }
1109 Placed {
1110 bounds: axis.bounds,
1111 ..Placed::default()
1112 }
1113}
1114
1115pub fn cut(text: &str, width: usize, g: &Glyphs) -> String {
1117 if text.width() <= width {
1118 return text.to_string();
1119 }
1120 let keep = width.saturating_sub(g.ellipsis.width());
1121 let mut used = 0;
1122 let mut out: String = text
1123 .chars()
1124 .take_while(|c| {
1125 used += c.width().unwrap_or(0);
1126 used <= keep
1127 })
1128 .collect();
1129 if keep + g.ellipsis.width() <= width {
1130 out.push_str(g.ellipsis);
1131 }
1132 out
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138 use crate::chart_data::{XAxisTemporalKind, x_axis_label_at};
1139 use ratatui::widgets::{Dataset, GraphType};
1140
1141 const FIVE_YEARS: [f64; 2] = [18262.0, 20088.0];
1143
1144 fn dates(bounds: (f64, f64)) -> TickLabel<'static> {
1146 let numbers = AxisFormat::new(&[], &AxisNumbers::default());
1147 Box::new(move |v, level| {
1148 x_axis_label_at(v, XAxisTemporalKind::Date, bounds, level, &numbers)
1149 })
1150 }
1151
1152 fn track(width: u16) -> Track {
1154 Track {
1155 start: 4,
1156 cells: width - 4,
1157 sub: 1,
1158 }
1159 }
1160
1161 fn labels_on(axis: &AxisSpec<'_>, width: u16) -> Vec<String> {
1162 fit_x_labels(axis, (0, width), track(width))
1163 .labels
1164 .into_iter()
1165 .map(|(_, l)| l)
1166 .collect()
1167 }
1168
1169 #[test]
1172 fn fixed_labels_drop_the_middle_then_shorten() {
1173 let [lo, hi] = FIVE_YEARS;
1174 let axis = AxisSpec::ends_and_middle(FIVE_YEARS, dates((lo, hi)), "");
1175 assert_eq!(
1176 labels_on(&axis, 40),
1177 ["2020-01-01", "2022-07-02", "2024-12-31"]
1178 );
1179 assert_eq!(labels_on(&axis, 24), ["2020-01-01", "2024-12-31"]);
1180 assert_eq!(labels_on(&axis, 17), ["2020-01", "2024-12"]);
1181 assert_eq!(labels_on(&axis, 16), ["2020", "2024"]);
1182 assert!(labels_on(&axis, 10).is_empty());
1183 }
1184
1185 #[test]
1187 fn labels_stand_apart_on_the_row() {
1188 let names = |i: f64, level| (level == 0).then(|| format!("column_{i}"));
1189 let axis = AxisSpec::fixed(
1190 [-0.5, 6.5],
1191 (0..7).map(f64::from).collect(),
1192 Box::new(names),
1193 "",
1194 );
1195 for width in 10..120 {
1196 let placed = fit_x_labels(&axis, (0, width), track(width)).labels;
1197 for pair in placed.windows(2) {
1198 let (x, label) = &pair[0];
1199 assert!(
1200 x + label.width() as u16 + LABEL_GAP <= pair[1].0,
1201 "{width}: {placed:?}"
1202 );
1203 }
1204 if let Some((x, label)) = placed.last() {
1205 assert!(x + label.width() as u16 <= width, "{width}: {placed:?}");
1206 assert_eq!(label, "column_6", "the last end stays: {placed:?}");
1207 assert_eq!(placed[0].1, "column_0", "the first end stays: {placed:?}");
1208 }
1209 }
1210 }
1211
1212 fn step_of(labels: &[String]) -> f64 {
1214 let values: Vec<f64> = labels.iter().map(|l| l.parse().unwrap()).collect();
1215 let step = values[1] - values[0];
1216 for w in values.windows(2) {
1217 assert!((w[1] - w[0] - step).abs() < 1e-9, "uneven: {labels:?}");
1218 }
1219 step
1220 }
1221
1222 fn assert_nice(labels: &[String]) {
1224 let step = step_of(labels);
1225 let mantissa = step / 10f64.powf(step.log10().floor());
1226 assert!(
1227 [1.0, 2.0, 2.5, 5.0]
1228 .iter()
1229 .any(|m| (mantissa - m).abs() < 1e-9),
1230 "step {step}: {labels:?}"
1231 );
1232 for l in labels {
1233 let k = l.parse::<f64>().unwrap() / step;
1234 assert!((k - k.round()).abs() < 1e-9, "{l} off the step {step}");
1235 }
1236 }
1237
1238 #[test]
1241 fn x_tick_density_follows_the_width() {
1242 let axis = AxisSpec::numbers([3.0, 997.0], &AxisNumbers::default(), "");
1243 for (width, least, most) in [(40, 2, 4), (60, 3, 5), (120, 6, 10), (300, 15, 22)] {
1244 let labels = labels_on(&axis, width);
1245 assert!(
1246 (least..=most).contains(&labels.len()),
1247 "{width} columns: {labels:?}"
1248 );
1249 assert_nice(&labels);
1250 }
1251 let wide = fit_x_labels(&axis, (0, 400), track(400));
1253 assert!(wide.minors.len() >= wide.majors.len(), "{wide:?}");
1254 let narrow = fit_x_labels(&axis, (0, 40), track(40));
1255 assert!(narrow.minors.iter().all(|m| !narrow.majors.contains(m)));
1256 }
1257
1258 #[test]
1261 fn y_tick_density_follows_the_height() {
1262 let axis = AxisSpec::y_numbers([3.0, 997.0], &AxisNumbers::default(), "");
1263 for rows in [12u16, 20, 40, 76] {
1264 let track = Track {
1265 start: 0,
1266 cells: rows,
1267 sub: 4,
1268 };
1269 let placed = fit_y_labels(&axis, track, 20);
1270 let labels: Vec<String> = placed
1271 .labels
1272 .iter()
1273 .map(|(_, l)| l.trim().to_string())
1274 .collect();
1275 let per_label = f64::from(rows) / labels.len() as f64;
1276 assert!((2.2..=7.0).contains(&per_label), "{rows} rows: {labels:?}");
1277 assert_nice(&labels);
1278 assert_eq!(placed.labels[0], (rows - 1, "0".to_string()), "{placed:?}");
1280 let (top, label) = placed.labels.last().unwrap();
1281 assert_eq!(*top, 0, "{rows} rows: {placed:?}");
1282 assert_eq!(placed.bounds[0], 0.0);
1283 assert!(placed.bounds[1] >= 997.0);
1284 assert_eq!(&placed.bounds[1].to_string(), label);
1285 }
1286 }
1287
1288 #[test]
1290 fn whole_numbers_tick_whole() {
1291 let whole = AxisNumbers {
1292 whole: true,
1293 ..AxisNumbers::default()
1294 };
1295 let axis = AxisSpec::numbers([0.0, 7.0], &whole, "");
1296 assert_eq!(
1297 labels_on(&axis, 200),
1298 ["0", "1", "2", "3", "4", "5", "6", "7"]
1299 );
1300 assert_eq!(labels_on(&axis, 30), ["0", "2", "4", "6"]);
1302 assert_eq!(labels_on(&axis, 20), ["0", "5"]);
1303 let axis = AxisSpec::y_numbers([3.0, 10.0], &whole, "");
1304 let track = Track {
1305 start: 0,
1306 cells: 9,
1307 sub: 1,
1308 };
1309 let placed = fit_y_labels(&axis, track, 10);
1310 assert_eq!(
1311 placed.labels,
1312 [
1313 (8, "0".to_string()),
1314 (4, "5".to_string()),
1315 (0, "10".to_string())
1316 ]
1317 );
1318 }
1319
1320 #[test]
1324 fn date_ticks_fall_on_the_calendar() {
1325 let axis = AxisSpec::calendar(
1326 FIVE_YEARS,
1327 XAxisTemporalKind::Date,
1328 &AxisNumbers::default(),
1329 "",
1330 );
1331 assert_eq!(
1332 labels_on(&axis, 80),
1333 ["2020", "2021", "2022", "2023", "2024"]
1334 );
1335 let wide = labels_on(&axis, 300);
1336 assert_eq!(
1337 wide[..4],
1338 ["2020", "Apr", "Jul", "Oct"],
1339 "quarters, the year at its turn: {wide:?}"
1340 );
1341 assert!(wide.contains(&"2024".to_string()), "{wide:?}");
1342 let narrow = labels_on(&axis, 16);
1343 assert_eq!(narrow, ["2020", "2024"], "{narrow:?}");
1344
1345 let march = [19783.0, 19813.0]; let axis = AxisSpec::calendar(march, XAxisTemporalKind::Date, &AxisNumbers::default(), "");
1348 let labels = labels_on(&axis, 80);
1349 assert_eq!(labels[0], "Mar 1 2024", "{labels:?}");
1350 assert!(
1351 labels[1..].iter().all(|l| l.parse::<u32>().is_ok()),
1352 "{labels:?}"
1353 );
1354
1355 let day = [1_709_251_200e6, 1_709_337_600e6]; let axis = AxisSpec::calendar(
1358 day,
1359 XAxisTemporalKind::DatetimeUs,
1360 &AxisNumbers::default(),
1361 "",
1362 );
1363 let labels = labels_on(&axis, 120);
1364 assert_eq!(labels[0], "Mar 1 2024", "{labels:?}");
1365 assert!(labels.contains(&"12:00".to_string()), "{labels:?}");
1366 assert_eq!(labels.last().unwrap(), "Mar 2", "{labels:?}");
1367 }
1368
1369 #[test]
1372 fn short_forms_tell_ticks_apart() {
1373 let axis = AxisSpec::numbers([1000.1, 1000.3], &AxisNumbers::default(), "");
1374 assert!(labels_on(&axis, 12).is_empty());
1375 assert_eq!(labels_on(&axis, 40), ["1000.1", "1000.2", "1000.3"]);
1376 }
1377
1378 fn axes<'a>(grid: bool) -> PlotAxes<'a> {
1379 PlotAxes {
1380 grid: grid.then(Style::default),
1381 ..PlotAxes::new(
1382 AxisSpec::numbers([0.0, 10.0], &AxisNumbers::default(), "x title"),
1383 AxisSpec::y_numbers([0.0, 1000.0], &AxisNumbers::default(), "y title"),
1384 Style::default(),
1385 Marker::Braille,
1386 )
1387 }
1388 }
1389
1390 fn render_with(axes: &PlotAxes<'_>, area: Rect, g: &Glyphs) -> (Buffer, PlotFrame) {
1391 let points = [(0.0, 0.0), (10.0, 1000.0)];
1392 let chart = Chart::new(vec![
1393 Dataset::default()
1394 .marker(Marker::Braille)
1395 .graph_type(GraphType::Line)
1396 .data(&points),
1397 ]);
1398 let mut buf = Buffer::empty(area);
1399 let frame = axes.render(chart, area, &mut buf, g);
1400 (buf, frame)
1401 }
1402
1403 fn render(area: Rect, g: &Glyphs) -> (Buffer, PlotFrame) {
1404 render_with(&axes(false), area, g)
1405 }
1406
1407 #[test]
1410 fn the_frame_matches_ratatuis_layout() {
1411 let g = crate::glyphs::unicode();
1412 for (w, h) in [(9, 6), (20, 5), (20, 7), (30, 12), (60, 20), (120, 40)] {
1413 let (buf, frame) = render(Rect::new(0, 0, w, h), g);
1414 let corner = (frame.graph.left() - 1, frame.graph.bottom());
1415 assert_eq!(buf[corner].symbol(), "└", "{w}x{h}: {:?}", frame.graph);
1416 }
1417 }
1418
1419 #[test]
1422 fn titles_give_up_their_rows_to_the_plot() {
1423 let g = crate::glyphs::unicode();
1424 let (_, frame) = render(Rect::new(0, 0, 30, 7), g);
1425 assert!(frame.y_title.is_some() && frame.x_title.is_some());
1426 assert_eq!(frame.graph.height, 3);
1427 let (_, frame) = render(Rect::new(0, 0, 30, 6), g);
1428 assert!(frame.y_title.is_some() && frame.x_title.is_none());
1429 let (_, frame) = render(Rect::new(0, 0, 30, 5), g);
1430 assert!(frame.y_title.is_none() && frame.x_title.is_none());
1431 }
1432
1433 #[test]
1436 fn labels_sit_on_their_tick_marks() {
1437 for g in [crate::glyphs::unicode(), crate::glyphs::ascii()] {
1438 let (buf, frame) = render(Rect::new(0, 0, 60, 20), g);
1439 let axis_x = frame.graph.left() - 1;
1440 for (row, label) in &frame.y.labels {
1441 assert_eq!(buf[(axis_x, *row)].symbol(), g.plot.tick_y, "{label}");
1442 let text: String = (frame.chart.left()..axis_x)
1443 .map(|x| buf[(x, *row)].symbol())
1444 .collect();
1445 assert_eq!(text.trim(), label.trim());
1446 }
1447 let row = frame.graph.bottom();
1448 let ticks: Vec<u16> = (frame.graph.left()..frame.graph.right())
1449 .filter(|&x| buf[(x, row)].symbol() == g.plot.tick_x)
1450 .collect();
1451 assert!(ticks.len() >= 3, "{ticks:?}");
1452 }
1453 }
1454
1455 fn text(buf: &Buffer) -> Vec<String> {
1456 let area = buf.area;
1457 (area.top()..area.bottom())
1458 .map(|y| {
1459 (area.left()..area.right())
1460 .map(|x| buf[(x, y)].symbol())
1461 .collect()
1462 })
1463 .collect()
1464 }
1465
1466 #[test]
1469 fn the_grid_toggles_and_never_hides_a_series() {
1470 for g in [crate::glyphs::unicode(), crate::glyphs::ascii()] {
1471 let area = Rect::new(0, 0, 60, 20);
1472 let (off, frame) = render_with(&axes(false), area, g);
1473 let (on, _) = render_with(&axes(true), area, g);
1474 let grid = |buf: &Buffer| {
1475 let all = text(buf).concat();
1476 all.matches(g.plot.grid_across).count() + all.matches(g.plot.grid_down).count()
1477 };
1478 assert_eq!(grid(&off), 0, "{:#?}", text(&off));
1479 assert!(grid(&on) > 50, "{:#?}", text(&on));
1480 let graph = frame.graph;
1481 for y in graph.top()..graph.bottom() {
1482 for x in graph.left()..graph.right() {
1483 let mark = off[(x, y)].symbol();
1484 if !matches!(mark, " " | "\u{2800}") {
1485 assert_eq!(on[(x, y)].symbol(), mark, "({x}, {y})");
1486 }
1487 }
1488 }
1489 for (row, _) in &frame.y.labels {
1492 let across = (graph.left()..graph.right())
1493 .filter(|&x| on[(x, *row)].symbol() == g.plot.grid_across)
1494 .count();
1495 if row + 1 < graph.bottom() {
1496 assert!(across > graph.width as usize / 2, "row {row}");
1497 } else {
1498 assert_eq!(across, 0, "the x axis row");
1499 }
1500 }
1501 }
1502 }
1503
1504 fn two_names() -> Legend {
1505 Legend {
1506 entries: vec![
1507 ("first".to_string(), Style::default()),
1508 ("second".to_string(), Style::default()),
1509 ],
1510 }
1511 }
1512
1513 #[test]
1517 fn the_legend_takes_the_emptiest_corner_without_a_frame() {
1518 let g = crate::glyphs::unicode();
1519 let mut axes = axes(false);
1520 axes.legend = Some(two_names());
1521 let rising: Vec<(f64, f64)> = (0..=100)
1522 .map(|i| (f64::from(i) / 10.0, f64::from(i) * 10.0))
1523 .collect();
1524 let chart = Chart::new(vec![
1525 Dataset::default()
1526 .marker(Marker::Braille)
1527 .graph_type(GraphType::Line)
1528 .data(&rising),
1529 ]);
1530 let area = Rect::new(0, 0, 60, 24);
1531 let mut buf = Buffer::empty(area);
1532 let frame = axes.render(chart, area, &mut buf, g);
1533 let (x, y) = (frame.graph.left(), frame.graph.top());
1534 let row = |y: u16| -> String {
1535 (x..x + 10)
1536 .map(|x| buf[(x, y)].symbol())
1537 .collect::<String>()
1538 };
1539 assert_eq!(
1540 row(y),
1541 format!(" {} first ", g.bar_eighths[7]),
1542 "{:#?}",
1543 text(&buf)
1544 );
1545 assert_eq!(row(y + 1), format!(" {} second ", g.bar_eighths[7]));
1546 let all = text(&buf).join("\n");
1547 for frame_mark in ["┌", "┐", "┘"] {
1548 assert!(!all.contains(frame_mark), "{all}");
1549 }
1550 }
1551
1552 #[test]
1555 fn the_legend_avoids_a_dense_corner() {
1556 let g = crate::glyphs::unicode();
1557 let mut axes = axes(false);
1558 axes.legend = Some(two_names());
1559 let mut points = Vec::new();
1562 for i in 0..=100 {
1563 for j in 0..=100 {
1564 let (x, y) = (f64::from(i) / 10.0, f64::from(j) * 10.0);
1565 if x > 3.0 || y > 300.0 {
1566 points.push((x, y));
1567 }
1568 }
1569 }
1570 for i in 0..10 {
1571 points.push((f64::from(i) * 0.3, f64::from(i) * 30.0));
1572 }
1573 let chart = Chart::new(vec![
1574 Dataset::default()
1575 .marker(Marker::Braille)
1576 .graph_type(GraphType::Scatter)
1577 .data(&points),
1578 ]);
1579 let area = Rect::new(0, 0, 60, 24);
1580 let mut buf = Buffer::empty(area);
1581 let frame = axes.render(chart, area, &mut buf, g);
1582 let swatch = (frame.graph.left() + 1, frame.graph.bottom() - 2);
1583 assert_eq!(buf[swatch].symbol(), g.bar_eighths[7], "{:#?}", text(&buf));
1584 }
1585
1586 #[test]
1589 fn a_narrow_axis_takes_more_labels_when_they_fit() {
1590 let g = crate::glyphs::unicode();
1591 let whole = AxisNumbers {
1592 whole: true,
1593 ..AxisNumbers::default()
1594 };
1595 let axes = PlotAxes::new(
1596 AxisSpec::numbers([0.0, 7.0], &whole, ""),
1597 AxisSpec::y_numbers([0.0, 1000.0], &AxisNumbers::default(), ""),
1598 Style::default(),
1599 Marker::Braille,
1600 );
1601 let (buf, frame) = render_with(&axes, Rect::new(0, 0, 34, 12), g);
1602 let row = frame.labels.expect("a label row").y;
1603 let labels: Vec<String> = text(&buf)[row as usize]
1604 .split_whitespace()
1605 .map(str::to_string)
1606 .collect();
1607 assert_eq!(labels, ["0", "2", "4", "6"], "{:#?}", text(&buf));
1608 }
1609
1610 #[test]
1612 fn a_long_title_is_cut_to_its_row() {
1613 assert_eq!(cut("a long title", 6, crate::glyphs::unicode()), "a lon…");
1614 assert_eq!(cut("a long title", 6, crate::glyphs::ascii()), "a l...");
1615 assert_eq!(cut("short", 6, crate::glyphs::ascii()), "short");
1616 }
1617}