chart_js_wrapper/
options.rs

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