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}