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 serde::{Deserialize, Serialize};
14use std::fmt;
15use substring::Substring;
16
17pub static NIL_VALUE: f32 = f32::MIN;
18
19pub(crate) static THOUSANDS_FORMAT_LABEL: &str = "{t}";
20pub(crate) static SERIES_NAME_FORMAT_LABEL: &str = "{a}";
21pub(crate) static CATEGORY_NAME_FORMAT_LABEL: &str = "{b}";
22pub(crate) static VALUE_FORMAT_LABEL: &str = "{c}";
23pub(crate) static PERCENTAGE_FORMAT_LABEL: &str = "{d}";
24
25#[derive(Clone, Copy, PartialEq, Debug, Default)]
26pub struct Point {
27    pub x: f32,
28    pub y: f32,
29}
30impl From<(f32, f32)> for Point {
31    fn from(val: (f32, f32)) -> Self {
32        Point { x: val.0, y: val.1 }
33    }
34}
35impl fmt::Display for Point {
36    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37        let m = format!("({},{})", format_float(self.x), format_float(self.y));
38        write!(f, "{m}")
39    }
40}
41
42#[derive(Serialize, Deserialize, Clone, Debug, Default)]
43pub struct Box {
44    pub left: f32,
45    pub top: f32,
46    pub right: f32,
47    pub bottom: f32,
48}
49impl Box {
50    pub fn width(&self) -> f32 {
51        self.right - self.left
52    }
53    pub fn height(&self) -> f32 {
54        self.bottom - self.top
55    }
56    pub fn outer_width(&self) -> f32 {
57        self.right
58    }
59    pub fn outer_height(&self) -> f32 {
60        self.bottom
61    }
62}
63impl fmt::Display for Box {
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        let m = format!(
66            "({},{},{},{})",
67            format_float(self.left),
68            format_float(self.top),
69            format_float(self.right),
70            format_float(self.bottom)
71        );
72        write!(f, "{m}")
73    }
74}
75
76impl From<f32> for Box {
77    fn from(val: f32) -> Self {
78        Box {
79            left: val,
80            top: val,
81            right: val,
82            bottom: val,
83        }
84    }
85}
86impl From<(f32, f32)> for Box {
87    fn from(val: (f32, f32)) -> Self {
88        Box {
89            left: val.0,
90            top: val.1,
91            right: val.0,
92            bottom: val.1,
93        }
94    }
95}
96impl From<(f32, f32, f32)> for Box {
97    fn from(val: (f32, f32, f32)) -> Self {
98        Box {
99            left: val.0,
100            top: val.1,
101            right: val.2,
102            bottom: val.1,
103        }
104    }
105}
106impl From<(f32, f32, f32, f32)> for Box {
107    fn from(val: (f32, f32, f32, f32)) -> Self {
108        Box {
109            left: val.0,
110            top: val.1,
111            right: val.2,
112            bottom: val.3,
113        }
114    }
115}
116
117fn parse_precision(formatter: &str) -> Option<usize> {
118    if formatter.is_empty() {
119        return None;
120    }
121    // 1. parse usize
122    if let Ok(precision) = formatter.parse::<usize>() {
123        return Some(precision);
124    }
125
126    // 2. if formatter is "{:.N}", parse N
127    if let Some(inner) = formatter
128        .strip_prefix("{:.")
129        .and_then(|s| s.strip_suffix("}"))
130    {
131        if let Ok(precision) = inner.parse::<usize>() {
132            return Some(precision);
133        }
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}
202#[derive(Clone, Debug, Default)]
203pub struct AxisValues {
204    pub data: Vec<String>,
205    pub min: f32,
206    pub max: f32,
207}
208
209impl AxisValues {
210    fn get_offset(&self) -> f32 {
211        self.max - self.min
212    }
213    pub(crate) fn get_offset_height(&self, value: f32, max_height: f32) -> f32 {
214        let percent = (value - self.min) / self.get_offset();
215        max_height - percent * max_height
216    }
217}
218
219const K_VALUE: f32 = 1000.00_f32;
220const M_VALUE: f32 = K_VALUE * K_VALUE;
221const G_VALUE: f32 = M_VALUE * K_VALUE;
222const T_VALUE: f32 = G_VALUE * K_VALUE;
223
224pub(crate) fn get_axis_values(params: AxisValueParams) -> AxisValues {
225    let mut min = f32::MAX;
226    let mut max = f32::MIN;
227
228    let mut split_number = params.split_number;
229    if split_number == 0 {
230        split_number = 6;
231    }
232    for item in params.data_list.iter() {
233        let value = item.to_owned();
234        if value == NIL_VALUE {
235            continue;
236        }
237        if value > max {
238            max = value;
239        }
240        if value < min {
241            min = value;
242        }
243    }
244    let mut is_custom_min = false;
245
246    if let Some(value) = params.min {
247        if value < min {
248            min = value;
249            is_custom_min = true;
250        }
251    }
252    // it should use 0, if min gt 0 and not custom value
253    if !is_custom_min && min > 0.0 {
254        min = 0.0;
255    }
256    let mut is_custom_max = false;
257    if let Some(value) = params.max {
258        if value > max {
259            max = value;
260            is_custom_max = true
261        }
262    }
263    let mut unit = (max - min) / split_number as f32;
264    if !is_custom_max {
265        let ceil_value = (unit * 10.0).ceil();
266        if ceil_value < 12.0 {
267            unit = ceil_value / 10.0;
268        } else {
269            let mut new_unit = unit as i32;
270            let adjust_unit = |current: i32, small_unit: i32| -> i32 {
271                if current % small_unit == 0 {
272                    return current + small_unit;
273                }
274                ((current / small_unit) + 1) * small_unit
275            };
276            if new_unit < 10 {
277                new_unit = adjust_unit(new_unit, 2);
278            } else if new_unit < 100 {
279                new_unit = adjust_unit(new_unit, 5);
280            } else if new_unit < 500 {
281                new_unit = adjust_unit(new_unit, 10);
282            } else if new_unit < 1000 {
283                new_unit = adjust_unit(new_unit, 20);
284            } else if new_unit < 5000 {
285                new_unit = adjust_unit(new_unit, 50);
286            } else if new_unit < 10000 {
287                new_unit = adjust_unit(new_unit, 100);
288            } else {
289                let small_unit = ((max - min) / 20.0) as i32;
290                new_unit = adjust_unit(new_unit, small_unit / 100 * 100);
291            }
292            unit = new_unit as f32;
293        }
294    }
295    let split_unit = unit;
296
297    let mut data = vec![];
298    for i in 0..=split_number {
299        let mut value = min + (i as f32) * split_unit;
300        if params.thousands_format {
301            data.push(thousands_format_float(value));
302            continue;
303        }
304        let mut unit = "";
305        value = if value >= T_VALUE {
306            unit = "T";
307            value / T_VALUE
308        } else if value >= G_VALUE {
309            unit = "G";
310            value / G_VALUE
311        } else if value >= M_VALUE {
312            unit = "M";
313            value / M_VALUE
314        } else if value >= K_VALUE {
315            unit = "k";
316            value / K_VALUE
317        } else {
318            value
319        };
320        data.push(format_float(value) + unit);
321    }
322    if params.reverse.unwrap_or_default() {
323        data.reverse();
324    }
325
326    AxisValues {
327        data,
328        min,
329        max: min + split_unit * split_number as f32,
330    }
331}
332pub fn convert_to_points(values: &[(f32, f32)]) -> Vec<Point> {
333    values.iter().map(|item| item.to_owned().into()).collect()
334}
335
336pub fn get_quadrant(cx: f32, cy: f32, point: &Point) -> u8 {
337    if point.x > cx {
338        if point.y > cy {
339            4
340        } else {
341            1
342        }
343    } else if point.y > cy {
344        3
345    } else {
346        2
347    }
348}
349
350#[derive(Clone, Debug, Default)]
351pub(crate) struct LabelOption {
352    pub series_name: String,
353    pub category_name: String,
354    pub value: f32,
355    pub percentage: f32,
356    pub formatter: String,
357}
358impl LabelOption {
359    pub fn format(&self) -> String {
360        // {a} for series name, {b} for category name, {c} for data value, {d} for percentage
361        let value = format_float(self.value);
362        let percentage = format_float(self.percentage * 100.0) + "%";
363        if self.formatter.is_empty() {
364            return value;
365        }
366        self.formatter
367            .replace(SERIES_NAME_FORMAT_LABEL, &self.series_name)
368            .replace(CATEGORY_NAME_FORMAT_LABEL, &self.category_name)
369            .replace(VALUE_FORMAT_LABEL, &value)
370            .replace(PERCENTAGE_FORMAT_LABEL, &percentage)
371            .replace(THOUSANDS_FORMAT_LABEL, &thousands_format_float(self.value))
372    }
373}
374
375pub fn format_string(value: &str, formatter: &str) -> String {
376    if formatter.is_empty() {
377        value.to_string()
378    } else {
379        formatter
380            .replace(VALUE_FORMAT_LABEL, value)
381            .replace(THOUSANDS_FORMAT_LABEL, value)
382    }
383}
384
385pub(crate) fn get_pie_point(cx: f32, cy: f32, r: f32, angle: f32) -> Point {
386    let value = angle / 180.0 * std::f32::consts::PI;
387    let x = cx + r * value.sin();
388    let y = cy - r * value.cos();
389    Point { x, y }
390}
391pub(crate) fn get_box_of_points(points: &[Point]) -> Box {
392    let mut b = Box {
393        left: f32::MAX,
394        top: f32::MAX,
395        ..Default::default()
396    };
397    for p in points.iter() {
398        if p.x < b.left {
399            b.left = p.x;
400        }
401        if p.x > b.right {
402            b.right = p.x;
403        }
404        if p.y < b.top {
405            b.top = p.y;
406        }
407        if p.y > b.bottom {
408            b.bottom = p.y;
409        }
410    }
411    b
412}
413
414#[cfg(test)]
415mod tests {
416    use crate::thousands_format_float;
417
418    use super::{
419        convert_to_points, format_float, get_axis_values, get_box_of_points, AxisValueParams, Box,
420        Point,
421    };
422    use pretty_assertions::assert_eq;
423
424    #[test]
425    fn point() {
426        let p: Point = (1.2, 1.3).into();
427
428        assert_eq!(1.2, p.x);
429        assert_eq!(1.3, p.y);
430    }
431
432    #[test]
433    fn box_width_height() {
434        let b: Box = (10.0).into();
435
436        assert_eq!(10.0, b.left);
437        assert_eq!(10.0, b.top);
438        assert_eq!(10.0, b.right);
439        assert_eq!(10.0, b.bottom);
440        assert_eq!(0.0, b.width());
441        assert_eq!(10.0, b.outer_width());
442        assert_eq!(0.0, b.height());
443        assert_eq!(10.0, b.outer_height());
444
445        let b: Box = (5.0, 10.0, 30.0, 50.0).into();
446        assert_eq!(5.0, b.left);
447        assert_eq!(10.0, b.top);
448        assert_eq!(30.0, b.right);
449        assert_eq!(50.0, b.bottom);
450        assert_eq!(25.0, b.width());
451        assert_eq!(30.0, b.outer_width());
452        assert_eq!(40.0, b.height());
453        assert_eq!(50.0, b.outer_height());
454    }
455
456    #[test]
457    fn format() {
458        assert_eq!("1", format_float(1.0));
459        assert_eq!("1.1", format_float(1.12));
460        assert_eq!("100.1", format_float(100.14));
461        assert_eq!("100", format_float(100.04));
462        assert_eq!("1000.1", format_float(1000.14));
463    }
464    #[test]
465    fn thousands_format() {
466        assert_eq!("1", thousands_format_float(1.0));
467        assert_eq!("1.1", thousands_format_float(1.12));
468        assert_eq!("100.1", thousands_format_float(100.14));
469        assert_eq!("100", thousands_format_float(100.04));
470        assert_eq!("1,000", thousands_format_float(1000.14));
471        assert_eq!("100,000", thousands_format_float(100000.14));
472        assert_eq!("1,000,000", thousands_format_float(1_000_000.1));
473    }
474
475    #[test]
476    fn axis_values() {
477        let values = get_axis_values(AxisValueParams {
478            data_list: vec![1.0, 10.0, 13.5, 18.9],
479            ..Default::default()
480        });
481
482        assert_eq!(vec!["0", "4", "8", "12", "16", "20", "24"], values.data);
483        assert_eq!(0.0, values.min);
484        assert_eq!(24.0, values.max);
485        assert_eq!(24.0, values.get_offset());
486        assert_eq!(50.0, values.get_offset_height(12.0, 100.0));
487    }
488
489    #[test]
490    fn get_box() {
491        let points: Vec<Point> = convert_to_points(&[
492            (2.0, 10.0),
493            (50.0, 10.0),
494            (50.0, 30.0),
495            (150.0, 30.0),
496            (150.0, 80.0),
497            (210.0, 60.0),
498            (250.0, 90.0),
499        ]);
500        let b = get_box_of_points(&points);
501        assert_eq!(2.0, b.left);
502        assert_eq!(10.0, b.top);
503        assert_eq!(250.0, b.right);
504        assert_eq!(90.0, b.bottom);
505    }
506}