use core::hash::Hash;
use std::ops::RangeInclusive;
use std::sync::Arc;
use egui::Mesh;
use egui::Rgba;
use egui::Shape;
use egui::Stroke;
use egui::Ui;
use egui::epaint::BandPoint;
use egui::epaint::BandShape;
use egui::epaint::ColorMode;
use egui::epaint::PathStroke;
use egui::{Color32, Rangef};
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::item_id::ItemId;
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,
pub(crate) allow_band: bool,
}
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,
allow_band: true,
}
}
#[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: f32) -> Self {
self.stroke.width = width;
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: f32) -> Self {
self.fill = Some(y_reference);
self
}
#[inline]
pub fn fill_alpha(mut self, alpha: f32) -> Self {
self.fill_alpha = alpha;
self
}
#[inline]
pub fn style(mut self, style: LineStyle) -> Self {
self.style = style;
self
}
#[inline]
pub fn allow_band(mut self, allow_band: bool) -> Self {
self.allow_band = allow_band;
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 Hash) -> Self {
self.base_mut().id = ItemId::new(id);
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 line_runs = visible_line_runs(series.points(), transform, ui.clip_rect());
let n_values: usize = line_runs.iter().map(Vec::len).sum();
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.saturating_sub(1) * 2);
mesh.reserve_vertices(n_values * 2 + expected_intersections);
for values_tf in &line_runs {
for [prev, next] in values_tf.array_windows::<2>() {
let fill_color = fill_color_for_point(*prev);
let i = mesh.vertices.len() as u32;
mesh.colored_vertex(*prev, fill_color);
mesh.colored_vertex(pos2(prev.x, y), fill_color);
if let Some(x) = y_intersection(prev, next, 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);
}
}
if let Some(&last) = values_tf.last() {
let fill_color = fill_color_for_point(last);
mesh.colored_vertex(last, fill_color);
mesh.colored_vertex(pos2(last.x, y), fill_color);
}
}
if !mesh.vertices.is_empty() {
shapes.push(Shape::Mesh(std::sync::Arc::new(mesh)));
}
}
for values_tf in line_runs {
if self.allow_band
&& self.gradient_color.is_none()
&& *style == LineStyle::Solid
&& let ColorMode::Solid(stroke_color) = final_stroke.color
&& values_tf.array_windows().all(|[previous, next]| previous.x <= next.x)
&& let mut band_points = band_points(&values_tf)
&& 1 < band_points.len()
&& band_points.len() < values_tf.len()
{
for point in &mut band_points {
let center = point.y.center();
let mut width = point.y.span();
width += final_stroke.width;
if base.highlight {
width *= 2.0;
}
point.y = Rangef::new(center - 0.5 * width, center + 0.5 * width);
}
shapes.push(BandShape::filled(band_points, stroke_color).into());
} else {
style.style_line(values_tf, final_stroke.clone(), 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
.array_windows::<2>()
.enumerate()
.map(|(i, [v0, v1])| {
let p0 = transform.position_from_point(v0);
let p1 = transform.position_from_point(v1);
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()
}
}
fn band_points(values_tf: &[Pos2]) -> Vec<BandPoint> {
let mut band_points: Vec<BandPoint> = Vec::with_capacity(values_tf.len());
let bucket_width = 1.0;
for &point in values_tf {
if let Some(band_point) = band_points.last_mut()
&& band_point.is_finite()
&& point.is_finite()
&& band_point.x <= point.x
&& point.x - band_point.x <= bucket_width
{
band_point.y.min = band_point.y.min.min(point.y);
band_point.y.max = band_point.y.max.max(point.y);
} else {
band_points.push(BandPoint::new(point.x, point.y..=point.y));
}
}
for bp in band_points.iter_mut().skip(1) {
bp.x += 0.5 * bucket_width;
}
if let (Some(last_bp), Some(last_pos)) = (band_points.last_mut(), values_tf.last()) {
last_bp.x = last_pos.x;
}
band_points
}
fn visible_line_runs(points: &[PlotPoint], transform: &PlotTransform, clip_rect: Rect) -> Vec<Vec<Pos2>> {
let clip_min_x = transform.value_from_position(clip_rect.min).x;
let clip_max_x = transform.value_from_position(clip_rect.max).x;
let clip_x_range = clip_min_x.min(clip_max_x)..=clip_min_x.max(clip_max_x);
let mut line_runs = Vec::new();
let mut line_run = Vec::new();
for [previous, next] in points.array_windows::<2>() {
if segment_intersects_x_range(*previous, *next, &clip_x_range) {
let previous_position = transform.position_from_point(previous);
let next_position = transform.position_from_point(next);
if !clip_rect.intersects(Rect::from_two_pos(previous_position, next_position)) {
if !line_run.is_empty() {
line_runs.push(std::mem::take(&mut line_run));
}
continue;
}
if line_run.last().copied() != Some(previous_position) {
line_run.push(previous_position);
}
line_run.push(next_position);
} else if !line_run.is_empty() {
line_runs.push(std::mem::take(&mut line_run));
}
}
if points.len() == 1 {
let point = transform.position_from_point(&points[0]);
if clip_rect.contains(point) {
line_runs.push(vec![point]);
}
} else if !line_run.is_empty() {
line_runs.push(line_run);
}
line_runs
}
fn segment_intersects_x_range(previous: PlotPoint, next: PlotPoint, x_range: &RangeInclusive<f64>) -> bool {
previous.x.min(next.x) <= *x_range.end() && *x_range.start() <= previous.x.max(next.x)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn culls_segments_outside_the_clip_x_range() {
let x_range = 0.0..=1.0;
assert!(!segment_intersects_x_range(
PlotPoint::new(-2.0, 0.0),
PlotPoint::new(-1.0, 0.0),
&x_range
));
assert!(segment_intersects_x_range(
PlotPoint::new(-1.0, 0.0),
PlotPoint::new(0.0, 0.0),
&x_range
));
}
#[test]
fn bands_points_more_than_a_pixel_from_the_bucket_start() {
let band_points = band_points(&[pos2(0.0, 2.0), pos2(0.4, -1.0), pos2(1.01, 3.0)]);
assert_eq!(
band_points,
vec![BandPoint::new(0.0, -1.0..=2.0), BandPoint::new(1.01, 3.0..=3.0)]
);
}
#[test]
fn bands_keep_points_more_than_a_pixel_apart() {
let band_points = band_points(&[pos2(0.0, 2.0), pos2(1.01, -1.0)]);
assert_eq!(
band_points,
vec![BandPoint::new(0.0, 2.0..=2.0), BandPoint::new(1.01, -1.0..=-1.0)]
);
}
}