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}