1use crate::error::{ChartError, ChartResult};
4use core::fmt::Debug;
5
6#[cfg(not(feature = "std"))]
8use alloc::boxed::Box;
9#[cfg(feature = "std")]
10use std::boxed::Box;
11
12#[cfg(all(feature = "floating-point", not(feature = "std")))]
14use micromath::F32Ext;
15
16pub trait ScaleTransform: Debug {
18 fn transform(&self, value: f32) -> ChartResult<f32>;
20
21 fn inverse(&self, normalized: f32) -> ChartResult<f32>;
23
24 fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>>;
26
27 fn format_value(&self, value: f32) -> heapless::String<16>;
29}
30
31#[derive(Debug, Clone, Copy)]
33pub struct ScaleConfig {
34 pub min: f32,
36 pub max: f32,
38 pub include_zero: bool,
40 pub nice_bounds: bool,
42}
43
44impl Default for ScaleConfig {
45 fn default() -> Self {
46 Self {
47 min: 0.0,
48 max: 100.0,
49 include_zero: false,
50 nice_bounds: true,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct LinearScale {
58 config: ScaleConfig,
59 range: f32,
60}
61
62impl LinearScale {
63 pub fn new(config: ScaleConfig) -> ChartResult<Self> {
65 if config.min >= config.max {
66 return Err(ChartError::InvalidRange);
67 }
68
69 let range = config.max - config.min;
70 Ok(Self { config, range })
71 }
72}
73
74impl ScaleTransform for LinearScale {
75 fn transform(&self, value: f32) -> ChartResult<f32> {
76 if value.is_nan() || value.is_infinite() {
77 return Err(ChartError::InvalidData);
78 }
79
80 let normalized = (value - self.config.min) / self.range;
81 Ok(normalized.clamp(0.0, 1.0))
82 }
83
84 fn inverse(&self, normalized: f32) -> ChartResult<f32> {
85 if !(0.0..=1.0).contains(&normalized) {
86 return Err(ChartError::InvalidRange);
87 }
88
89 Ok(self.config.min + normalized * self.range)
90 }
91
92 fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
93 let mut ticks = heapless::Vec::new();
94
95 if count == 0 {
96 return Ok(ticks);
97 }
98
99 if count == 1 {
100 let _ = ticks.push((self.config.min + self.config.max) / 2.0);
101 return Ok(ticks);
102 }
103
104 let step = self.range / (count - 1) as f32;
105 for i in 0..count {
106 let tick = self.config.min + (i as f32) * step;
107 if ticks.push(tick).is_err() {
108 break;
109 }
110 }
111
112 Ok(ticks)
113 }
114
115 fn format_value(&self, value: f32) -> heapless::String<16> {
116 let mut s = heapless::String::new();
117
118 if value == 0.0 {
120 let _ = write!(s, "0");
121 } else if value.abs() >= 1000.0 {
122 let _ = write!(s, "{:.1}k", value / 1000.0);
123 } else if value.abs() >= 1.0 {
124 let _ = write!(s, "{value:.0}");
125 } else if value.abs() >= 0.01 {
126 let _ = write!(s, "{value:.2}");
127 } else {
128 let _ = write!(s, "{value:.1e}");
129 }
130
131 s
132 }
133}
134
135#[derive(Debug, Clone)]
137pub struct LogarithmicScale {
138 config: ScaleConfig,
139 base: f32,
140 log_min: f32,
141 #[allow(dead_code)]
142 log_max: f32,
143 log_range: f32,
144}
145
146impl LogarithmicScale {
147 pub fn new(config: ScaleConfig, base: f32) -> ChartResult<Self> {
149 if config.min <= 0.0 || config.max <= 0.0 {
150 return Err(ChartError::InvalidRange);
151 }
152
153 if config.min >= config.max {
154 return Err(ChartError::InvalidRange);
155 }
156
157 if base <= 0.0 || base == 1.0 {
158 return Err(ChartError::InvalidConfiguration);
159 }
160
161 #[cfg(feature = "std")]
162 let (log_min, log_max) = (config.min.log(base), config.max.log(base));
163
164 #[cfg(not(feature = "std"))]
165 let (log_min, log_max) = (config.min.log(base), config.max.log(base));
166 let log_range = log_max - log_min;
167
168 Ok(Self {
169 config,
170 base,
171 log_min,
172 log_max,
173 log_range,
174 })
175 }
176
177 pub fn base10(config: ScaleConfig) -> ChartResult<Self> {
179 Self::new(config, 10.0)
180 }
181
182 pub fn natural(config: ScaleConfig) -> ChartResult<Self> {
184 Self::new(config, core::f32::consts::E)
185 }
186}
187
188impl ScaleTransform for LogarithmicScale {
189 fn transform(&self, value: f32) -> ChartResult<f32> {
190 if value <= 0.0 {
191 return Err(ChartError::InvalidData);
192 }
193
194 if value.is_nan() || value.is_infinite() {
195 return Err(ChartError::InvalidData);
196 }
197
198 #[cfg(feature = "std")]
199 let log_value = value.log(self.base);
200
201 #[cfg(not(feature = "std"))]
202 let log_value = value.log(self.base);
203 let normalized = (log_value - self.log_min) / self.log_range;
204 Ok(normalized.clamp(0.0, 1.0))
205 }
206
207 fn inverse(&self, normalized: f32) -> ChartResult<f32> {
208 if !(0.0..=1.0).contains(&normalized) {
209 return Err(ChartError::InvalidRange);
210 }
211
212 let log_value = self.log_min + normalized * self.log_range;
213 #[cfg(feature = "std")]
214 let result = self.base.powf(log_value);
215
216 #[cfg(not(feature = "std"))]
217 let result = self.base.powf(log_value);
218
219 Ok(result)
220 }
221
222 fn get_ticks(&self, _count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
223 let mut ticks = heapless::Vec::new();
224
225 #[cfg(feature = "std")]
227 let start_power = self.config.min.log(self.base).floor();
228
229 #[cfg(not(feature = "std"))]
230 let start_power = self.config.min.log(self.base).floor();
231
232 let mut power = start_power;
233
234 for _ in 0..20 {
236 #[cfg(feature = "std")]
237 let value = self.base.powf(power);
238
239 #[cfg(not(feature = "std"))]
240 let value = self.base.powf(power);
241
242 if value > self.config.max * 1.1 {
243 break;
245 }
246
247 if value >= self.config.min * 0.9 && value <= self.config.max * 1.1 {
248 let rounded = if self.base == 10.0 {
250 let log_val = value.log10();
252 if (log_val - log_val.round()).abs() < 0.01 {
253 10.0_f32.powf(log_val.round())
254 } else {
255 value
256 }
257 } else {
258 value
259 };
260
261 if rounded >= self.config.min && rounded <= self.config.max {
262 let _ = ticks.push(rounded);
263 }
264 }
265
266 if self.base == 10.0 && !ticks.is_full() && power < 3.0 {
269 for i in 2..10 {
270 if ticks.is_full() {
271 break;
272 }
273 let intermediate = value * (i as f32);
274 if intermediate > self.config.max {
275 break;
276 }
277 if intermediate >= self.config.min {
278 let _ = ticks.push(intermediate);
279 }
280 }
281 }
282
283 power += 1.0;
284 }
285
286 Ok(ticks)
287 }
288
289 fn format_value(&self, value: f32) -> heapless::String<16> {
290 let mut s = heapless::String::new();
291
292 if self.base == 10.0 {
293 #[cfg(feature = "std")]
295 let log_value = value.log10();
296
297 #[cfg(not(feature = "std"))]
298 let log_value = value.log10();
299 if (log_value - log_value.round()).abs() < 0.01 && value < 1000.0 {
300 let _ = write!(s, "10^{:.0}", log_value.round());
301 } else if value >= 1000.0 {
302 let _ = write!(s, "{value:.0}");
303 } else {
304 let _ = write!(s, "{value:.1}");
305 }
306 } else {
307 if value >= 1000.0 {
309 let _ = write!(s, "{value:.0}");
310 } else if value >= 1.0 {
311 let _ = write!(s, "{value:.1}");
312 } else {
313 let _ = write!(s, "{value:.2}");
314 }
315 }
316
317 s
318 }
319}
320
321pub struct CustomScale<F, I>
323where
324 F: Fn(f32) -> ChartResult<f32>,
325 I: Fn(f32) -> ChartResult<f32>,
326{
327 config: ScaleConfig,
328 transform_fn: F,
329 inverse_fn: I,
330}
331
332impl<F, I> core::fmt::Debug for CustomScale<F, I>
333where
334 F: Fn(f32) -> ChartResult<f32>,
335 I: Fn(f32) -> ChartResult<f32>,
336{
337 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
338 f.debug_struct("CustomScale")
339 .field("config", &self.config)
340 .field("transform_fn", &"<function>")
341 .field("inverse_fn", &"<function>")
342 .finish()
343 }
344}
345
346impl<F, I> CustomScale<F, I>
347where
348 F: Fn(f32) -> ChartResult<f32>,
349 I: Fn(f32) -> ChartResult<f32>,
350{
351 pub fn new(config: ScaleConfig, transform_fn: F, inverse_fn: I) -> Self {
353 Self {
354 config,
355 transform_fn,
356 inverse_fn,
357 }
358 }
359}
360
361impl<F, I> ScaleTransform for CustomScale<F, I>
362where
363 F: Fn(f32) -> ChartResult<f32>,
364 I: Fn(f32) -> ChartResult<f32>,
365{
366 fn transform(&self, value: f32) -> ChartResult<f32> {
367 (self.transform_fn)(value)
368 }
369
370 fn inverse(&self, normalized: f32) -> ChartResult<f32> {
371 (self.inverse_fn)(normalized)
372 }
373
374 fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
375 LinearScale::new(self.config)?.get_ticks(count)
377 }
378
379 fn format_value(&self, value: f32) -> heapless::String<16> {
380 let mut s = heapless::String::new();
382 let _ = write!(s, "{value:.2}");
383 s
384 }
385}
386
387#[derive(Debug, Clone, Copy, PartialEq)]
389pub enum AxisScaleType {
390 Linear,
392 Log10,
394 LogE,
396 LogBase(f32),
398 Custom,
400}
401
402impl Default for AxisScaleType {
403 fn default() -> Self {
404 Self::Linear
405 }
406}
407
408#[derive(Debug)]
410pub enum AxisScale {
411 Linear(LinearScale),
413 Logarithmic(LogarithmicScale),
415 Custom(Box<dyn ScaleTransform>),
417}
418
419impl AxisScale {
420 pub fn new(scale_type: AxisScaleType, config: ScaleConfig) -> ChartResult<Self> {
422 match scale_type {
423 AxisScaleType::Linear => Ok(Self::Linear(LinearScale::new(config)?)),
424 AxisScaleType::Log10 => Ok(Self::Logarithmic(LogarithmicScale::base10(config)?)),
425 AxisScaleType::LogE => Ok(Self::Logarithmic(LogarithmicScale::natural(config)?)),
426 AxisScaleType::LogBase(base) => {
427 Ok(Self::Logarithmic(LogarithmicScale::new(config, base)?))
428 }
429 AxisScaleType::Custom => Err(ChartError::InvalidConfiguration),
430 }
431 }
432
433 pub fn transform(&self, value: f32) -> ChartResult<f32> {
435 match self {
436 Self::Linear(scale) => scale.transform(value),
437 Self::Logarithmic(scale) => scale.transform(value),
438 Self::Custom(scale) => scale.transform(value),
439 }
440 }
441
442 pub fn inverse(&self, normalized: f32) -> ChartResult<f32> {
444 match self {
445 Self::Linear(scale) => scale.inverse(normalized),
446 Self::Logarithmic(scale) => scale.inverse(normalized),
447 Self::Custom(scale) => scale.inverse(normalized),
448 }
449 }
450
451 pub fn get_ticks(&self, count: usize) -> ChartResult<heapless::Vec<f32, 16>> {
453 match self {
454 Self::Linear(scale) => scale.get_ticks(count),
455 Self::Logarithmic(scale) => scale.get_ticks(count),
456 Self::Custom(scale) => scale.get_ticks(count),
457 }
458 }
459
460 pub fn format_value(&self, value: f32) -> heapless::String<16> {
462 match self {
463 Self::Linear(scale) => scale.format_value(value),
464 Self::Logarithmic(scale) => scale.format_value(value),
465 Self::Custom(scale) => scale.format_value(value),
466 }
467 }
468}
469
470use core::fmt::Write;
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_linear_scale() {
479 let config = ScaleConfig {
480 min: 0.0,
481 max: 100.0,
482 ..Default::default()
483 };
484
485 let scale = LinearScale::new(config).unwrap();
486
487 assert_eq!(scale.transform(0.0).unwrap(), 0.0);
489 assert_eq!(scale.transform(50.0).unwrap(), 0.5);
490 assert_eq!(scale.transform(100.0).unwrap(), 1.0);
491
492 assert_eq!(scale.inverse(0.0).unwrap(), 0.0);
494 assert_eq!(scale.inverse(0.5).unwrap(), 50.0);
495 assert_eq!(scale.inverse(1.0).unwrap(), 100.0);
496
497 let ticks = scale.get_ticks(5).unwrap();
499 assert_eq!(ticks.len(), 5);
500 assert_eq!(ticks[0], 0.0);
501 assert_eq!(ticks[4], 100.0);
502 }
503
504 #[test]
505 fn test_logarithmic_scale() {
506 let config = ScaleConfig {
507 min: 1.0,
508 max: 1000.0,
509 ..Default::default()
510 };
511
512 let scale = LogarithmicScale::base10(config).unwrap();
513
514 assert!((scale.transform(1.0).unwrap() - 0.0).abs() < 0.001);
516 assert!((scale.transform(10.0).unwrap() - 0.333).abs() < 0.01);
517 assert!((scale.transform(100.0).unwrap() - 0.667).abs() < 0.01);
518 assert!((scale.transform(1000.0).unwrap() - 1.0).abs() < 0.001);
519
520 assert!(scale.transform(0.0).is_err());
522 assert!(scale.transform(-1.0).is_err());
523 }
524}