use ratatui::prelude::*;
use super::ChartState;
use super::render::compute_graph_area;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct ChartAnnotation {
pub x: f64,
pub y: f64,
pub label: String,
pub color: Color,
}
impl ChartAnnotation {
pub fn new(x: f64, y: f64, label: impl Into<String>, color: Color) -> Self {
Self {
x,
y,
label: label.into(),
color,
}
}
}
pub(super) struct AxisBounds {
pub(super) x_min: f64,
pub(super) x_max: f64,
pub(super) y_min: f64,
pub(super) y_max: f64,
pub(super) is_log: bool,
}
pub(super) fn render_annotations(
state: &ChartState,
frame: &mut Frame,
area: Rect,
y_labels: &[String],
x_labels: &[String],
bounds: AxisBounds,
) {
if state.annotations.is_empty() {
return;
}
let graph_area = compute_graph_area(area, y_labels, x_labels);
if graph_area.width == 0 || graph_area.height == 0 {
return;
}
let x_range = bounds.x_max - bounds.x_min;
let y_range = bounds.y_max - bounds.y_min;
if x_range <= 0.0 || y_range <= 0.0 {
return;
}
let buf = frame.buffer_mut();
for ann in &state.annotations {
let ann_y = if bounds.is_log {
state.y_scale.transform(ann.y.max(f64::MIN_POSITIVE))
} else {
ann.y
};
let x_frac = (ann.x - bounds.x_min) / x_range;
let y_frac = (ann_y - bounds.y_min) / y_range;
if !(0.0..=1.0).contains(&x_frac) || !(0.0..=1.0).contains(&y_frac) {
continue;
}
let screen_x = graph_area.x + (x_frac * (graph_area.width as f64 - 1.0)).round() as u16;
let screen_y = graph_area
.bottom()
.saturating_sub(1)
.saturating_sub((y_frac * (graph_area.height as f64 - 1.0)).round() as u16);
let label_x = screen_x.saturating_add(1);
let label_y = screen_y.saturating_sub(1);
if label_y < graph_area.y || label_y >= graph_area.bottom() {
continue;
}
for (i, ch) in ann.label.chars().enumerate() {
let cx = label_x + i as u16;
if cx >= graph_area.right() {
break;
}
if let Some(cell) = buf.cell_mut(Position::new(cx, label_y)) {
if cell.symbol() == " " || cell.symbol().trim().is_empty() {
cell.set_char(ch);
cell.set_fg(ann.color);
}
}
}
}
}