presentar_core/
chart.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Chart rendering algorithms for Presentar.
3//!
4//! This module provides mathematical algorithms for chart rendering:
5//! - Interpolation (linear, cubic spline, Catmull-Rom, Bezier)
6//! - Path tessellation for GPU rendering
7//! - Histogram binning
8//! - Arc geometry computation
9//! - Data normalization and scaling
10//!
11//! # Example
12//!
13//! ```
14//! use presentar_core::chart::{Interpolator, CubicSpline, Point2D};
15//!
16//! // Create a spline from control points
17//! let points = vec![
18//!     Point2D::new(0.0, 0.0),
19//!     Point2D::new(1.0, 2.0),
20//!     Point2D::new(2.0, 1.5),
21//!     Point2D::new(3.0, 3.0),
22//! ];
23//! let spline = CubicSpline::from_points(&points);
24//!
25//! // Interpolate at any x value
26//! let y = spline.interpolate(1.5);
27//! ```
28
29use serde::{Deserialize, Serialize};
30use std::f64::consts::PI;
31
32/// 2D point for chart calculations.
33#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
34pub struct Point2D {
35    pub x: f64,
36    pub y: f64,
37}
38
39impl Point2D {
40    /// Create a new point.
41    #[must_use]
42    pub const fn new(x: f64, y: f64) -> Self {
43        Self { x, y }
44    }
45
46    /// Origin point (0, 0).
47    pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
48
49    /// Distance to another point.
50    #[must_use]
51    pub fn distance(&self, other: &Self) -> f64 {
52        let dx = self.x - other.x;
53        let dy = self.y - other.y;
54        dx.hypot(dy)
55    }
56
57    /// Linear interpolation between two points.
58    #[must_use]
59    pub fn lerp(&self, other: &Self, t: f64) -> Self {
60        Self {
61            x: (other.x - self.x).mul_add(t, self.x),
62            y: (other.y - self.y).mul_add(t, self.y),
63        }
64    }
65}
66
67impl std::ops::Add for Point2D {
68    type Output = Self;
69
70    fn add(self, rhs: Self) -> Self::Output {
71        Self {
72            x: self.x + rhs.x,
73            y: self.y + rhs.y,
74        }
75    }
76}
77
78impl std::ops::Sub for Point2D {
79    type Output = Self;
80
81    fn sub(self, rhs: Self) -> Self::Output {
82        Self {
83            x: self.x - rhs.x,
84            y: self.y - rhs.y,
85        }
86    }
87}
88
89impl std::ops::Mul<f64> for Point2D {
90    type Output = Self;
91
92    fn mul(self, rhs: f64) -> Self::Output {
93        Self {
94            x: self.x * rhs,
95            y: self.y * rhs,
96        }
97    }
98}
99
100/// Interpolation trait for different curve types.
101pub trait Interpolator {
102    /// Interpolate y value at given x.
103    fn interpolate(&self, x: f64) -> f64;
104
105    /// Generate points along the curve.
106    fn sample(&self, start: f64, end: f64, num_points: usize) -> Vec<Point2D> {
107        if num_points < 2 {
108            return vec![];
109        }
110        let step = (end - start) / (num_points - 1) as f64;
111        (0..num_points)
112            .map(|i| {
113                let x = (i as f64).mul_add(step, start);
114                Point2D::new(x, self.interpolate(x))
115            })
116            .collect()
117    }
118}
119
120/// Linear interpolation between points.
121#[derive(Debug, Clone)]
122pub struct LinearInterpolator {
123    points: Vec<Point2D>,
124}
125
126impl LinearInterpolator {
127    /// Create from points (must be sorted by x).
128    #[must_use]
129    pub fn from_points(points: &[Point2D]) -> Self {
130        let mut sorted = points.to_vec();
131        sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
132        Self { points: sorted }
133    }
134
135    /// Create from x,y data.
136    #[must_use]
137    pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
138        let points: Vec<_> = xs
139            .iter()
140            .zip(ys.iter())
141            .map(|(&x, &y)| Point2D::new(x, y))
142            .collect();
143        Self::from_points(&points)
144    }
145
146    /// Get the underlying points.
147    #[must_use]
148    pub fn points(&self) -> &[Point2D] {
149        &self.points
150    }
151
152    /// Find the segment containing x.
153    fn find_segment(&self, x: f64) -> Option<(usize, f64)> {
154        if self.points.len() < 2 {
155            return None;
156        }
157        for i in 0..self.points.len() - 1 {
158            let p1 = &self.points[i];
159            let p2 = &self.points[i + 1];
160            if x >= p1.x && x <= p2.x {
161                let t = if (p2.x - p1.x).abs() < 1e-10 {
162                    0.0
163                } else {
164                    (x - p1.x) / (p2.x - p1.x)
165                };
166                return Some((i, t));
167            }
168        }
169        // Extrapolate
170        if x < self.points[0].x {
171            Some((
172                0,
173                (x - self.points[0].x) / (self.points[1].x - self.points[0].x),
174            ))
175        } else {
176            let n = self.points.len();
177            Some((
178                n - 2,
179                (x - self.points[n - 2].x) / (self.points[n - 1].x - self.points[n - 2].x),
180            ))
181        }
182    }
183}
184
185impl Interpolator for LinearInterpolator {
186    fn interpolate(&self, x: f64) -> f64 {
187        if self.points.is_empty() {
188            return 0.0;
189        }
190        if self.points.len() == 1 {
191            return self.points[0].y;
192        }
193
194        if let Some((i, t)) = self.find_segment(x) {
195            let p1 = &self.points[i];
196            let p2 = &self.points[i + 1];
197            (p2.y - p1.y).mul_add(t, p1.y)
198        } else {
199            0.0
200        }
201    }
202}
203
204/// Cubic spline interpolation (natural spline).
205#[derive(Debug, Clone)]
206pub struct CubicSpline {
207    points: Vec<Point2D>,
208    /// Second derivatives at each point
209    y2: Vec<f64>,
210}
211
212impl CubicSpline {
213    /// Create from points (must have at least 3 points).
214    #[must_use]
215    pub fn from_points(points: &[Point2D]) -> Self {
216        let mut sorted = points.to_vec();
217        sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
218
219        let n = sorted.len();
220        if n < 3 {
221            return Self {
222                points: sorted,
223                y2: vec![0.0; n],
224            };
225        }
226
227        // Compute second derivatives using natural spline boundary conditions
228        let mut y2 = vec![0.0; n];
229        let mut u = vec![0.0; n];
230
231        // Forward sweep
232        for i in 1..n - 1 {
233            let h_prev = sorted[i].x - sorted[i - 1].x;
234            let h_next = sorted[i + 1].x - sorted[i].x;
235
236            if h_prev.abs() < 1e-10 || h_next.abs() < 1e-10 {
237                continue;
238            }
239
240            let sig = h_prev / (h_prev + h_next);
241            let p = sig.mul_add(y2[i - 1], 2.0);
242            y2[i] = (sig - 1.0) / p;
243            u[i] =
244                (sorted[i + 1].y - sorted[i].y) / h_next - (sorted[i].y - sorted[i - 1].y) / h_prev;
245            u[i] = sig.mul_add(-u[i - 1], 6.0 * u[i] / (h_prev + h_next)) / p;
246        }
247
248        // Back substitution
249        for k in (0..n - 1).rev() {
250            y2[k] = y2[k].mul_add(y2[k + 1], u[k]);
251        }
252
253        Self { points: sorted, y2 }
254    }
255
256    /// Create from x,y data.
257    #[must_use]
258    pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
259        let points: Vec<_> = xs
260            .iter()
261            .zip(ys.iter())
262            .map(|(&x, &y)| Point2D::new(x, y))
263            .collect();
264        Self::from_points(&points)
265    }
266
267    /// Get the underlying points.
268    #[must_use]
269    pub fn points(&self) -> &[Point2D] {
270        &self.points
271    }
272}
273
274impl Interpolator for CubicSpline {
275    fn interpolate(&self, x: f64) -> f64 {
276        let n = self.points.len();
277        if n == 0 {
278            return 0.0;
279        }
280        if n == 1 {
281            return self.points[0].y;
282        }
283        if n == 2 {
284            // Fall back to linear
285            let t = (x - self.points[0].x) / (self.points[1].x - self.points[0].x);
286            return (self.points[1].y - self.points[0].y).mul_add(t, self.points[0].y);
287        }
288
289        // Find segment
290        let mut lo = 0;
291        let mut hi = n - 1;
292        while hi - lo > 1 {
293            let mid = (hi + lo) / 2;
294            if self.points[mid].x > x {
295                hi = mid;
296            } else {
297                lo = mid;
298            }
299        }
300
301        let h = self.points[hi].x - self.points[lo].x;
302        if h.abs() < 1e-10 {
303            return self.points[lo].y;
304        }
305
306        let a = (self.points[hi].x - x) / h;
307        let b = (x - self.points[lo].x) / h;
308
309        a.mul_add(self.points[lo].y, b * self.points[hi].y)
310            + (a * a)
311                .mul_add(a, -a)
312                .mul_add(self.y2[lo], (b * b).mul_add(b, -b) * self.y2[hi])
313                * h
314                * h
315                / 6.0
316    }
317}
318
319/// Catmull-Rom spline interpolation.
320#[derive(Debug, Clone)]
321pub struct CatmullRom {
322    points: Vec<Point2D>,
323    /// Tension parameter (0.0 to 1.0)
324    tension: f64,
325}
326
327impl CatmullRom {
328    /// Create from points with default tension.
329    #[must_use]
330    pub fn from_points(points: &[Point2D]) -> Self {
331        Self::with_tension(points, 0.5)
332    }
333
334    /// Create from points with custom tension.
335    #[must_use]
336    pub fn with_tension(points: &[Point2D], tension: f64) -> Self {
337        let mut sorted = points.to_vec();
338        sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
339        Self {
340            points: sorted,
341            tension: tension.clamp(0.0, 1.0),
342        }
343    }
344
345    /// Get the underlying points.
346    #[must_use]
347    pub fn points(&self) -> &[Point2D] {
348        &self.points
349    }
350
351    /// Generate a smooth path through the points.
352    #[must_use]
353    pub fn to_path(&self, segments_per_span: usize) -> Vec<Point2D> {
354        if self.points.len() < 2 {
355            return self.points.clone();
356        }
357        if self.points.len() == 2 {
358            return self.points.clone();
359        }
360
361        let mut path = Vec::new();
362        let n = self.points.len();
363
364        for i in 0..n - 1 {
365            let p0 = if i == 0 {
366                self.points[0]
367            } else {
368                self.points[i - 1]
369            };
370            let p1 = self.points[i];
371            let p2 = self.points[i + 1];
372            let p3 = if i + 2 < n {
373                self.points[i + 2]
374            } else {
375                self.points[n - 1]
376            };
377
378            for j in 0..segments_per_span {
379                let t = j as f64 / segments_per_span as f64;
380                let point = self.catmull_rom_point(p0, p1, p2, p3, t);
381                path.push(point);
382            }
383        }
384
385        // Add final point
386        path.push(self.points[n - 1]);
387        path
388    }
389
390    /// Compute point on Catmull-Rom curve.
391    fn catmull_rom_point(
392        &self,
393        p0: Point2D,
394        p1: Point2D,
395        p2: Point2D,
396        p3: Point2D,
397        t: f64,
398    ) -> Point2D {
399        let t2 = t * t;
400        let t3 = t2 * t;
401
402        let tau = self.tension;
403
404        // Catmull-Rom basis matrix with tension
405        let c0 = tau.mul_add(-t, (-tau).mul_add(t3, 2.0 * tau * t2));
406        let c1 = (2.0 - tau).mul_add(t3, (tau - 3.0) * t2) + 1.0;
407        let c2 = tau.mul_add(t, (tau - 2.0).mul_add(t3, 2.0f64.mul_add(-tau, 3.0) * t2));
408        let c3 = tau.mul_add(t3, -(tau * t2));
409
410        Point2D::new(
411            c3.mul_add(p3.x, c0 * p0.x + c1 * p1.x + c2 * p2.x),
412            c3.mul_add(p3.y, c0 * p0.y + c1 * p1.y + c2 * p2.y),
413        )
414    }
415}
416
417impl Interpolator for CatmullRom {
418    fn interpolate(&self, x: f64) -> f64 {
419        // For Catmull-Rom, we need to find the segment and interpolate
420        if self.points.is_empty() {
421            return 0.0;
422        }
423        if self.points.len() == 1 {
424            return self.points[0].y;
425        }
426
427        // Find segment containing x
428        let mut idx = 0;
429        for i in 0..self.points.len() - 1 {
430            if x >= self.points[i].x && x <= self.points[i + 1].x {
431                idx = i;
432                break;
433            }
434            if x < self.points[i].x {
435                idx = i.saturating_sub(1);
436                break;
437            }
438            idx = i;
439        }
440
441        let p1 = &self.points[idx];
442        let p2 = &self.points[(idx + 1).min(self.points.len() - 1)];
443
444        let t = if (p2.x - p1.x).abs() < 1e-10 {
445            0.0
446        } else {
447            ((x - p1.x) / (p2.x - p1.x)).clamp(0.0, 1.0)
448        };
449
450        let p0 = if idx == 0 { *p1 } else { self.points[idx - 1] };
451        let p3 = if idx + 2 < self.points.len() {
452            self.points[idx + 2]
453        } else {
454            *p2
455        };
456
457        self.catmull_rom_point(p0, *p1, *p2, p3, t).y
458    }
459}
460
461/// Bezier curve segment.
462#[derive(Debug, Clone, Copy, PartialEq)]
463pub struct CubicBezier {
464    /// Start point
465    pub p0: Point2D,
466    /// First control point
467    pub p1: Point2D,
468    /// Second control point
469    pub p2: Point2D,
470    /// End point
471    pub p3: Point2D,
472}
473
474impl CubicBezier {
475    /// Create a new cubic Bezier curve.
476    #[must_use]
477    pub const fn new(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D) -> Self {
478        Self { p0, p1, p2, p3 }
479    }
480
481    /// Evaluate the curve at parameter t (0 to 1).
482    #[must_use]
483    pub fn evaluate(&self, t: f64) -> Point2D {
484        let t = t.clamp(0.0, 1.0);
485        let mt = 1.0 - t;
486        let mt2 = mt * mt;
487        let mt3 = mt2 * mt;
488        let t2 = t * t;
489        let t3 = t2 * t;
490
491        Point2D::new(
492            (3.0 * mt * t2).mul_add(self.p2.x, mt3 * self.p0.x + 3.0 * mt2 * t * self.p1.x)
493                + t3 * self.p3.x,
494            (3.0 * mt * t2).mul_add(self.p2.y, mt3 * self.p0.y + 3.0 * mt2 * t * self.p1.y)
495                + t3 * self.p3.y,
496        )
497    }
498
499    /// Convert to a polyline with given number of segments.
500    #[must_use]
501    pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
502        let segments = segments.max(1);
503        (0..=segments)
504            .map(|i| self.evaluate(i as f64 / segments as f64))
505            .collect()
506    }
507
508    /// Compute approximate arc length.
509    #[must_use]
510    pub fn arc_length(&self, segments: usize) -> f64 {
511        let points = self.to_polyline(segments);
512        points.windows(2).map(|w| w[0].distance(&w[1])).sum()
513    }
514
515    /// Split curve at parameter t.
516    #[must_use]
517    pub fn split(&self, t: f64) -> (Self, Self) {
518        let t = t.clamp(0.0, 1.0);
519
520        // De Casteljau's algorithm
521        let p01 = self.p0.lerp(&self.p1, t);
522        let p12 = self.p1.lerp(&self.p2, t);
523        let p23 = self.p2.lerp(&self.p3, t);
524
525        let p012 = p01.lerp(&p12, t);
526        let p123 = p12.lerp(&p23, t);
527
528        let p0123 = p012.lerp(&p123, t);
529
530        let left = Self::new(self.p0, p01, p012, p0123);
531        let right = Self::new(p0123, p123, p23, self.p3);
532
533        (left, right)
534    }
535}
536
537/// Histogram binning configuration.
538#[derive(Debug, Clone)]
539pub struct HistogramBins {
540    /// Bin edges (n+1 edges for n bins)
541    pub edges: Vec<f64>,
542    /// Bin counts
543    pub counts: Vec<usize>,
544    /// Bin densities (normalized)
545    pub densities: Vec<f64>,
546}
547
548impl HistogramBins {
549    /// Create histogram from data with specified number of bins.
550    #[must_use]
551    pub fn from_data(data: &[f64], num_bins: usize) -> Self {
552        if data.is_empty() || num_bins == 0 {
553            return Self {
554                edges: vec![],
555                counts: vec![],
556                densities: vec![],
557            };
558        }
559
560        let num_bins = num_bins.max(1);
561        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
562        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
563
564        Self::from_data_range(data, num_bins, min, max)
565    }
566
567    /// Create histogram from data with explicit range.
568    #[must_use]
569    pub fn from_data_range(data: &[f64], num_bins: usize, min: f64, max: f64) -> Self {
570        let num_bins = num_bins.max(1);
571        let range = (max - min).max(1e-10);
572        let bin_width = range / num_bins as f64;
573
574        // Create edges
575        let edges: Vec<f64> = (0..=num_bins)
576            .map(|i| (i as f64).mul_add(bin_width, min))
577            .collect();
578
579        // Count values in each bin
580        let mut counts = vec![0usize; num_bins];
581        for &value in data {
582            let bin = ((value - min) / bin_width).floor() as usize;
583            let bin = bin.min(num_bins - 1); // Handle edge case where value == max
584            counts[bin] += 1;
585        }
586
587        // Compute densities (probability density)
588        let total = data.len() as f64;
589        let densities: Vec<f64> = counts
590            .iter()
591            .map(|&c| (c as f64) / (total * bin_width))
592            .collect();
593
594        Self {
595            edges,
596            counts,
597            densities,
598        }
599    }
600
601    /// Get number of bins.
602    #[must_use]
603    pub fn num_bins(&self) -> usize {
604        self.counts.len()
605    }
606
607    /// Get bin width (assumes uniform bins).
608    #[must_use]
609    pub fn bin_width(&self) -> f64 {
610        if self.edges.len() < 2 {
611            return 0.0;
612        }
613        self.edges[1] - self.edges[0]
614    }
615
616    /// Get bin center for given index.
617    #[must_use]
618    pub fn bin_center(&self, index: usize) -> Option<f64> {
619        if index >= self.counts.len() {
620            return None;
621        }
622        Some((self.edges[index] + self.edges[index + 1]) / 2.0)
623    }
624
625    /// Get bin range for given index.
626    #[must_use]
627    pub fn bin_range(&self, index: usize) -> Option<(f64, f64)> {
628        if index >= self.counts.len() {
629            return None;
630        }
631        Some((self.edges[index], self.edges[index + 1]))
632    }
633
634    /// Total count across all bins.
635    #[must_use]
636    pub fn total_count(&self) -> usize {
637        self.counts.iter().sum()
638    }
639
640    /// Maximum count in any bin.
641    #[must_use]
642    pub fn max_count(&self) -> usize {
643        self.counts.iter().copied().max().unwrap_or(0)
644    }
645}
646
647/// Arc geometry for pie charts.
648#[derive(Debug, Clone, Copy, PartialEq)]
649pub struct ArcGeometry {
650    /// Center point
651    pub center: Point2D,
652    /// Radius
653    pub radius: f64,
654    /// Start angle (radians)
655    pub start_angle: f64,
656    /// End angle (radians)
657    pub end_angle: f64,
658}
659
660impl ArcGeometry {
661    /// Create a new arc.
662    #[must_use]
663    pub const fn new(center: Point2D, radius: f64, start_angle: f64, end_angle: f64) -> Self {
664        Self {
665            center,
666            radius,
667            start_angle,
668            end_angle,
669        }
670    }
671
672    /// Create a full circle.
673    #[must_use]
674    pub fn circle(center: Point2D, radius: f64) -> Self {
675        Self::new(center, radius, 0.0, 2.0 * PI)
676    }
677
678    /// Get the sweep angle.
679    #[must_use]
680    pub fn sweep(&self) -> f64 {
681        self.end_angle - self.start_angle
682    }
683
684    /// Point on arc at given angle.
685    #[must_use]
686    pub fn point_at_angle(&self, angle: f64) -> Point2D {
687        Point2D::new(
688            self.radius.mul_add(angle.cos(), self.center.x),
689            self.radius.mul_add(angle.sin(), self.center.y),
690        )
691    }
692
693    /// Start point of arc.
694    #[must_use]
695    pub fn start_point(&self) -> Point2D {
696        self.point_at_angle(self.start_angle)
697    }
698
699    /// End point of arc.
700    #[must_use]
701    pub fn end_point(&self) -> Point2D {
702        self.point_at_angle(self.end_angle)
703    }
704
705    /// Midpoint of arc.
706    #[must_use]
707    pub fn mid_point(&self) -> Point2D {
708        let mid_angle = (self.start_angle + self.end_angle) / 2.0;
709        self.point_at_angle(mid_angle)
710    }
711
712    /// Arc length.
713    #[must_use]
714    pub fn arc_length(&self) -> f64 {
715        self.radius * self.sweep().abs()
716    }
717
718    /// Convert arc to polyline for rendering.
719    #[must_use]
720    pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
721        let segments = segments.max(1);
722        let sweep = self.sweep();
723        (0..=segments)
724            .map(|i| {
725                let t = i as f64 / segments as f64;
726                let angle = self.start_angle + t * sweep;
727                self.point_at_angle(angle)
728            })
729            .collect()
730    }
731
732    /// Convert arc to pie slice (includes center point).
733    #[must_use]
734    pub fn to_pie_slice(&self, segments: usize) -> Vec<Point2D> {
735        let mut points = vec![self.center];
736        points.extend(self.to_polyline(segments));
737        points.push(self.center);
738        points
739    }
740
741    /// Check if angle is within arc sweep.
742    #[must_use]
743    pub fn contains_angle(&self, angle: f64) -> bool {
744        let normalized = Self::normalize_angle(angle);
745        let start = Self::normalize_angle(self.start_angle);
746        let end = Self::normalize_angle(self.end_angle);
747
748        if start <= end {
749            normalized >= start && normalized <= end
750        } else {
751            normalized >= start || normalized <= end
752        }
753    }
754
755    /// Normalize angle to [0, 2π).
756    fn normalize_angle(angle: f64) -> f64 {
757        let mut a = angle % (2.0 * PI);
758        if a < 0.0 {
759            a += 2.0 * PI;
760        }
761        a
762    }
763}
764
765/// Data normalization for chart rendering.
766#[derive(Debug, Clone, Copy)]
767pub struct DataNormalizer {
768    /// Minimum value
769    pub min: f64,
770    /// Maximum value
771    pub max: f64,
772}
773
774impl DataNormalizer {
775    /// Create normalizer from data range.
776    #[must_use]
777    pub fn new(min: f64, max: f64) -> Self {
778        Self { min, max }
779    }
780
781    /// Create normalizer from data.
782    #[must_use]
783    pub fn from_data(data: &[f64]) -> Self {
784        if data.is_empty() {
785            return Self::new(0.0, 1.0);
786        }
787        let min = data.iter().copied().fold(f64::INFINITY, f64::min);
788        let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
789        Self::new(min, max)
790    }
791
792    /// Normalize a value to [0, 1].
793    #[must_use]
794    pub fn normalize(&self, value: f64) -> f64 {
795        let range = self.max - self.min;
796        if range.abs() < 1e-10 {
797            return 0.5;
798        }
799        (value - self.min) / range
800    }
801
802    /// Denormalize a value from [0, 1] to original range.
803    #[must_use]
804    pub fn denormalize(&self, normalized: f64) -> f64 {
805        normalized.mul_add(self.max - self.min, self.min)
806    }
807
808    /// Normalize all values in a slice.
809    #[must_use]
810    pub fn normalize_all(&self, data: &[f64]) -> Vec<f64> {
811        data.iter().map(|&v| self.normalize(v)).collect()
812    }
813
814    /// Get nice axis bounds (rounded for display).
815    #[must_use]
816    pub fn nice_bounds(&self) -> (f64, f64) {
817        let range = self.max - self.min;
818        if range.abs() < 1e-10 {
819            return (self.min - 1.0, self.max + 1.0);
820        }
821
822        let magnitude = 10.0_f64.powf(range.log10().floor());
823        let nice_min = (self.min / magnitude).floor() * magnitude;
824        let nice_max = (self.max / magnitude).ceil() * magnitude;
825
826        (nice_min, nice_max)
827    }
828}
829
830/// Path tessellation for GPU rendering.
831#[derive(Debug, Clone, Default)]
832pub struct PathTessellator {
833    /// Tolerance for curve flattening
834    pub tolerance: f64,
835    /// Generated vertices (x, y)
836    pub vertices: Vec<[f32; 2]>,
837    /// Triangle indices
838    pub indices: Vec<u32>,
839}
840
841impl PathTessellator {
842    /// Create a new tessellator.
843    #[must_use]
844    pub fn new(tolerance: f64) -> Self {
845        Self {
846            tolerance: tolerance.max(0.001),
847            vertices: Vec::new(),
848            indices: Vec::new(),
849        }
850    }
851
852    /// Create with default tolerance.
853    #[must_use]
854    pub fn with_default_tolerance() -> Self {
855        Self::new(0.25)
856    }
857
858    /// Clear the tessellator.
859    pub fn clear(&mut self) {
860        self.vertices.clear();
861        self.indices.clear();
862    }
863
864    /// Tessellate a filled polygon.
865    pub fn tessellate_polygon(&mut self, points: &[Point2D]) {
866        if points.len() < 3 {
867            return;
868        }
869
870        let base_idx = self.vertices.len() as u32;
871
872        // Add vertices
873        for p in points {
874            self.vertices.push([p.x as f32, p.y as f32]);
875        }
876
877        // Fan triangulation (simple, works for convex polygons)
878        for i in 1..points.len() as u32 - 1 {
879            self.indices.push(base_idx);
880            self.indices.push(base_idx + i);
881            self.indices.push(base_idx + i + 1);
882        }
883    }
884
885    /// Tessellate a stroked polyline.
886    pub fn tessellate_stroke(&mut self, points: &[Point2D], width: f64) {
887        if points.len() < 2 {
888            return;
889        }
890
891        let half_width = width / 2.0;
892
893        for window in points.windows(2) {
894            let p1 = window[0];
895            let p2 = window[1];
896
897            // Compute perpendicular direction
898            let dx = p2.x - p1.x;
899            let dy = p2.y - p1.y;
900            let len = dx.hypot(dy);
901            if len < 1e-10 {
902                continue;
903            }
904
905            let nx = -dy / len * half_width;
906            let ny = dx / len * half_width;
907
908            let base_idx = self.vertices.len() as u32;
909
910            // Add quad vertices
911            self.vertices.push([(p1.x + nx) as f32, (p1.y + ny) as f32]);
912            self.vertices.push([(p1.x - nx) as f32, (p1.y - ny) as f32]);
913            self.vertices.push([(p2.x + nx) as f32, (p2.y + ny) as f32]);
914            self.vertices.push([(p2.x - nx) as f32, (p2.y - ny) as f32]);
915
916            // Two triangles for quad
917            self.indices.push(base_idx);
918            self.indices.push(base_idx + 1);
919            self.indices.push(base_idx + 2);
920
921            self.indices.push(base_idx + 1);
922            self.indices.push(base_idx + 3);
923            self.indices.push(base_idx + 2);
924        }
925    }
926
927    /// Tessellate a circle.
928    pub fn tessellate_circle(&mut self, center: Point2D, radius: f64, segments: usize) {
929        let segments = segments.max(8);
930        let base_idx = self.vertices.len() as u32;
931
932        // Center vertex
933        self.vertices.push([center.x as f32, center.y as f32]);
934
935        // Perimeter vertices
936        for i in 0..segments {
937            let angle = 2.0 * PI * i as f64 / segments as f64;
938            let x = radius.mul_add(angle.cos(), center.x);
939            let y = radius.mul_add(angle.sin(), center.y);
940            self.vertices.push([x as f32, y as f32]);
941        }
942
943        // Fan triangles
944        for i in 0..segments as u32 {
945            self.indices.push(base_idx); // Center
946            self.indices.push(base_idx + 1 + i);
947            self.indices.push(base_idx + 1 + (i + 1) % segments as u32);
948        }
949    }
950
951    /// Tessellate a rectangle.
952    pub fn tessellate_rect(&mut self, x: f64, y: f64, width: f64, height: f64) {
953        let base_idx = self.vertices.len() as u32;
954
955        self.vertices.push([x as f32, y as f32]);
956        self.vertices.push([(x + width) as f32, y as f32]);
957        self.vertices
958            .push([(x + width) as f32, (y + height) as f32]);
959        self.vertices.push([x as f32, (y + height) as f32]);
960
961        // Two triangles
962        self.indices.push(base_idx);
963        self.indices.push(base_idx + 1);
964        self.indices.push(base_idx + 2);
965
966        self.indices.push(base_idx);
967        self.indices.push(base_idx + 2);
968        self.indices.push(base_idx + 3);
969    }
970
971    /// Get vertex count.
972    #[must_use]
973    pub fn vertex_count(&self) -> usize {
974        self.vertices.len()
975    }
976
977    /// Get index count.
978    #[must_use]
979    pub fn index_count(&self) -> usize {
980        self.indices.len()
981    }
982
983    /// Get triangle count.
984    #[must_use]
985    pub fn triangle_count(&self) -> usize {
986        self.indices.len() / 3
987    }
988}
989
990/// Draw call batching for GPU efficiency.
991#[derive(Debug, Clone, Default)]
992pub struct DrawBatch {
993    /// Batched circles (`center_x`, `center_y`, radius, `color_rgba`)
994    pub circles: Vec<[f32; 7]>,
995    /// Batched rectangles (x, y, w, h, `color_rgba`)
996    pub rects: Vec<[f32; 8]>,
997    /// Batched lines (x1, y1, x2, y2, width, `color_rgba`)
998    pub lines: Vec<[f32; 9]>,
999}
1000
1001impl DrawBatch {
1002    /// Create a new batch.
1003    #[must_use]
1004    pub fn new() -> Self {
1005        Self::default()
1006    }
1007
1008    /// Add a circle to the batch.
1009    pub fn add_circle(&mut self, x: f32, y: f32, radius: f32, r: f32, g: f32, b: f32, a: f32) {
1010        self.circles.push([x, y, radius, r, g, b, a]);
1011    }
1012
1013    /// Add a rectangle to the batch.
1014    #[allow(clippy::too_many_arguments)]
1015    pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) {
1016        self.rects.push([x, y, w, h, r, g, b, a]);
1017    }
1018
1019    /// Add a line to the batch.
1020    #[allow(clippy::too_many_arguments)]
1021    pub fn add_line(
1022        &mut self,
1023        x1: f32,
1024        y1: f32,
1025        x2: f32,
1026        y2: f32,
1027        width: f32,
1028        r: f32,
1029        g: f32,
1030        b: f32,
1031        a: f32,
1032    ) {
1033        self.lines.push([x1, y1, x2, y2, width, r, g, b, a]);
1034    }
1035
1036    /// Clear all batches.
1037    pub fn clear(&mut self) {
1038        self.circles.clear();
1039        self.rects.clear();
1040        self.lines.clear();
1041    }
1042
1043    /// Total draw calls if not batched.
1044    #[must_use]
1045    pub fn unbatched_draw_calls(&self) -> usize {
1046        self.circles.len() + self.rects.len() + self.lines.len()
1047    }
1048
1049    /// Actual draw calls with batching (3 max: circles, rects, lines).
1050    #[must_use]
1051    pub fn batched_draw_calls(&self) -> usize {
1052        let mut calls = 0;
1053        if !self.circles.is_empty() {
1054            calls += 1;
1055        }
1056        if !self.rects.is_empty() {
1057            calls += 1;
1058        }
1059        if !self.lines.is_empty() {
1060            calls += 1;
1061        }
1062        calls
1063    }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069
1070    // =========================================================================
1071    // Point2D Tests
1072    // =========================================================================
1073
1074    #[test]
1075    fn test_point2d_new() {
1076        let p = Point2D::new(1.0, 2.0);
1077        assert_eq!(p.x, 1.0);
1078        assert_eq!(p.y, 2.0);
1079    }
1080
1081    #[test]
1082    fn test_point2d_origin() {
1083        assert_eq!(Point2D::ORIGIN, Point2D::new(0.0, 0.0));
1084    }
1085
1086    #[test]
1087    fn test_point2d_distance() {
1088        let p1 = Point2D::new(0.0, 0.0);
1089        let p2 = Point2D::new(3.0, 4.0);
1090        assert!((p1.distance(&p2) - 5.0).abs() < 1e-10);
1091    }
1092
1093    #[test]
1094    fn test_point2d_lerp() {
1095        let p1 = Point2D::new(0.0, 0.0);
1096        let p2 = Point2D::new(10.0, 20.0);
1097        let mid = p1.lerp(&p2, 0.5);
1098        assert!((mid.x - 5.0).abs() < 1e-10);
1099        assert!((mid.y - 10.0).abs() < 1e-10);
1100    }
1101
1102    #[test]
1103    fn test_point2d_add() {
1104        let p1 = Point2D::new(1.0, 2.0);
1105        let p2 = Point2D::new(3.0, 4.0);
1106        let sum = p1 + p2;
1107        assert_eq!(sum, Point2D::new(4.0, 6.0));
1108    }
1109
1110    #[test]
1111    fn test_point2d_sub() {
1112        let p1 = Point2D::new(5.0, 7.0);
1113        let p2 = Point2D::new(2.0, 3.0);
1114        let diff = p1 - p2;
1115        assert_eq!(diff, Point2D::new(3.0, 4.0));
1116    }
1117
1118    #[test]
1119    fn test_point2d_mul() {
1120        let p = Point2D::new(2.0, 3.0);
1121        let scaled = p * 2.0;
1122        assert_eq!(scaled, Point2D::new(4.0, 6.0));
1123    }
1124
1125    // =========================================================================
1126    // LinearInterpolator Tests
1127    // =========================================================================
1128
1129    #[test]
1130    fn test_linear_empty() {
1131        let interp = LinearInterpolator::from_points(&[]);
1132        assert_eq!(interp.interpolate(0.0), 0.0);
1133    }
1134
1135    #[test]
1136    fn test_linear_single_point() {
1137        let interp = LinearInterpolator::from_points(&[Point2D::new(1.0, 5.0)]);
1138        assert_eq!(interp.interpolate(0.0), 5.0);
1139        assert_eq!(interp.interpolate(2.0), 5.0);
1140    }
1141
1142    #[test]
1143    fn test_linear_two_points() {
1144        let interp =
1145            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 20.0)]);
1146        assert!((interp.interpolate(0.0) - 0.0).abs() < 1e-10);
1147        assert!((interp.interpolate(5.0) - 10.0).abs() < 1e-10);
1148        assert!((interp.interpolate(10.0) - 20.0).abs() < 1e-10);
1149    }
1150
1151    #[test]
1152    fn test_linear_multiple_points() {
1153        let interp = LinearInterpolator::from_points(&[
1154            Point2D::new(0.0, 0.0),
1155            Point2D::new(1.0, 2.0),
1156            Point2D::new(2.0, 1.0),
1157            Point2D::new(3.0, 3.0),
1158        ]);
1159
1160        // Test interpolation at known points
1161        assert!((interp.interpolate(0.0) - 0.0).abs() < 1e-10);
1162        assert!((interp.interpolate(1.0) - 2.0).abs() < 1e-10);
1163        assert!((interp.interpolate(2.0) - 1.0).abs() < 1e-10);
1164        assert!((interp.interpolate(3.0) - 3.0).abs() < 1e-10);
1165
1166        // Test interpolation between points
1167        assert!((interp.interpolate(0.5) - 1.0).abs() < 1e-10);
1168        assert!((interp.interpolate(1.5) - 1.5).abs() < 1e-10);
1169    }
1170
1171    #[test]
1172    fn test_linear_from_xy() {
1173        let xs = [0.0, 1.0, 2.0];
1174        let ys = [0.0, 10.0, 20.0];
1175        let interp = LinearInterpolator::from_xy(&xs, &ys);
1176        assert!((interp.interpolate(1.5) - 15.0).abs() < 1e-10);
1177    }
1178
1179    #[test]
1180    fn test_linear_sample() {
1181        let interp =
1182            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
1183        let samples = interp.sample(0.0, 10.0, 11);
1184        assert_eq!(samples.len(), 11);
1185        assert!((samples[0].x - 0.0).abs() < 1e-10);
1186        assert!((samples[10].x - 10.0).abs() < 1e-10);
1187    }
1188
1189    // =========================================================================
1190    // CubicSpline Tests
1191    // =========================================================================
1192
1193    #[test]
1194    fn test_spline_empty() {
1195        let spline = CubicSpline::from_points(&[]);
1196        assert_eq!(spline.interpolate(0.0), 0.0);
1197    }
1198
1199    #[test]
1200    fn test_spline_single_point() {
1201        let spline = CubicSpline::from_points(&[Point2D::new(1.0, 5.0)]);
1202        assert_eq!(spline.interpolate(0.0), 5.0);
1203    }
1204
1205    #[test]
1206    fn test_spline_two_points() {
1207        let spline = CubicSpline::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 20.0)]);
1208        assert!((spline.interpolate(5.0) - 10.0).abs() < 1e-10);
1209    }
1210
1211    #[test]
1212    fn test_spline_passes_through_points() {
1213        let points = vec![
1214            Point2D::new(0.0, 0.0),
1215            Point2D::new(1.0, 2.0),
1216            Point2D::new(2.0, 1.5),
1217            Point2D::new(3.0, 3.0),
1218        ];
1219        let spline = CubicSpline::from_points(&points);
1220
1221        for p in &points {
1222            assert!(
1223                (spline.interpolate(p.x) - p.y).abs() < 0.01,
1224                "Spline should pass through control points"
1225            );
1226        }
1227    }
1228
1229    #[test]
1230    fn test_spline_smooth() {
1231        let points = vec![
1232            Point2D::new(0.0, 0.0),
1233            Point2D::new(1.0, 1.0),
1234            Point2D::new(2.0, 0.0),
1235        ];
1236        let spline = CubicSpline::from_points(&points);
1237
1238        // Check smoothness by sampling
1239        let samples = spline.sample(0.0, 2.0, 100);
1240        for w in samples.windows(3) {
1241            // No sudden jumps
1242            let dy1 = (w[1].y - w[0].y).abs();
1243            let dy2 = (w[2].y - w[1].y).abs();
1244            assert!(dy1 < 0.5 && dy2 < 0.5, "Spline should be smooth");
1245        }
1246    }
1247
1248    // =========================================================================
1249    // CatmullRom Tests
1250    // =========================================================================
1251
1252    #[test]
1253    fn test_catmull_rom_empty() {
1254        let cr = CatmullRom::from_points(&[]);
1255        assert_eq!(cr.interpolate(0.0), 0.0);
1256    }
1257
1258    #[test]
1259    fn test_catmull_rom_single() {
1260        let cr = CatmullRom::from_points(&[Point2D::new(1.0, 5.0)]);
1261        assert_eq!(cr.interpolate(0.0), 5.0);
1262    }
1263
1264    #[test]
1265    fn test_catmull_rom_passes_through() {
1266        let points = vec![
1267            Point2D::new(0.0, 0.0),
1268            Point2D::new(1.0, 2.0),
1269            Point2D::new(2.0, 1.0),
1270            Point2D::new(3.0, 3.0),
1271        ];
1272        let cr = CatmullRom::from_points(&points);
1273
1274        // Catmull-Rom should pass through control points
1275        for p in &points {
1276            let y = cr.interpolate(p.x);
1277            assert!(
1278                (y - p.y).abs() < 0.1,
1279                "Catmull-Rom should pass through points: expected {} at x={}, got {}",
1280                p.y,
1281                p.x,
1282                y
1283            );
1284        }
1285    }
1286
1287    #[test]
1288    fn test_catmull_rom_to_path() {
1289        let points = vec![
1290            Point2D::new(0.0, 0.0),
1291            Point2D::new(1.0, 1.0),
1292            Point2D::new(2.0, 0.0),
1293        ];
1294        let cr = CatmullRom::from_points(&points);
1295        let path = cr.to_path(10);
1296
1297        assert!(path.len() > points.len());
1298        assert_eq!(path.first().unwrap().x, 0.0);
1299        assert_eq!(path.last().unwrap().x, 2.0);
1300    }
1301
1302    #[test]
1303    fn test_catmull_rom_tension() {
1304        let points = vec![
1305            Point2D::new(0.0, 0.0),
1306            Point2D::new(1.0, 1.0),
1307            Point2D::new(2.0, 0.0),
1308        ];
1309
1310        let low_tension = CatmullRom::with_tension(&points, 0.0);
1311        let high_tension = CatmullRom::with_tension(&points, 1.0);
1312
1313        // Different tensions should produce different curves
1314        let y_low = low_tension.interpolate(0.5);
1315        let y_high = high_tension.interpolate(0.5);
1316
1317        // They should be different (tension affects curvature)
1318        assert!((y_low - y_high).abs() > 0.01 || (y_low - y_high).abs() < 0.5);
1319    }
1320
1321    // =========================================================================
1322    // CubicBezier Tests
1323    // =========================================================================
1324
1325    #[test]
1326    fn test_bezier_endpoints() {
1327        let bezier = CubicBezier::new(
1328            Point2D::new(0.0, 0.0),
1329            Point2D::new(1.0, 2.0),
1330            Point2D::new(2.0, 2.0),
1331            Point2D::new(3.0, 0.0),
1332        );
1333
1334        let start = bezier.evaluate(0.0);
1335        let end = bezier.evaluate(1.0);
1336
1337        assert!((start.x - 0.0).abs() < 1e-10);
1338        assert!((start.y - 0.0).abs() < 1e-10);
1339        assert!((end.x - 3.0).abs() < 1e-10);
1340        assert!((end.y - 0.0).abs() < 1e-10);
1341    }
1342
1343    #[test]
1344    fn test_bezier_midpoint() {
1345        let bezier = CubicBezier::new(
1346            Point2D::new(0.0, 0.0),
1347            Point2D::new(0.0, 1.0),
1348            Point2D::new(1.0, 1.0),
1349            Point2D::new(1.0, 0.0),
1350        );
1351
1352        let mid = bezier.evaluate(0.5);
1353        // Midpoint should be between control points
1354        assert!(mid.x > 0.0 && mid.x < 1.0);
1355        assert!(mid.y > 0.0 && mid.y < 1.0);
1356    }
1357
1358    #[test]
1359    fn test_bezier_to_polyline() {
1360        let bezier = CubicBezier::new(
1361            Point2D::new(0.0, 0.0),
1362            Point2D::new(1.0, 2.0),
1363            Point2D::new(2.0, 2.0),
1364            Point2D::new(3.0, 0.0),
1365        );
1366
1367        let polyline = bezier.to_polyline(10);
1368        assert_eq!(polyline.len(), 11);
1369        assert_eq!(polyline[0], bezier.evaluate(0.0));
1370        assert_eq!(polyline[10], bezier.evaluate(1.0));
1371    }
1372
1373    #[test]
1374    fn test_bezier_arc_length() {
1375        let line = CubicBezier::new(
1376            Point2D::new(0.0, 0.0),
1377            Point2D::new(1.0, 0.0),
1378            Point2D::new(2.0, 0.0),
1379            Point2D::new(3.0, 0.0),
1380        );
1381
1382        let length = line.arc_length(100);
1383        assert!((length - 3.0).abs() < 0.01);
1384    }
1385
1386    #[test]
1387    fn test_bezier_split() {
1388        let bezier = CubicBezier::new(
1389            Point2D::new(0.0, 0.0),
1390            Point2D::new(1.0, 2.0),
1391            Point2D::new(2.0, 2.0),
1392            Point2D::new(3.0, 0.0),
1393        );
1394
1395        let (left, right) = bezier.split(0.5);
1396
1397        // Left should start at original start
1398        assert_eq!(left.p0, bezier.p0);
1399
1400        // Right should end at original end
1401        assert_eq!(right.p3, bezier.p3);
1402
1403        // They should meet in the middle
1404        assert!((left.p3.x - right.p0.x).abs() < 1e-10);
1405        assert!((left.p3.y - right.p0.y).abs() < 1e-10);
1406    }
1407
1408    // =========================================================================
1409    // HistogramBins Tests
1410    // =========================================================================
1411
1412    #[test]
1413    fn test_histogram_empty() {
1414        let hist = HistogramBins::from_data(&[], 10);
1415        assert_eq!(hist.num_bins(), 0);
1416    }
1417
1418    #[test]
1419    fn test_histogram_single_value() {
1420        let hist = HistogramBins::from_data(&[5.0], 10);
1421        assert_eq!(hist.total_count(), 1);
1422    }
1423
1424    #[test]
1425    fn test_histogram_uniform() {
1426        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1427        let hist = HistogramBins::from_data(&data, 10);
1428
1429        assert_eq!(hist.num_bins(), 10);
1430        assert_eq!(hist.total_count(), 100);
1431
1432        // Each bin should have approximately 10 values
1433        for &count in &hist.counts {
1434            assert!(count >= 9 && count <= 11);
1435        }
1436    }
1437
1438    #[test]
1439    fn test_histogram_bin_width() {
1440        let hist = HistogramBins::from_data_range(&[0.0], 5, 0.0, 10.0);
1441        assert!((hist.bin_width() - 2.0).abs() < 1e-10);
1442    }
1443
1444    #[test]
1445    fn test_histogram_bin_center() {
1446        let hist = HistogramBins::from_data_range(&[0.0], 4, 0.0, 8.0);
1447        assert_eq!(hist.bin_center(0), Some(1.0));
1448        assert_eq!(hist.bin_center(1), Some(3.0));
1449        assert_eq!(hist.bin_center(4), None);
1450    }
1451
1452    #[test]
1453    fn test_histogram_bin_range() {
1454        let hist = HistogramBins::from_data_range(&[0.0], 4, 0.0, 8.0);
1455        assert_eq!(hist.bin_range(0), Some((0.0, 2.0)));
1456        assert_eq!(hist.bin_range(3), Some((6.0, 8.0)));
1457    }
1458
1459    #[test]
1460    fn test_histogram_densities() {
1461        let data = vec![0.5, 1.5, 1.5, 2.5, 2.5, 2.5];
1462        let hist = HistogramBins::from_data_range(&data, 3, 0.0, 3.0);
1463
1464        // Densities should integrate to 1
1465        let total_density: f64 = hist.densities.iter().map(|d| d * hist.bin_width()).sum();
1466        assert!((total_density - 1.0).abs() < 1e-10);
1467    }
1468
1469    #[test]
1470    fn test_histogram_max_count() {
1471        let data = vec![1.0, 1.0, 1.0, 2.0];
1472        let hist = HistogramBins::from_data_range(&data, 2, 0.0, 4.0);
1473        assert_eq!(hist.max_count(), 3);
1474    }
1475
1476    // =========================================================================
1477    // ArcGeometry Tests
1478    // =========================================================================
1479
1480    #[test]
1481    fn test_arc_new() {
1482        let arc = ArcGeometry::new(Point2D::new(0.0, 0.0), 10.0, 0.0, PI);
1483        assert_eq!(arc.center, Point2D::ORIGIN);
1484        assert_eq!(arc.radius, 10.0);
1485    }
1486
1487    #[test]
1488    fn test_arc_circle() {
1489        let circle = ArcGeometry::circle(Point2D::new(5.0, 5.0), 3.0);
1490        assert!((circle.sweep() - 2.0 * PI).abs() < 1e-10);
1491    }
1492
1493    #[test]
1494    fn test_arc_sweep() {
1495        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
1496        assert!((arc.sweep() - PI / 2.0).abs() < 1e-10);
1497    }
1498
1499    #[test]
1500    fn test_arc_point_at_angle() {
1501        let arc = ArcGeometry::circle(Point2D::ORIGIN, 1.0);
1502
1503        let p0 = arc.point_at_angle(0.0);
1504        assert!((p0.x - 1.0).abs() < 1e-10);
1505        assert!((p0.y - 0.0).abs() < 1e-10);
1506
1507        let p90 = arc.point_at_angle(PI / 2.0);
1508        assert!((p90.x - 0.0).abs() < 1e-10);
1509        assert!((p90.y - 1.0).abs() < 1e-10);
1510    }
1511
1512    #[test]
1513    fn test_arc_start_end_points() {
1514        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
1515
1516        let start = arc.start_point();
1517        assert!((start.x - 1.0).abs() < 1e-10);
1518
1519        let end = arc.end_point();
1520        assert!((end.x - (-1.0)).abs() < 1e-10);
1521    }
1522
1523    #[test]
1524    fn test_arc_mid_point() {
1525        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
1526        let mid = arc.mid_point();
1527        assert!((mid.y - 1.0).abs() < 1e-10);
1528    }
1529
1530    #[test]
1531    fn test_arc_length() {
1532        let semicircle = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
1533        assert!((semicircle.arc_length() - PI).abs() < 1e-10);
1534
1535        let full = ArcGeometry::circle(Point2D::ORIGIN, 1.0);
1536        assert!((full.arc_length() - 2.0 * PI).abs() < 1e-10);
1537    }
1538
1539    #[test]
1540    fn test_arc_to_polyline() {
1541        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
1542        let poly = arc.to_polyline(4);
1543
1544        assert_eq!(poly.len(), 5);
1545        assert!((poly[0].x - 1.0).abs() < 1e-10); // Start
1546        assert!((poly[4].x - (-1.0)).abs() < 1e-10); // End
1547    }
1548
1549    #[test]
1550    fn test_arc_to_pie_slice() {
1551        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
1552        let slice = arc.to_pie_slice(4);
1553
1554        // Should have center at start and end
1555        assert_eq!(slice[0], Point2D::ORIGIN);
1556        assert_eq!(slice[slice.len() - 1], Point2D::ORIGIN);
1557    }
1558
1559    #[test]
1560    fn test_arc_contains_angle() {
1561        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI);
1562        assert!(arc.contains_angle(PI / 2.0));
1563        assert!(arc.contains_angle(0.0));
1564        assert!(!arc.contains_angle(3.0 * PI / 2.0));
1565    }
1566
1567    // =========================================================================
1568    // DataNormalizer Tests
1569    // =========================================================================
1570
1571    #[test]
1572    fn test_normalizer_new() {
1573        let norm = DataNormalizer::new(0.0, 100.0);
1574        assert_eq!(norm.min, 0.0);
1575        assert_eq!(norm.max, 100.0);
1576    }
1577
1578    #[test]
1579    fn test_normalizer_from_data() {
1580        let data = vec![10.0, 20.0, 30.0, 40.0, 50.0];
1581        let norm = DataNormalizer::from_data(&data);
1582        assert_eq!(norm.min, 10.0);
1583        assert_eq!(norm.max, 50.0);
1584    }
1585
1586    #[test]
1587    fn test_normalizer_from_empty() {
1588        let norm = DataNormalizer::from_data(&[]);
1589        assert_eq!(norm.min, 0.0);
1590        assert_eq!(norm.max, 1.0);
1591    }
1592
1593    #[test]
1594    fn test_normalizer_normalize() {
1595        let norm = DataNormalizer::new(0.0, 100.0);
1596        assert!((norm.normalize(0.0) - 0.0).abs() < 1e-10);
1597        assert!((norm.normalize(50.0) - 0.5).abs() < 1e-10);
1598        assert!((norm.normalize(100.0) - 1.0).abs() < 1e-10);
1599    }
1600
1601    #[test]
1602    fn test_normalizer_denormalize() {
1603        let norm = DataNormalizer::new(0.0, 100.0);
1604        assert!((norm.denormalize(0.0) - 0.0).abs() < 1e-10);
1605        assert!((norm.denormalize(0.5) - 50.0).abs() < 1e-10);
1606        assert!((norm.denormalize(1.0) - 100.0).abs() < 1e-10);
1607    }
1608
1609    #[test]
1610    fn test_normalizer_roundtrip() {
1611        let norm = DataNormalizer::new(-50.0, 150.0);
1612        let values = vec![-50.0, 0.0, 50.0, 100.0, 150.0];
1613
1614        for &v in &values {
1615            let normalized = norm.normalize(v);
1616            let denormalized = norm.denormalize(normalized);
1617            assert!((v - denormalized).abs() < 1e-10);
1618        }
1619    }
1620
1621    #[test]
1622    fn test_normalizer_normalize_all() {
1623        let norm = DataNormalizer::new(0.0, 10.0);
1624        let data = vec![0.0, 5.0, 10.0];
1625        let normalized = norm.normalize_all(&data);
1626
1627        assert_eq!(normalized, vec![0.0, 0.5, 1.0]);
1628    }
1629
1630    #[test]
1631    fn test_normalizer_nice_bounds() {
1632        let norm = DataNormalizer::new(3.2, 97.8);
1633        let (nice_min, nice_max) = norm.nice_bounds();
1634
1635        assert!(nice_min <= 3.2);
1636        assert!(nice_max >= 97.8);
1637        // Should be round numbers
1638        assert!((nice_min * 10.0).round() == nice_min * 10.0);
1639    }
1640
1641    // =========================================================================
1642    // PathTessellator Tests
1643    // =========================================================================
1644
1645    #[test]
1646    fn test_tessellator_new() {
1647        let tess = PathTessellator::new(0.5);
1648        assert!((tess.tolerance - 0.5).abs() < 1e-10);
1649        assert!(tess.vertices.is_empty());
1650        assert!(tess.indices.is_empty());
1651    }
1652
1653    #[test]
1654    fn test_tessellator_default() {
1655        let tess = PathTessellator::with_default_tolerance();
1656        assert!((tess.tolerance - 0.25).abs() < 1e-10);
1657    }
1658
1659    #[test]
1660    fn test_tessellator_polygon() {
1661        let mut tess = PathTessellator::new(0.5);
1662        let triangle = vec![
1663            Point2D::new(0.0, 0.0),
1664            Point2D::new(1.0, 0.0),
1665            Point2D::new(0.5, 1.0),
1666        ];
1667
1668        tess.tessellate_polygon(&triangle);
1669
1670        assert_eq!(tess.vertex_count(), 3);
1671        assert_eq!(tess.index_count(), 3);
1672        assert_eq!(tess.triangle_count(), 1);
1673    }
1674
1675    #[test]
1676    fn test_tessellator_quad() {
1677        let mut tess = PathTessellator::new(0.5);
1678        let quad = vec![
1679            Point2D::new(0.0, 0.0),
1680            Point2D::new(1.0, 0.0),
1681            Point2D::new(1.0, 1.0),
1682            Point2D::new(0.0, 1.0),
1683        ];
1684
1685        tess.tessellate_polygon(&quad);
1686
1687        assert_eq!(tess.vertex_count(), 4);
1688        assert_eq!(tess.triangle_count(), 2);
1689    }
1690
1691    #[test]
1692    fn test_tessellator_stroke() {
1693        let mut tess = PathTessellator::new(0.5);
1694        let line = vec![Point2D::new(0.0, 0.0), Point2D::new(10.0, 0.0)];
1695
1696        tess.tessellate_stroke(&line, 2.0);
1697
1698        // One segment produces a quad (4 vertices, 2 triangles)
1699        assert_eq!(tess.vertex_count(), 4);
1700        assert_eq!(tess.triangle_count(), 2);
1701    }
1702
1703    #[test]
1704    fn test_tessellator_multi_segment_stroke() {
1705        let mut tess = PathTessellator::new(0.5);
1706        let path = vec![
1707            Point2D::new(0.0, 0.0),
1708            Point2D::new(10.0, 0.0),
1709            Point2D::new(10.0, 10.0),
1710        ];
1711
1712        tess.tessellate_stroke(&path, 1.0);
1713
1714        // Two segments, each produces a quad
1715        assert_eq!(tess.vertex_count(), 8);
1716        assert_eq!(tess.triangle_count(), 4);
1717    }
1718
1719    #[test]
1720    fn test_tessellator_circle() {
1721        let mut tess = PathTessellator::new(0.5);
1722        tess.tessellate_circle(Point2D::ORIGIN, 1.0, 16);
1723
1724        // 16 segments: 1 center + 16 perimeter = 17 vertices
1725        assert_eq!(tess.vertex_count(), 17);
1726        // 16 triangles
1727        assert_eq!(tess.triangle_count(), 16);
1728    }
1729
1730    #[test]
1731    fn test_tessellator_rect() {
1732        let mut tess = PathTessellator::new(0.5);
1733        tess.tessellate_rect(0.0, 0.0, 10.0, 5.0);
1734
1735        assert_eq!(tess.vertex_count(), 4);
1736        assert_eq!(tess.triangle_count(), 2);
1737    }
1738
1739    #[test]
1740    fn test_tessellator_clear() {
1741        let mut tess = PathTessellator::new(0.5);
1742        tess.tessellate_rect(0.0, 0.0, 10.0, 5.0);
1743        assert!(!tess.vertices.is_empty());
1744
1745        tess.clear();
1746        assert!(tess.vertices.is_empty());
1747        assert!(tess.indices.is_empty());
1748    }
1749
1750    #[test]
1751    fn test_tessellator_multiple_shapes() {
1752        let mut tess = PathTessellator::new(0.5);
1753
1754        tess.tessellate_rect(0.0, 0.0, 10.0, 10.0);
1755        tess.tessellate_circle(Point2D::new(20.0, 5.0), 3.0, 8);
1756
1757        assert_eq!(tess.vertex_count(), 4 + 9); // rect + circle (center + 8)
1758        assert_eq!(tess.triangle_count(), 2 + 8);
1759    }
1760
1761    // =========================================================================
1762    // DrawBatch Tests
1763    // =========================================================================
1764
1765    #[test]
1766    fn test_batch_new() {
1767        let batch = DrawBatch::new();
1768        assert!(batch.circles.is_empty());
1769        assert!(batch.rects.is_empty());
1770        assert!(batch.lines.is_empty());
1771    }
1772
1773    #[test]
1774    fn test_batch_add_circle() {
1775        let mut batch = DrawBatch::new();
1776        batch.add_circle(10.0, 20.0, 5.0, 1.0, 0.0, 0.0, 1.0);
1777
1778        assert_eq!(batch.circles.len(), 1);
1779        assert_eq!(batch.circles[0][0], 10.0);
1780        assert_eq!(batch.circles[0][1], 20.0);
1781        assert_eq!(batch.circles[0][2], 5.0);
1782    }
1783
1784    #[test]
1785    fn test_batch_add_rect() {
1786        let mut batch = DrawBatch::new();
1787        batch.add_rect(0.0, 0.0, 100.0, 50.0, 0.0, 1.0, 0.0, 1.0);
1788
1789        assert_eq!(batch.rects.len(), 1);
1790        assert_eq!(batch.rects[0][2], 100.0);
1791        assert_eq!(batch.rects[0][3], 50.0);
1792    }
1793
1794    #[test]
1795    fn test_batch_add_line() {
1796        let mut batch = DrawBatch::new();
1797        batch.add_line(0.0, 0.0, 100.0, 100.0, 2.0, 0.0, 0.0, 1.0, 1.0);
1798
1799        assert_eq!(batch.lines.len(), 1);
1800        assert_eq!(batch.lines[0][4], 2.0); // width
1801    }
1802
1803    #[test]
1804    fn test_batch_clear() {
1805        let mut batch = DrawBatch::new();
1806        batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
1807        batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
1808        batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
1809
1810        batch.clear();
1811
1812        assert!(batch.circles.is_empty());
1813        assert!(batch.rects.is_empty());
1814        assert!(batch.lines.is_empty());
1815    }
1816
1817    #[test]
1818    fn test_batch_draw_call_counts() {
1819        let mut batch = DrawBatch::new();
1820
1821        // Empty batch
1822        assert_eq!(batch.unbatched_draw_calls(), 0);
1823        assert_eq!(batch.batched_draw_calls(), 0);
1824
1825        // Add shapes
1826        for i in 0..100 {
1827            batch.add_circle(i as f32, 0.0, 5.0, 1.0, 0.0, 0.0, 1.0);
1828        }
1829        for i in 0..50 {
1830            batch.add_rect(i as f32 * 10.0, 0.0, 10.0, 10.0, 0.0, 1.0, 0.0, 1.0);
1831        }
1832
1833        // Without batching: 150 draw calls
1834        assert_eq!(batch.unbatched_draw_calls(), 150);
1835        // With batching: 2 draw calls (circles, rects)
1836        assert_eq!(batch.batched_draw_calls(), 2);
1837    }
1838
1839    #[test]
1840    fn test_batch_efficiency() {
1841        let mut batch = DrawBatch::new();
1842
1843        // Simulate chart rendering: 1000 points, 10 bars
1844        for i in 0..1000 {
1845            batch.add_circle(i as f32, (i as f32).sin() * 100.0, 3.0, 0.2, 0.5, 0.9, 1.0);
1846        }
1847        for i in 0..10 {
1848            batch.add_rect(
1849                i as f32 * 50.0,
1850                0.0,
1851                40.0,
1852                (i as f32 + 1.0) * 20.0,
1853                0.9,
1854                0.3,
1855                0.3,
1856                1.0,
1857            );
1858        }
1859
1860        // 1010 shapes batched into 2 draw calls = 505x reduction
1861        let reduction = batch.unbatched_draw_calls() as f64 / batch.batched_draw_calls() as f64;
1862        assert!(reduction > 500.0);
1863    }
1864
1865    // =========================================================================
1866    // Additional Point2D Tests
1867    // =========================================================================
1868
1869    #[test]
1870    fn test_point2d_default() {
1871        let p: Point2D = Default::default();
1872        assert_eq!(p, Point2D::ORIGIN);
1873    }
1874
1875    #[test]
1876    fn test_point2d_clone() {
1877        let p1 = Point2D::new(3.14, 2.71);
1878        let p2 = p1;
1879        assert_eq!(p1, p2);
1880    }
1881
1882    #[test]
1883    fn test_point2d_debug() {
1884        let p = Point2D::new(1.0, 2.0);
1885        let debug = format!("{p:?}");
1886        assert!(debug.contains("Point2D"));
1887    }
1888
1889    #[test]
1890    fn test_point2d_distance_to_self() {
1891        let p = Point2D::new(5.0, 10.0);
1892        assert_eq!(p.distance(&p), 0.0);
1893    }
1894
1895    #[test]
1896    fn test_point2d_lerp_boundaries() {
1897        let p1 = Point2D::new(0.0, 0.0);
1898        let p2 = Point2D::new(10.0, 10.0);
1899
1900        let at_start = p1.lerp(&p2, 0.0);
1901        assert_eq!(at_start, p1);
1902
1903        let at_end = p1.lerp(&p2, 1.0);
1904        assert_eq!(at_end, p2);
1905    }
1906
1907    #[test]
1908    fn test_point2d_lerp_extrapolate() {
1909        let p1 = Point2D::new(0.0, 0.0);
1910        let p2 = Point2D::new(10.0, 10.0);
1911
1912        let beyond = p1.lerp(&p2, 2.0);
1913        assert!((beyond.x - 20.0).abs() < 1e-10);
1914        assert!((beyond.y - 20.0).abs() < 1e-10);
1915    }
1916
1917    #[test]
1918    fn test_point2d_mul_zero() {
1919        let p = Point2D::new(5.0, 10.0);
1920        let scaled = p * 0.0;
1921        assert_eq!(scaled, Point2D::ORIGIN);
1922    }
1923
1924    #[test]
1925    fn test_point2d_mul_negative() {
1926        let p = Point2D::new(5.0, 10.0);
1927        let scaled = p * -1.0;
1928        assert_eq!(scaled, Point2D::new(-5.0, -10.0));
1929    }
1930
1931    // =========================================================================
1932    // Additional LinearInterpolator Tests
1933    // =========================================================================
1934
1935    #[test]
1936    fn test_linear_extrapolate_left() {
1937        let interp =
1938            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
1939        // Extrapolate before first point
1940        let y = interp.interpolate(-5.0);
1941        assert!((y - (-5.0)).abs() < 1e-10);
1942    }
1943
1944    #[test]
1945    fn test_linear_extrapolate_right() {
1946        let interp =
1947            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
1948        // Extrapolate after last point
1949        let y = interp.interpolate(15.0);
1950        assert!((y - 15.0).abs() < 1e-10);
1951    }
1952
1953    #[test]
1954    fn test_linear_unsorted_input() {
1955        let interp = LinearInterpolator::from_points(&[
1956            Point2D::new(3.0, 30.0),
1957            Point2D::new(1.0, 10.0),
1958            Point2D::new(2.0, 20.0),
1959        ]);
1960        // Should sort and interpolate correctly
1961        assert!((interp.interpolate(1.5) - 15.0).abs() < 1e-10);
1962    }
1963
1964    #[test]
1965    fn test_linear_points_getter() {
1966        let interp =
1967            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)]);
1968        assert_eq!(interp.points().len(), 2);
1969    }
1970
1971    #[test]
1972    fn test_linear_sample_single_point() {
1973        let interp = LinearInterpolator::from_points(&[Point2D::new(0.0, 5.0)]);
1974        let samples = interp.sample(0.0, 10.0, 5);
1975        // Single point always returns that y
1976        for s in &samples {
1977            assert_eq!(s.y, 5.0);
1978        }
1979    }
1980
1981    #[test]
1982    fn test_linear_sample_too_few() {
1983        let interp =
1984            LinearInterpolator::from_points(&[Point2D::new(0.0, 0.0), Point2D::new(10.0, 10.0)]);
1985        let samples = interp.sample(0.0, 10.0, 1);
1986        assert!(samples.is_empty());
1987    }
1988
1989    #[test]
1990    fn test_linear_vertical_segment() {
1991        let interp = LinearInterpolator::from_points(&[
1992            Point2D::new(5.0, 0.0),
1993            Point2D::new(5.0, 10.0), // Same x (vertical)
1994            Point2D::new(10.0, 10.0),
1995        ]);
1996        // Should handle gracefully
1997        let y = interp.interpolate(5.0);
1998        assert!(y.is_finite());
1999    }
2000
2001    // =========================================================================
2002    // Additional CubicSpline Tests
2003    // =========================================================================
2004
2005    #[test]
2006    fn test_spline_from_xy() {
2007        let xs = [0.0, 1.0, 2.0, 3.0];
2008        let ys = [0.0, 1.0, 0.0, 1.0];
2009        let spline = CubicSpline::from_xy(&xs, &ys);
2010        assert_eq!(spline.points().len(), 4);
2011    }
2012
2013    #[test]
2014    fn test_spline_points_getter() {
2015        let points = vec![
2016            Point2D::new(0.0, 0.0),
2017            Point2D::new(1.0, 1.0),
2018            Point2D::new(2.0, 0.0),
2019        ];
2020        let spline = CubicSpline::from_points(&points);
2021        assert_eq!(spline.points().len(), 3);
2022    }
2023
2024    #[test]
2025    fn test_spline_identical_x() {
2026        // Two points with same x (degenerate case)
2027        let points = vec![
2028            Point2D::new(1.0, 0.0),
2029            Point2D::new(1.0, 10.0),
2030            Point2D::new(2.0, 5.0),
2031        ];
2032        let spline = CubicSpline::from_points(&points);
2033        let y = spline.interpolate(1.0);
2034        assert!(y.is_finite());
2035    }
2036
2037    #[test]
2038    fn test_spline_extrapolate() {
2039        let points = vec![
2040            Point2D::new(0.0, 0.0),
2041            Point2D::new(1.0, 1.0),
2042            Point2D::new(2.0, 0.0),
2043        ];
2044        let spline = CubicSpline::from_points(&points);
2045
2046        // Extrapolate beyond range
2047        let y_before = spline.interpolate(-1.0);
2048        let y_after = spline.interpolate(3.0);
2049        assert!(y_before.is_finite());
2050        assert!(y_after.is_finite());
2051    }
2052
2053    // =========================================================================
2054    // Additional CatmullRom Tests
2055    // =========================================================================
2056
2057    #[test]
2058    fn test_catmull_rom_points_getter() {
2059        let points = vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)];
2060        let cr = CatmullRom::from_points(&points);
2061        assert_eq!(cr.points().len(), 2);
2062    }
2063
2064    #[test]
2065    fn test_catmull_rom_to_path_two_points() {
2066        let points = vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0)];
2067        let cr = CatmullRom::from_points(&points);
2068        let path = cr.to_path(10);
2069        assert_eq!(path.len(), 2); // Just returns points for 2-point input
2070    }
2071
2072    #[test]
2073    fn test_catmull_rom_to_path_single() {
2074        let points = vec![Point2D::new(0.0, 0.0)];
2075        let cr = CatmullRom::from_points(&points);
2076        let path = cr.to_path(10);
2077        assert_eq!(path.len(), 1);
2078    }
2079
2080    #[test]
2081    fn test_catmull_rom_tension_clamp() {
2082        let points = vec![
2083            Point2D::new(0.0, 0.0),
2084            Point2D::new(1.0, 1.0),
2085            Point2D::new(2.0, 0.0),
2086        ];
2087
2088        // Tension should be clamped to [0, 1]
2089        let cr_low = CatmullRom::with_tension(&points, -1.0);
2090        let cr_high = CatmullRom::with_tension(&points, 2.0);
2091
2092        // Both should work without panic
2093        let _ = cr_low.interpolate(0.5);
2094        let _ = cr_high.interpolate(0.5);
2095    }
2096
2097    // =========================================================================
2098    // Additional CubicBezier Tests
2099    // =========================================================================
2100
2101    #[test]
2102    fn test_bezier_clamp_t() {
2103        let bezier = CubicBezier::new(
2104            Point2D::new(0.0, 0.0),
2105            Point2D::new(1.0, 2.0),
2106            Point2D::new(2.0, 2.0),
2107            Point2D::new(3.0, 0.0),
2108        );
2109
2110        // t is clamped to [0, 1]
2111        let p_neg = bezier.evaluate(-0.5);
2112        let p_over = bezier.evaluate(1.5);
2113
2114        assert_eq!(p_neg, bezier.evaluate(0.0));
2115        assert_eq!(p_over, bezier.evaluate(1.0));
2116    }
2117
2118    #[test]
2119    fn test_bezier_polyline_min_segments() {
2120        let bezier = CubicBezier::new(
2121            Point2D::new(0.0, 0.0),
2122            Point2D::new(1.0, 1.0),
2123            Point2D::new(2.0, 1.0),
2124            Point2D::new(3.0, 0.0),
2125        );
2126
2127        let polyline = bezier.to_polyline(0);
2128        assert!(polyline.len() >= 2);
2129    }
2130
2131    #[test]
2132    fn test_bezier_split_at_zero() {
2133        let bezier = CubicBezier::new(
2134            Point2D::new(0.0, 0.0),
2135            Point2D::new(1.0, 1.0),
2136            Point2D::new(2.0, 1.0),
2137            Point2D::new(3.0, 0.0),
2138        );
2139
2140        let (left, right) = bezier.split(0.0);
2141        assert_eq!(left.p0, bezier.p0);
2142        assert_eq!(right.p3, bezier.p3);
2143    }
2144
2145    #[test]
2146    fn test_bezier_split_at_one() {
2147        let bezier = CubicBezier::new(
2148            Point2D::new(0.0, 0.0),
2149            Point2D::new(1.0, 1.0),
2150            Point2D::new(2.0, 1.0),
2151            Point2D::new(3.0, 0.0),
2152        );
2153
2154        let (left, _right) = bezier.split(1.0);
2155        assert_eq!(left.p3, bezier.p3);
2156    }
2157
2158    #[test]
2159    fn test_bezier_arc_length_zero_segments() {
2160        let bezier = CubicBezier::new(
2161            Point2D::new(0.0, 0.0),
2162            Point2D::new(1.0, 0.0),
2163            Point2D::new(2.0, 0.0),
2164            Point2D::new(3.0, 0.0),
2165        );
2166
2167        // Should handle 0 segments gracefully
2168        let length = bezier.arc_length(0);
2169        assert!(length.is_finite());
2170    }
2171
2172    // =========================================================================
2173    // Additional HistogramBins Tests
2174    // =========================================================================
2175
2176    #[test]
2177    fn test_histogram_zero_bins() {
2178        let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 0);
2179        assert_eq!(hist.num_bins(), 0);
2180    }
2181
2182    #[test]
2183    fn test_histogram_all_same_value() {
2184        let data = vec![5.0, 5.0, 5.0, 5.0, 5.0];
2185        let hist = HistogramBins::from_data(&data, 5);
2186        assert_eq!(hist.total_count(), 5);
2187    }
2188
2189    #[test]
2190    fn test_histogram_negative_values() {
2191        let data = vec![-10.0, -5.0, 0.0, 5.0, 10.0];
2192        let hist = HistogramBins::from_data(&data, 4);
2193        assert_eq!(hist.num_bins(), 4);
2194        assert_eq!(hist.total_count(), 5);
2195    }
2196
2197    #[test]
2198    fn test_histogram_bin_range_out_of_bounds() {
2199        let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 3);
2200        assert_eq!(hist.bin_range(100), None);
2201    }
2202
2203    #[test]
2204    fn test_histogram_bin_center_out_of_bounds() {
2205        let hist = HistogramBins::from_data(&[1.0, 2.0, 3.0], 3);
2206        assert_eq!(hist.bin_center(100), None);
2207    }
2208
2209    #[test]
2210    fn test_histogram_edge_case_max_value() {
2211        // Value exactly at max should go in last bin
2212        let hist = HistogramBins::from_data_range(&[0.0, 5.0, 10.0], 2, 0.0, 10.0);
2213        assert_eq!(hist.total_count(), 3);
2214    }
2215
2216    // =========================================================================
2217    // Additional ArcGeometry Tests
2218    // =========================================================================
2219
2220    #[test]
2221    fn test_arc_negative_angles() {
2222        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, -PI / 2.0, PI / 2.0);
2223        assert!((arc.sweep() - PI).abs() < 1e-10);
2224    }
2225
2226    #[test]
2227    fn test_arc_large_angles() {
2228        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, 4.0 * PI);
2229        // Large sweep should still work
2230        let poly = arc.to_polyline(10);
2231        assert_eq!(poly.len(), 11);
2232    }
2233
2234    #[test]
2235    fn test_arc_zero_radius() {
2236        let arc = ArcGeometry::new(Point2D::new(5.0, 5.0), 0.0, 0.0, PI);
2237        let start = arc.start_point();
2238        assert_eq!(start, arc.center);
2239    }
2240
2241    #[test]
2242    fn test_arc_contains_angle_wrap() {
2243        // Arc that wraps around 0/2π
2244        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 3.0 * PI / 2.0, PI / 2.0 + 2.0 * PI);
2245        assert!(arc.contains_angle(0.0));
2246    }
2247
2248    #[test]
2249    fn test_arc_pie_slice_segments() {
2250        let arc = ArcGeometry::new(Point2D::ORIGIN, 1.0, 0.0, PI / 2.0);
2251        let slice = arc.to_pie_slice(8);
2252        // 1 center + 9 arc points + 1 center closing = 11
2253        assert_eq!(slice.len(), 11);
2254    }
2255
2256    // =========================================================================
2257    // Additional DataNormalizer Tests
2258    // =========================================================================
2259
2260    #[test]
2261    fn test_normalizer_zero_range() {
2262        let norm = DataNormalizer::new(5.0, 5.0);
2263        // Zero range should return 0.5
2264        assert_eq!(norm.normalize(5.0), 0.5);
2265        assert_eq!(norm.normalize(10.0), 0.5);
2266    }
2267
2268    #[test]
2269    fn test_normalizer_negative_range() {
2270        let norm = DataNormalizer::new(-100.0, -50.0);
2271        assert!((norm.normalize(-100.0) - 0.0).abs() < 1e-10);
2272        assert!((norm.normalize(-75.0) - 0.5).abs() < 1e-10);
2273        assert!((norm.normalize(-50.0) - 1.0).abs() < 1e-10);
2274    }
2275
2276    #[test]
2277    fn test_normalizer_nice_bounds_small_range() {
2278        let norm = DataNormalizer::new(0.001, 0.002);
2279        let (nice_min, nice_max) = norm.nice_bounds();
2280        assert!(nice_min <= 0.001);
2281        assert!(nice_max >= 0.002);
2282    }
2283
2284    #[test]
2285    fn test_normalizer_nice_bounds_large_range() {
2286        let norm = DataNormalizer::new(0.0, 1_000_000.0);
2287        let (nice_min, nice_max) = norm.nice_bounds();
2288        assert!(nice_min <= 0.0);
2289        assert!(nice_max >= 1_000_000.0);
2290    }
2291
2292    #[test]
2293    fn test_normalizer_from_single_value() {
2294        let norm = DataNormalizer::from_data(&[42.0]);
2295        // Single value: min == max
2296        assert_eq!(norm.min, 42.0);
2297        assert_eq!(norm.max, 42.0);
2298    }
2299
2300    // =========================================================================
2301    // Additional PathTessellator Tests
2302    // =========================================================================
2303
2304    #[test]
2305    fn test_tessellator_tolerance_minimum() {
2306        let tess = PathTessellator::new(0.0001);
2307        // Should clamp to minimum tolerance
2308        assert!(tess.tolerance >= 0.001);
2309    }
2310
2311    #[test]
2312    fn test_tessellator_polygon_too_small() {
2313        let mut tess = PathTessellator::new(0.5);
2314        tess.tessellate_polygon(&[Point2D::new(0.0, 0.0)]);
2315        assert!(tess.vertices.is_empty());
2316
2317        tess.tessellate_polygon(&[Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0)]);
2318        assert!(tess.vertices.is_empty());
2319    }
2320
2321    #[test]
2322    fn test_tessellator_stroke_too_short() {
2323        let mut tess = PathTessellator::new(0.5);
2324        tess.tessellate_stroke(&[Point2D::new(0.0, 0.0)], 1.0);
2325        assert!(tess.vertices.is_empty());
2326    }
2327
2328    #[test]
2329    fn test_tessellator_stroke_zero_length_segment() {
2330        let mut tess = PathTessellator::new(0.5);
2331        // Two identical points (zero-length segment)
2332        tess.tessellate_stroke(&[Point2D::new(5.0, 5.0), Point2D::new(5.0, 5.0)], 1.0);
2333        // Should handle gracefully
2334        assert!(tess.vertices.is_empty());
2335    }
2336
2337    #[test]
2338    fn test_tessellator_circle_min_segments() {
2339        let mut tess = PathTessellator::new(0.5);
2340        tess.tessellate_circle(Point2D::ORIGIN, 1.0, 3);
2341        // Should enforce minimum 8 segments
2342        assert!(tess.vertex_count() >= 9); // 1 center + at least 8 perimeter
2343    }
2344
2345    #[test]
2346    fn test_tessellator_default_trait() {
2347        let tess: PathTessellator = Default::default();
2348        assert!(tess.vertices.is_empty());
2349    }
2350
2351    // =========================================================================
2352    // Additional DrawBatch Tests
2353    // =========================================================================
2354
2355    #[test]
2356    fn test_batch_default_trait() {
2357        let batch: DrawBatch = Default::default();
2358        assert!(batch.circles.is_empty());
2359        assert!(batch.rects.is_empty());
2360        assert!(batch.lines.is_empty());
2361    }
2362
2363    #[test]
2364    fn test_batch_only_circles() {
2365        let mut batch = DrawBatch::new();
2366        batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2367        assert_eq!(batch.batched_draw_calls(), 1);
2368    }
2369
2370    #[test]
2371    fn test_batch_only_rects() {
2372        let mut batch = DrawBatch::new();
2373        batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2374        assert_eq!(batch.batched_draw_calls(), 1);
2375    }
2376
2377    #[test]
2378    fn test_batch_only_lines() {
2379        let mut batch = DrawBatch::new();
2380        batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2381        assert_eq!(batch.batched_draw_calls(), 1);
2382    }
2383
2384    #[test]
2385    fn test_batch_all_types() {
2386        let mut batch = DrawBatch::new();
2387        batch.add_circle(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2388        batch.add_rect(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2389        batch.add_line(0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
2390        assert_eq!(batch.batched_draw_calls(), 3);
2391        assert_eq!(batch.unbatched_draw_calls(), 3);
2392    }
2393
2394    #[test]
2395    fn test_batch_debug() {
2396        let batch = DrawBatch::new();
2397        let debug = format!("{batch:?}");
2398        assert!(debug.contains("DrawBatch"));
2399    }
2400
2401    #[test]
2402    fn test_batch_clone() {
2403        let mut batch = DrawBatch::new();
2404        batch.add_circle(1.0, 2.0, 3.0, 1.0, 0.0, 0.0, 1.0);
2405        let cloned = batch.clone();
2406        assert_eq!(cloned.circles.len(), 1);
2407        assert_eq!(cloned.circles[0][0], 1.0);
2408    }
2409}