chart_js_wrapper/
options.rs

1use std::cmp::PartialEq;
2use std::time::{Instant, SystemTime};
3use crate::common::{Padding, Rgb, Size};
4use crate::options::ChartData::Vector2D;
5use crate::render::Chart;
6use crate::ChartData::VectorWithRadius;
7use ndarray::{Array1, Array2};
8use ndarray_linalg::error::LinalgError;
9use ndarray_linalg::LeastSquaresSvd;
10use ndarray_linalg::{Lapack, Scalar};
11use serde::{Deserialize, Serialize};
12use serde::ser::SerializeSeq;
13use uuid::Uuid;
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
16#[serde(rename_all = "camelCase")]
17pub struct ChartConfig<X,Y> {
18
19    data: ChartDataSection<X,Y>,
20    
21    options: ChartOptions<X,Y>
22}
23
24impl<X, Y> ChartConfig<X, Y> where ChartConfig<X, Y>:Serialize {
25
26    pub fn new(options: ChartOptions<X,Y>) -> Self {
27        Self {
28            data: ChartDataSection::default(),
29            options
30        }
31    }
32
33    pub fn set_x_axis(mut self, conf: ScaleConfig<X>) -> Self {
34        let mut scales = self.options.scales;
35        if scales.is_none() {
36            scales = Some(ScalingConfig{
37                x: Some(conf),
38                y: None
39            });
40        }else {
41            scales.as_mut().unwrap().x = Some(conf);
42        }
43        self.options.scales = scales;
44        self
45    }
46
47    pub fn set_y_axis(mut self, conf: ScaleConfig<Y>) -> Self {
48        let mut scales = self.options.scales;
49        if scales.is_none() {
50            scales = Some(ScalingConfig{
51                x: None,
52                y: Some(conf)
53            });
54        }else {
55            scales.as_mut().unwrap().y = Some(conf);
56        }
57        self.options.scales = scales;
58        self
59    }
60
61
62    pub fn title_str(mut self, text: String) -> Self {
63        self.options.plugins.title = Some(Title{
64                display: true,
65                full_size: false,
66                text: vec![text],
67                padding: None,
68                position: None,
69        });
70        self
71    }
72
73
74    pub fn add_series<T: Into<ChartData<X,Y>>>(mut self, r#type: ChartType, title:String, data: T)->Self{
75        self.data.datasets.push(Dataset{
76            r#type,
77            label: title,
78            data: data.into(),
79            fill: None,
80            border_color: None,
81            background_color: None,
82        });
83        self
84    }
85
86
87    pub fn enable_legend(mut self) -> Self{
88        if self.options.plugins.legend.is_none() {
89            self.options.plugins.legend = Some(Legend {
90                display: true,
91                full_size: false,
92                position: None,
93                align: None
94            });
95        }else {
96            self.options.plugins.legend.as_mut().unwrap().display = true;
97        }
98        self
99    }
100    
101    pub fn build(self, width: Size, height: Size) -> Chart<X,Y>{
102        Chart::new(Uuid::new_v4().to_string(), width, height, self)
103    }
104    
105}
106
107
108
109impl<X> ChartConfig<X, X> where ChartConfig<X, X>:Serialize, X:Scalar + Lapack + Clone + Into<f64> {
110    
111    pub fn add_linear_regression_series<T: Into<ChartData<X,X>>>(self, title: &str, data: T) -> Result<Self, LinalgError> {
112        let data:Vec<(X,X)>  = data.into().into();
113        let n = data.len();
114        let config_with_scatter = self.add_series(ChartType::Scatter, title.to_string(), data.clone());
115        
116        // Allocate design matrix with shape (n, 2)
117        let mut x_matrix = Array2::<X>::zeros((n, 2));
118        let mut y_array = Array1::<X>::zeros(n);
119
120        for (i, (x, y)) in data.into_iter().enumerate() {
121            x_matrix[[i, 0]] = X::one(); // intercept term
122            x_matrix[[i, 1]] = x;
123            y_array[i] = y;
124        }
125        
126        let beta = x_matrix.least_squares(&y_array)?.solution;
127
128        let y_pred = x_matrix.dot(&beta);
129        let r2 = Self::r_squared(&y_array, &y_pred);
130        let mut reg_data = Vec::<(X,X)>::with_capacity(n);
131        
132        y_pred.into_iter().enumerate().for_each(|(i, x)| {
133            reg_data.push((x_matrix[[i,1]],x));
134        });
135        
136        let config_with_both_charts = 
137            config_with_scatter.add_series(ChartType::Line, format!("{} regression(R^2 = {:.4})", title, r2), reg_data);
138        
139        Ok(config_with_both_charts)
140    }
141
142
143    fn r_squared(y_true: &Array1<X>, y_pred: &Array1<X>) -> f64
144    {
145        let n = y_true.len();
146        if n == 0 {
147            return 0.0; // or maybe panic/error, depending on your use case
148        }
149        let n_f = n as f64;
150
151        // Calculate mean once
152        let y_mean = y_true.iter().map(|&v| v.into()).sum::<f64>() / n_f;
153
154        // Sum of squared residuals (errors)
155        let ss_res = y_true
156            .iter()
157            .zip(y_pred.iter())
158            .map(|(&y, &y_hat)| {
159                let y_f = y.into();
160                let y_hat_f = y_hat.into();
161                let diff = y_f - y_hat_f;
162                diff * diff
163            })
164            .sum::<f64>();
165
166        // Total sum of squares
167        let ss_tot = y_true
168            .iter()
169            .map(|&y| {
170                let y_f = y.into();
171                let diff = y_f - y_mean;
172                diff * diff
173            })
174            .sum::<f64>();
175        
176        if ss_tot == 0.0 {
177            // All y_true are constant
178            return if ss_res == 0.0 {
179                1.0  // Perfect fit
180            } else {
181                0.0  // Model fails to match - treat as no explanatory power
182            }
183        }
184        
185        1.0 - ss_res / ss_tot
186    }
187}
188
189
190impl<X,Y> Default  for ChartDataSection<X,Y>{
191    fn default() -> Self {
192        ChartDataSection {
193            datasets: vec![],
194        }
195    }
196}
197
198impl<X:WithScaleType, Y:WithScaleType> Default for ChartConfig<X,Y>{
199    fn default() -> Self {
200        ChartConfig{
201            data: ChartDataSection::default(),
202            options: ChartOptions{
203                scales: Some(ScalingConfig{
204                    x:Some(ScaleConfig{
205                        r#type: Some(X::scale_type()),
206                        ..ScaleConfig::default()
207                    }),
208                    y:Some(ScaleConfig{
209                        r#type: Some(Y::scale_type()),
210                        reverse: Y::scale_type() == ScaleType::Category,
211                        ..ScaleConfig::default()
212                    }),
213                }),
214                aspect_ratio: None,
215                plugins: Plugins::default(),
216            },
217        }
218    }
219}
220
221
222pub trait WithScaleType{
223    fn scale_type()->ScaleType;
224}
225
226#[macro_export]
227macro_rules! impl_scale_type {
228    ($type:ident for $($t:ty)*) => ($(
229        impl WithScaleType for $t {
230            fn scale_type() -> ScaleType {
231                ScaleType::$type
232            }
233        }
234    )*)
235}
236
237impl_scale_type!(Linear for u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64);
238impl_scale_type!(Category for &str String);
239impl_scale_type!(Time for SystemTime Instant);
240
241#[derive(Serialize, Deserialize, Debug, Clone)]
242#[serde(rename_all = "lowercase")]
243pub enum ChartType {
244    Bubble,
245    Bar,
246    Line,
247    Doughnut,
248    Pie,
249    Radar,
250    PolarArea,
251    Scatter
252}
253
254#[derive(Serialize, Deserialize, Debug, Clone)]
255#[serde(rename_all = "camelCase")]
256pub struct ChartDataSection<X,Y>{
257    datasets: Vec<Dataset<X,Y>>
258}
259
260#[derive(Serialize, Deserialize, Debug, Clone)]
261#[serde(rename_all = "camelCase")]
262pub struct Dataset<X,Y>{
263
264    r#type: ChartType,
265
266    label: String,
267
268    data: ChartData<X,Y>,
269
270    #[serde(skip_serializing_if = "Option::is_none")]
271    fill: Option<Fill>,
272
273    #[serde(skip_serializing_if = "Option::is_none")]
274    border_color: Option<Rgb>,
275
276    #[serde(skip_serializing_if = "Option::is_none")]
277    background_color: Option<Rgb>
278}
279
280
281impl<X,Y> From<DataElement<X,Y>> for (X,Y){
282    fn from(value: DataElement<X,Y>) -> Self {
283        (value.x, value.y)
284    }
285}
286
287
288impl<X,Y> From<ChartData<X,Y>> for Vec<(X,Y)>{
289    fn from(value: ChartData<X, Y>) -> Self {
290        match value {
291            Vector2D(v) => v,
292            VectorWithRadius(val)=> val.into_iter().map(|v| v.into()).collect()
293        }
294    }
295}
296
297impl<X,Y> From<Vec<(X,Y)>> for ChartData<X,Y>{
298    fn from(value: Vec<(X, Y)>) -> Self {
299       Vector2D(value)
300    }
301}
302
303impl<const N: usize,X,Y> From<[(X,Y);N]> for ChartData<X,Y> where X:Clone, Y:Clone {
304    fn from(value: [(X,Y);N]) -> Self {
305        Vector2D(value.to_vec())
306    }
307}
308
309#[derive(Deserialize, Debug, Clone)]
310#[serde(untagged)]
311pub enum ChartData<X,Y>{
312    Vector2D(Vec<(X,Y)>),
313    VectorWithRadius(Vec<DataElement<X,Y>>)
314}
315
316
317impl<X,Y> Serialize for ChartData<X,Y> where X:Serialize, Y:Serialize{
318    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer {
319        match self {
320            Vector2D(v) => {
321                let mut v_ser = serializer.serialize_seq(Some(v.len()))?;
322                for (x,y) in v {
323                    let  point = DataPoint{x,y};
324                    v_ser.serialize_element(&point)?;
325                }
326                v_ser.end()
327            },
328            VectorWithRadius(v) => v.serialize(serializer)
329        }
330    }
331}
332
333
334#[derive(Serialize, Deserialize, Debug, Clone)]
335pub struct DataPoint<X,Y>{
336    x: X,
337    y: Y
338}
339
340
341
342#[derive(Serialize, Deserialize, Debug, Clone)]
343pub struct DataElement<X,Y>{
344    x: X,
345    y: Y,
346    r: u32
347}
348
349#[derive(Serialize, Deserialize, Debug, Clone)]
350#[serde(untagged)]
351enum FillVariant{
352    AbsIndex(u8),
353    RelativeIndex(String),
354    Boundary(Boundary),
355    AxisValue(AxisValue)
356}
357
358#[derive(Serialize, Deserialize, Debug, Clone)]
359struct AxisValue{
360    value: AxisValueVariant
361}
362
363#[derive(Serialize, Deserialize, Debug, Clone)]
364enum AxisValueVariant{
365    Str(String),
366    Num(f64)
367}
368
369#[derive(Serialize, Deserialize, Debug, Clone)]
370enum Boundary{
371    Start,
372    End,
373    Origin,
374    Stack,
375    Shape
376}
377
378#[derive(Serialize, Deserialize, Debug, Clone)]
379#[serde(rename_all = "camelCase")]
380struct  Fill {
381    target: FillVariant,
382
383    #[serde(skip_serializing_if = "Option::is_none")]
384    above: Option<Rgb>,
385
386    #[serde(skip_serializing_if = "Option::is_none")]
387    below: Option<Rgb>
388}
389
390#[derive(Serialize, Deserialize, Debug, Clone)]
391#[serde(rename_all = "lowercase")]
392enum AxisName{
393    X,
394    Y
395}
396
397#[derive(Serialize, Deserialize, Debug, Clone)]
398#[serde(rename_all = "camelCase")]
399pub struct ChartOptions<X,Y>{
400    #[serde(skip_serializing_if = "Option::is_none")]
401    scales: Option<ScalingConfig<X,Y>>,
402
403    #[serde(skip_serializing_if = "Option::is_none")]
404    aspect_ratio: Option<u8>,
405
406    plugins: Plugins,
407}
408
409impl<X,Y> Default for ChartOptions<X,Y>{
410    fn default() -> Self {
411        ChartOptions{
412            scales: None,
413            aspect_ratio: None,
414            plugins: Plugins::default(),
415        }
416    }
417}
418
419#[derive(Serialize, Deserialize, Debug, Clone)]
420#[serde(rename_all = "camelCase")]
421pub struct ScalingConfig<X,Y>{
422    #[serde(skip_serializing_if = "Option::is_none")]
423    x: Option<ScaleConfig<X>>,
424
425    #[serde(skip_serializing_if = "Option::is_none")]
426    y: Option<ScaleConfig<Y>>
427}
428
429#[derive(Serialize, Deserialize, Debug, Clone)]
430#[serde(rename_all = "camelCase")]
431pub struct ScaleConfig<T>{
432
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub r#type: Option<ScaleType>,
435
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub labels: Option<Vec<T>>,
438
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub align_to_pixels: Option<bool>,
441
442    pub reverse: bool,
443
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub max: Option<T>,
446
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub min: Option<T>,
449
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub title: Option<AxisTitle>
452}
453
454
455impl<T> Default for ScaleConfig<T>{
456    fn default() -> Self {
457        ScaleConfig{
458            r#type: None,
459            labels: None,
460            align_to_pixels: None,
461            reverse: false,
462            max: None,
463            min: None,
464            title: None
465        }
466    }
467}
468
469impl<T> ScaleConfig<T>{
470
471    pub fn new_category(reverse:bool,values: Vec<T>) -> Self {
472        Self {
473            r#type: Some(ScaleType::Category),
474            labels: Some(values),
475            reverse,
476            ..ScaleConfig::<T>::default()
477        }
478    }
479}
480
481
482#[derive(Serialize, Deserialize, Debug, Clone,PartialEq)]
483#[serde(rename_all = "lowercase")]
484pub enum ScaleType{
485    Linear,
486    Logarithmic,
487    Category,
488    Time,
489    TimeSeries,
490    RadialLinear
491}
492
493#[derive(Serialize, Deserialize, Debug, Clone)]
494#[serde(rename_all = "camelCase")]
495pub struct AxisTitle{
496    display: bool,
497    text: String,
498    align: Alignment,
499}
500
501#[derive(Serialize, Deserialize, Debug, Clone)]
502#[serde(rename_all = "lowercase")]
503pub enum Alignment{
504    Start,
505    Center,
506    End
507}
508
509#[derive(Serialize, Deserialize, Debug, Clone)]
510#[serde(rename_all = "lowercase")]
511#[derive(Default)]
512struct Plugins{
513    #[serde(skip_serializing_if = "Option::is_none")]
514    title: Option<Title>,
515
516    #[serde(skip_serializing_if = "Option::is_none")]
517    subtitle: Option<Title>,
518    
519    #[serde(skip_serializing_if = "Option::is_none")]
520    legend: Option<Legend>
521    
522}
523
524#[derive(Serialize, Deserialize, Debug, Clone)]
525#[serde(rename_all = "camelCase")]
526pub struct Legend{
527    display: bool,
528
529    #[serde(skip_serializing_if = "Option::is_none")]
530    position: Option<Position>,
531
532    #[serde(skip_serializing_if = "Option::is_none")]
533    align: Option<Alignment>,
534
535    pub full_size: bool,
536}
537
538#[derive(Serialize, Deserialize, Debug, Clone)]
539#[serde(rename_all = "lowercase")]
540enum Position{
541    Top,
542    Left,
543    Bottom,
544    Right
545}
546
547#[derive(Serialize, Deserialize, Debug, Clone)]
548#[serde(rename_all = "lowercase")]
549enum Align{
550    Start,
551    Center,
552    End
553}
554
555#[derive(Serialize, Deserialize, Debug, Clone)]
556#[serde(rename_all = "camelCase")]
557pub struct Title{
558    ///Is the title shown?
559    display: bool,
560
561    ///Marks that this box should take the full width/height of the canvas. If false, the box is sized and placed above/beside the chart area.
562    full_size: bool,
563
564    ///Title text to display. If specified as an array, text is rendered on multiple lines.
565    text: Vec<String>,
566
567    ///Padding to apply around the title. Only top and bottom are implemented.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    padding: Option<Padding>,
570
571    ///position of the title
572    #[serde(skip_serializing_if = "Option::is_none")]
573    position: Option<Position>
574
575}