chart_js_rs/
utils.rs

1use std::cell::RefCell;
2
3use js_sys::{Array, Function, Object, Reflect};
4use serde::{de, Deserialize, Serialize};
5use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
6
7use crate::{exports::*, FnWithArgsOrAny};
8
9pub fn get_order_fn(
10    lhs: &crate::NumberOrDateString,
11    rhs: &crate::NumberOrDateString,
12) -> std::cmp::Ordering {
13    crate::utils::ORDER_FN.with_borrow(|f| f(lhs, rhs))
14}
15pub fn set_order_fn<
16    F: Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering + 'static,
17>(
18    f: F,
19) {
20    let _ = ORDER_FN.replace(Box::new(f));
21}
22
23thread_local! {
24    #[allow(clippy::type_complexity)]
25    pub static ORDER_FN: RefCell<
26        Box<dyn Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering>,
27    > = RefCell::new({
28        Box::new(
29            |lhs: &crate::NumberOrDateString, rhs: &crate::NumberOrDateString| -> std::cmp::Ordering {
30                lhs.cmp(rhs)
31            },
32        )as Box<_>
33    });
34}
35
36pub fn uncircle_chartjs_value_to_serde_json_value(
37    js: impl AsRef<JsValue>,
38) -> Result<serde_json::Value, String> {
39    // this makes sure we don't get any circular objects, `JsValue` allows this, `serde_json::Value` does not!
40    let blacklist_function =
41        js_sys::Function::new_with_args("key, val", "if (!key.startsWith('$')) { return val; }");
42    let js_string =
43        js_sys::JSON::stringify_with_replacer(js.as_ref(), &JsValue::from(blacklist_function))
44            .map_err(|e| e.as_string().unwrap_or_default())?
45            .as_string()
46            .unwrap();
47
48    serde_json::from_str(&js_string).map_err(|e| e.to_string())
49}
50
51fn rationalise_1_level<const N: usize>(obj: &JsValue, name: &'static str) {
52    if let Ok(a) = Reflect::get(obj, &name.into()) {
53        // If the property is undefined, dont try serialize it
54        if a == JsValue::UNDEFINED {
55            return;
56        }
57
58        if let Ok(o) = serde_wasm_bindgen::from_value::<FnWithArgsOrAny<N>>(a) {
59            match o {
60                FnWithArgsOrAny::Any(_) => (),
61                FnWithArgsOrAny::FnWithArgs(fnwa) => {
62                    let _ = Reflect::set(obj, &name.into(), &fnwa.build());
63                }
64            }
65        }
66    }
67}
68fn rationalise_2_levels<const N: usize>(obj: &JsValue, name: (&'static str, &'static str)) {
69    if let Ok(a) = Reflect::get(obj, &name.0.into()) {
70        // If the property is undefined, dont try serialize it
71        if a == JsValue::UNDEFINED {
72            return;
73        }
74
75        if let Ok(b) = Reflect::get(&a, &name.1.into()) {
76            // If the property is undefined, dont try serialize it
77            if b == JsValue::UNDEFINED {
78                return;
79            }
80
81            if let Ok(o) = serde_wasm_bindgen::from_value::<FnWithArgsOrAny<N>>(b) {
82                match o {
83                    FnWithArgsOrAny::Any(_) => (),
84                    FnWithArgsOrAny::FnWithArgs(fnwa) => {
85                        let _ = Reflect::set(&a, &name.1.into(), &fnwa.build());
86                    }
87                }
88            }
89        }
90    }
91}
92
93#[wasm_bindgen]
94#[derive(Clone)]
95#[must_use = "\nAppend .render()\n"]
96pub struct Chart {
97    pub(crate) obj: JsValue,
98    pub(crate) id: String,
99    pub(crate) mutate: bool,
100    pub(crate) plugins: String,
101    pub(crate) defaults: String,
102}
103
104/// Walks the JsValue object to get the value of a nested property
105/// using the JS dot notation
106fn get_path(j: &JsValue, item: &str) -> Option<JsValue> {
107    let mut path = item.split('.');
108    let item = &path.next().unwrap().to_string().into();
109    let k = Reflect::get(j, item);
110
111    if k.is_err() {
112        return None;
113    }
114
115    let k = k.unwrap();
116    if path.clone().count() > 0 {
117        return get_path(&k, path.collect::<Vec<&str>>().join(".").as_str());
118    }
119
120    Some(k)
121}
122
123/// Get values of an object as an array at the given path.
124/// See get_path()
125fn object_values_at(j: &JsValue, item: &str) -> Option<JsValue> {
126    let o = get_path(j, item);
127    o.and_then(|o| {
128        if o == JsValue::UNDEFINED {
129            None
130        } else {
131            Some(o)
132        }
133    })
134}
135
136impl Chart {
137    // pub fn new(chart: JsValue, id: String) -> Option<Self> {
138    //     chart.is_object().then_some(Self{
139    //         obj: chart,
140    //         id,
141    //         mutate: false,
142    //         plugins: String::new(),
143    //     })
144    // }
145
146    #[must_use = "\nAppend .render()\n"]
147    pub fn mutate(&mut self) -> Self {
148        self.mutate = true;
149        self.clone()
150    }
151
152    #[must_use = "\nAppend .render()\n"]
153    pub fn plugins(&mut self, plugins: impl Into<String>) -> Self {
154        self.plugins = plugins.into();
155        self.clone()
156    }
157
158    #[must_use = "\nAppend .render()\n"]
159    pub fn defaults(&mut self, defaults: impl Into<String>) -> Self {
160        self.defaults = format!("{}\n{}", self.defaults, defaults.into());
161        self.to_owned()
162    }
163
164    pub fn render(self) {
165        self.rationalise_js();
166        render_chart(self.obj, &self.id, self.mutate, self.plugins, self.defaults);
167    }
168
169    pub fn update(self, animate: bool) -> bool {
170        self.rationalise_js();
171        update_chart(self.obj, &self.id, animate)
172    }
173
174    /// Converts serialized FnWithArgs to JS Function's
175    /// For new chart options, this will need to be updated
176    pub fn rationalise_js(&self) {
177        // Handle data.datasets
178        Array::from(&get_path(&self.obj, "data.datasets").unwrap())
179            .iter()
180            .for_each(|dataset| {
181                rationalise_1_level::<2>(&dataset, "backgroundColor");
182                rationalise_2_levels::<1>(&dataset, ("segment", "borderDash"));
183                rationalise_2_levels::<1>(&dataset, ("segment", "borderColor"));
184                rationalise_2_levels::<1>(&dataset, ("datalabels", "align"));
185                rationalise_2_levels::<1>(&dataset, ("datalabels", "anchor"));
186                rationalise_2_levels::<1>(&dataset, ("datalabels", "backgroundColor"));
187                rationalise_2_levels::<2>(&dataset, ("datalabels", "formatter"));
188                rationalise_2_levels::<1>(&dataset, ("datalabels", "offset"));
189            });
190
191        // Handle options.scales
192        if let Some(scales) = object_values_at(&self.obj, "options.scales") {
193            Object::values(&scales.dyn_into().unwrap())
194                .iter()
195                .for_each(|scale| {
196                    rationalise_2_levels::<3>(&scale, ("ticks", "callback"));
197                });
198        }
199
200        // Handle options.plugins.legend
201        if let Some(legend) = object_values_at(&self.obj, "options.plugins.legend") {
202            rationalise_2_levels::<2>(&legend, ("labels", "filter"));
203        }
204        // Handle options.plugins.tooltip
205        if let Some(legend) = object_values_at(&self.obj, "options.plugins.tooltip") {
206            rationalise_1_level::<1>(&legend, "filter");
207            rationalise_2_levels::<1>(&legend, ("callbacks", "label"));
208            rationalise_2_levels::<1>(&legend, ("callbacks", "title"));
209        }
210    }
211}
212
213#[derive(Debug, Deserialize, Serialize)]
214struct JavascriptFunction {
215    args: Vec<String>,
216    body: String,
217    return_value: String,
218    closure_id: Option<String>,
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
222pub struct FnWithArgs<const N: usize> {
223    pub(crate) args: [String; N],
224    pub(crate) body: String,
225    pub(crate) return_value: String,
226    pub(crate) closure_id: Option<String>,
227}
228const ALPHABET: [&str; 32] = [
229    "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
230    "t", "u", "v", "w", "x", "y", "z", "aa", "bb", "cc", "dd", "ee", "ff",
231];
232impl<const N: usize> Default for FnWithArgs<N> {
233    fn default() -> Self {
234        Self {
235            args: (0..N)
236                .map(|idx| ALPHABET[idx].to_string())
237                .collect::<Vec<_>>()
238                .try_into()
239                .unwrap(),
240            body: Default::default(),
241            return_value: Default::default(),
242            closure_id: None,
243        }
244    }
245}
246impl<'de, const N: usize> Deserialize<'de> for FnWithArgs<N> {
247    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
248    where
249        D: serde::Deserializer<'de>,
250    {
251        let js = JavascriptFunction::deserialize(deserializer)?;
252        Ok(FnWithArgs::<N> {
253            args: js.args.clone().try_into().map_err(|_| {
254                de::Error::custom(format!("Array had length {}, needed {}.", js.args.len(), N))
255            })?,
256            body: js.body,
257            return_value: js.return_value,
258            closure_id: js.closure_id,
259        })
260    }
261}
262impl<const N: usize> Serialize for FnWithArgs<N> {
263    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
264    where
265        S: serde::Serializer,
266    {
267        JavascriptFunction::serialize(
268            &JavascriptFunction {
269                args: self.args.to_vec(),
270                body: self.body.clone(),
271                return_value: self.return_value.clone(),
272                closure_id: self.closure_id.clone(),
273            },
274            serializer,
275        )
276    }
277}
278
279impl<const N: usize> FnWithArgs<N> {
280    pub fn is_empty(&self) -> bool {
281        self.body.is_empty()
282    }
283
284    pub fn new() -> Self {
285        Self::default()
286    }
287
288    pub fn args<S: AsRef<str>>(mut self, args: [S; N]) -> Self {
289        self.args = args
290            .into_iter()
291            .enumerate()
292            .map(|(idx, s)| {
293                let arg = s.as_ref();
294                if arg.is_empty() { ALPHABET[idx] } else { arg }.to_string()
295            })
296            .collect::<Vec<_>>()
297            .try_into()
298            .unwrap();
299        self
300    }
301
302    pub fn js_body(mut self, body: &str) -> Self {
303        self.body = format!("{}\n{body}", self.body);
304        self.to_owned()
305    }
306
307    pub fn js_return_value(self, return_value: &str) -> Self {
308        let mut s = if self.body.is_empty() {
309            self.js_body("")
310        } else {
311            self
312        };
313        s.return_value = return_value.to_string();
314        s.to_owned()
315    }
316
317    pub fn build(self) -> Function {
318        if let Some(id) = self.closure_id {
319            let args = self.args.join(", ");
320            Function::new_with_args(&args, &format!("{{ return window['{id}']({args}) }}"))
321        } else {
322            Function::new_with_args(
323                &self.args.join(", "),
324                &format!("{{ {}\nreturn {} }}", self.body, self.return_value),
325            )
326        }
327    }
328}
329
330impl FnWithArgs<1> {
331    pub fn run_rust_fn<A, B, FN: Fn(A) -> B>(mut self, _func: FN) -> Self {
332        let fn_name = std::any::type_name::<FN>()
333            .split("::")
334            .collect::<Vec<_>>()
335            .into_iter()
336            .next_back()
337            .unwrap();
338
339        self.body = format!(
340            "{}\nconst _out_ = window.callbacks.{}({});",
341            self.body,
342            fn_name,
343            self.args.join(", ")
344        );
345        self.js_return_value("_out_")
346    }
347
348    #[track_caller]
349    pub fn rust_closure<F: Fn(JsValue) -> JsValue + 'static>(mut self, closure: F) -> Self {
350        let js_closure = wasm_bindgen::closure::Closure::wrap(
351            Box::new(closure) as Box<dyn Fn(JsValue) -> JsValue>
352        );
353        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
354
355        let js_window = gloo_utils::window();
356        let id = uuid::Uuid::new_v4().to_string();
357        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
358        js_closure.forget();
359
360        gloo_console::debug!(format!(
361            "Closure at {}:{}:{} set at window.['{id}'].",
362            file!(),
363            line!(),
364            column!()
365        ));
366        self.closure_id = Some(id);
367        self
368    }
369}
370
371impl FnWithArgs<2> {
372    pub fn run_rust_fn<A, B, C, FN: Fn(A, B) -> C>(mut self, _func: FN) -> Self {
373        let fn_name = std::any::type_name::<FN>()
374            .split("::")
375            .collect::<Vec<_>>()
376            .into_iter()
377            .next_back()
378            .unwrap();
379
380        self.body = format!(
381            "{}\nconst _out_ = window.callbacks.{}({});",
382            self.body,
383            fn_name,
384            self.args.join(", ")
385        );
386        self.js_return_value("_out_")
387    }
388
389    #[track_caller]
390    pub fn rust_closure<F: Fn(JsValue, JsValue) -> JsValue + 'static>(
391        mut self,
392        closure: F,
393    ) -> Self {
394        let js_closure = wasm_bindgen::closure::Closure::wrap(
395            Box::new(closure) as Box<dyn Fn(JsValue, JsValue) -> JsValue>
396        );
397        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
398
399        let js_window = gloo_utils::window();
400        let id = uuid::Uuid::new_v4().to_string();
401        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
402        js_closure.forget();
403
404        gloo_console::debug!(format!(
405            "Closure at {}:{}:{} set at window.['{id}'].",
406            file!(),
407            line!(),
408            column!()
409        ));
410        self.closure_id = Some(id);
411        self
412    }
413}
414
415impl FnWithArgs<3> {
416    pub fn run_rust_fn<A, B, C, D, FN: Fn(A, B, C) -> D>(mut self, _func: FN) -> Self {
417        let fn_name = std::any::type_name::<FN>()
418            .split("::")
419            .collect::<Vec<_>>()
420            .into_iter()
421            .next_back()
422            .unwrap();
423
424        self.body = format!(
425            "{}\nconst _out_ = window.callbacks.{}({});",
426            self.body,
427            fn_name,
428            self.args.join(", ")
429        );
430        self.js_return_value("_out_")
431    }
432
433    #[track_caller]
434    pub fn rust_closure<F: Fn(JsValue, JsValue, JsValue) -> JsValue + 'static>(
435        mut self,
436        closure: F,
437    ) -> Self {
438        let js_closure = wasm_bindgen::closure::Closure::wrap(
439            Box::new(closure) as Box<dyn Fn(JsValue, JsValue, JsValue) -> JsValue>
440        );
441        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
442
443        let js_window = gloo_utils::window();
444        let id = uuid::Uuid::new_v4().to_string();
445        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
446        js_closure.forget();
447
448        gloo_console::debug!(format!(
449            "Closure at {}:{}:{} set at window.['{id}'].",
450            file!(),
451            line!(),
452            column!()
453        ));
454        self.closure_id = Some(id);
455        self
456    }
457}
458
459impl FnWithArgs<4> {
460    pub fn run_rust_fn<A, B, C, D, E, FN: Fn(A, B, C, D) -> E>(mut self, _func: FN) -> Self {
461        let fn_name = std::any::type_name::<FN>()
462            .split("::")
463            .collect::<Vec<_>>()
464            .into_iter()
465            .next_back()
466            .unwrap();
467
468        self.body = format!(
469            "{}\nconst _out_ = window.callbacks.{}({});",
470            self.body,
471            fn_name,
472            self.args.join(", ")
473        );
474        self.js_return_value("_out_")
475    }
476
477    #[track_caller]
478    pub fn rust_closure<F: Fn(JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static>(
479        mut self,
480        closure: F,
481    ) -> Self {
482        let js_closure = wasm_bindgen::closure::Closure::wrap(
483            Box::new(closure) as Box<dyn Fn(JsValue, JsValue, JsValue, JsValue) -> JsValue>
484        );
485        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
486
487        let js_window = gloo_utils::window();
488        let id = uuid::Uuid::new_v4().to_string();
489        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
490        js_closure.forget();
491
492        gloo_console::debug!(format!(
493            "Closure at {}:{}:{} set at window.['{id}'].",
494            file!(),
495            line!(),
496            column!()
497        ));
498        self.closure_id = Some(id);
499        self
500    }
501}
502
503impl FnWithArgs<5> {
504    pub fn run_rust_fn<A, B, C, D, E, F, FN: Fn(A, B, C, D, E) -> F>(mut self, _func: FN) -> Self {
505        let fn_name = std::any::type_name::<FN>()
506            .split("::")
507            .collect::<Vec<_>>()
508            .into_iter()
509            .next_back()
510            .unwrap();
511
512        self.body = format!(
513            "{}\nconst _out_ = window.callbacks.{}({});",
514            self.body,
515            fn_name,
516            self.args.join(", ")
517        );
518        self.js_return_value("_out_")
519    }
520
521    #[track_caller]
522    pub fn rust_closure<F: Fn(JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static>(
523        mut self,
524        closure: F,
525    ) -> Self {
526        let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure)
527            as Box<dyn Fn(JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue>);
528        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
529
530        let js_window = gloo_utils::window();
531        let id = uuid::Uuid::new_v4().to_string();
532        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
533        js_closure.forget();
534
535        gloo_console::debug!(format!(
536            "Closure at {}:{}:{} set at window.['{id}'].",
537            file!(),
538            line!(),
539            column!()
540        ));
541        self.closure_id = Some(id);
542        self
543    }
544}
545
546impl FnWithArgs<6> {
547    pub fn run_rust_fn<A, B, C, D, E, F, G, FN: Fn(A, B, C, D, E, F) -> G>(
548        mut self,
549        _func: FN,
550    ) -> Self {
551        let fn_name = std::any::type_name::<FN>()
552            .split("::")
553            .collect::<Vec<_>>()
554            .into_iter()
555            .next_back()
556            .unwrap();
557
558        self.body = format!(
559            "{}\nconst _out_ = window.callbacks.{}({});",
560            self.body,
561            fn_name,
562            self.args.join(", ")
563        );
564        self.js_return_value("_out_")
565    }
566
567    #[track_caller]
568    pub fn rust_closure<
569        F: Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static,
570    >(
571        mut self,
572        closure: F,
573    ) -> Self {
574        let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure)
575            as Box<dyn Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue>);
576        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
577
578        let js_window = gloo_utils::window();
579        let id = uuid::Uuid::new_v4().to_string();
580        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
581        js_closure.forget();
582
583        gloo_console::debug!(format!(
584            "Closure at {}:{}:{} set at window.['{id}'].",
585            file!(),
586            line!(),
587            column!()
588        ));
589        self.closure_id = Some(id);
590        self
591    }
592}
593
594// 7 is the maximum wasm_bindgen can handle rn AFAIK
595impl FnWithArgs<7> {
596    pub fn run_rust_fn<A, B, C, D, E, F, G, H, FN: Fn(A, B, C, D, E, F, G) -> H>(
597        mut self,
598        _func: FN,
599    ) -> Self {
600        let fn_name = std::any::type_name::<FN>()
601            .split("::")
602            .collect::<Vec<_>>()
603            .into_iter()
604            .next_back()
605            .unwrap();
606
607        self.body = format!(
608            "{}\nconst _out_ = window.callbacks.{}({});",
609            self.body,
610            fn_name,
611            self.args.join(", ")
612        );
613        self.js_return_value("_out_")
614    }
615
616    #[track_caller]
617    pub fn rust_closure<
618        F: Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static,
619    >(
620        mut self,
621        closure: F,
622    ) -> Self {
623        let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure)
624            as Box<
625                dyn Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue,
626            >);
627        let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref();
628
629        let js_window = gloo_utils::window();
630        let id = uuid::Uuid::new_v4().to_string();
631        Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap();
632        js_closure.forget();
633
634        gloo_console::debug!(format!(
635            "Closure at {}:{}:{} set at window.['{id}'].",
636            file!(),
637            line!(),
638            column!()
639        ));
640        self.closure_id = Some(id);
641        self
642    }
643}