pub mod area;
pub mod line;
pub mod point;
pub mod rect;
pub mod text;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GapPolicy {
#[default]
Break,
Connect,
Skip,
}
pub(crate) fn gap_runs(points: &[(f64, f64)], policy: GapPolicy) -> Vec<Vec<(f64, f64)>> {
let is_finite = |&(px, py): &(f64, f64)| px.is_finite() && py.is_finite();
if policy == GapPolicy::Skip && points.iter().any(|p| !is_finite(p)) {
return Vec::new();
}
match policy {
GapPolicy::Skip | GapPolicy::Connect => {
let run: Vec<(f64, f64)> = points.iter().copied().filter(is_finite).collect();
if run.is_empty() {
Vec::new()
} else {
vec![run]
}
}
GapPolicy::Break => {
let mut runs = Vec::new();
let mut current: Vec<(f64, f64)> = Vec::new();
for &p in points {
if is_finite(&p) {
current.push(p);
} else if !current.is_empty() {
runs.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
runs.push(current);
}
runs
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
impl LineCap {
pub fn as_str(self) -> &'static str {
match self {
LineCap::Butt => "butt",
LineCap::Round => "round",
LineCap::Square => "square",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
impl LineJoin {
pub fn as_str(self) -> &'static str {
match self {
LineJoin::Miter => "miter",
LineJoin::Round => "round",
LineJoin::Bevel => "bevel",
}
}
}
#[derive(Debug, Clone)]
pub struct MarkStyle {
pub color: String,
pub stroke_width: f64,
pub fill_alpha: f64,
pub gap_policy: GapPolicy,
pub line_cap: LineCap,
pub line_join: LineJoin,
}
impl Default for MarkStyle {
fn default() -> Self {
Self {
color: "#4d90fe".to_owned(),
stroke_width: 1.5,
fill_alpha: 1.0,
gap_policy: GapPolicy::default(),
line_cap: LineCap::default(),
line_join: LineJoin::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nan_point() -> (f64, f64) {
(f64::NAN, 1.0)
}
#[test]
fn gap_runs_break_splits_at_every_non_finite_point() {
let points = vec![(0.0, 0.0), (1.0, 1.0), nan_point(), (3.0, 3.0), (4.0, 4.0)];
let runs = gap_runs(&points, GapPolicy::Break);
assert_eq!(runs.len(), 2, "a single interior gap must split into exactly 2 runs");
assert_eq!(runs[0], vec![(0.0, 0.0), (1.0, 1.0)]);
assert_eq!(runs[1], vec![(3.0, 3.0), (4.0, 4.0)]);
}
#[test]
fn gap_runs_connect_filters_non_finite_and_returns_one_run() {
let points = vec![(0.0, 0.0), (1.0, 1.0), nan_point(), (3.0, 3.0), (4.0, 4.0)];
let runs = gap_runs(&points, GapPolicy::Connect);
assert_eq!(runs.len(), 1, "Connect must bridge the gap into a single continuous run");
assert_eq!(runs[0], vec![(0.0, 0.0), (1.0, 1.0), (3.0, 3.0), (4.0, 4.0)]);
}
#[test]
fn gap_runs_skip_draws_nothing_when_any_point_is_non_finite() {
let points = vec![(0.0, 0.0), (1.0, 1.0), nan_point(), (3.0, 3.0)];
assert!(gap_runs(&points, GapPolicy::Skip).is_empty(), "Skip must draw NOTHING when the series has any gap");
}
#[test]
fn gap_runs_skip_matches_break_when_there_is_no_gap_at_all() {
let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
assert_eq!(gap_runs(&points, GapPolicy::Skip), gap_runs(&points, GapPolicy::Break));
assert_eq!(gap_runs(&points, GapPolicy::Skip), gap_runs(&points, GapPolicy::Connect));
}
#[test]
fn gap_runs_leading_and_trailing_non_finite_points_are_trimmed_for_every_policy() {
let points = vec![nan_point(), (1.0, 1.0), (2.0, 2.0), nan_point()];
for policy in [GapPolicy::Break, GapPolicy::Connect] {
let runs = gap_runs(&points, policy);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0], vec![(1.0, 1.0), (2.0, 2.0)]);
}
assert!(gap_runs(&points, GapPolicy::Skip).is_empty());
}
#[test]
fn gap_runs_all_non_finite_produces_no_runs_for_any_policy() {
let points = vec![nan_point(), (f64::NAN, f64::NAN), (f64::INFINITY, 1.0)];
for policy in [GapPolicy::Break, GapPolicy::Connect, GapPolicy::Skip] {
assert!(gap_runs(&points, policy).is_empty(), "{policy:?} must produce no runs when every point is non-finite");
}
}
#[test]
fn gap_runs_single_valid_point_among_gaps_never_produces_a_drawable_run() {
let points = vec![nan_point(), (5.0, 5.0), nan_point()];
for policy in [GapPolicy::Break, GapPolicy::Connect] {
let runs = gap_runs(&points, policy);
assert!(runs.iter().all(|r| r.len() < 2));
}
assert!(gap_runs(&points, GapPolicy::Skip).is_empty());
}
#[test]
fn line_cap_and_line_join_as_str_match_the_render_context_string_convention() {
assert_eq!(LineCap::Butt.as_str(), "butt");
assert_eq!(LineCap::Round.as_str(), "round");
assert_eq!(LineCap::Square.as_str(), "square");
assert_eq!(LineJoin::Miter.as_str(), "miter");
assert_eq!(LineJoin::Round.as_str(), "round");
assert_eq!(LineJoin::Bevel.as_str(), "bevel");
}
#[test]
fn mark_style_default_preserves_todays_rendered_behavior() {
let style = MarkStyle::default();
assert_eq!(style.gap_policy, GapPolicy::Break);
assert_eq!(style.line_cap, LineCap::Butt);
assert_eq!(style.line_join, LineJoin::Miter);
}
}