chart_js_wrapper/
options.rs

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