use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Color as RatColor;
use ratatui_core::style::Style;
use ratatui_core::widgets::{StatefulWidget, Widget};
use crate::data::Series;
use crate::mark::Mark;
use crate::plot::{Frame, Mapping, Plot, Viewport};
use crate::render::{Charset, Color, ColorMode, display_width};
use crate::theme::Theme;
impl Plot<'_> {
pub fn widget(&self) -> PlotWidget<'_> {
PlotWidget {
plot: self,
charset: Charset::Quadrants,
theme: Theme::DARK,
crosshair: true,
readout: true,
snap: true,
#[cfg(feature = "pixel")]
graphics: None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlotWidget<'a> {
plot: &'a Plot<'a>,
charset: Charset,
theme: Theme,
crosshair: bool,
readout: bool,
snap: bool,
#[cfg(feature = "pixel")]
graphics: Option<crate::pixel::Graphics>,
}
impl PlotWidget<'_> {
#[must_use]
pub fn charset(mut self, charset: Charset) -> Self {
self.charset = charset;
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
#[must_use]
pub fn crosshair(mut self, on: bool) -> Self {
self.crosshair = on;
self
}
#[must_use]
pub fn readout(mut self, on: bool) -> Self {
self.readout = on;
self
}
#[must_use]
pub fn snap(mut self, on: bool) -> Self {
self.snap = on;
self
}
#[cfg(feature = "pixel")]
#[must_use]
pub fn graphics(mut self, graphics: crate::pixel::Graphics) -> Self {
self.graphics = Some(graphics);
self
}
fn frame(&self, area: Rect) -> Frame {
Frame {
width: area.width as usize,
height: area.height as usize,
charset: self.charset,
color: ColorMode::TrueColor,
theme: self.theme,
}
}
fn draw(&self, area: Rect, buffer: &mut Buffer) {
let surface = self.plot.rasterize(&self.frame(area));
blit(&surface, area, buffer);
}
fn draw_stateful(&self, area: Rect, buffer: &mut Buffer, state: &mut PlotState) {
#[cfg(feature = "pixel")]
if let Some(graphics) = self.graphics
&& self.draw_stateful_pixels(&graphics, area, buffer, state)
{
return;
}
let plot = self.plot.clone().viewport(state.view);
let (surface, mapping) = plot.rasterize_mapped(&self.frame(area));
blit(&surface, area, buffer);
state.area = area;
state.mapping = Some(mapping);
self.overlays(buffer, state);
}
#[cfg(feature = "pixel")]
fn draw_stateful_pixels(
&self,
graphics: &crate::pixel::Graphics,
area: Rect,
buffer: &mut Buffer,
state: &mut PlotState,
) -> bool {
let unchanged = state.pixel_area == Some(area) && state.pixel_view == Some(state.view);
if unchanged
&& state
.pixel_pace
.is_some_and(|last| last.elapsed() < PIXEL_PACE)
{
for y in area.y..area.bottom() {
for x in area.x..area.right() {
let cell = &mut buffer[(x, y)];
cell.reset();
cell.set_diff_option(ratatui_core::buffer::CellDiffOption::Skip);
}
}
state.area = area;
return true;
}
let mut plot = self.plot.clone().viewport(state.view);
if let Some(cursor_x) = state.hover_data_x()
&& cursor_x.is_finite()
&& let Some(mapping) = state.mapping().cloned()
{
let cursor_y = state
.cursor_data()
.map(|(_, y)| y)
.filter(|y| y.is_finite());
let (x_low, x_high) = mapping.x_domain();
let (y_low, y_high) = mapping.y_domain();
if mapping.x_categories().is_none() && state.view.x().is_none() {
plot = plot.x_domain(x_low, x_high);
}
if mapping.y_categories().is_none() && state.view.y().is_none() {
plot = plot.y_domain(y_low, y_high);
}
if self.crosshair {
plot = plot.layer(
crate::Rule::v(cursor_x)
.color(Color::BrightBlack)
.dash(crate::Dash::Dotted),
);
if let Some(cursor_y) = cursor_y {
plot = plot.layer(
crate::Rule::h(cursor_y)
.color(Color::BrightBlack)
.dash(crate::Dash::Dotted),
);
}
}
let snapped = if self.snap {
self.snapped(state)
} else {
Vec::new()
};
let ink = if self.theme == Theme::LIGHT {
Color::Black
} else {
Color::BrightWhite
};
for snap in &snapped {
if let Some(value) = snap.value {
plot = plot.layer(crate::Text::at(snap.x, value, "*").color(ink));
}
}
if self.readout
&& let Some(panel) = mapping.plot_area()
&& let Some(line) =
self.readout_line(state, &snapped, panel.width.saturating_sub(2) as u16)
{
let line = line.replace(['·', '—'], "-");
let x = x_low + (x_high - x_low) * 0.02;
let y = y_high - (y_high - y_low) * 0.06;
plot = plot.layer(crate::Text::at(x, y, line).color(Color::BrightBlack));
}
}
let id = *state.pixel_id.get_or_insert_with(next_pixel_id);
let kitty_id = (graphics.protocol == crate::pixel::Protocol::Kitty).then_some(id);
let Ok((block, mapping)) = crate::pixel::try_render_mapped(
&plot,
&self.frame(area),
graphics,
area.x as usize,
kitty_id,
) else {
return false;
};
let fresh = state.pixel_area != Some(area);
let diff = if fresh {
ratatui_core::buffer::CellDiffOption::None
} else {
ratatui_core::buffer::CellDiffOption::Skip
};
for y in area.y..area.bottom() {
for x in area.x..area.right() {
let cell = &mut buffer[(x, y)];
cell.reset();
cell.set_diff_option(diff);
}
}
state.area = area;
state.mapping = Some(mapping);
state.pixel_area = Some(area);
state.pixel_pace = Some(std::time::Instant::now());
state.pixel_view = Some(state.view);
let block = block.replace('\n', "\r\n");
let hash = block_hash(&block);
if state.pixel_sent != Some((area, hash)) {
state.pixels = Some((area, block, hash));
}
true
}
}
impl Widget for PlotWidget<'_> {
fn render(self, area: Rect, buffer: &mut Buffer) {
self.draw(area, buffer);
}
}
impl Widget for &PlotWidget<'_> {
fn render(self, area: Rect, buffer: &mut Buffer) {
self.draw(area, buffer);
}
}
impl StatefulWidget for PlotWidget<'_> {
type State = PlotState;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut PlotState) {
self.draw_stateful(area, buffer, state);
}
}
impl StatefulWidget for &PlotWidget<'_> {
type State = PlotState;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut PlotState) {
self.draw_stateful(area, buffer, state);
}
}
fn blit(surface: &crate::render::Surface, area: Rect, buffer: &mut Buffer) {
for (column, row, glyph, foreground, background) in surface.cells() {
let x = area.x + column as u16;
let y = area.y + row as u16;
if x >= area.right() || y >= area.bottom() {
continue;
}
let cell = &mut buffer[(x, y)];
let mut symbol = [0u8; 4];
cell.set_symbol(glyph.encode_utf8(&mut symbol));
if let Some(fg) = convert(foreground) {
cell.set_fg(fg);
}
if let Some(bg) = convert(background) {
cell.set_bg(bg);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Mouse {
Moved {
column: u16,
row: u16,
},
Down {
button: MouseButton,
column: u16,
row: u16,
},
Drag {
button: MouseButton,
column: u16,
row: u16,
},
Up {
button: MouseButton,
column: u16,
row: u16,
},
ScrollUp {
column: u16,
row: u16,
},
ScrollDown {
column: u16,
row: u16,
},
ScrollLeft {
column: u16,
row: u16,
},
ScrollRight {
column: u16,
row: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MouseButton {
Left,
Right,
Middle,
}
#[derive(Debug, Clone, Copy)]
enum Drag {
Pan { last: (u16, u16) },
Select {
anchor: (u16, u16),
current: (u16, u16),
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Hover {
Cell { column: u16, row: u16 },
Column { column: u16 },
}
#[derive(Debug, Clone, Default)]
pub struct PlotState {
area: Rect,
mapping: Option<Mapping>,
view: Viewport,
hover: Option<Hover>,
drag: Option<Drag>,
#[cfg(feature = "pixel")]
pixels: Option<(Rect, String, u64)>,
#[cfg(feature = "pixel")]
pixel_area: Option<Rect>,
#[cfg(feature = "pixel")]
pixel_id: Option<u32>,
#[cfg(feature = "pixel")]
pixel_placement: u32,
#[cfg(feature = "pixel")]
pixel_sent: Option<(Rect, u64)>,
#[cfg(feature = "pixel")]
pixel_pace: Option<std::time::Instant>,
#[cfg(feature = "pixel")]
pixel_view: Option<Viewport>,
}
impl PlotState {
pub fn data_at(&self, column: u16, row: u16) -> Option<(f64, f64)> {
let mapping = self.mapping.as_ref()?;
let local_column = column.checked_sub(self.area.x)? as usize;
let local_row = row.checked_sub(self.area.y)? as usize;
mapping.data_at(local_column, local_row)
}
pub fn plot_area(&self) -> Option<Rect> {
let panel = self.mapping.as_ref()?.plot_area()?;
Some(Rect::new(
self.area.x.saturating_add(panel.column as u16),
self.area.y.saturating_add(panel.row as u16),
panel.width.min(u16::MAX as usize) as u16,
panel.height.min(u16::MAX as usize) as u16,
))
}
pub fn mapping(&self) -> Option<&Mapping> {
self.mapping.as_ref()
}
pub fn cursor(&self) -> Option<(u16, u16)> {
match self.hover? {
Hover::Cell { column, row } => Some((column, row)),
Hover::Column { .. } => None,
}
}
pub fn cursor_data(&self) -> Option<(f64, f64)> {
let (column, row) = self.cursor()?;
self.data_at(column, row)
}
pub fn hover_x(&mut self, x: f64) -> bool {
let hover = self
.mapping
.as_ref()
.and_then(|mapping| mapping.column_at(x))
.map(|column| Hover::Column {
column: self
.area
.x
.saturating_add(column.min(u16::MAX as usize) as u16),
});
let changed = hover != self.hover;
self.hover = hover;
changed
}
fn hover_data_x(&self) -> Option<f64> {
let rect = self.plot_area()?;
let (Hover::Cell { column, .. } | Hover::Column { column }) = self.hover?;
self.data_at(column, rect.y).map(|(x, _)| x)
}
pub fn viewport(&self) -> Viewport {
self.view
}
pub fn set_viewport(&mut self, viewport: Viewport) {
self.view = viewport;
}
#[cfg(feature = "pixel")]
pub fn invalidate_pixels(&mut self) {
self.pixels = None;
self.pixel_area = None;
self.pixel_pace = None;
self.pixel_view = None;
self.pixel_sent = None;
}
pub fn reset_view(&mut self) {
self.view = self.view.reset();
}
pub fn zoom_in(&mut self) -> bool {
self.zoom_center(ZOOM_IN)
}
pub fn zoom_out(&mut self) -> bool {
self.zoom_center(ZOOM_OUT)
}
pub fn pan_left(&mut self) -> bool {
self.pan_step(-PAN_STEP)
}
pub fn pan_right(&mut self) -> bool {
self.pan_step(PAN_STEP)
}
fn pan_step(&mut self, fraction: f64) -> bool {
let Some(mapping) = self.mapping.clone() else {
return false;
};
if mapping.x_categories().is_some() {
return false;
}
self.view = self.seed_x(&mapping).pan_x(fraction);
true
}
pub fn on_mouse(&mut self, mouse: Mouse) -> bool {
match mouse {
Mouse::Moved { column, row } => self.hover(column, row),
Mouse::Down {
button: MouseButton::Left,
column,
row,
} => {
let hovered = self.hover(column, row);
if self.inside(column, row) {
self.drag = Some(Drag::Pan {
last: (column, row),
});
return true;
}
hovered
}
Mouse::Drag {
button: MouseButton::Left,
column,
row,
} => {
let hovered = self.hover(column, row);
let Some(Drag::Pan { last }) = self.drag else {
return hovered;
};
self.drag = Some(Drag::Pan {
last: (column, row),
});
self.pan_by(last, (column, row)) || hovered
}
Mouse::Up {
button: MouseButton::Left,
..
} => {
let panning = matches!(self.drag, Some(Drag::Pan { .. }));
if panning {
self.drag = None;
}
panning
}
Mouse::Down {
button: MouseButton::Right,
column,
row,
} => {
if self.inside(column, row) {
self.drag = Some(Drag::Select {
anchor: (column, row),
current: (column, row),
});
return true;
}
false
}
Mouse::Drag {
button: MouseButton::Right,
column,
row,
} => {
let hovered = self.hover(column, row);
let Some(Drag::Select { anchor, .. }) = self.drag else {
return hovered;
};
let Some(rect) = self.plot_area() else {
return hovered;
};
self.drag = Some(Drag::Select {
anchor,
current: clamp_into(rect, column, row),
});
true
}
Mouse::Up {
button: MouseButton::Right,
..
} => {
let Some(Drag::Select { anchor, current }) = self.drag else {
return false;
};
self.drag = None;
self.apply_selection(anchor, current);
true
}
Mouse::ScrollUp { column, row } => self.wheel(ZOOM_IN, column, row),
Mouse::ScrollDown { column, row } => self.wheel(ZOOM_OUT, column, row),
Mouse::ScrollLeft { column, row } => self.scroll_pan(-SCROLL_PAN, column, row),
Mouse::ScrollRight { column, row } => self.scroll_pan(SCROLL_PAN, column, row),
_ => false,
}
}
fn hover(&mut self, column: u16, row: u16) -> bool {
let hover = self
.inside(column, row)
.then_some(Hover::Cell { column, row });
let changed = hover != self.hover;
self.hover = hover;
changed
}
fn inside(&self, column: u16, row: u16) -> bool {
self.plot_area().is_some_and(|rect| {
column >= rect.x && column < rect.right() && row >= rect.y && row < rect.bottom()
})
}
fn wheel(&mut self, factor: f64, column: u16, row: u16) -> bool {
if !self.inside(column, row) {
return false;
}
let Some((anchor, _)) = self.data_at(column, row) else {
return false;
};
let Some(mapping) = &self.mapping else {
return false;
};
if mapping.x_categories().is_some() {
return false;
}
self.view = self.seed_x(mapping).zoom_x(factor, anchor);
true
}
fn scroll_pan(&mut self, fraction: f64, column: u16, row: u16) -> bool {
if !self.inside(column, row) {
return false;
}
let Some(mapping) = self.mapping.clone() else {
return false;
};
if mapping.x_categories().is_some() {
return false;
}
self.view = self.seed_x(&mapping).pan_x(fraction);
true
}
fn zoom_center(&mut self, factor: f64) -> bool {
let Some(mapping) = self.mapping.clone() else {
return false;
};
let Some(panel) = mapping.plot_area() else {
return false;
};
if mapping.x_categories().is_some() {
return false;
}
let Some((anchor, _)) =
mapping.data_at(panel.column + panel.width / 2, panel.row + panel.height / 2)
else {
return false;
};
self.view = self.seed_x(&mapping).zoom_x(factor, anchor);
true
}
fn pan_by(&mut self, from: (u16, u16), to: (u16, u16)) -> bool {
let Some(mapping) = &self.mapping else {
return false;
};
let Some(panel) = mapping.plot_area() else {
return false;
};
let (columns, rows) = (panel.width, panel.height);
let dx = f64::from(to.0) - f64::from(from.0);
let dy = f64::from(to.1) - f64::from(from.1);
if dx == 0.0 && dy == 0.0 {
return false;
}
let view = self.seeded(mapping);
self.view = view
.pan_x(-dx / columns.max(1) as f64)
.pan_y(dy / rows.max(1) as f64);
true
}
fn seeded(&self, mapping: &Mapping) -> Viewport {
let mut seeded = mapping.viewport();
if let Some((low, high)) = self.view.x() {
seeded = seeded.with_x(low, high);
}
if let Some((low, high)) = self.view.y() {
seeded = seeded.with_y(low, high);
}
seeded
}
fn apply_selection(&mut self, anchor: (u16, u16), current: (u16, u16)) {
let Some(mapping) = &self.mapping else {
return;
};
if anchor.0.abs_diff(current.0) < 2 || anchor.1.abs_diff(current.1) < 2 {
return;
}
let Some((ax, ay)) = self.data_at(anchor.0, anchor.1) else {
return;
};
let Some((cx, cy)) = self.data_at(current.0, current.1) else {
return;
};
let mut view = mapping.viewport();
if mapping.x_categories().is_none() {
view = view.with_x(ax, cx);
}
if mapping.y_categories().is_none() {
view = view.with_y(ay, cy);
}
self.view = view;
}
fn seed_x(&self, mapping: &Mapping) -> Viewport {
let mut seeded = mapping.viewport();
if let Some((low, high)) = self.view.x() {
seeded = seeded.with_x(low, high);
}
match self.view.y() {
Some((low, high)) => seeded.with_y(low, high),
None => seeded.reset_y(),
}
}
}
const ZOOM_IN: f64 = 0.8;
const ZOOM_OUT: f64 = 1.25;
const PAN_STEP: f64 = 0.1;
const SCROLL_PAN: f64 = 0.03;
#[cfg(feature = "pixel")]
const PIXEL_PACE: std::time::Duration = std::time::Duration::from_millis(33);
#[cfg(feature = "pixel")]
impl crate::pixel::Graphics {
pub fn present(
&self,
out: &mut impl std::io::Write,
panels: &mut [&mut PlotState],
) -> std::io::Result<()> {
use std::fmt::Write as _;
let kitty = self.protocol == crate::pixel::Protocol::Kitty;
let states = panels;
let mut blocks = String::new();
for state in states.iter_mut() {
if let Some((rect, block, hash)) = state.pixels.take() {
let _ = write!(blocks, "\x1b[{};{}H", rect.y + 1, rect.x + 1);
blocks.push_str(&block);
if kitty
&& let Some(id) = state.pixel_id
&& let Some(mapping) = state.mapping()
&& let Some(panel) = mapping.plot_area()
{
let placement = if state.pixel_placement == 1 { 2 } else { 1 };
let _ = write!(
blocks,
"\x1b[{};{}H\x1b_Ga=p,i={id},p={placement},c={},r={},C=1,q=2\x1b\\",
rect.y as usize + panel.row + 1,
rect.x as usize + panel.column + 1,
panel.width,
panel.height,
);
if state.pixel_placement != 0 {
let _ = write!(
blocks,
"\x1b_Ga=d,d=i,i={id},p={},q=2\x1b\\",
state.pixel_placement
);
}
state.pixel_placement = placement;
}
state.pixel_sent = Some((rect, hash));
}
}
if blocks.is_empty() {
return Ok(());
}
let mut swap = String::with_capacity(blocks.len() + 32);
swap.push_str("\x1b[?2026h");
swap.push_str(&blocks);
swap.push_str("\x1b[?2026l");
out.write_all(swap.as_bytes())?;
out.flush()
}
pub fn retire(
&self,
out: &mut impl std::io::Write,
panels: &mut [&mut PlotState],
) -> std::io::Result<()> {
use std::fmt::Write as _;
let mut goodbye = String::new();
if self.protocol == crate::pixel::Protocol::Kitty {
for state in panels.iter() {
if let Some(id) = state.pixel_id
&& state.pixel_placement != 0
{
let _ = write!(goodbye, "\x1b_Ga=d,d=I,i={id},q=2\x1b\\");
}
}
}
for state in panels.iter_mut() {
state.invalidate_pixels();
state.pixel_placement = 0;
}
if goodbye.is_empty() {
return Ok(());
}
out.write_all(goodbye.as_bytes())?;
out.flush()
}
}
#[cfg(feature = "pixel")]
fn block_hash(block: &str) -> u64 {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut hasher = DefaultHasher::new();
block.hash(&mut hasher);
hasher.finish()
}
#[cfg(feature = "pixel")]
fn next_pixel_id() -> u32 {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let base = std::process::id().wrapping_mul(0x9E37_79B1);
let id = base.wrapping_add(COUNTER.fetch_add(1, Ordering::Relaxed));
if id == 0 { 1 } else { id }
}
fn clamp_into(rect: Rect, column: u16, row: u16) -> (u16, u16) {
(
column.clamp(rect.x, rect.right().saturating_sub(1).max(rect.x)),
row.clamp(rect.y, rect.bottom().saturating_sub(1).max(rect.y)),
)
}
impl PlotWidget<'_> {
fn overlays(&self, buffer: &mut Buffer, state: &PlotState) {
let Some(rect) = state.plot_area() else {
return;
};
let light = self.theme == Theme::LIGHT;
let tint = if light {
RatColor::Gray
} else {
RatColor::DarkGray
};
if let Some(Drag::Select { anchor, current }) = state.drag {
let (x0, x1) = (anchor.0.min(current.0), anchor.0.max(current.0));
let (y0, y1) = (anchor.1.min(current.1), anchor.1.max(current.1));
for y in y0..=y1 {
for x in x0..=x1 {
buffer[(x, y)].set_bg(tint);
}
}
}
let hover = match state.hover {
Some(Hover::Cell { column, row })
if column >= rect.x
&& column < rect.right()
&& row >= rect.y
&& row < rect.bottom() =>
{
Some((column, Some(row)))
}
Some(Hover::Column { column }) if column >= rect.x && column < rect.right() => {
Some((column, None))
}
_ => None,
};
if self.crosshair
&& let Some((column, row)) = hover
{
if let Some(row) = row {
for x in rect.x..rect.right() {
let cell = &mut buffer[(x, row)];
if cell.bg == RatColor::Reset {
cell.set_bg(tint);
}
}
}
for y in rect.y..rect.bottom() {
let cell = &mut buffer[(column, y)];
if cell.bg == RatColor::Reset {
cell.set_bg(tint);
}
}
}
if hover.is_none() {
return;
}
let snapped = if self.snap {
self.snapped(state)
} else {
Vec::new()
};
let highlight = if light {
RatColor::DarkGray
} else {
RatColor::Gray
};
for snap in &snapped {
if let Some(value) = snap.value
&& let Some(mapping) = state.mapping()
&& let Some((column, row)) = mapping.cell_at(snap.x, value)
{
let x = state.area.x.saturating_add(column as u16);
let y = state.area.y.saturating_add(row as u16);
if x < rect.right() && y < rect.bottom() {
buffer[(x, y)].set_bg(highlight);
}
}
}
if self.readout
&& let Some(text) = self.readout_line(state, &snapped, rect.width.saturating_sub(2))
{
let width = display_width(&text) as u16;
buffer.set_string(
rect.right() - width - 1,
rect.y,
&text,
Style::new().fg(highlight),
);
}
}
fn readout_line(&self, state: &PlotState, snapped: &[Snapped], budget: u16) -> Option<String> {
let cursor_x = state.hover_data_x()?;
let mapping = state.mapping()?;
let x_part = match snapped {
[only] => mapping.format_x(only.x),
[] | [..] => mapping.format_x(cursor_x),
};
let mut parts = vec![x_part];
if snapped.is_empty()
&& let Some((_, cursor_y)) = state.cursor_data()
{
parts.push(mapping.format_y(cursor_y));
}
for snap in snapped {
let value = snap
.value
.map_or_else(|| "—".to_string(), |value| mapping.format_y(value));
parts.push(match &snap.label {
Some(label) => format!("{label}: {value}"),
None => value,
});
}
loop {
let text = parts.join(" · ");
if display_width(&text) as u16 <= budget {
return Some(text);
}
if parts.len() <= 2 {
return None;
}
parts.pop();
}
}
fn snapped(&self, state: &PlotState) -> Vec<Snapped> {
let Some(cursor_x) = state.hover_data_x() else {
return Vec::new();
};
let Some(mapping) = state.mapping() else {
return Vec::new();
};
let window = mapping.x_domain();
let mut snapped = Vec::new();
for layer in self.plot.layers() {
let (x, y, label) = match layer {
Mark::Line(line) => match line.channels() {
Some((x, y)) => (x, y, line.label.as_ref()),
None => continue,
},
Mark::Points(points) => (points.x.as_ref(), &points.y, points.label.as_ref()),
_ => continue,
};
let Some(index) = nearest_visible(x.map(Series::as_slice), y.len(), cursor_x, window)
else {
continue;
};
snapped.push(Snapped {
label: label.cloned(),
x: x.map_or(index as f64, |series| series.as_slice()[index]),
value: y.as_slice().get(index).copied().filter(|v| v.is_finite()),
});
}
snapped
}
}
struct Snapped {
label: Option<String>,
x: f64,
value: Option<f64>,
}
fn nearest_visible(
x: Option<&[f64]>,
len: usize,
target: f64,
window: (f64, f64),
) -> Option<usize> {
match x {
Some(values) => {
let mut best: Option<(usize, f64)> = None;
for (index, &value) in values.iter().enumerate() {
if !value.is_finite() || value < window.0 || value > window.1 {
continue;
}
let distance = (value - target).abs();
if best.is_none_or(|(_, nearest)| distance < nearest) {
best = Some((index, distance));
}
}
best.map(|(index, _)| index)
}
None => {
let mut index = target.round();
if index < window.0 {
index = target.ceil();
} else if index > window.1 {
index = target.floor();
}
if index < window.0.max(0.0) || index > window.1 {
return None;
}
let index = index as usize;
(index < len).then_some(index)
}
}
}
fn convert(color: Color) -> Option<RatColor> {
Some(match color {
Color::Default => return None,
Color::Black => RatColor::Black,
Color::Red => RatColor::Red,
Color::Green => RatColor::Green,
Color::Yellow => RatColor::Yellow,
Color::Blue => RatColor::Blue,
Color::Magenta => RatColor::Magenta,
Color::Cyan => RatColor::Cyan,
Color::White => RatColor::White,
Color::BrightBlack => RatColor::DarkGray,
Color::BrightRed => RatColor::LightRed,
Color::BrightGreen => RatColor::LightGreen,
Color::BrightYellow => RatColor::LightYellow,
Color::BrightBlue => RatColor::LightBlue,
Color::BrightMagenta => RatColor::LightMagenta,
Color::BrightCyan => RatColor::LightCyan,
Color::BrightWhite => RatColor::White,
Color::Ansi256(index) => RatColor::Indexed(index),
Color::Rgb(r, g, b) => RatColor::Rgb(r, g, b),
})
}
#[cfg(test)]
#[path = "adapter_tests.rs"]
mod tests;