1use 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
24const MIN_PLOT_ROWS: u16 = 3;
26const MAX_LEVELS: usize = 8;
28const LABEL_GAP: u16 = 2;
31const X_SPACING: f64 = 15.0;
33const X_LEAST: f64 = 6.0;
34const Y_SPACING: f64 = 4.0;
36const Y_LEAST: f64 = 2.0;
37const X_MINOR_GAP: f64 = 3.0;
39const Y_MINOR_GAP: f64 = 3.0;
40
41pub type TickLabel<'a> = Box<dyn Fn(f64, usize) -> Option<String> + 'a>;
43
44enum Scale<'a> {
46 Numbers { numbers: AxisNumbers, widen: bool },
49 Calendar {
51 kind: XAxisTemporalKind,
52 numbers: AxisNumbers,
53 },
54 Log { numbers: AxisNumbers },
57 Fixed {
59 ticks: Vec<f64>,
60 label: TickLabel<'a>,
61 },
62}
63
64pub struct AxisSpec<'a> {
66 pub bounds: [f64; 2],
67 scale: Scale<'a>,
68 pub title: &'a str,
69 pad: usize,
71}
72
73#[derive(Clone, Debug, Default)]
76pub struct TickSet {
77 pub ticks: Vec<f64>,
78 pub minor: Vec<f64>,
79 pub levels: Vec<Vec<String>>,
80 pub bounds: [f64; 2],
82}
83
84impl<'a> AxisSpec<'a> {
85 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 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 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 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 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 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 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 pub fn padded(self, width: usize) -> Self {
184 Self { pad: width, ..self }
185 }
186
187 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
251fn with_ticks(sets: Vec<TickSet>) -> Vec<TickSet> {
253 sets.into_iter().filter(|s| s.ticks.len() >= 2).collect()
254}
255
256struct Candidate<T> {
259 set: T,
260 gap: f64,
261 ticks: usize,
262}
263
264fn 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 None => options
288 .into_iter()
289 .last()
290 .map(|o| o.set)
291 .into_iter()
292 .collect(),
293 }
294}
295
296fn 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 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
367fn 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 let options: Vec<(Vec<f64>, Vec<f64>)> = if high <= 1.0 {
392 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 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 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 if every > 1 || mantissas.len() == 1 {
427 values.retain(|&v| v != 1.0 || low >= 1.0);
428 }
429 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 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
502fn 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 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 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#[derive(Clone, Copy, Debug)]
584pub struct Track {
585 pub start: u16,
586 pub cells: u16,
587 pub sub: u16,
588}
589
590impl Track {
591 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 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
605pub 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#[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#[derive(Clone, Debug, Default)]
628pub struct Legend {
629 pub entries: Vec<(String, Style)>,
630}
631
632const LEGEND_NAME_MAX: usize = 24;
634
635impl Legend {
636 fn size(&self, name_width: usize) -> (u16, u16) {
639 (name_width as u16 + 4, self.entries.len() as u16)
640 }
641}
642
643pub struct PlotAxes<'a> {
645 pub x: AxisSpec<'a>,
646 pub y: AxisSpec<'a>,
647 pub line: Style,
649 pub labels: Style,
650 pub titles: Style,
651 pub grid: Option<Style>,
653 pub marker: Marker,
655 pub legend: Option<Legend>,
657}
658
659pub struct PlotFrame {
661 pub y_title: Option<Rect>,
662 pub chart: Rect,
664 pub graph: Rect,
666 pub labels: Option<Rect>,
668 pub x_title: Option<Rect>,
669 pub y: Placed,
671 y_width: u16,
672}
673
674impl<'a> PlotAxes<'a> {
675 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 pub fn frame(&self, area: Rect) -> PlotFrame {
693 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 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 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 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 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 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#[derive(Clone, Copy, Debug)]
826struct LegendPlace {
827 area: Rect,
828 name_width: usize,
829}
830
831fn 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 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
900fn 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
917fn 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
950fn 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
973fn 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
1007fn 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
1016fn fraction(v: f64, [lo, hi]: [f64; 2]) -> f64 {
1018 if hi > lo { (v - lo) / (hi - lo) } else { 0.0 }
1019}
1020
1021pub 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 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
1071fn 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
1077pub 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
1121pub 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;