Skip to main content

ggplot_rs/coord/
cartesian.rs

1use crate::render::Rect;
2
3use super::Coord;
4
5/// Standard Cartesian coordinate system, with optional zoom limits.
6///
7/// Unlike `xlim()`/`ylim()` on scales, zoom limits do NOT filter data —
8/// they only clip the viewport. Data outside the limits is still computed
9/// by stats and positions, just not visible.
10pub struct CoordCartesian {
11    xlim: Option<(f64, f64)>,
12    ylim: Option<(f64, f64)>,
13    reverse_x: bool,
14    reverse_y: bool,
15    /// Keep the scales' expansion on each side: top, right, bottom, left
16    /// (ggplot2's `coord_cartesian(expand =)`, #411).
17    expand: [bool; 4],
18}
19
20impl CoordCartesian {
21    pub fn new() -> Self {
22        CoordCartesian {
23            xlim: None,
24            ylim: None,
25            reverse_x: false,
26            reverse_y: false,
27            expand: [true; 4],
28        }
29    }
30
31    /// ggplot2's `coord_cartesian(expand = c(top, right, bottom, left))`
32    /// (#411): `false` on a side drops the continuous scale's padding there,
33    /// so the data (or the zoom limit) meets that panel edge. Sides are as
34    /// drawn, so they follow [`reverse`](Self::reverse). `expand(false,
35    /// false, false, false)` is `expand = FALSE`.
36    pub fn expand(mut self, top: bool, right: bool, bottom: bool, left: bool) -> Self {
37        self.expand = [top, right, bottom, left];
38        self
39    }
40
41    /// Reverse the x and / or y direction at draw time (ggplot2 4.0's
42    /// `coord_cartesian(reverse = "x" | "y" | "xy")`) — the scales and stats
43    /// are unchanged, the panel is mirrored.
44    pub fn reverse(mut self, x: bool, y: bool) -> Self {
45        self.reverse_x = x;
46        self.reverse_y = y;
47        self
48    }
49
50    /// Set x-axis zoom limits (data coordinates).
51    pub fn xlim(mut self, min: f64, max: f64) -> Self {
52        self.xlim = Some((min, max));
53        self
54    }
55
56    /// Set y-axis zoom limits (data coordinates).
57    pub fn ylim(mut self, min: f64, max: f64) -> Self {
58        self.ylim = Some((min, max));
59        self
60    }
61
62    /// Get x zoom limits for use by the renderer.
63    pub fn get_xlim(&self) -> Option<(f64, f64)> {
64        self.xlim
65    }
66
67    /// Get y zoom limits for use by the renderer.
68    pub fn get_ylim(&self) -> Option<(f64, f64)> {
69        self.ylim
70    }
71}
72
73impl Default for CoordCartesian {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl Coord for CoordCartesian {
80    fn transform(&self, point: (f64, f64), plot_area: &Rect) -> (f64, f64) {
81        let (nx, ny) = point;
82        let nx = if self.reverse_x { 1.0 - nx } else { nx };
83        let ny = if self.reverse_y { 1.0 - ny } else { ny };
84        let px = plot_area.x + nx * plot_area.width;
85        // Y is flipped: 0 at bottom, 1 at top
86        let py = plot_area.y + (1.0 - ny) * plot_area.height;
87        (px, py)
88    }
89
90    fn zoom_x(&self) -> Option<(f64, f64)> {
91        self.xlim
92    }
93
94    fn zoom_y(&self) -> Option<(f64, f64)> {
95        self.ylim
96    }
97
98    fn train_scales(&mut self, scales: &mut crate::scale::ScaleSet) {
99        let [top, right, bottom, left] = self.expand.map(|e| !e);
100        // (lower, upper) data ends per axis, mirrored when reversed.
101        let x = if self.reverse_x {
102            (right, left)
103        } else {
104            (left, right)
105        };
106        let y = if self.reverse_y {
107            (top, bottom)
108        } else {
109            (bottom, top)
110        };
111        for (aes, (lo, hi)) in [(crate::aes::Aesthetic::X, x), (crate::aes::Aesthetic::Y, y)] {
112            if lo || hi {
113                if let Some(s) = scales.get_mut(&aes) {
114                    s.clear_expand(lo, hi);
115                }
116            }
117        }
118    }
119}