Skip to main content

chartml_core/shapes/
area.rs

1/// Generates SVG path `d` strings for area charts.
2/// Equivalent to D3's `d3.area()`.
3use super::line::{CurveType, LineGenerator};
4
5/// Generates SVG path `d` strings for area charts.
6pub struct AreaGenerator {
7    curve: CurveType,
8}
9
10impl AreaGenerator {
11    /// Create a new AreaGenerator with Linear curve type.
12    pub fn new() -> Self {
13        Self {
14            curve: CurveType::Linear,
15        }
16    }
17
18    /// Set the curve interpolation type.
19    pub fn curve(mut self, curve: CurveType) -> Self {
20        self.curve = curve;
21        self
22    }
23
24    /// Generate an SVG path for just the top edge of the area (the stroke line).
25    ///
26    /// This traces only the (x, y1) points — no baseline return, no close.
27    /// Used to render a visible stroke line on top of the filled area.
28    pub fn generate_line(&self, points: &[(f64, f64, f64)]) -> String {
29        if points.is_empty() {
30            return String::new();
31        }
32        let top_points: Vec<(f64, f64)> = points.iter().map(|&(x, _y0, y1)| (x, y1)).collect();
33        let line_gen = LineGenerator::new().curve(self.curve);
34        line_gen.generate(&top_points)
35    }
36
37    /// Generate an SVG path from a series of (x, y0, y1) points.
38    ///
39    /// The area is formed by:
40    /// 1. Forward path along (x, y1) -- the top line
41    /// 2. Reverse path along (x, y0) -- the bottom line (reversed)
42    /// 3. Close with "Z"
43    ///
44    /// For non-stacked areas, y0 is typically the baseline (e.g., the bottom of the chart).
45    pub fn generate(&self, points: &[(f64, f64, f64)]) -> String {
46        if points.is_empty() {
47            return String::new();
48        }
49
50        // Top line: (x, y1)
51        let top_points: Vec<(f64, f64)> = points.iter().map(|&(x, _y0, y1)| (x, y1)).collect();
52        // Bottom line reversed: (x, y0)
53        let bottom_points: Vec<(f64, f64)> = points.iter().rev().map(|&(x, y0, _y1)| (x, y0)).collect();
54
55        let line_gen = LineGenerator::new().curve(self.curve);
56        let top_path = line_gen.generate(&top_points);
57        let bottom_path = line_gen.generate(&bottom_points);
58
59        // The bottom path starts with "M..." but we need "L..." to connect from the top path.
60        let bottom_continuation = if bottom_path.len() > 1 {
61            format!("L{}", &bottom_path[1..])
62        } else {
63            String::new()
64        };
65
66        format!("{}{}Z", top_path, bottom_continuation)
67    }
68}
69
70impl Default for AreaGenerator {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn area_basic() {
82        let gen = AreaGenerator::new();
83        let path = gen.generate(&[
84            (0.0, 100.0, 10.0),
85            (50.0, 100.0, 20.0),
86            (100.0, 100.0, 5.0),
87        ]);
88        assert!(path.starts_with("M"), "Path should start with M, got: {}", path);
89        assert!(path.contains("L"), "Path should contain L commands, got: {}", path);
90        assert!(path.ends_with("Z"), "Path should end with Z, got: {}", path);
91    }
92
93    #[test]
94    fn area_empty() {
95        let gen = AreaGenerator::new();
96        let path = gen.generate(&[]);
97        assert_eq!(path, "");
98    }
99}