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 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
60fn 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
79fn 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 #[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 pub fn rationalise_js(&self) {
133 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 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 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 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}