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<
24 F: Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering + 'static,
25>(
26 f: F,
27) {
28 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 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
72fn 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
91fn 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 #[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 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 pub fn update(self, animate: bool) -> bool {
137 update_chart(self.obj, &self.id, animate)
138 }
139
140 pub fn rationalise_js(&self) {
143 rationalise(&self.obj);
144 }
145}
146
147pub(crate) fn rationalise(obj: &JsValue) {
156 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 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 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 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}