chart_js_rs/
utils.rs

1use js_sys::{Array, Object, Reflect};
2use std::cell::RefCell;
3use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
4
5use crate::{exports::*, BoolString, FnWithArgs, FnWithArgsOrT, NumberString};
6
7pub fn get_order_fn(
8    lhs: &crate::NumberOrDateString,
9    rhs: &crate::NumberOrDateString,
10) -> std::cmp::Ordering {
11    crate::utils::ORDER_FN.with_borrow(|f| f(lhs, rhs))
12}
13pub fn set_order_fn<
14    F: Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering + 'static,
15>(
16    f: F,
17) {
18    let _ = ORDER_FN.replace(Box::new(f));
19}
20
21thread_local! {
22    #[allow(clippy::type_complexity)]
23    pub static ORDER_FN: RefCell<
24        Box<dyn Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering>,
25    > = RefCell::new({
26        Box::new(
27            |lhs: &crate::NumberOrDateString, rhs: &crate::NumberOrDateString| -> std::cmp::Ordering {
28                lhs.cmp(rhs)
29            },
30        )as Box<_>
31    });
32}
33
34pub fn uncircle_chartjs_value_to_serde_json_value(
35    js: impl AsRef<JsValue>,
36) -> Result<serde_json::Value, String> {
37    // this makes sure we don't get any circular objects, `JsValue` allows this, `serde_json::Value` does not!
38    let blacklist_function =
39        js_sys::Function::new_with_args("key, val", "if (!key.startsWith('$')) { return val; }");
40    let js_string =
41        js_sys::JSON::stringify_with_replacer(js.as_ref(), &JsValue::from(blacklist_function))
42            .map_err(|e| e.as_string().unwrap_or_default())?
43            .as_string()
44            .unwrap();
45
46    serde_json::from_str(&js_string).map_err(|e| e.to_string())
47}
48
49#[wasm_bindgen]
50#[derive(Clone)]
51#[must_use = "\nAppend .render()\n"]
52pub struct Chart {
53    pub(crate) obj: JsValue,
54    pub(crate) id: String,
55    pub(crate) mutate: bool,
56    pub(crate) plugins: String,
57    pub(crate) defaults: String,
58}
59
60/// Walks the JsValue object to get the value of a nested property
61/// using the JS dot notation
62fn get_path(j: &JsValue, item: &str) -> Option<JsValue> {
63    let mut path = item.split('.');
64    let item = &path.next().unwrap().to_string().into();
65    let k = Reflect::get(j, item);
66
67    if k.is_err() {
68        return None;
69    }
70
71    let k = k.unwrap();
72    if path.clone().count() > 0 {
73        return get_path(&k, path.collect::<Vec<&str>>().join(".").as_str());
74    }
75
76    Some(k)
77}
78
79/// Get values of an object as an array at the given path.
80/// See get_path()
81fn object_values_at(j: &JsValue, item: &str) -> Option<JsValue> {
82    let o = get_path(j, item);
83    o.and_then(|o| {
84        if o == JsValue::UNDEFINED {
85            None
86        } else {
87            Some(o)
88        }
89    })
90}
91
92impl Chart {
93    // pub fn new(chart: JsValue, id: String) -> Option<Self> {
94    //     chart.is_object().then_some(Self{
95    //         obj: chart,
96    //         id,
97    //         mutate: false,
98    //         plugins: String::new(),
99    //     })
100    // }
101
102    #[must_use = "\nAppend .render()\n"]
103    pub fn mutate(&mut self) -> Self {
104        self.mutate = true;
105        self.clone()
106    }
107
108    #[must_use = "\nAppend .render()\n"]
109    pub fn plugins(&mut self, plugins: impl Into<String>) -> Self {
110        self.plugins = plugins.into();
111        self.clone()
112    }
113
114    #[must_use = "\nAppend .render()\n"]
115    pub fn defaults(&mut self, defaults: impl Into<String>) -> Self {
116        self.defaults = format!("{}\n{}", self.defaults, defaults.into());
117        self.to_owned()
118    }
119
120    pub fn render(self) {
121        self.rationalise_js();
122        render_chart(self.obj, &self.id, self.mutate, self.plugins, self.defaults);
123    }
124
125    pub fn update(self, animate: bool) -> bool {
126        self.rationalise_js();
127        update_chart(self.obj, &self.id, animate)
128    }
129
130    /// Converts serialized FnWithArgs to JS Function's
131    /// For new chart options, this will need to be updated
132    pub fn rationalise_js(&self) {
133        // Handle data.datasets
134        Array::from(&get_path(&self.obj, "data.datasets").unwrap())
135            .iter()
136            .for_each(|dataset| {
137                FnWithArgsOrT::<2, String>::rationalise_1_level(&dataset, "backgroundColor");
138                FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderDash"));
139                FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderColor"));
140                FnWithArgsOrT::<1, String>::rationalise_2_levels(&dataset, ("datalabels", "align"));
141                FnWithArgsOrT::<1, String>::rationalise_2_levels(
142                    &dataset,
143                    ("datalabels", "anchor"),
144                );
145                FnWithArgsOrT::<1, String>::rationalise_2_levels(
146                    &dataset,
147                    ("datalabels", "backgroundColor"),
148                );
149                FnWithArgs::<2>::rationalise_2_levels(&dataset, ("datalabels", "formatter"));
150                FnWithArgsOrT::<1, NumberString>::rationalise_2_levels(
151                    &dataset,
152                    ("datalabels", "offset"),
153                );
154                FnWithArgsOrT::<1, BoolString>::rationalise_2_levels(
155                    &dataset,
156                    ("datalabels", "display"),
157                );
158            });
159
160        // Handle options.scales
161        if let Some(scales) = object_values_at(&self.obj, "options.scales") {
162            Object::values(&scales.dyn_into().unwrap())
163                .iter()
164                .for_each(|scale| {
165                    FnWithArgs::<3>::rationalise_2_levels(&scale, ("ticks", "callback"));
166                });
167        }
168
169        // Handle options.plugins.legend
170        if let Some(legend) = object_values_at(&self.obj, "options.plugins.legend") {
171            FnWithArgs::<2>::rationalise_2_levels(&legend, ("labels", "filter"));
172            FnWithArgs::<3>::rationalise_2_levels(&legend, ("labels", "sort"));
173        }
174        // Handle options.plugins.tooltip
175        if let Some(legend) = object_values_at(&self.obj, "options.plugins.tooltip") {
176            FnWithArgs::<1>::rationalise_1_level(&legend, "filter");
177            FnWithArgs::<1>::rationalise_2_levels(&legend, ("callbacks", "label"));
178            FnWithArgs::<1>::rationalise_2_levels(&legend, ("callbacks", "title"));
179        }
180    }
181}