Skip to main content

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}
13/// Set the comparator used to sort dataset points during serialization.
14///
15/// `ORDER_FN` is a `thread_local`, and for a worker-rendered chart the
16/// serialization (`into_json`) runs on the *worker* thread, not the main one.
17/// Calling this on the main thread therefore does **not** affect worker charts —
18/// the worker has its own (default) comparator. To customize ordering for a
19/// worker chart, set it on the worker via [`WorkerChart::worker_setup`] /
20/// [`ChartWorker::run_setup`] (the closure runs on the worker), e.g.
21/// `worker_setup(|| chart_js_rs::set_order_fn(my_cmp))`. The main-thread
22/// `Chart::render` path is unaffected.
23pub fn set_order_fn<
24    F: Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering + 'static,
25>(
26    f: F,
27) {
28    // `replace` hands back the previous comparator; drop it explicitly (a bare
29    // statement trips `unused_must_use` on the boxed Fn).
30    drop(ORDER_FN.replace(Box::new(f)));
31}
32
33thread_local! {
34    #[allow(clippy::type_complexity)]
35    pub static ORDER_FN: RefCell<
36        Box<dyn Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering>,
37    > = RefCell::new({
38        Box::new(
39            |lhs: &crate::NumberOrDateString, rhs: &crate::NumberOrDateString| -> std::cmp::Ordering {
40                lhs.cmp(rhs)
41            },
42        )as Box<_>
43    });
44}
45
46pub fn uncircle_chartjs_value_to_serde_json_value(
47    js: impl AsRef<JsValue>,
48) -> Result<serde_json::Value, String> {
49    // this makes sure we don't get any circular objects, `JsValue` allows this, `serde_json::Value` does not!
50    let blacklist_function =
51        js_sys::Function::new_with_args("key, val", "if (!key.startsWith('$')) { return val; }");
52    let js_string =
53        js_sys::JSON::stringify_with_replacer(js.as_ref(), &JsValue::from(blacklist_function))
54            .map_err(|e| e.as_string().unwrap_or_default())?
55            .as_string()
56            .unwrap();
57
58    serde_json::from_str(&js_string).map_err(|e| e.to_string())
59}
60
61#[wasm_bindgen]
62#[derive(Clone)]
63#[must_use = "\nAppend .render()\n"]
64pub struct Chart {
65    pub(crate) obj: JsValue,
66    pub(crate) id: String,
67    pub(crate) mutate: bool,
68    pub(crate) plugins: String,
69    pub(crate) defaults: String,
70}
71
72/// Walks the JsValue object to get the value of a nested property
73/// using the JS dot notation
74fn get_path(j: &JsValue, item: &str) -> Option<JsValue> {
75    let mut path = item.split('.');
76    let item = &path.next().unwrap().to_string().into();
77    let k = Reflect::get(j, item);
78
79    if k.is_err() {
80        return None;
81    }
82
83    let k = k.unwrap();
84    if path.clone().count() > 0 {
85        return get_path(&k, path.collect::<Vec<&str>>().join(".").as_str());
86    }
87
88    Some(k)
89}
90
91/// Get values of an object as an array at the given path.
92/// See get_path()
93fn object_values_at(j: &JsValue, item: &str) -> Option<JsValue> {
94    let o = get_path(j, item);
95    o.filter(|o| o != &JsValue::UNDEFINED)
96}
97
98impl Chart {
99    // pub fn new(chart: JsValue, id: String) -> Option<Self> {
100    //     chart.is_object().then_some(Self{
101    //         obj: chart,
102    //         id,
103    //         mutate: false,
104    //         plugins: String::new(),
105    //     })
106    // }
107
108    #[must_use = "\nAppend .render()\n"]
109    pub fn mutate(&mut self) -> Self {
110        self.mutate = true;
111        self.clone()
112    }
113
114    #[must_use = "\nAppend .render()\n"]
115    pub fn plugins(&mut self, plugins: impl Into<String>) -> Self {
116        self.plugins = plugins.into();
117        self.clone()
118    }
119
120    #[must_use = "\nAppend .render()\n"]
121    pub fn defaults(&mut self, defaults: impl Into<String>) -> Self {
122        self.defaults = format!("{}\n{}", self.defaults, defaults.into());
123        self.to_owned()
124    }
125
126    /// This should not be used on a chart with a worker attached.
127    /// If it is, it will do nothing.
128    pub fn render(self) {
129        self.rationalise_js();
130
131        render_chart(self.obj, &self.id, self.mutate, self.plugins, self.defaults);
132    }
133
134    /// This should not be used on a chart with a worker attached.
135    /// If it is, it will always return `false`
136    pub fn update(self, animate: bool) -> bool {
137        update_chart(self.obj, &self.id, animate)
138    }
139
140    /// Converts serialized `FnWithArgs` to JS `Function`s, in place.
141    /// See [`rationalise`]; for new chart options, update that fn.
142    pub fn rationalise_js(&self) {
143        rationalise(&self.obj);
144    }
145}
146
147/// Converts serialized `FnWithArgs` in a chart config into real JS `Function`s,
148/// in place, at the known closure-bearing paths.
149///
150/// Shared by the main-thread render (`Chart::render`) and the worker render
151/// (`worker::build_chart`), so neither walks the whole config — datasets and all
152/// their points included — looking for closures. Visiting only these paths is
153/// O(callback-sites) instead of O(data). When adding a chart option that can
154/// hold a callback, add its path here.
155pub(crate) fn rationalise(obj: &JsValue) {
156    // data.datasets[*]
157    if let Some(datasets) = object_values_at(obj, "data.datasets") {
158        Array::from(&datasets).iter().for_each(|dataset| {
159            FnWithArgsOrT::<2, String>::rationalise_1_level(&dataset, "backgroundColor");
160            FnWithArgsOrT::<1, String>::rationalise_1_level(&dataset, "backgroundColor");
161            FnWithArgsOrT::<2, String>::rationalise_1_level(&dataset, "hoverBackgroundColor");
162            FnWithArgsOrT::<1, String>::rationalise_1_level(&dataset, "hoverBackgroundColor");
163            FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderDash"));
164            FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderColor"));
165            FnWithArgsOrT::<1, String>::rationalise_2_levels(&dataset, ("datalabels", "align"));
166            FnWithArgsOrT::<1, String>::rationalise_2_levels(&dataset, ("datalabels", "anchor"));
167            FnWithArgsOrT::<1, String>::rationalise_2_levels(
168                &dataset,
169                ("datalabels", "backgroundColor"),
170            );
171            FnWithArgs::<2>::rationalise_2_levels(&dataset, ("datalabels", "formatter"));
172            FnWithArgsOrT::<1, NumberString>::rationalise_2_levels(
173                &dataset,
174                ("datalabels", "offset"),
175            );
176            FnWithArgsOrT::<1, BoolString>::rationalise_2_levels(
177                &dataset,
178                ("datalabels", "display"),
179            );
180        });
181    }
182
183    // options.scales[*]
184    if let Some(scales) = object_values_at(obj, "options.scales") {
185        if let Ok(scales) = scales.dyn_into::<Object>() {
186            Object::values(&scales).iter().for_each(|scale| {
187                FnWithArgs::<3>::rationalise_2_levels(&scale, ("ticks", "callback"));
188            });
189        }
190    }
191
192    // options.plugins.legend
193    if let Some(legend) = object_values_at(obj, "options.plugins.legend") {
194        FnWithArgs::<2>::rationalise_2_levels(&legend, ("labels", "filter"));
195        FnWithArgs::<3>::rationalise_2_levels(&legend, ("labels", "sort"));
196        FnWithArgs::<1>::rationalise_2_levels(&legend, ("labels", "generateLabels"));
197    }
198    // options.plugins.tooltip
199    if let Some(tooltip) = object_values_at(obj, "options.plugins.tooltip") {
200        FnWithArgs::<1>::rationalise_1_level(&tooltip, "filter");
201        FnWithArgs::<1>::rationalise_2_levels(&tooltip, ("callbacks", "label"));
202        FnWithArgs::<1>::rationalise_2_levels(&tooltip, ("callbacks", "title"));
203    }
204}