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 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 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 a == JsValue::UNDEFINED {
72 return;
73 }
74
75 if let Ok(b) = Reflect::get(&a, &name.1.into()) {
76 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
104fn 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
123fn 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 #[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 pub fn rationalise_js(&self) {
177 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 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 if let Some(legend) = object_values_at(&self.obj, "options.plugins.legend") {
202 rationalise_2_levels::<2>(&legend, ("labels", "filter"));
203 }
204 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
594impl 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}