datui_lib/widgets/
crosshair.rs1use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::Style,
9 text::{Line, Span},
10};
11use unicode_width::UnicodeWidthStr;
12
13use crate::chart::chart_data::{XAxisTemporalKind, x_datetime, x_time};
14use crate::glyphs::Glyphs;
15use crate::widgets::axes::{Track, cut};
16use crate::widgets::axis_numbers::AxisNumbers;
17
18const READOUT_GAP: usize = 3;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct PlotPlace {
25 pub graph: Rect,
26 pub x_bounds: [f64; 2],
27 pub sub: u16,
29}
30
31impl PlotPlace {
32 pub fn column(&self, x: f64) -> u16 {
34 let [lo, hi] = self.x_bounds;
35 let f = if hi > lo { (x - lo) / (hi - lo) } else { 0.0 };
36 Track {
37 start: self.graph.left(),
38 cells: self.graph.width,
39 sub: self.sub,
40 }
41 .cell(f)
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum Move {
48 Left,
49 Right,
50 First,
51 Last,
52}
53
54pub fn xs(series: &[Vec<(f64, f64)>]) -> Vec<f64> {
56 let mut xs: Vec<f64> = series
57 .iter()
58 .flatten()
59 .map(|&(x, _)| x)
60 .filter(|x| x.is_finite())
61 .collect();
62 xs.sort_by(f64::total_cmp);
63 xs.dedup();
64 xs
65}
66
67pub fn nearest(xs: &[f64], x: f64) -> Option<f64> {
69 let i = xs.partition_point(|&v| v < x);
70 [i.checked_sub(1), Some(i)]
71 .into_iter()
72 .flatten()
73 .filter_map(|i| xs.get(i).copied())
74 .min_by(|a, b| (a - x).abs().total_cmp(&(b - x).abs()))
75}
76
77pub fn step(xs: &[f64], place: &PlotPlace, from: f64, to: Move) -> Option<f64> {
81 let here = place.column(from);
82 match to {
83 Move::First => xs.first().copied(),
84 Move::Last => xs.last().copied(),
85 Move::Right => {
86 let i = xs.partition_point(|&v| place.column(v) <= here);
87 xs.get(i).copied()
88 }
89 Move::Left => {
90 let i = xs.partition_point(|&v| place.column(v) < here);
91 i.checked_sub(1).and_then(|prev| {
93 let column = place.column(xs[prev]);
94 let first = xs.partition_point(|&v| place.column(v) < column);
95 xs.get(first).copied()
96 })
97 }
98 }
99 .or_else(|| nearest(xs, from))
100}
101
102pub fn at_column(xs: &[f64], place: &PlotPlace, column: u16) -> Option<f64> {
104 let i = xs.partition_point(|&v| place.column(v) < column);
105 [i.checked_sub(1), Some(i)]
106 .into_iter()
107 .flatten()
108 .filter_map(|i| xs.get(i).copied())
109 .min_by_key(|&v| place.column(v).abs_diff(column))
110}
111
112pub fn format_x(x: f64, kind: XAxisTemporalKind, numbers: &AxisNumbers) -> String {
115 let when = match kind {
116 XAxisTemporalKind::Numeric => None,
117 XAxisTemporalKind::Date => x_datetime(x, kind).map(|d| d.format("%Y-%m-%d").to_string()),
118 XAxisTemporalKind::Time => x_time(x).map(|t| t.format("%H:%M:%S%.f").to_string()),
119 _ => x_datetime(x, kind).map(|d| d.format("%Y-%m-%d %H:%M:%S%.f").to_string()),
120 };
121 when.unwrap_or_else(|| numbers.write(x))
122}
123
124pub fn values_at(series: &[Vec<(f64, f64)>], x: f64) -> Vec<Option<f64>> {
127 series
128 .iter()
129 .map(|points| points.iter().find(|&&(px, _)| px == x).map(|&(_, y)| y))
130 .collect()
131}
132
133pub struct Entry {
135 pub name: String,
136 pub name_style: Style,
137 pub value: String,
138 pub value_style: Style,
139}
140
141impl Entry {
142 fn width(&self) -> usize {
143 self.name.width() + 2 + self.value.width()
144 }
145}
146
147pub fn readout_lines(entries: &[Entry], width: usize, g: &Glyphs) -> Vec<Line<'static>> {
150 let mut lines: Vec<Line<'static>> = Vec::new();
151 let mut used = 0;
152 for entry in entries {
153 let w = entry.width();
154 if lines.is_empty() || used + READOUT_GAP + w > width {
155 lines.push(Line::default());
156 used = 0;
157 } else {
158 if let Some(line) = lines.last_mut() {
159 line.push_span(Span::raw(" ".repeat(READOUT_GAP)));
160 }
161 used += READOUT_GAP;
162 }
163 let line = lines.last_mut().expect("a line was pushed");
164 let name = cut(&format!("{}:", entry.name), width, g);
165 let room = width.saturating_sub(name.width() + 1);
166 used += name.width();
167 line.push_span(Span::styled(name, entry.name_style));
168 if room > 0 {
169 let value = cut(&entry.value, room, g);
170 used += 1 + value.width();
171 line.push_span(Span::raw(" "));
172 line.push_span(Span::styled(value, entry.value_style));
173 }
174 }
175 lines
176}
177
178pub fn draw(buf: &mut Buffer, place: &PlotPlace, x: f64, style: Style, g: &Glyphs) {
181 let graph = place.graph;
182 if graph.is_empty() {
183 return;
184 }
185 let column = place.column(x);
186 for y in graph.top()..graph.bottom() {
187 let cell = &mut buf[(column, y)];
188 let symbol = cell.symbol();
189 if matches!(symbol, " " | "\u{2800}")
190 || symbol == g.plot.grid_across
191 || symbol == g.plot.grid_down
192 {
193 cell.set_symbol(g.plot.axis.vertical).set_style(style);
194 }
195 }
196 let axis = graph.bottom();
197 if axis < buf.area.bottom() {
198 let cell = &mut buf[(column, axis)];
199 if cell.symbol() == g.plot.axis.horizontal || cell.symbol() == g.plot.tick_x {
200 cell.set_symbol(g.plot.tick_x).set_style(style);
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests;