1use std::sync::Arc;
9
10use crate::view::Range;
11
12#[derive(Clone, Default)]
17pub enum AxisFormatter {
18 #[default]
20 Default,
21 Custom(Arc<dyn Fn(f64) -> String + Send + Sync>),
26}
27
28impl AxisFormatter {
29 pub fn format(&self, value: f64) -> String {
31 match self {
32 Self::Default => format!("{value:.6}"),
33 Self::Custom(formatter) => formatter(value),
34 }
35 }
36}
37
38impl std::fmt::Debug for AxisFormatter {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::Default => write!(f, "AxisFormatter::Default"),
42 Self::Custom(_) => write!(f, "AxisFormatter::Custom(..)"),
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
53pub struct AxisConfig {
54 title: Option<String>,
55 units: Option<String>,
56 formatter: AxisFormatter,
57 tick_config: TickConfig,
58 show_grid: bool,
59 show_minor_grid: bool,
60 show_zero_line: bool,
61 show_border: bool,
62 label_size: f32,
63}
64
65impl AxisConfig {
66 pub fn new() -> Self {
70 Self {
71 title: None,
72 units: None,
73 formatter: AxisFormatter::default(),
74 tick_config: TickConfig::default(),
75 show_grid: true,
76 show_minor_grid: false,
77 show_zero_line: false,
78 show_border: true,
79 label_size: 12.0,
80 }
81 }
82
83 pub fn builder() -> AxisConfigBuilder {
85 AxisConfigBuilder { axis: Self::new() }
86 }
87
88 pub fn title(&self) -> Option<&str> {
90 self.title.as_deref()
91 }
92
93 pub fn units(&self) -> Option<&str> {
95 self.units.as_deref()
96 }
97
98 pub fn formatter(&self) -> &AxisFormatter {
100 &self.formatter
101 }
102
103 pub fn format_value(&self, value: f64) -> String {
105 self.formatter.format(value)
106 }
107
108 pub fn tick_config(&self) -> TickConfig {
110 self.tick_config
111 }
112
113 pub fn show_grid(&self) -> bool {
115 self.show_grid
116 }
117
118 pub fn show_minor_grid(&self) -> bool {
120 self.show_minor_grid
121 }
122
123 pub fn show_zero_line(&self) -> bool {
125 self.show_zero_line
126 }
127
128 pub fn show_border(&self) -> bool {
130 self.show_border
131 }
132
133 pub fn label_size(&self) -> f32 {
135 self.label_size
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct AxisConfigBuilder {
142 axis: AxisConfig,
143}
144
145impl AxisConfigBuilder {
146 pub fn title(mut self, title: impl Into<String>) -> Self {
148 self.axis.title = Some(title.into());
149 self
150 }
151
152 pub fn units(mut self, units: impl Into<String>) -> Self {
154 self.axis.units = Some(units.into());
155 self
156 }
157
158 pub fn formatter(mut self, formatter: AxisFormatter) -> Self {
162 self.axis.formatter = formatter;
163 self
164 }
165
166 pub fn tick_config(mut self, config: TickConfig) -> Self {
170 self.axis.tick_config = config;
171 self
172 }
173
174 pub fn grid(mut self, enabled: bool) -> Self {
176 self.axis.show_grid = enabled;
177 self
178 }
179
180 pub fn minor_grid(mut self, enabled: bool) -> Self {
182 self.axis.show_minor_grid = enabled;
183 self
184 }
185
186 pub fn zero_line(mut self, enabled: bool) -> Self {
188 self.axis.show_zero_line = enabled;
189 self
190 }
191
192 pub fn border(mut self, enabled: bool) -> Self {
194 self.axis.show_border = enabled;
195 self
196 }
197
198 pub fn label_size(mut self, size: f32) -> Self {
200 self.axis.label_size = size;
201 self
202 }
203
204 pub fn build(self) -> AxisConfig {
206 self.axis
207 }
208}
209
210impl Default for AxisConfig {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq)]
221pub struct TickConfig {
222 pub pixel_spacing: f32,
224 pub minor_count: usize,
226}
227
228impl Default for TickConfig {
229 fn default() -> Self {
230 Self {
231 pixel_spacing: 80.0,
232 minor_count: 4,
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq)]
239pub(crate) struct Tick {
240 pub(crate) value: f64,
242 pub(crate) label: String,
244 pub(crate) is_major: bool,
246}
247
248#[derive(Debug, Clone)]
250pub(crate) struct AxisLayout {
251 pub(crate) ticks: Vec<Tick>,
253 pub(crate) max_label_size: (f32, f32),
255}
256
257impl Default for AxisLayout {
258 fn default() -> Self {
259 Self {
260 ticks: Vec::new(),
261 max_label_size: (0.0, 0.0),
262 }
263 }
264}
265
266#[derive(Debug, Clone, PartialEq)]
267struct AxisLayoutKey {
268 range: Range,
269 pixels: u32,
270 tick_config: TickConfig,
271}
272
273#[derive(Debug, Default, Clone)]
275pub(crate) struct AxisLayoutCache {
276 key: Option<AxisLayoutKey>,
277 layout: AxisLayout,
278}
279
280impl AxisLayoutCache {
281 pub(crate) fn update(
283 &mut self,
284 axis: &AxisConfig,
285 range: Range,
286 pixels: u32,
287 measurer: &impl TextMeasurer,
288 ) -> &AxisLayout {
289 let key = AxisLayoutKey {
290 range,
291 pixels,
292 tick_config: axis.tick_config(),
293 };
294 if self.key.as_ref() == Some(&key) {
295 return &self.layout;
296 }
297
298 let ticks = generate_ticks(axis, range, pixels as f32);
299 let mut max_size = (0.0_f32, 0.0_f32);
300 for tick in &ticks {
301 if tick.label.is_empty() {
302 continue;
303 }
304 let (w, h) = measurer.measure(&tick.label, axis.label_size());
305 max_size.0 = max_size.0.max(w);
306 max_size.1 = max_size.1.max(h);
307 }
308
309 self.layout = AxisLayout {
310 ticks,
311 max_label_size: max_size,
312 };
313 self.key = Some(key);
314 &self.layout
315 }
316}
317
318pub(crate) trait TextMeasurer {
320 fn measure(&self, text: &str, size: f32) -> (f32, f32);
322}
323
324fn generate_ticks(axis: &AxisConfig, range: Range, pixel_length: f32) -> Vec<Tick> {
326 if !range.is_valid() || pixel_length <= 0.0 {
327 return Vec::new();
328 }
329 generate_linear_ticks(axis, range, pixel_length)
330}
331
332fn generate_linear_ticks(axis: &AxisConfig, range: Range, pixel_length: f32) -> Vec<Tick> {
333 let target = (pixel_length / axis.tick_config().pixel_spacing).max(2.0);
334 let raw_step = range.span() / target as f64;
335 let step = nice_step(raw_step);
336 if !step.is_finite() || step <= 0.0 {
337 return Vec::new();
338 }
339
340 let minor_count = axis.tick_config().minor_count;
341 let minor_step = step / (minor_count as f64 + 1.0);
342
343 let mut ticks = Vec::new();
344 let mut value = (range.min / step).floor() * step;
345 if value == -0.0 {
346 value = 0.0;
347 }
348 let max_value = range.max + step * 0.5;
349
350 while value <= max_value {
351 if value >= range.min - step * 0.5 {
352 ticks.push(Tick {
353 value,
354 label: axis.format_value(value),
355 is_major: true,
356 });
357 }
358 for i in 1..=minor_count {
359 let minor = value + minor_step * i as f64;
360 if minor >= range.min && minor <= range.max {
361 ticks.push(Tick {
362 value: minor,
363 label: String::new(),
364 is_major: false,
365 });
366 }
367 }
368 value += step;
369 }
370
371 ticks
372}
373
374fn nice_step(step: f64) -> f64 {
375 if step <= 0.0 {
376 return 0.0;
377 }
378 let exp = step.log10().floor();
379 let base = 10_f64.powf(exp);
380 let fraction = step / base;
381 let nice = if fraction <= 1.0 {
382 1.0
383 } else if fraction <= 2.0 {
384 2.0
385 } else if fraction <= 5.0 {
386 5.0
387 } else {
388 10.0
389 };
390 nice * base
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn linear_ticks_generate_major() {
399 let axis = AxisConfig::new();
400 let ticks = generate_ticks(&axis, Range::new(0.0, 10.0), 400.0);
401 assert!(ticks.iter().any(|tick| tick.is_major));
402 }
403}