Skip to main content

ggplot_rs/coord/
radar.rs

1//! Radar (spider) coordinates: polar with *straight* segments between spokes.
2
3use crate::aes::Aesthetic;
4use crate::data::Value;
5use crate::render::Rect;
6use crate::scale::ScaleSet;
7
8use super::Coord;
9
10/// Radar / spider-chart coordinates (`coord_radar()`).
11///
12/// `x` (usually discrete — one spoke per level) maps to angle, with the first
13/// level at 12 o'clock and levels running clockwise; `y` maps to the distance
14/// from the centre, which is the y scale's value `0` (the y scale is trained
15/// to include 0). Geoms connect points with straight segments: `geom_polygon`
16/// draws closed, filled series polygons and `geom_line` closes its path.
17/// Gridlines become rings (polygons through the spokes at the y breaks) and
18/// spokes, with the x levels labelled around the outside.
19pub struct CoordRadar {
20    /// Angle of the first spoke (radians, 0 = 12 o'clock, clockwise).
21    pub start: f64,
22    /// Outer radius as a fraction of half the panel's smaller side; the rest
23    /// is left for the spoke labels.
24    pub radius_frac: f64,
25    /// Number of discrete x levels (spokes), learned from the x scale.
26    n_spokes: Option<usize>,
27    /// Normalized y position of the centre (the y scale's `0`).
28    r0: f64,
29}
30
31impl CoordRadar {
32    pub fn new() -> Self {
33        CoordRadar {
34            start: 0.0,
35            radius_frac: 0.78,
36            n_spokes: None,
37            r0: 0.0,
38        }
39    }
40
41    /// Rotate the first spoke to `start` radians (0 = 12 o'clock).
42    pub fn start(mut self, start: f64) -> Self {
43        self.start = start;
44        self
45    }
46
47    /// Outer radius as a fraction of half the panel's smaller side.
48    pub fn radius_frac(mut self, frac: f64) -> Self {
49        self.radius_frac = frac.clamp(0.1, 1.0);
50        self
51    }
52
53    /// Spoke positions: the discrete x level centres sit at `(i + 0.5) / n`;
54    /// shift so level 0 points at `start`.
55    fn angle(&self, nx: f64) -> f64 {
56        let offset = match self.n_spokes {
57            Some(n) if n > 0 => -std::f64::consts::PI / n as f64,
58            _ => 0.0,
59        };
60        self.start + offset + nx * std::f64::consts::TAU
61    }
62}
63
64impl Default for CoordRadar {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl Coord for CoordRadar {
71    fn transform(&self, (nx, ny): (f64, f64), plot_area: &Rect) -> (f64, f64) {
72        let max_r = plot_area.width.min(plot_area.height) / 2.0 * self.radius_frac;
73        let span = (1.0 - self.r0).max(1e-9);
74        let frac = ((ny - self.r0) / span).max(0.0);
75        let r = if frac.is_finite() { frac * max_r } else { 0.0 };
76        let a = self.angle(nx);
77        let cx = plot_area.x + plot_area.width / 2.0;
78        let cy = plot_area.y + plot_area.height / 2.0;
79        (cx + r * a.sin(), cy - r * a.cos())
80    }
81
82    fn gridlines(&self) -> bool {
83        false
84    }
85
86    fn is_polar(&self) -> bool {
87        true
88    }
89
90    fn is_radar(&self) -> bool {
91        true
92    }
93
94    fn train_scales(&mut self, scales: &mut ScaleSet) {
95        if let Some(y) = scales.get_mut(&Aesthetic::Y) {
96            if !y.is_discrete() {
97                y.train(&[Value::Float(0.0)]);
98                self.r0 = y.map(&Value::Float(0.0));
99            }
100        }
101        self.n_spokes = scales
102            .get(&Aesthetic::X)
103            .filter(|s| s.is_discrete())
104            .map(|s| s.breaks().len());
105    }
106}