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, VectorWithText};
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
15const DISPLAY_FN: &'static str = "
16                        function(context){
17                            context = context[0];
18                            let ttp = context.raw.tooltip || '';
19                            if(ttp) return ttp;
20                        }";
21
22#[derive(Debug, Clone)]
23pub struct ChartConfig<X,Y> {
24    pub data: ChartDataSection<X,Y>,
25    pub options: ChartOptions<X,Y>
26}
27
28impl<X, Y> ChartConfig<X, Y> where X: Serialize, Y: Serialize {
29
30    pub fn new(options: ChartOptions<X,Y>) -> Self {
31        Self {
32            data: ChartDataSection::default(),
33            options
34        }
35    }
36
37    pub fn set_x_axis(mut self, conf: ScaleConfig<X>) -> Self {
38        let mut scales = self.options.scales;
39        if scales.is_none() {
40            scales = Some(ScalingConfig{
41                x: Some(conf),
42                y: None
43            });
44        }else {
45            scales.as_mut().unwrap().x = Some(conf);
46        }
47        self.options.scales = scales;
48        self
49    }
50
51    pub fn set_y_axis(mut self, conf: ScaleConfig<Y>) -> Self {
52        let mut scales = self.options.scales;
53        if scales.is_none() {
54            scales = Some(ScalingConfig{
55                x: None,
56                y: Some(conf)
57            });
58        }else {
59            scales.as_mut().unwrap().y = Some(conf);
60        }
61        self.options.scales = scales;
62        self
63    }
64
65
66    pub fn title_str(mut self, text: String) -> Self {
67        self.options.plugins.title = Some(Title{
68                display: true,
69                full_size: false,
70                text: vec![text],
71                padding: None,
72                position: None,
73        });
74        self
75    }
76
77    pub fn with_aspect_ratio(mut self, ratio: f32) -> Self {
78        self.options.aspect_ratio = Some(ratio);
79        self
80    }
81
82    pub fn add_series<T: Into<ChartData<X,Y>>>(mut self, r#type: ChartType, title:String, data: T)->Self{
83        let data = data.into();
84        if let VectorWithText(d) = data {
85            let ttp = self.options.plugins.tooltip.get_or_insert_default();
86            let callbacks = ttp.callbacks.get_or_insert_default();
87            callbacks.title = Some(JsExpr(DISPLAY_FN));
88            self.data.datasets.push(Dataset {
89                r#type,
90                label: title,
91                data: VectorWithText(d),
92                fill: None,
93                border_color: None,
94                background_color: None,
95            });
96        }else {
97            self.data.datasets.push(Dataset {
98                r#type,
99                label: title,
100                data,
101                fill: None,
102                border_color: None,
103                background_color: None,
104            });
105        }
106        self
107    }
108
109
110    pub fn enable_legend(mut self) -> Self{
111        if self.options.plugins.legend.is_none() {
112            self.options.plugins.legend = Some(Legend {
113                display: true,
114                full_size: false,
115                position: None,
116                align: None
117            });
118        }else {
119            self.options.plugins.legend.as_mut().unwrap().display = true;
120        }
121        self
122    }
123    
124    pub fn build(self, width: Size, height: Size) -> Chart<X,Y>{
125        Chart::new(Uuid::new_v4().to_string(), width, height, self)
126    }
127    
128}
129
130
131
132impl<X> ChartConfig<X, X> where  X:Serialize + Scalar + Lapack + Clone + Into<f64> {
133    
134    pub fn add_linear_regression_series<T: Into<ChartData<X,X>>>(self, title: &str, data: T) -> Result<Self, LinalgError> {
135        let data:Vec<(X,X)>  = data.into().into();
136        let n = data.len();
137        let config_with_scatter = self.add_series(ChartType::Scatter, title.to_string(), data.clone());
138        
139        // Allocate design matrix with shape (n, 2)
140        let mut x_matrix = Array2::<X>::zeros((n, 2));
141        let mut y_array = Array1::<X>::zeros(n);
142
143        for (i, (x, y)) in data.into_iter().enumerate() {
144            x_matrix[[i, 0]] = X::one(); // intercept term
145            x_matrix[[i, 1]] = x;
146            y_array[i] = y;
147        }
148        
149        let beta = x_matrix.least_squares(&y_array)?.solution;
150
151        let y_pred = x_matrix.dot(&beta);
152        let r2 = Self::r_squared(&y_array, &y_pred);
153        let mut reg_data = Vec::<(X,X)>::with_capacity(n);
154        
155        y_pred.into_iter().enumerate().for_each(|(i, x)| {
156            reg_data.push((x_matrix[[i,1]],x));
157        });
158        
159        let config_with_both_charts = 
160            config_with_scatter.add_series(ChartType::Line, format!("{} regression(R^2 = {:.4})", title, r2), reg_data);
161        
162        Ok(config_with_both_charts)
163    }
164
165
166    fn r_squared(y_true: &Array1<X>, y_pred: &Array1<X>) -> f64
167    {
168        let n = y_true.len();
169        if n == 0 {
170            return 0.0; // or maybe panic/error, depending on your use case
171        }
172        let n_f = n as f64;
173
174        // Calculate mean once
175        let y_mean = y_true.iter().map(|&v| v.into()).sum::<f64>() / n_f;
176
177        // Sum of squared residuals (errors)
178        let ss_res = y_true
179            .iter()
180            .zip(y_pred.iter())
181            .map(|(&y, &y_hat)| {
182                let y_f = y.into();
183                let y_hat_f = y_hat.into();
184                let diff = y_f - y_hat_f;
185                diff * diff
186            })
187            .sum::<f64>();
188
189        // Total sum of squares
190        let ss_tot = y_true
191            .iter()
192            .map(|&y| {
193                let y_f = y.into();
194                let diff = y_f - y_mean;
195                diff * diff
196            })
197            .sum::<f64>();
198        
199        if ss_tot == 0.0 {
200            // All y_true are constant
201            return if ss_res == 0.0 {
202                1.0  // Perfect fit
203            } else {
204                0.0  // Model fails to match - treat as no explanatory power
205            }
206        }
207        
208        1.0 - ss_res / ss_tot
209    }
210}
211
212
213impl<X,Y> Default  for ChartDataSection<X,Y>{
214    fn default() -> Self {
215        ChartDataSection {
216            datasets: vec![],
217        }
218    }
219}
220
221impl<X:WithScaleType, Y:WithScaleType> Default for ChartConfig<X,Y>{
222    fn default() -> Self {
223        ChartConfig{
224            data: ChartDataSection::default(),
225            options: ChartOptions{
226                scales: Some(ScalingConfig{
227                    x:Some(ScaleConfig{
228                        r#type: Some(X::scale_type()),
229                        ..ScaleConfig::default()
230                    }),
231                    y:Some(ScaleConfig{
232                        r#type: Some(Y::scale_type()),
233                        reverse: Y::scale_type() == ScaleType::Category,
234                        ..ScaleConfig::default()
235                    }),
236                }),
237                aspect_ratio: None,
238                plugins: Plugins::default(),
239            },
240        }
241    }
242}
243
244
245pub trait WithScaleType{
246    fn scale_type()->ScaleType;
247}
248
249#[macro_export]
250macro_rules! impl_scale_type {
251    ($type:ident for $($t:ty)*) => ($(
252        impl WithScaleType for $t {
253            fn scale_type() -> ScaleType {
254                ScaleType::$type
255            }
256        }
257    )*)
258}
259
260impl_scale_type!(Linear for u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64);
261impl_scale_type!(Category for &str String);
262impl_scale_type!(Time for SystemTime Instant);
263
264#[derive(Serialize, Deserialize, Debug, Clone)]
265#[serde(rename_all = "lowercase")]
266pub enum ChartType {
267    Bubble,
268    Bar,
269    Line,
270    Doughnut,
271    Pie,
272    Radar,
273    PolarArea,
274    Scatter
275}
276
277#[derive(Serialize, Deserialize, Debug, Clone)]
278#[serde(rename_all = "camelCase")]
279pub struct ChartDataSection<X,Y>{
280    datasets: Vec<Dataset<X,Y>>
281}
282
283#[derive(Serialize, Deserialize, Debug, Clone)]
284#[serde(rename_all = "camelCase")]
285pub struct Dataset<X,Y>{
286
287    r#type: ChartType,
288
289    label: String,
290
291    data: ChartData<X,Y>,
292
293    #[serde(skip_serializing_if = "Option::is_none")]
294    fill: Option<Fill>,
295
296    #[serde(skip_serializing_if = "Option::is_none")]
297    border_color: Option<Rgb>,
298
299    #[serde(skip_serializing_if = "Option::is_none")]
300    background_color: Option<Rgb>
301}
302
303
304impl<X,Y> From<DataPointWithRadius<X,Y>> for (X, Y){
305    fn from(value: DataPointWithRadius<X,Y>) -> Self {
306        (value.x, value.y)
307    }
308}
309
310impl<X,Y> From<DataPoint<X,Y>> for (X,Y){
311    fn from(value: DataPoint<X,Y>) -> Self {
312        (value.x, value.y)
313    }
314}
315
316impl<X,Y> From<DataPointWithTooltip<X,Y>> for (X, Y){
317    fn from(value: DataPointWithTooltip<X,Y>) -> Self {
318        (value.x, value.y)
319    }
320}
321
322impl<X,Y> From<DataPointWithTooltip<X,Y>> for (X, Y, String){
323    fn from(value: DataPointWithTooltip<X,Y>) -> Self {
324        (value.x, value.y, value.tooltip)
325    }
326}
327
328
329impl<X,Y> From<(X, Y, String)> for DataPointWithTooltip<X,Y>{
330    fn from(value: (X, Y, String)) -> Self {
331        DataPointWithTooltip{
332            x: value.0,
333            y: value.1,
334            tooltip: value.2
335        }
336    }
337}
338
339impl<X,Y> From<(X, Y, &str)> for DataPointWithTooltip<X,Y>{
340    fn from(value: (X, Y, &str)) -> Self {
341        DataPointWithTooltip{
342            x: value.0,
343            y: value.1,
344            tooltip: value.2.to_string()
345        }
346    }
347}
348
349impl<X,Y> From<ChartData<X,Y>> for Vec<(X,Y)>{
350    fn from(value: ChartData<X, Y>) -> Self {
351        match value {
352            Vector2D(v) => v,
353            VectorWithRadius(val)=> val.into_iter().map(|v| v.into()).collect(),
354            VectorWithText(val)=> val.into_iter().map(|v| v.into()).collect()
355        }
356    }
357}
358
359impl<X,Y> From<Vec<(X,Y,String)>> for ChartData<X,Y>{
360    fn from(value: Vec<(X, Y, String)>) -> Self {
361        VectorWithText(value.into_iter().map(|v| v.into()).collect())
362    }
363}
364
365impl<const N: usize,X,Y> From<[(X,Y,String);N]> for ChartData<X,Y>{
366    fn from(value: [(X,Y,String);N]) -> Self {
367        VectorWithText(value.into_iter().map(|v| v.into()).collect())
368    }
369}
370
371impl<X,Y> From<Vec<(X,Y,&str)>> for ChartData<X,Y>{
372    fn from(value: Vec<(X, Y,&str)>) -> Self {
373        VectorWithText(value.into_iter().map(|v| v.into()).collect())
374    }
375}
376
377impl<const N: usize,X,Y> From<[(X,Y,&str);N]> for ChartData<X,Y>{
378    fn from(value: [(X,Y,&str);N]) -> Self {
379        VectorWithText(value.into_iter().map(|v| v.into()).collect())
380    }
381}
382
383
384
385
386
387impl<X,Y> From<Vec<(X,Y)>> for ChartData<X,Y>{
388    fn from(value: Vec<(X, Y)>) -> Self {
389       Vector2D(value)
390    }
391}
392
393impl<const N: usize,X,Y> From<[(X,Y);N]> for ChartData<X,Y> where X:Clone, Y:Clone {
394    fn from(value: [(X,Y);N]) -> Self {
395        Vector2D(value.to_vec())
396    }
397}
398
399#[derive(Deserialize, Debug, Clone)]
400#[serde(untagged)]
401pub enum ChartData<X,Y>{
402    Vector2D(Vec<(X,Y)>),
403    VectorWithRadius(Vec<DataPointWithRadius<X,Y>>),
404    VectorWithText(Vec<DataPointWithTooltip<X,Y>>)
405}
406
407
408impl<X,Y> Serialize for ChartData<X,Y> where X:Serialize, Y:Serialize{
409    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer {
410        match self {
411            Vector2D(v) => {
412                let mut v_ser = serializer.serialize_seq(Some(v.len()))?;
413                for (x,y) in v {
414                    let  point = DataPoint{x,y};
415                    v_ser.serialize_element(&point)?;
416                }
417                v_ser.end()
418            },
419            VectorWithRadius(v) => v.serialize(serializer),
420            VectorWithText(v) => v.serialize(serializer)
421        }
422    }
423}
424
425
426#[derive(Serialize, Deserialize, Debug, Clone)]
427pub struct DataPoint<X,Y>{
428    x: X,
429    y: Y
430}
431
432#[derive(Serialize, Deserialize, Debug, Clone)]
433pub struct DataPointWithTooltip<X,Y>{
434    x: X,
435    y: Y,
436    tooltip: String
437}
438
439
440#[derive(Serialize, Deserialize, Debug, Clone)]
441pub struct DataPointWithRadius<X,Y>{
442    x: X,
443    y: Y,
444    r: u32
445}
446
447#[derive(Serialize, Deserialize, Debug, Clone)]
448#[serde(untagged)]
449enum FillVariant{
450    AbsIndex(u8),
451    RelativeIndex(String),
452    Boundary(Boundary),
453    AxisValue(AxisValue)
454}
455
456#[derive(Serialize, Deserialize, Debug, Clone)]
457struct AxisValue{
458    value: AxisValueVariant
459}
460
461#[derive(Serialize, Deserialize, Debug, Clone)]
462enum AxisValueVariant{
463    Str(String),
464    Num(f64)
465}
466
467#[derive(Serialize, Deserialize, Debug, Clone)]
468enum Boundary{
469    Start,
470    End,
471    Origin,
472    Stack,
473    Shape
474}
475
476#[derive(Serialize, Deserialize, Debug, Clone)]
477#[serde(rename_all = "camelCase")]
478struct  Fill {
479    target: FillVariant,
480
481    #[serde(skip_serializing_if = "Option::is_none")]
482    above: Option<Rgb>,
483
484    #[serde(skip_serializing_if = "Option::is_none")]
485    below: Option<Rgb>
486}
487
488#[derive(Serialize, Deserialize, Debug, Clone)]
489#[serde(rename_all = "lowercase")]
490enum AxisName{
491    X,
492    Y
493}
494
495#[derive(Debug, Clone)]
496pub struct ChartOptions<X,Y>{
497    pub(crate) scales: Option<ScalingConfig<X,Y>>,
498    pub aspect_ratio: Option<f32>,
499    pub plugins: Plugins,
500}
501
502impl<X,Y> Default for ChartOptions<X,Y>{
503    fn default() -> Self {
504        ChartOptions{
505            scales: None,
506            aspect_ratio: None,
507            plugins: Plugins::default(),
508        }
509    }
510}
511
512#[derive(Serialize, Deserialize, Debug, Clone)]
513#[serde(rename_all = "camelCase")]
514pub struct ScalingConfig<X,Y>{
515    #[serde(skip_serializing_if = "Option::is_none")]
516    x: Option<ScaleConfig<X>>,
517
518    #[serde(skip_serializing_if = "Option::is_none")]
519    y: Option<ScaleConfig<Y>>
520}
521
522#[derive(Serialize, Deserialize, Debug, Clone)]
523#[serde(rename_all = "camelCase")]
524pub struct ScaleConfig<T>{
525
526    #[serde(skip_serializing_if = "Option::is_none")]
527    r#type: Option<ScaleType>,
528
529    #[serde(skip_serializing_if = "Option::is_none")]
530    labels: Option<Vec<T>>,
531
532    #[serde(skip_serializing_if = "Option::is_none")]
533    align_to_pixels: Option<bool>,
534
535    reverse: bool,
536
537    #[serde(skip_serializing_if = "Option::is_none")]
538    max: Option<T>,
539
540    #[serde(skip_serializing_if = "Option::is_none")]
541    min: Option<T>,
542
543    #[serde(skip_serializing_if = "Option::is_none")]
544    title: Option<AxisTitle>
545}
546
547
548impl<T> Default for ScaleConfig<T>{
549    fn default() -> Self {
550        ScaleConfig{
551            r#type: None,
552            labels: None,
553            align_to_pixels: None,
554            reverse: false,
555            max: None,
556            min: None,
557            title: None
558        }
559    }
560}
561
562impl<T> ScaleConfig<T>{
563
564    pub fn new_category(reverse:bool,values: Vec<T>) -> Self {
565        Self {
566            r#type: Some(ScaleType::Category),
567            labels: Some(values),
568            reverse,
569            ..ScaleConfig::<T>::default()
570        }
571    }
572
573    pub fn with_type(mut self, r#type: ScaleType) -> Self {
574        self.r#type = Some(r#type);
575        self
576    }
577
578    pub fn with_align_to_pixels(mut self, align_to_pixels: bool) -> Self {
579        self.align_to_pixels = Some(align_to_pixels);
580        self
581    }
582
583    pub fn with_max(mut self, max: T) -> Self {
584        self.max = Some(max);
585        self
586    }
587
588    pub fn with_min(mut self, min: T) -> Self {
589        self.min = Some(min);
590        self
591    }
592
593    pub fn with_str_title(mut self, title: &str) -> Self {
594        self.title = Some(title.into());
595        self
596    }
597
598    pub fn with_title(mut self, title: AxisTitle) -> Self {
599        self.title = Some(title);
600        self
601    }
602
603    pub fn with_labels(mut self, labels: Vec<T>) -> Self {
604        self.labels = Some(labels);
605        self
606    }
607
608    pub fn with_reverse(mut self, reverse: bool) -> Self {
609        self.reverse = reverse;
610        self
611    }
612
613}
614
615#[derive(Serialize, Deserialize, Debug, Clone,PartialEq)]
616#[serde(rename_all = "lowercase")]
617pub enum ScaleType{
618    Linear,
619    Logarithmic,
620    Category,
621    Time,
622    TimeSeries,
623    RadialLinear
624}
625
626#[derive(Serialize, Deserialize, Debug, Clone)]
627#[serde(rename_all = "camelCase")]
628pub struct AxisTitle{
629    display: bool,
630    text: String,
631    align: Alignment,
632}
633
634
635impl From<&str> for AxisTitle {
636    fn from(value: &str) -> Self {
637        Self{
638            display: true,
639            text: value.to_string(),
640            align: Alignment::Center
641        }
642    }
643}
644
645impl AxisTitle{
646    pub fn new(text: String) -> Self {
647        Self{
648            display: true,
649            text,
650            align: Alignment::Center
651        }
652    }
653
654    pub fn with_display(mut self, display: bool) -> Self {
655        self.display = display;
656        self
657    }
658
659    pub fn with_text(mut self, text: String) -> Self {
660        self.text = text;
661        self
662    }
663
664    pub fn with_align(mut self, align: Alignment) -> Self {
665        self.align = align;
666        self
667    }
668
669}
670
671#[derive(Serialize, Deserialize, Debug, Clone)]
672#[serde(rename_all = "lowercase")]
673pub enum Alignment{
674    Start,
675    Center,
676    End
677}
678
679#[derive(Debug, Clone)]
680#[derive(Default)]
681pub struct Plugins{
682    pub title: Option<Title>,
683    pub subtitle: Option<Title>,
684    pub legend: Option<Legend>,
685    pub tooltip: Option<Tooltip>
686}
687
688#[derive(Serialize, Deserialize, Debug, Clone)]
689#[serde(rename_all = "camelCase")]
690pub struct Legend{
691    display: bool,
692
693    #[serde(skip_serializing_if = "Option::is_none")]
694    position: Option<Position>,
695
696    #[serde(skip_serializing_if = "Option::is_none")]
697    align: Option<Alignment>,
698
699    pub full_size: bool,
700}
701
702
703impl Default for Legend{
704    fn default() -> Self {
705        Legend{
706            display: true,
707            position: None,
708            align: None,
709            full_size: false
710        }
711    }
712}
713
714impl Legend {
715    pub fn new_position(position: Position) -> Self {
716        Self {
717            display: true,
718            position: Some(position),
719            align: None,
720            full_size: false
721        }
722    }
723
724    pub fn  with_align(mut self, align: Alignment) -> Self {
725        self.align = Some(align);
726        self
727    }
728
729    pub fn  with_full_size(mut self, full_size: bool) -> Self {
730        self.full_size = full_size;
731        self
732    }
733
734    pub fn  with_display(mut self, display: bool) -> Self {
735        self.display = display;
736        self
737    }
738
739    pub fn  with_position(mut self, position: Position) -> Self {
740        self.position = Some(position);
741        self
742    }
743
744}
745
746#[derive(Serialize, Deserialize, Debug, Clone)]
747#[serde(rename_all = "lowercase")]
748pub enum Position{
749    Top,
750    Left,
751    Bottom,
752    Right
753}
754
755#[derive(Serialize, Deserialize, Debug, Clone)]
756#[serde(rename_all = "lowercase")]
757pub enum Align{
758    Start,
759    Center,
760    End
761}
762
763#[derive(Serialize, Deserialize, Debug, Clone)]
764#[serde(rename_all = "camelCase")]
765pub struct Title{
766    ///Is the title shown?
767    display: bool,
768
769    ///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.
770    full_size: bool,
771
772    ///Title text to display. If specified as an array, text is rendered on multiple lines.
773    text: Vec<String>,
774
775    ///Padding to apply around the title. Only top and bottom are implemented.
776    #[serde(skip_serializing_if = "Option::is_none")]
777    padding: Option<Padding>,
778
779    ///position of the title
780    #[serde(skip_serializing_if = "Option::is_none")]
781    position: Option<Position>
782
783}
784
785
786#[derive(Serialize, Deserialize, Debug, Clone)]
787#[serde(rename_all = "lowercase")]
788pub enum TooltipMode{
789    Average,
790    Nearest
791}
792
793
794impl Default for Tooltip {
795    fn default() -> Self {
796        Tooltip{
797            enabled: true,
798            mode: None,
799            background_color: None,
800            title_color: None,
801            callbacks: None
802        }
803    }
804}
805
806#[derive(Debug, Clone)]
807pub struct Tooltip{
808    pub enabled: bool,
809    pub mode: Option<TooltipMode>,
810    pub background_color: Option<Rgb>,
811    pub title_color: Option<Rgb>,
812    pub callbacks: Option<TooltipCallbacks>
813}
814
815#[derive(Debug, Clone,Default)]
816pub struct TooltipCallbacks{
817    pub before_title: Option<JsExpr>,
818    pub title: Option<JsExpr>,
819    pub after_title: Option<JsExpr>,
820    pub before_body: Option<JsExpr>,
821    pub before_label: Option<JsExpr>,
822    pub label: Option<JsExpr>,
823    pub label_color: Option<JsExpr>,
824    pub label_text_color: Option<JsExpr>,
825    pub after_label: Option<JsExpr>,
826    pub after_body: Option<JsExpr>,
827    pub before_footer: Option<JsExpr>,
828    pub footer: Option<JsExpr>,
829    pub after_footer: Option<JsExpr>,
830}
831
832
833#[derive(Debug, Clone)]
834pub struct JsExpr(pub &'static str);
835
836impl std::fmt::Display for JsExpr {
837    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838        // Print raw JS, no quotes
839        f.write_str(&self.0)
840    }
841}
842