#![expect(
clippy::type_complexity,
reason = "TODO(#163): simplify some of the callback types with type aliases"
)]
use std::ops::RangeInclusive;
use egui::Align2;
use egui::Color32;
use egui::Id;
use egui::PopupAnchor;
use egui::Pos2;
use egui::Shape;
use egui::TextStyle;
use egui::Ui;
use egui::vec2;
use emath::Float as _;
use crate::aesthetics::Orientation;
use crate::axis::PlotTransform;
use crate::bounds::PlotBounds;
use crate::bounds::PlotPoint;
use crate::cursor::Cursor;
pub use crate::items::arrows::Arrows;
pub use crate::items::bar_chart::Bar;
pub use crate::items::bar_chart::BarChart;
pub use crate::items::box_plot::BoxElem;
pub use crate::items::box_plot::BoxPlot;
pub use crate::items::box_plot::BoxSpread;
pub use crate::items::filled_area::FilledArea;
pub use crate::items::heatmap::Heatmap;
pub use crate::items::line::HLine;
pub use crate::items::line::VLine;
pub use crate::items::line::horizontal_line;
pub use crate::items::line::vertical_line;
pub use crate::items::plot_image::PlotImage;
pub use crate::items::points::Points;
pub use crate::items::polygon::Polygon;
pub use crate::items::series::Line;
pub use crate::items::span::Span;
pub use crate::items::text::Text;
use crate::label::LabelFormatter;
use crate::rect_elem::RectElement;
mod arrows;
mod bar_chart;
mod box_plot;
mod filled_area;
mod heatmap;
mod line;
mod plot_image;
mod points;
mod polygon;
mod series;
mod span;
mod text;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlotItemBase {
name: String,
id: Id,
highlight: bool,
allow_hover: bool,
}
impl PlotItemBase {
pub fn new(name: String) -> Self {
let id = Id::new(&name);
Self {
name,
id,
highlight: false,
allow_hover: true,
}
}
}
pub struct PlotConfig<'a> {
pub ui: &'a Ui,
pub transform: &'a PlotTransform,
pub show_x: bool,
pub show_y: bool,
pub show_crosshair: bool,
}
pub trait PlotItem {
fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);
fn initialize(&mut self, x_range: RangeInclusive<f64>);
fn name(&self) -> &str {
&self.base().name
}
fn color(&self) -> Color32;
fn highlight(&mut self) {
self.base_mut().highlight = true;
}
fn highlighted(&self) -> bool {
self.base().highlight
}
fn allow_hover(&self) -> bool {
self.base().allow_hover
}
fn geometry(&self) -> PlotGeometry<'_>;
fn bounds(&self) -> PlotBounds;
fn base(&self) -> &PlotItemBase;
fn base_mut(&mut self) -> &mut PlotItemBase;
fn id(&self) -> Id {
self.base().id
}
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
match self.geometry() {
PlotGeometry::None => None,
PlotGeometry::Points(points) => points
.iter()
.enumerate()
.map(|(index, value)| {
let pos = transform.position_from_point(value);
let dist_sq = point.distance_sq(pos);
ClosestElem { index, dist_sq }
})
.min_by_key(|e| e.dist_sq.ord()),
PlotGeometry::Rects => {
panic!("If the PlotItem is made of rects, it should implement find_closest()")
}
}
}
fn on_hover(
&self,
plot_area_response: &egui::Response,
elem: ClosestElem,
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
plot: &PlotConfig<'_>,
label_formatter: &Option<LabelFormatter<'_>>,
) {
let points = match self.geometry() {
PlotGeometry::Points(points) => points,
PlotGeometry::None => {
panic!("If the PlotItem has no geometry, on_hover() must not be called")
}
PlotGeometry::Rects => {
panic!("If the PlotItem is made of rects, it should implement on_hover()")
}
};
let line_color = if plot.ui.visuals().dark_mode {
Color32::from_gray(100).additive()
} else {
Color32::from_black_alpha(180)
};
let value = points[elem.index];
let pointer = plot.transform.position_from_point(&value);
shapes.push(Shape::circle_filled(pointer, 3.0, line_color));
rulers_and_tooltip_at_value(plot_area_response, value, self.name(), plot, cursors, label_formatter);
}
}
fn add_rulers_and_text(
elem: &dyn RectElement,
plot: &PlotConfig<'_>,
text: Option<String>,
shapes: &mut Vec<Shape>,
cursors: &mut Vec<Cursor>,
) {
let orientation = elem.orientation();
let show_argument =
plot.show_x && orientation == Orientation::Vertical || plot.show_y && orientation == Orientation::Horizontal;
let show_values =
plot.show_y && orientation == Orientation::Vertical || plot.show_x && orientation == Orientation::Horizontal;
if show_argument {
for pos in elem.arguments_with_ruler() {
cursors.push(match orientation {
Orientation::Horizontal => Cursor::Horizontal { y: pos.y },
Orientation::Vertical => Cursor::Vertical { x: pos.x },
});
}
}
if show_values {
for pos in elem.values_with_ruler() {
cursors.push(match orientation {
Orientation::Horizontal => Cursor::Vertical { x: pos.x },
Orientation::Vertical => Cursor::Horizontal { y: pos.y },
});
}
}
let text = text.unwrap_or_else(|| {
let mut text = elem.name().to_owned();
if show_values {
text.push('\n');
text.push_str(&elem.default_values_format(plot.transform));
}
text
});
let font_id = TextStyle::Body.resolve(plot.ui.style());
let corner_value = elem.corner_value();
plot.ui.fonts_mut(|f| {
shapes.push(Shape::text(
f,
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
font_id,
plot.ui.visuals().text_color(),
));
});
}
pub(super) fn rulers_and_tooltip_at_value(
plot_area_response: &egui::Response,
value: PlotPoint,
name: &str,
plot: &PlotConfig<'_>,
cursors: &mut Vec<Cursor>,
label_formatter: &Option<LabelFormatter<'_>>,
) {
if plot.show_crosshair {
if plot.show_x {
cursors.push(Cursor::Vertical { x: value.x });
}
if plot.show_y {
cursors.push(Cursor::Horizontal { y: value.y });
}
}
let Some(custom_label) = label_formatter else {
return;
};
let text = custom_label(name, &value);
if text.is_empty() {
return;
}
let mut tooltip = egui::Tooltip::always_open(
plot_area_response.ctx.clone(),
plot_area_response.layer_id,
plot_area_response.id,
PopupAnchor::Pointer,
);
let tooltip_width = plot_area_response.ctx.global_style().spacing.tooltip_width;
tooltip.popup = tooltip.popup.width(tooltip_width);
tooltip.gap(12.0).show(|ui| {
ui.set_max_width(tooltip_width);
ui.label(text);
});
}
pub enum PlotGeometry<'a> {
None,
Points(&'a [PlotPoint]),
Rects,
}
pub struct ClosestElem {
pub index: usize,
pub dist_sq: f32,
}