use std::ops::RangeInclusive;
use std::sync::Arc;
use egui::Color32;
use egui::Id;
use egui::Mesh;
use egui::Rgba;
use egui::Shape;
use egui::Stroke;
use egui::Ui;
use egui::epaint::PathStroke;
use emath::Float as _;
use emath::NumExt as _;
use emath::Pos2;
use emath::Rect;
use emath::pos2;
use crate::aesthetics::LineStyle;
use crate::axis::PlotTransform;
use crate::bounds::PlotBounds;
use crate::bounds::PlotPoint;
use crate::colors::DEFAULT_FILL_ALPHA;
use crate::data::PlotPoints;
use crate::items::ClosestElem;
use crate::items::PlotGeometry;
use crate::items::PlotItem;
use crate::items::PlotItemBase;
use crate::math::{dist_sq_to_segment, y_intersection};
pub struct Line<'a> {
base: PlotItemBase,
pub(crate) series: PlotPoints<'a>,
pub(crate) stroke: Stroke,
pub(crate) fill: Option<f32>,
pub(crate) fill_alpha: f32,
pub(crate) gradient_color: Option<Arc<dyn Fn(PlotPoint) -> Color32 + Send + Sync>>,
pub(crate) gradient_fill: bool,
pub(crate) style: LineStyle,
}
impl<'a> Line<'a> {
pub fn new(name: impl Into<String>, series: impl Into<PlotPoints<'a>>) -> Self {
Self {
base: PlotItemBase::new(name.into()),
series: series.into(),
stroke: Stroke::new(1.5, Color32::TRANSPARENT),
fill: None,
fill_alpha: DEFAULT_FILL_ALPHA,
gradient_color: None,
gradient_fill: false,
style: LineStyle::Solid,
}
}
#[inline]
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = stroke.into();
self
}
#[inline]
pub fn gradient_color(
mut self,
callback: Arc<dyn Fn(PlotPoint) -> Color32 + Send + Sync>,
gradient_fill: bool,
) -> Self {
self.gradient_color = Some(callback);
self.gradient_fill = gradient_fill;
self
}
#[inline]
pub fn width(mut self, width: impl Into<f32>) -> Self {
self.stroke.width = width.into();
self
}
#[inline]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.stroke.color = color.into();
self
}
#[inline]
pub fn fill(mut self, y_reference: impl Into<f32>) -> Self {
self.fill = Some(y_reference.into());
self
}
#[inline]
pub fn fill_alpha(mut self, alpha: impl Into<f32>) -> Self {
self.fill_alpha = alpha.into();
self
}
#[inline]
pub fn style(mut self, style: LineStyle) -> Self {
self.style = style;
self
}
#[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
#[inline]
pub fn name(mut self, name: impl ToString) -> Self {
self.base_mut().name = name.to_string();
self
}
#[inline]
pub fn highlight(mut self, highlight: bool) -> Self {
self.base_mut().highlight = highlight;
self
}
#[inline]
pub fn allow_hover(mut self, hovering: bool) -> Self {
self.base_mut().allow_hover = hovering;
self
}
#[inline]
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.base_mut().id = id.into();
self
}
}
impl PlotItem for Line<'_> {
fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let Self {
base,
series,
stroke,
fill,
gradient_fill,
style,
..
} = self;
let mut fill = *fill;
let final_stroke: PathStroke = if let Some(gradient_callback) = self.gradient_color.clone() {
let local_transform = *transform;
let wrapped_callback = move |_rec: Rect, pos: Pos2| -> Color32 {
let point = local_transform.value_from_position(pos);
gradient_callback(point)
};
PathStroke::new_uv(stroke.width, wrapped_callback.clone())
} else {
(*stroke).into()
};
let values_tf: Vec<_> = series
.points()
.iter()
.map(|v| transform.position_from_point(v))
.collect();
let n_values = values_tf.len();
if n_values < 2 {
fill = None;
}
if let Some(y_reference) = fill {
let mut fill_alpha = self.fill_alpha;
if base.highlight {
fill_alpha = (2.0 * fill_alpha).at_most(1.0);
}
let y = transform.position_from_point(&PlotPoint::new(0.0, y_reference)).y;
let default_fill_color = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha).into();
let fill_color_for_point = |pos| {
if *gradient_fill && let Some(gradient_fallback) = &self.gradient_color {
Rgba::from(gradient_fallback(transform.value_from_position(pos)))
.to_opaque()
.multiply(fill_alpha)
.into()
} else {
default_fill_color
}
};
let mut mesh = Mesh::default();
let expected_intersections = 20;
mesh.reserve_triangles((n_values - 1) * 2);
mesh.reserve_vertices(n_values * 2 + expected_intersections);
values_tf.windows(2).for_each(|w| {
let fill_color = fill_color_for_point(w[0]);
let i = mesh.vertices.len() as u32;
mesh.colored_vertex(w[0], fill_color);
mesh.colored_vertex(pos2(w[0].x, y), fill_color);
if let Some(x) = y_intersection(&w[0], &w[1], y) {
let point = pos2(x, y);
mesh.colored_vertex(point, fill_color_for_point(point));
mesh.add_triangle(i, i + 1, i + 2);
mesh.add_triangle(i + 2, i + 3, i + 4);
} else {
mesh.add_triangle(i, i + 1, i + 2);
mesh.add_triangle(i + 1, i + 2, i + 3);
}
});
let last = values_tf[n_values - 1];
let fill_color = fill_color_for_point(last);
mesh.colored_vertex(last, fill_color);
mesh.colored_vertex(pos2(last.x, y), fill_color);
shapes.push(Shape::Mesh(std::sync::Arc::new(mesh)));
}
style.style_line(values_tf, final_stroke, base.highlight, shapes);
}
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
let points = self.series.points();
if points.len() <= 1 {
return 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());
}
points
.windows(2)
.enumerate()
.map(|(i, w)| {
let p0 = transform.position_from_point(&w[0]);
let p1 = transform.position_from_point(&w[1]);
let dist_sq = dist_sq_to_segment(point, [p0, p1]);
let index = if point.distance_sq(p0) <= point.distance_sq(p1) {
i
} else {
i + 1
};
ClosestElem { index, dist_sq }
})
.min_by_key(|e| e.dist_sq.ord())
}
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
self.series.generate_points(x_range);
}
fn color(&self) -> Color32 {
self.stroke.color
}
fn base(&self) -> &PlotItemBase {
&self.base
}
fn base_mut(&mut self) -> &mut PlotItemBase {
&mut self.base
}
fn geometry(&self) -> PlotGeometry<'_> {
PlotGeometry::Points(self.series.points())
}
fn bounds(&self) -> PlotBounds {
self.series.bounds()
}
}