chart_js_rs/
coordinate.rs

1#![allow(dead_code)]
2
3use {
4    gloo_utils::format::JsValueSerdeExt,
5    js_sys::{Array, Reflect},
6    std::{
7        any::type_name,
8        error::Error,
9        fmt::{self, Display},
10        str::FromStr,
11    },
12    wasm_bindgen::{JsCast, JsValue},
13};
14
15/// All the possible error states that result from asserting a given ChartJS coordinate is valid
16#[derive(Debug)]
17pub enum CoordinateError {
18    InvalidX {
19        ty: &'static str,
20        error: Box<dyn Error + Sync + Send + 'static>,
21        input: String,
22    },
23    InvalidY {
24        ty: &'static str,
25        error: Box<dyn Error + Sync + Send + 'static>,
26        input: serde_json::Number,
27    },
28
29    Deserialize {
30        value: JsValue,
31        error: serde_json::Error,
32    },
33    MissingKey {
34        value: JsValue,
35        key: String,
36    },
37    GetTypedKey {
38        value: JsValue,
39        key: String,
40        ty: &'static str,
41    },
42}
43
44impl Display for CoordinateError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            CoordinateError::InvalidX { ty, error, input } => write!(
48                f,
49                "Invalid X coordinate of type `{ty}`: `{input}`, parsing failed due to: {error:?}"
50            ),
51            CoordinateError::InvalidY { ty, error, input } => write!(
52                f,
53                "Invalid Y coordinate of type `{ty}`: `{input}`, parsing failed due to: {error:?}"
54            ),
55            CoordinateError::Deserialize { value, error } => {
56                write!(f, "Error deserializing value `{value:?}`: {error:?}")
57            }
58            CoordinateError::MissingKey { value, key } => {
59                write!(f, "Key `{key}` is missing from object `{value:?}`")
60            }
61            CoordinateError::GetTypedKey { value, key, ty } => write!(
62                f,
63                "Key `{key}` (of type {ty}) is missing form object `{value:?}`"
64            ),
65        }
66    }
67}
68
69impl Error for CoordinateError {}
70
71/// A representation in rust of a ChartJS coordinate, allowing convenient formatting when constructing tooltips
72#[derive(Debug)]
73#[non_exhaustive]
74pub struct Coordinate<T, U> {
75    pub x: T,
76    pub y: U,
77}
78
79impl<T, TE, U, UE> Coordinate<T, U>
80where
81    T: FromStr<Err = TE>,
82    U: FromStr<Err = UE>,
83    TE: std::error::Error + Sync + Send + 'static,
84    UE: std::error::Error + Sync + Send + 'static,
85{
86    fn from_raw(coord: Coordinate_) -> Result<Coordinate<T, U>, CoordinateError> {
87        Ok(Coordinate {
88            x: coord.x.parse().map_err(|e| CoordinateError::InvalidX {
89                ty: type_name::<T>(),
90                error: Box::new(e),
91                input: coord.x.to_string(),
92            })?,
93            y: coord
94                .y
95                .to_string()
96                .parse()
97                .map_err(|e| CoordinateError::InvalidY {
98                    ty: type_name::<U>(),
99                    error: Box::new(e),
100                    input: coord.y.clone(),
101                })?,
102        })
103    }
104
105    /// This should be used inside a `#[wasm_bindgen]` function
106    /// where it is known that the parameter passed to the function
107    /// will have the shape of an individual coordinate
108    ///
109    /// 1. first create a function
110    ///
111    /// ```rust no_run
112    /// #[wasm_bindgen]
113    /// pub fn chart_data_label_formatter(a: JsValue, _: JsValue) -> JsValue {
114    ///     match Coordinate::<NaiveDate, Dollars>::from_js_value(a) {
115    ///         Ok(val) => JsValue::from_str(&val.y.format().to_string()),
116    ///         Err(e) => {
117    ///             console_dbg!("Error converting to serde", e);
118    ///             JsValue::from_str("")
119    ///         }
120    ///     }
121    /// }
122    /// ```
123    ///
124    /// 2. then use it with the [`crate::objects::DataLabels`] builder:
125    ///
126    /// ```rust no_run
127    /// DataLabels::new()
128    /// .formatter(FnWithArgs::<2>::new().run_rust_fn(chart_data_label_formatter)),
129    /// ```
130    pub fn from_js_value(val: JsValue) -> Result<Self, CoordinateError> {
131        JsValueSerdeExt::into_serde::<Coordinate_>(&val)
132            .map_err(|error| CoordinateError::Deserialize {
133                value: val.clone(),
134                error,
135            })
136            .and_then(Coordinate::<T, U>::from_raw)
137    }
138}
139
140#[derive(serde::Deserialize)]
141struct Coordinate_ {
142    x: String,
143    y: serde_json::Number,
144}
145
146/// A representation in rust of a ChartJS poinrt, generally exposed via the tooltips plugin
147#[derive(Debug)]
148#[non_exhaustive]
149pub struct ChartJsPoint<T, U> {
150    /// Probably don't use this
151    pub formatted_value: String,
152    /// Probably don't use this
153    pub label: String,
154    /// Details about the dataset that are seen by the viewer of the chart
155    pub dataset: ChartJsPointDataset,
156    /// The raw coordinate value for the point
157    pub raw: Coordinate<T, U>,
158}
159
160fn get_field(val: &JsValue, key: &str) -> Result<JsValue, CoordinateError> {
161    match Reflect::get(val, &JsValue::from_str(key)) {
162        Ok(v) => Ok(v),
163        Err(_) => Err(CoordinateError::MissingKey {
164            value: val.clone(),
165            key: key.to_string(),
166        }),
167    }
168}
169
170fn get_string(val: &JsValue, key: &str) -> Result<String, CoordinateError> {
171    get_field(val, key)?
172        .as_string()
173        .ok_or_else(|| CoordinateError::GetTypedKey {
174            value: val.clone(),
175            key: key.to_string(),
176            ty: "String",
177        })
178}
179
180fn get_f64(val: &JsValue, key: &str) -> Result<f64, CoordinateError> {
181    get_field(val, key)?
182        .as_f64()
183        .ok_or_else(|| CoordinateError::GetTypedKey {
184            value: val.clone(),
185            key: key.to_string(),
186            ty: "f64",
187        })
188}
189
190impl<T, TE, U, UE> ChartJsPoint<T, U>
191where
192    T: FromStr<Err = TE>,
193    U: FromStr<Err = UE>,
194    TE: std::error::Error + Sync + Send + 'static,
195    UE: std::error::Error + Sync + Send + 'static,
196    T: fmt::Debug,
197    U: fmt::Debug,
198{
199    /// This should be used for parameter of [`crate::objects::TooltipCallbacks::label`]
200    ///
201    /// 1. first create a function
202    ///
203    ///  ```rust no_run
204    /// #[wasm_bindgen]
205    /// pub fn tooltip_value_callback(context: JsValue) -> JsValue {
206    ///     match ChartJsPoint::<NaiveDate, Dollars>::parse(context) {
207    ///         Ok(val) => {
208    ///             let label = val.dataset.label;
209    ///             let y = val.raw.y.format();
210    ///             JsValue::from_str(&format!("{label}: {y}"))
211    ///         }
212    ///         Err(e) => {
213    ///             console_dbg!("Error parsing", e);
214    ///             JsValue::from_str("")
215    ///         }
216    ///     }
217    /// }
218    /// ```
219    ///
220    /// 2. then use with the builder:
221    ///
222    /// ```rust no_run
223    /// ChartOptions::new()
224    /// .plugins(
225    ///     ChartPlugins::new()
226    ///         .tooltip(
227    ///             TooltipPlugin::new().callbacks(
228    ///                 TooltipCallbacks::new()
229    ///                     .label(
230    ///                         FnWithArgs::<1>::new()
231    ///                             .run_rust_fn(tooltip_value_callback),
232    ///                     )
233    ///             ),
234    ///         ),
235    /// )
236    /// ```
237    pub fn parse(val: JsValue) -> Result<Self, CoordinateError> {
238        Ok(ChartJsPoint {
239            formatted_value: get_string(&val, "formattedValue")?,
240            label: get_string(&val, "label")?,
241            dataset: JsValueSerdeExt::into_serde::<ChartJsPointDataset>(&get_field(
242                &val, "dataset",
243            )?)
244            .map_err(|error| CoordinateError::Deserialize {
245                value: val.clone(),
246                error,
247            })?,
248            raw: Coordinate::from_js_value(get_field(&val, "raw")?)?,
249        })
250    }
251
252    /// This should be used for the parameter of [`crate::objects::TooltipCallbacks::title`]
253    ///
254    /// 1. first create a function
255    ///
256    /// ```rust no_run
257    /// #[wasm_bindgen]
258    /// pub fn tooltip_date_title_callback(context: JsValue) -> JsValue {
259    ///     match ChartJsPoint::<NaiveDate, Dollars>::parse_array(context) {
260    ///         Ok(vals) if !vals.is_empty() => JsValue::from_str(vals[0].label.split(",").next().unwrap_or_default()),
261    ///         Ok(_) => {
262    ///             console_dbg!("Empty array");
263    ///             JsValue::from_str("")
264    ///         }
265    ///         Err(e) => {
266    ///             console_dbg!("Error parsing", e);
267    ///             JsValue::from_str("")
268    ///         }
269    ///     }
270    /// }
271    /// ```
272    ///
273    /// 2. then use with the builder:
274    ///
275    /// ```rust no_run
276    /// ChartOptions::new()
277    /// .plugins(
278    ///     ChartPlugins::new()
279    ///         .tooltip(
280    ///             TooltipPlugin::new().callbacks(
281    ///                 TooltipCallbacks::new()
282    ///                     .title(
283    ///                         FnWithArgs::<1>::new()
284    ///                             .run_rust_fn(tooltip_date_title_callback),
285    ///                     )
286    ///             ),
287    ///         ),
288    /// )
289    /// ```
290    pub fn parse_array(val: JsValue) -> Result<Vec<Self>, CoordinateError> {
291        let vec = if val.is_array() {
292            let array = val.dyn_into::<Array>().unwrap_or_default().to_vec();
293            let mut parsed = Vec::new();
294            for item in array {
295                parsed.push(Self::parse(item)?);
296            }
297            parsed
298        } else {
299            Vec::from([Self::parse(val)?])
300        };
301        Ok(vec)
302    }
303}
304
305#[derive(serde::Deserialize)]
306struct ChartJsPoint_ {
307    formatted_value: String,
308    label: String,
309    dataset: ChartJsPointDataset,
310    raw: Coordinate_,
311}
312
313#[derive(serde::Deserialize, Debug)]
314pub struct ChartJsPointDataset {
315    /// The label for the dataset that is seen by the viewer of the chart
316    pub label: String,
317    pub r#type: String,
318}