Skip to main content

gpui_liveplot/
view.rs

1//! View models and data ranges.
2//!
3//! The view layer is responsible for describing the data ranges visible on screen
4//! and how those ranges are updated as new data arrives.
5
6/// Numeric range with inclusive bounds.
7///
8/// `Range` is used for axis limits, data bounds, and viewport calculations. The
9/// constructor automatically swaps bounds to maintain `min <= max`.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Range {
12    /// Minimum value.
13    pub min: f64,
14    /// Maximum value.
15    pub max: f64,
16}
17
18impl Range {
19    /// Create a new range, swapping bounds if needed.
20    pub fn new(mut min: f64, mut max: f64) -> Self {
21        if min > max {
22            std::mem::swap(&mut min, &mut max);
23        }
24        Self { min, max }
25    }
26
27    /// Span of the range.
28    pub fn span(&self) -> f64 {
29        self.max - self.min
30    }
31
32    /// Check whether both bounds are finite.
33    pub fn is_finite(&self) -> bool {
34        self.min.is_finite() && self.max.is_finite()
35    }
36
37    /// Check whether the range has positive span and finite bounds.
38    pub fn is_valid(&self) -> bool {
39        self.is_finite() && self.span() > 0.0
40    }
41
42    /// Expand the range to include a value.
43    ///
44    /// Non-finite values are ignored.
45    pub fn expand_to_include(&mut self, value: f64) {
46        if !value.is_finite() {
47            return;
48        }
49        if value < self.min {
50            self.min = value;
51        }
52        if value > self.max {
53            self.max = value;
54        }
55    }
56
57    /// Union two ranges if both are finite.
58    pub fn union(a: Self, b: Self) -> Option<Self> {
59        if !a.is_finite() || !b.is_finite() {
60            return None;
61        }
62        Some(Self {
63            min: a.min.min(b.min),
64            max: a.max.max(b.max),
65        })
66    }
67
68    /// Clamp a value into the range.
69    pub fn clamp(&self, value: f64) -> f64 {
70        value.max(self.min).min(self.max)
71    }
72
73    /// Add padding around the range.
74    ///
75    /// `frac` is applied to the current span and clamped by `min_padding`.
76    pub fn padded(&self, frac: f64, min_padding: f64) -> Self {
77        let span = self.span().abs();
78        let padding = (span * frac).max(min_padding);
79        Self {
80            min: self.min - padding,
81            max: self.max + padding,
82        }
83    }
84
85    /// Ensure the range has at least the given span.
86    pub fn with_min_span(&self, min_span: f64) -> Self {
87        let span = self.span();
88        if span >= min_span {
89            return *self;
90        }
91        let center = (self.min + self.max) * 0.5;
92        let half = min_span * 0.5;
93        Self {
94            min: center - half,
95            max: center + half,
96        }
97    }
98}
99
100/// The active view mode for a plot.
101///
102/// View modes control how the viewport responds to new data and user
103/// interactions. Any explicit interaction typically switches the plot to
104/// [`View::Manual`].
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum View {
107    /// Automatically show the full data range (default).
108    AutoAll {
109        /// Allow automatic X range expansion.
110        auto_x: bool,
111        /// Allow automatic Y range expansion.
112        auto_y: bool,
113    },
114    /// Manual view that does not auto-update.
115    Manual,
116    /// Follow the last N points on X.
117    FollowLastN {
118        /// Number of points to keep in view.
119        points: usize,
120    },
121    /// Follow the last N points on X and auto-scale Y.
122    FollowLastNXY {
123        /// Number of points to keep in view.
124        points: usize,
125    },
126}
127
128impl Default for View {
129    fn default() -> Self {
130        Self::AutoAll {
131            auto_x: true,
132            auto_y: true,
133        }
134    }
135}
136
137/// Visible data ranges on both axes.
138///
139/// A `Viewport` is the canonical input to coordinate transforms and decimation
140/// decisions.
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct Viewport {
143    /// X axis range.
144    pub x: Range,
145    /// Y axis range.
146    pub y: Range,
147}
148
149impl Viewport {
150    /// Create a viewport from X and Y ranges.
151    pub fn new(x: Range, y: Range) -> Self {
152        Self { x, y }
153    }
154
155    /// Check whether both axes are valid.
156    pub fn is_valid(&self) -> bool {
157        self.x.is_valid() && self.y.is_valid()
158    }
159
160    /// Apply padding to both axes.
161    pub fn padded(&self, frac: f64, min_padding: f64) -> Self {
162        Self {
163            x: self.x.padded(frac, min_padding),
164            y: self.y.padded(frac, min_padding),
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn range_with_min_span_expands() {
175        let range = Range::new(2.0, 2.0);
176        let expanded = range.with_min_span(1.0);
177        assert!(expanded.span() >= 1.0);
178        assert!((expanded.min + expanded.max) * 0.5 - 2.0 < 1e-9);
179    }
180}