Skip to main content

flowsurface_data/chart/
axis.rs

1use super::{
2    ticks::{
3        Y_LABEL_DENSITY,
4        y::{TickValue, YAxisScale},
5        y_labels_that_fit,
6    },
7    value::{PanelValueLabelPolicy, PanelValueScale, YUnit, YUnitRange},
8};
9
10const MAX_AUTOSCALE_SPAN_FACTOR: f64 = 1_000.0;
11pub const ZOOM_SENSITIVITY: f32 = 30.0;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct PanelYValueRange {
15    min: f64,
16    max: f64,
17}
18
19impl PanelYValueRange {
20    fn new(min: f64, max: f64) -> Option<Self> {
21        (min.is_finite() && max.is_finite() && min < max).then_some(Self { min, max })
22    }
23
24    pub fn from_units(range: YUnitRange, scale: PanelValueScale) -> Option<Self> {
25        let (min, max) = scale.values(range);
26        Self::new(min, max)
27    }
28
29    pub fn to_units(self, scale: PanelValueScale) -> YUnitRange {
30        YUnitRange::new(scale.value_to_unit(self.min), scale.value_to_unit(self.max))
31    }
32
33    pub fn axis_labels(
34        self,
35        scale: PanelValueScale,
36        policy: PanelValueLabelPolicy,
37        top: f32,
38        height: f32,
39        text_size: f32,
40    ) -> Vec<(String, f32)> {
41        let ticks = YAxisScale::Quantized {
42            step: scale.unit_to_value(YUnit(1)),
43        }
44        .ticks(
45            self.min,
46            self.max,
47            y_labels_that_fit(height, text_size, Y_LABEL_DENSITY).max(2) as i32,
48        );
49        let step = ticks.step.unwrap_or(1.0);
50
51        let mut labels: Vec<_> = ticks
52            .ticks
53            .into_iter()
54            .filter_map(|tick| {
55                let TickValue::Float(value) = tick.value else {
56                    return None;
57                };
58                let y = f64::from(top) + (1.0 - tick.ratio) * f64::from(height);
59                y.is_finite()
60                    .then_some(y as f32)
61                    .filter(|y| y.is_finite())
62                    .map(|y| {
63                        (
64                            policy.format_axis_value(scale, value, step),
65                            y.clamp(top, top + height),
66                        )
67                    })
68            })
69            .collect();
70        labels.dedup_by(|left, right| left.0 == right.0);
71        labels
72    }
73
74    pub fn normalized(min_value: f64, max_value: f64) -> Option<Self> {
75        if !min_value.is_finite() || !max_value.is_finite() {
76            return None;
77        }
78        let (mut min_value, mut max_value) = if min_value <= max_value {
79            (min_value, max_value)
80        } else {
81            (max_value, min_value)
82        };
83        if (max_value - min_value).abs() <= 1e-8 {
84            let pad = min_value.abs().max(1.0) * 0.01;
85            min_value -= pad;
86            max_value += pad;
87        }
88        Self::new(min_value, max_value)
89    }
90
91    pub fn min_value(self) -> f64 {
92        self.min
93    }
94
95    pub fn max_value(self) -> f64 {
96        self.max
97    }
98
99    pub fn span(self) -> f64 {
100        (self.max - self.min).abs()
101    }
102
103    pub fn zoom_around_ratio(
104        self,
105        anchor_ratio: f64,
106        zoom_scale: f64,
107        zoom_in: bool,
108    ) -> Option<Self> {
109        if !zoom_scale.is_finite() {
110            return None;
111        }
112        let anchor_ratio = anchor_ratio.clamp(0.0, 1.0);
113        let span = self.span().max(1e-8);
114        let zoom_scale = zoom_scale.abs().max(1.0);
115        let new_span = if zoom_in {
116            span / zoom_scale
117        } else if span <= f64::MAX / zoom_scale {
118            span * zoom_scale
119        } else {
120            f64::MAX
121        };
122        if !new_span.is_finite() {
123            return None;
124        }
125        let anchor_value = self.min + anchor_ratio * span;
126        let new_min = anchor_value - anchor_ratio * new_span;
127        Self::new(new_min, new_min + new_span)
128    }
129
130    fn clamp_span(self, min_span: f64, max_span: f64) -> Option<Self> {
131        if !self.min.is_finite()
132            || !self.max.is_finite()
133            || !min_span.is_finite()
134            || !max_span.is_finite()
135        {
136            return None;
137        }
138
139        let min_span = min_span.abs().max(1e-8);
140        let max_span = max_span.abs().max(min_span);
141        let span = self.span();
142        let target_span = span.clamp(min_span, max_span);
143        if target_span == span {
144            return Some(self);
145        }
146        let center = self.min * 0.5 + self.max * 0.5;
147        let min_value = center - target_span * 0.5;
148        Self::new(min_value, min_value + target_span)
149    }
150
151    fn clamp_to_bounds(self, lower: f64, upper: f64) -> Option<Self> {
152        if !lower.is_finite() || !upper.is_finite() || lower >= upper {
153            return None;
154        }
155        if self.min >= lower && self.max <= upper {
156            return Some(self);
157        }
158
159        let span = self.span().min(upper - lower);
160        let mut min_value = self.min.max(lower);
161        let mut max_value = min_value + span;
162        if max_value > upper {
163            max_value = upper;
164            min_value = max_value - span;
165        }
166
167        Self::new(min_value, max_value)
168    }
169
170    pub fn pan_by_ratio(self, delta_ratio: f64) -> Option<Self> {
171        if !delta_ratio.is_finite() {
172            return None;
173        }
174        let span = self.span().max(1e-8);
175        let shift = delta_ratio * span;
176        Self::normalized(self.min + shift, self.max + shift)
177    }
178}
179
180#[derive(Debug, Clone, Copy)]
181pub struct PanelYViewportLimits {
182    pub min_span: f64,
183    pub max_span: f64,
184    pub bounds: (f64, f64),
185}
186
187impl PanelYViewportLimits {
188    pub fn for_scale(
189        scale: PanelValueScale,
190        autoscale_span: f64,
191        physical_height: f64,
192    ) -> Option<Self> {
193        let (min_span, max_span) = autoscale_span_limits(
194            autoscale_span,
195            physical_height,
196            scale.minimum_span().min(autoscale_span),
197        )?;
198        let bounds = scale.value_bounds();
199        let max_span = max_span.min(bounds.1 - bounds.0);
200        (max_span >= min_span).then_some(Self {
201            min_span,
202            max_span,
203            bounds,
204        })
205    }
206
207    pub fn zoom(
208        self,
209        viewport: PanelYValueRange,
210        anchor_ratio: f64,
211        delta: f32,
212    ) -> Option<PanelYValueRange> {
213        if !delta.is_finite() {
214            return None;
215        }
216        let zoom_in = delta > 0.0;
217        let zoom_scale = (f64::from(delta.abs()) / f64::from(ZOOM_SENSITIVITY))
218            .min(f64::MAX.ln())
219            .exp();
220        let scale_limit = if zoom_in {
221            viewport.span() / self.min_span
222        } else {
223            self.max_span / viewport.span()
224        };
225        let zoom_scale = zoom_scale.min(scale_limit.max(1.0));
226        let viewport = viewport.zoom_around_ratio(anchor_ratio, zoom_scale, zoom_in)?;
227        self.apply(viewport)
228    }
229
230    pub fn apply(self, viewport: PanelYValueRange) -> Option<PanelYValueRange> {
231        let viewport = viewport.clamp_span(self.min_span, self.max_span)?;
232        viewport.clamp_to_bounds(self.bounds.0, self.bounds.1)
233    }
234}
235
236pub fn autoscale_span_limits(
237    autoscale_span: f64,
238    physical_height: f64,
239    min_span: f64,
240) -> Option<(f64, f64)> {
241    if autoscale_span.is_nan()
242        || autoscale_span < 0.0
243        || !physical_height.is_finite()
244        || physical_height <= 0.0
245        || !min_span.is_finite()
246    {
247        return None;
248    }
249
250    let min_span = min_span.abs().max(1e-8);
251    let factor = physical_height.clamp(1.0, MAX_AUTOSCALE_SPAN_FACTOR);
252    let autoscale_span = autoscale_span.min(f64::MAX).max(min_span);
253    let max_span = if autoscale_span > f64::MAX / factor {
254        f64::MAX
255    } else {
256        autoscale_span * factor
257    };
258    Some((min_span, max_span.max(min_span)))
259}
260
261pub fn place_non_overlapping_labels(
262    desired_centers: &[f32],
263    min_y: f32,
264    max_y: f32,
265    label_height: f32,
266    gap: f32,
267) -> Vec<Option<f32>> {
268    if desired_centers.is_empty() {
269        return Vec::new();
270    }
271
272    let label_height = label_height.max(1.0);
273    let min_center = min_y + label_height * 0.5;
274    let max_center = (max_y - label_height * 0.5).max(min_center);
275    let spacing = (label_height + gap.max(0.0)).max(label_height);
276    let capacity = ((max_center - min_center) / spacing).floor() as usize + 1;
277    let visible_count = desired_centers.len().min(capacity.max(1));
278
279    let mut candidates: Vec<(usize, f32)> = desired_centers.iter().copied().enumerate().collect();
280    candidates.sort_by(|(_, left), (_, right)| left.total_cmp(right));
281
282    let selected = if visible_count == candidates.len() {
283        candidates
284    } else {
285        (0..visible_count)
286            .map(|slot| {
287                let source_index = if visible_count == 1 {
288                    candidates.len() / 2
289                } else {
290                    slot * (candidates.len() - 1) / (visible_count - 1)
291                };
292                candidates[source_index]
293            })
294            .collect()
295    };
296
297    let mut centers: Vec<f32> = selected
298        .iter()
299        .map(|(_, center)| center.clamp(min_center, max_center))
300        .collect();
301
302    if centers.len() > 1 {
303        centers[0] = centers[0].max(min_center);
304
305        for index in 1..centers.len() {
306            centers[index] = centers[index].max(centers[index - 1] + spacing);
307        }
308
309        if centers[centers.len() - 1] > max_center {
310            let last = centers.len() - 1;
311            centers[last] = max_center;
312
313            for index in (0..last).rev() {
314                centers[index] = centers[index].min(centers[index + 1] - spacing);
315            }
316        }
317    }
318
319    let mut placements = vec![None; desired_centers.len()];
320    for ((source_index, _), center) in selected.into_iter().zip(centers) {
321        placements[source_index] = Some(center);
322    }
323
324    placements
325}