Skip to main content

charts_rs/charts/
util.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use super::common::AxisScale;
14use serde::{Deserialize, Serialize};
15use std::fmt;
16use substring::Substring;
17
18pub static NIL_VALUE: f32 = f32::MIN;
19
20pub(crate) static THOUSANDS_FORMAT_LABEL: &str = "{t}";
21pub(crate) static SERIES_NAME_FORMAT_LABEL: &str = "{a}";
22pub(crate) static CATEGORY_NAME_FORMAT_LABEL: &str = "{b}";
23pub(crate) static VALUE_FORMAT_LABEL: &str = "{c}";
24pub(crate) static PERCENTAGE_FORMAT_LABEL: &str = "{d}";
25
26#[derive(Clone, Copy, PartialEq, Debug, Default)]
27pub struct Point {
28    pub x: f32,
29    pub y: f32,
30}
31impl From<(f32, f32)> for Point {
32    fn from(val: (f32, f32)) -> Self {
33        Point { x: val.0, y: val.1 }
34    }
35}
36impl fmt::Display for Point {
37    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38        let m = format!("({},{})", format_float(self.x), format_float(self.y));
39        write!(f, "{m}")
40    }
41}
42
43#[derive(Serialize, Deserialize, Clone, Debug, Default)]
44pub struct Box {
45    pub left: f32,
46    pub top: f32,
47    pub right: f32,
48    pub bottom: f32,
49}
50impl Box {
51    pub fn width(&self) -> f32 {
52        self.right - self.left
53    }
54    pub fn height(&self) -> f32 {
55        self.bottom - self.top
56    }
57    pub fn outer_width(&self) -> f32 {
58        self.right
59    }
60    pub fn outer_height(&self) -> f32 {
61        self.bottom
62    }
63}
64impl fmt::Display for Box {
65    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
66        let m = format!(
67            "({},{},{},{})",
68            format_float(self.left),
69            format_float(self.top),
70            format_float(self.right),
71            format_float(self.bottom)
72        );
73        write!(f, "{m}")
74    }
75}
76
77impl From<f32> for Box {
78    fn from(val: f32) -> Self {
79        Box {
80            left: val,
81            top: val,
82            right: val,
83            bottom: val,
84        }
85    }
86}
87impl From<(f32, f32)> for Box {
88    fn from(val: (f32, f32)) -> Self {
89        Box {
90            left: val.0,
91            top: val.1,
92            right: val.0,
93            bottom: val.1,
94        }
95    }
96}
97impl From<(f32, f32, f32)> for Box {
98    fn from(val: (f32, f32, f32)) -> Self {
99        Box {
100            left: val.0,
101            top: val.1,
102            right: val.2,
103            bottom: val.1,
104        }
105    }
106}
107impl From<(f32, f32, f32, f32)> for Box {
108    fn from(val: (f32, f32, f32, f32)) -> Self {
109        Box {
110            left: val.0,
111            top: val.1,
112            right: val.2,
113            bottom: val.3,
114        }
115    }
116}
117
118fn parse_precision(formatter: &str) -> Option<usize> {
119    if formatter.is_empty() {
120        return None;
121    }
122    // 1. parse usize
123    if let Ok(precision) = formatter.parse::<usize>() {
124        return Some(precision);
125    }
126
127    // 2. if formatter is "{:.N}", parse N
128    if let Some(inner) = formatter
129        .strip_prefix("{:.")
130        .and_then(|s| s.strip_suffix("}"))
131        && let Ok(precision) = inner.parse::<usize>()
132    {
133        return Some(precision);
134    }
135
136    None
137}
138
139pub(crate) fn format_series_value(value: f32, formatter: &str) -> String {
140    if formatter == THOUSANDS_FORMAT_LABEL {
141        return thousands_format_float(value);
142    }
143    let mut str = if let Some(precision) = parse_precision(formatter) {
144        format!("{:.precision$}", value, precision = precision)
145    } else if value < 1.1 {
146        format!("{:.2}", value)
147    } else {
148        format!("{:.1}", value)
149    };
150    if str.contains('.') {
151        while str.ends_with('0') {
152            str.pop();
153        }
154
155        if str.ends_with('.') {
156            str.pop();
157        }
158    }
159
160    str
161}
162
163pub(crate) fn thousands_format_float(value: f32) -> String {
164    if value < 1000.0 {
165        return format_float(value);
166    }
167    let str = format!("{:.0}", value);
168    let unit = 3;
169    let mut index = str.len() % unit;
170    let mut arr = vec![];
171    if index != 0 {
172        arr.push(str.substring(0, index))
173    }
174
175    loop {
176        if index >= str.len() {
177            break;
178        }
179        arr.push(str.substring(index, index + unit));
180        index += unit;
181    }
182    arr.join(",")
183}
184
185pub(crate) fn format_float(value: f32) -> String {
186    let str = format!("{:.1}", value);
187    if str.ends_with(".0") {
188        return str.substring(0, str.len() - 2).to_string();
189    }
190    str
191}
192
193#[derive(Clone, Debug, Default)]
194pub(crate) struct AxisValueParams {
195    pub data_list: Vec<f32>,
196    pub min: Option<f32>,
197    pub max: Option<f32>,
198    pub split_number: usize,
199    pub reverse: Option<bool>,
200    pub thousands_format: bool,
201    pub scale: AxisScale,
202}
203#[derive(Clone, Debug, Default)]
204pub struct AxisValues {
205    pub data: Vec<String>,
206    pub min: f32,
207    pub max: f32,
208    pub scale: AxisScale,
209}
210
211impl AxisValues {
212    pub(crate) fn get_offset(&self) -> f32 {
213        self.max - self.min
214    }
215    pub(crate) fn get_offset_height(&self, value: f32, max_height: f32) -> f32 {
216        match &self.scale {
217            AxisScale::Linear => {
218                let offset = self.get_offset();
219                if offset == 0.0 {
220                    return max_height;
221                }
222                let percent = (value - self.min) / offset;
223                max_height - percent * max_height
224            }
225            AxisScale::Log(base) => {
226                let safe = value.max(f32::MIN_POSITIVE);
227                let log_val = safe.log(*base);
228                let log_min = self.min.max(f32::MIN_POSITIVE).log(*base);
229                let log_max = self.max.max(f32::MIN_POSITIVE).log(*base);
230                let log_range = log_max - log_min;
231                if log_range == 0.0 {
232                    return max_height;
233                }
234                let percent = (log_val - log_min) / log_range;
235                max_height - percent * max_height
236            }
237        }
238    }
239}
240
241const K_VALUE: f32 = 1000.00_f32;
242const M_VALUE: f32 = K_VALUE * K_VALUE;
243const G_VALUE: f32 = M_VALUE * K_VALUE;
244const T_VALUE: f32 = G_VALUE * K_VALUE;
245
246fn format_axis_value(value: f32) -> String {
247    let mut v = value;
248    let mut unit = "";
249    v = if v >= T_VALUE {
250        unit = "T";
251        v / T_VALUE
252    } else if v >= G_VALUE {
253        unit = "G";
254        v / G_VALUE
255    } else if v >= M_VALUE {
256        unit = "M";
257        v / M_VALUE
258    } else if v >= K_VALUE {
259        unit = "k";
260        v / K_VALUE
261    } else {
262        v
263    };
264    format_float(v) + unit
265}
266
267fn get_log_axis_values(params: AxisValueParams, base: f32) -> AxisValues {
268    let split_number = if params.split_number == 0 {
269        6
270    } else {
271        params.split_number
272    };
273
274    let mut min_val = f32::MAX;
275    let mut max_val = f32::MIN_POSITIVE;
276    for &v in &params.data_list {
277        if v != NIL_VALUE && v > 0.0 {
278            if v < min_val {
279                min_val = v;
280            }
281            if v > max_val {
282                max_val = v;
283            }
284        }
285    }
286    if let Some(m) = params.min
287        && m > 0.0
288        && m < min_val
289    {
290        min_val = m;
291    }
292    if let Some(m) = params.max
293        && m > 0.0
294        && m > max_val
295    {
296        max_val = m;
297    }
298
299    if min_val == f32::MAX || max_val <= 0.0 {
300        return AxisValues::default();
301    }
302
303    let exp_min = min_val.log(base).floor() as i32;
304    let exp_max = max_val.log(base).ceil() as i32;
305
306    // Choose a step so we generate at most split_number+1 ticks.
307    let num_powers = (exp_max - exp_min).max(1) as usize;
308    let step = ((num_powers as f32 / split_number as f32).ceil() as i32).max(1);
309
310    let mut data = vec![];
311    let mut exp = exp_min;
312    loop {
313        data.push(format_axis_value(base.powi(exp)));
314        if exp >= exp_max {
315            break;
316        }
317        exp = (exp + step).min(exp_max);
318    }
319
320    if params.reverse.unwrap_or_default() {
321        data.reverse();
322    }
323
324    AxisValues {
325        data,
326        min: base.powi(exp_min),
327        max: base.powi(exp_max),
328        scale: AxisScale::Log(base),
329    }
330}
331
332pub(crate) fn get_axis_values(params: AxisValueParams) -> AxisValues {
333    if let AxisScale::Log(base) = params.scale {
334        return get_log_axis_values(params, base);
335    }
336
337    let mut min = f32::MAX;
338    let mut max = f32::MIN;
339
340    let mut split_number = params.split_number;
341    if split_number == 0 {
342        split_number = 6;
343    }
344    for item in params.data_list.iter() {
345        let value = item.to_owned();
346        if value == NIL_VALUE {
347            continue;
348        }
349        if value > max {
350            max = value;
351        }
352        if value < min {
353            min = value;
354        }
355    }
356    let mut is_custom_min = false;
357
358    if let Some(value) = params.min
359        && value < min
360    {
361        min = value;
362        is_custom_min = true;
363    }
364    // it should use 0, if min gt 0 and not custom value
365    if !is_custom_min && min > 0.0 {
366        min = 0.0;
367    }
368    let mut is_custom_max = false;
369    if let Some(value) = params.max
370        && value > max
371    {
372        max = value;
373        is_custom_max = true
374    }
375    let mut unit = (max - min) / split_number as f32;
376    if !is_custom_max {
377        let ceil_value = (unit * 10.0).ceil();
378        if ceil_value < 12.0 {
379            unit = ceil_value / 10.0;
380        } else {
381            let mut new_unit = unit as i32;
382            let adjust_unit = |current: i32, small_unit: i32| -> i32 {
383                if current % small_unit == 0 {
384                    return current + small_unit;
385                }
386                ((current / small_unit) + 1) * small_unit
387            };
388            if new_unit < 10 {
389                new_unit = adjust_unit(new_unit, 2);
390            } else if new_unit < 100 {
391                new_unit = adjust_unit(new_unit, 5);
392            } else if new_unit < 500 {
393                new_unit = adjust_unit(new_unit, 10);
394            } else if new_unit < 1000 {
395                new_unit = adjust_unit(new_unit, 20);
396            } else if new_unit < 5000 {
397                new_unit = adjust_unit(new_unit, 50);
398            } else if new_unit < 10000 {
399                new_unit = adjust_unit(new_unit, 100);
400            } else {
401                let small_unit = ((max - min) / 20.0) as i32;
402                new_unit = adjust_unit(new_unit, small_unit / 100 * 100);
403            }
404            unit = new_unit as f32;
405        }
406    }
407    let split_unit = unit;
408
409    let mut data = vec![];
410    for i in 0..=split_number {
411        let value = min + (i as f32) * split_unit;
412        if params.thousands_format {
413            data.push(thousands_format_float(value));
414        } else {
415            data.push(format_axis_value(value));
416        }
417    }
418    if params.reverse.unwrap_or_default() {
419        data.reverse();
420    }
421
422    AxisValues {
423        data,
424        min,
425        max: min + split_unit * split_number as f32,
426        scale: AxisScale::Linear,
427    }
428}
429pub fn convert_to_points(values: &[(f32, f32)]) -> Vec<Point> {
430    values.iter().map(|item| item.to_owned().into()).collect()
431}
432
433pub fn get_quadrant(cx: f32, cy: f32, point: &Point) -> u8 {
434    if point.x > cx {
435        if point.y > cy { 4 } else { 1 }
436    } else if point.y > cy {
437        3
438    } else {
439        2
440    }
441}
442
443#[derive(Clone, Debug, Default)]
444pub(crate) struct LabelOption {
445    pub series_name: String,
446    pub category_name: String,
447    pub value: f32,
448    pub percentage: f32,
449    pub formatter: String,
450}
451impl LabelOption {
452    pub fn format(&self) -> String {
453        // {a} for series name, {b} for category name, {c} for data value, {d} for percentage
454        let value = format_float(self.value);
455        let percentage = format_float(self.percentage * 100.0) + "%";
456        if self.formatter.is_empty() {
457            return value;
458        }
459        self.formatter
460            .replace(SERIES_NAME_FORMAT_LABEL, &self.series_name)
461            .replace(CATEGORY_NAME_FORMAT_LABEL, &self.category_name)
462            .replace(VALUE_FORMAT_LABEL, &value)
463            .replace(PERCENTAGE_FORMAT_LABEL, &percentage)
464            .replace(THOUSANDS_FORMAT_LABEL, &thousands_format_float(self.value))
465    }
466}
467
468pub fn format_string(value: &str, formatter: &str) -> String {
469    if formatter.is_empty() {
470        value.to_string()
471    } else {
472        formatter
473            .replace(VALUE_FORMAT_LABEL, value)
474            .replace(THOUSANDS_FORMAT_LABEL, value)
475    }
476}
477
478pub(crate) fn get_pie_point(cx: f32, cy: f32, r: f32, angle: f32) -> Point {
479    let value = angle / 180.0 * std::f32::consts::PI;
480    let x = cx + r * value.sin();
481    let y = cy - r * value.cos();
482    Point { x, y }
483}
484pub(crate) fn get_box_of_points(points: &[Point]) -> Box {
485    let mut b = Box {
486        left: f32::MAX,
487        top: f32::MAX,
488        ..Default::default()
489    };
490    for p in points.iter() {
491        if p.x < b.left {
492            b.left = p.x;
493        }
494        if p.x > b.right {
495            b.right = p.x;
496        }
497        if p.y < b.top {
498            b.top = p.y;
499        }
500        if p.y > b.bottom {
501            b.bottom = p.y;
502        }
503    }
504    b
505}
506
507#[cfg(test)]
508mod tests {
509    use crate::{AxisScale, thousands_format_float};
510
511    use super::{
512        AxisValueParams, Box, Point, convert_to_points, format_float, get_axis_values,
513        get_box_of_points,
514    };
515    use pretty_assertions::assert_eq;
516
517    #[test]
518    fn point() {
519        let p: Point = (1.2, 1.3).into();
520
521        assert_eq!(1.2, p.x);
522        assert_eq!(1.3, p.y);
523    }
524
525    #[test]
526    fn box_width_height() {
527        let b: Box = (10.0).into();
528
529        assert_eq!(10.0, b.left);
530        assert_eq!(10.0, b.top);
531        assert_eq!(10.0, b.right);
532        assert_eq!(10.0, b.bottom);
533        assert_eq!(0.0, b.width());
534        assert_eq!(10.0, b.outer_width());
535        assert_eq!(0.0, b.height());
536        assert_eq!(10.0, b.outer_height());
537
538        let b: Box = (5.0, 10.0, 30.0, 50.0).into();
539        assert_eq!(5.0, b.left);
540        assert_eq!(10.0, b.top);
541        assert_eq!(30.0, b.right);
542        assert_eq!(50.0, b.bottom);
543        assert_eq!(25.0, b.width());
544        assert_eq!(30.0, b.outer_width());
545        assert_eq!(40.0, b.height());
546        assert_eq!(50.0, b.outer_height());
547    }
548
549    #[test]
550    fn format() {
551        assert_eq!("1", format_float(1.0));
552        assert_eq!("1.1", format_float(1.12));
553        assert_eq!("100.1", format_float(100.14));
554        assert_eq!("100", format_float(100.04));
555        assert_eq!("1000.1", format_float(1000.14));
556    }
557    #[test]
558    fn thousands_format() {
559        assert_eq!("1", thousands_format_float(1.0));
560        assert_eq!("1.1", thousands_format_float(1.12));
561        assert_eq!("100.1", thousands_format_float(100.14));
562        assert_eq!("100", thousands_format_float(100.04));
563        assert_eq!("1,000", thousands_format_float(1000.14));
564        assert_eq!("100,000", thousands_format_float(100000.14));
565        assert_eq!("1,000,000", thousands_format_float(1_000_000.1));
566    }
567
568    #[test]
569    fn axis_values() {
570        let values = get_axis_values(AxisValueParams {
571            data_list: vec![1.0, 10.0, 13.5, 18.9],
572            ..Default::default()
573        });
574
575        assert_eq!(vec!["0", "4", "8", "12", "16", "20", "24"], values.data);
576        assert_eq!(0.0, values.min);
577        assert_eq!(24.0, values.max);
578        assert_eq!(24.0, values.get_offset());
579        assert_eq!(50.0, values.get_offset_height(12.0, 100.0));
580    }
581
582    #[test]
583    fn axis_values_log() {
584        // Base-10 log scale over [1, 1000]: ticks at 1, 10, 100, 1000
585        let values = get_axis_values(AxisValueParams {
586            data_list: vec![1.0, 5.0, 100.0, 800.0],
587            scale: AxisScale::Log(10.0),
588            ..Default::default()
589        });
590        // exp_min = floor(log10(1)) = 0  → 10^0 = 1
591        // exp_max = ceil(log10(800)) = 3 → 10^3 = 1000
592        assert_eq!(1.0, values.min);
593        assert_eq!(1000.0, values.max);
594        // Ticks: 1, 10, 100, 1000  (step=1, 4 ticks ≤ split_number=6+1)
595        assert_eq!(vec!["1", "10", "100", "1k"], values.data);
596        // 10 is at 1/3 of the log range → pixel = 100 - 33.3.. = 66.6..
597        let h = values.get_offset_height(10.0, 100.0);
598        assert!((h - 66.67).abs() < 0.1, "expected ~66.67, got {h}");
599        // 100 is at 2/3 → pixel = 100 - 66.6.. = 33.3..
600        let h = values.get_offset_height(100.0, 100.0);
601        assert!((h - 33.33).abs() < 0.1, "expected ~33.33, got {h}");
602        // min maps to max_height, max maps to 0
603        assert!((values.get_offset_height(1.0, 100.0) - 100.0).abs() < 0.01);
604        assert!((values.get_offset_height(1000.0, 100.0)).abs() < 0.01);
605    }
606
607    #[test]
608    fn get_box() {
609        let points: Vec<Point> = convert_to_points(&[
610            (2.0, 10.0),
611            (50.0, 10.0),
612            (50.0, 30.0),
613            (150.0, 30.0),
614            (150.0, 80.0),
615            (210.0, 60.0),
616            (250.0, 90.0),
617        ]);
618        let b = get_box_of_points(&points);
619        assert_eq!(2.0, b.left);
620        assert_eq!(10.0, b.top);
621        assert_eq!(250.0, b.right);
622        assert_eq!(90.0, b.bottom);
623    }
624}