Skip to main content

ggplot_rs/coord/
polar.rs

1use crate::render::Rect;
2
3use super::Coord;
4
5/// Polar coordinate system.
6///
7/// Maps one aesthetic to angle and the other to radius.
8/// - `theta = "x"` (default): x maps to angle, y to radius (pie charts, wind roses)
9/// - `theta = "y"`: y maps to angle, x to radius (Coxcomb charts)
10pub struct CoordPolar {
11    /// Which variable maps to angle: "x" or "y".
12    pub theta: String,
13    /// Start angle in radians (0 = 12 o'clock position).
14    pub start: f64,
15    /// Direction: 1 = clockwise, -1 = counterclockwise.
16    pub direction: f64,
17    /// Inner radius as a fraction of the outer radius (0 = pie, 0.5 = donut).
18    pub inner_radius: f64,
19}
20
21impl CoordPolar {
22    pub fn new() -> Self {
23        CoordPolar {
24            theta: "x".to_string(),
25            start: 0.0,
26            direction: 1.0,
27            inner_radius: 0.0,
28        }
29    }
30
31    /// Set the inner radius (fraction of outer, `0.0`..`1.0`) to punch a donut hole.
32    pub fn inner_radius(mut self, frac: f64) -> Self {
33        self.inner_radius = frac.clamp(0.0, 0.95);
34        self
35    }
36
37    pub fn theta(mut self, theta: &str) -> Self {
38        self.theta = theta.to_string();
39        self
40    }
41
42    pub fn start(mut self, start: f64) -> Self {
43        self.start = start;
44        self
45    }
46
47    pub fn direction(mut self, dir: f64) -> Self {
48        self.direction = dir;
49        self
50    }
51}
52
53impl Default for CoordPolar {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl Coord for CoordPolar {
60    fn transform(&self, point: (f64, f64), plot_area: &Rect) -> (f64, f64) {
61        let (nx, ny) = point;
62
63        // Determine which normalized value maps to angle vs radius
64        let (angle_norm, radius_norm) = if self.theta == "x" {
65            (nx, ny)
66        } else {
67            (ny, nx)
68        };
69
70        // Convert to angle (full circle = 2π)
71        let angle = self.start + self.direction * angle_norm * std::f64::consts::TAU;
72
73        // Radius: fraction of the available radius (half the smaller dimension)
74        let max_radius = plot_area.width.min(plot_area.height) / 2.0;
75        // Map [0,1] into [inner_radius, 1] so a donut leaves a centre hole.
76        let radius = (self.inner_radius + radius_norm * (1.0 - self.inner_radius)) * max_radius;
77
78        // Center of the polar plot
79        let cx = plot_area.x + plot_area.width / 2.0;
80        let cy = plot_area.y + plot_area.height / 2.0;
81
82        // Convert polar to Cartesian pixel coordinates
83        // angle=0 points up (12 o'clock), increases clockwise
84        let px = cx + radius * angle.sin();
85        let py = cy - radius * angle.cos();
86
87        (px, py)
88    }
89
90    fn gridlines(&self) -> bool {
91        false
92    }
93
94    fn is_flipped(&self) -> bool {
95        false
96    }
97
98    fn is_polar(&self) -> bool {
99        true
100    }
101
102    fn polar_theta_is_x(&self) -> bool {
103        self.theta == "x"
104    }
105}