Skip to main content

plotlars_plotly/
ext.rs

1use std::env;
2
3use plotlars_core::Plot;
4
5use crate::render::{
6    build_plotly_result, ir_to_json, open_html_file, render_html_from_json,
7    render_inline_html_from_json,
8};
9
10/// Plotly rendering extension trait. Provides all visualization methods.
11pub trait PlotlyExt: Plot {
12    fn plot(&self);
13    fn write_html(&self, path: impl Into<String>);
14    fn to_json(&self) -> Result<String, serde_json::Error>;
15    fn to_html(&self) -> String;
16    fn to_inline_html(&self, plot_div_id: Option<&str>) -> String;
17
18    #[cfg(any(
19        feature = "export-chrome",
20        feature = "export-firefox",
21        feature = "export-default"
22    ))]
23    fn write_image(
24        &self,
25        path: impl Into<String>,
26        width: usize,
27        height: usize,
28        scale: f64,
29    ) -> Result<(), Box<dyn std::error::Error + 'static>>;
30}
31
32impl<T: Plot> PlotlyExt for T {
33    fn plot(&self) {
34        let result = build_plotly_result(self);
35        if result.layout_overrides.is_some() {
36            // For plots with layout overrides (scene/polar domains),
37            // we must serialize traces via Trace::to_json() to capture
38            // injected keys like "scene" on Surface traces.
39            let json = ir_to_json(self).unwrap_or_default();
40            let html = render_html_from_json(&json);
41            let temp_dir = std::env::temp_dir();
42            let timestamp = std::time::SystemTime::now()
43                .duration_since(std::time::UNIX_EPOCH)
44                .unwrap()
45                .as_nanos();
46            let temp_path = temp_dir.join(format!(
47                "plotlars_{}_{}.html",
48                std::process::id(),
49                timestamp
50            ));
51            std::fs::write(&temp_path, html).expect("failed to write temp html");
52            open_html_file(&temp_path);
53        } else {
54            match env::var("EVCXR_IS_RUNTIME") {
55                Ok(_) => result.plot.evcxr_display(),
56                _ => result.plot.show(),
57            }
58        }
59    }
60
61    fn write_html(&self, path: impl Into<String>) {
62        let result = build_plotly_result(self);
63        if result.layout_overrides.is_some() {
64            let json = ir_to_json(self).unwrap_or_default();
65            let html = render_html_from_json(&json);
66            std::fs::write(path.into(), html).expect("failed to write html output");
67        } else {
68            result.plot.write_html(path.into());
69        }
70    }
71
72    fn to_json(&self) -> Result<String, serde_json::Error> {
73        ir_to_json(self)
74    }
75
76    fn to_html(&self) -> String {
77        let result = build_plotly_result(self);
78        if result.layout_overrides.is_some() {
79            let json = ir_to_json(self).unwrap_or_default();
80            render_html_from_json(&json)
81        } else {
82            result.plot.to_html()
83        }
84    }
85
86    fn to_inline_html(&self, plot_div_id: Option<&str>) -> String {
87        let result = build_plotly_result(self);
88        let div_id = plot_div_id.unwrap_or("plotly-div");
89        if result.layout_overrides.is_some() {
90            let json = ir_to_json(self).unwrap_or_default();
91            render_inline_html_from_json(&json, div_id)
92        } else {
93            result.plot.to_inline_html(plot_div_id)
94        }
95    }
96
97    #[cfg(any(
98        feature = "export-chrome",
99        feature = "export-firefox",
100        feature = "export-default"
101    ))]
102    fn write_image(
103        &self,
104        path: impl Into<String>,
105        width: usize,
106        height: usize,
107        scale: f64,
108    ) -> Result<(), Box<dyn std::error::Error + 'static>> {
109        let path_string = path.into();
110        let result = build_plotly_result(self);
111
112        // Image export uses plotly.rs directly; layout overrides are not
113        // applicable here because the plotly.js static exporter only reads
114        // the standard Layout fields. For scene/polar faceted plots the
115        // JSON override path should be used for HTML output only.
116        if let Some((filename, extension)) = path_string.rsplit_once('.') {
117            let format = match extension {
118                "png" => plotly::ImageFormat::PNG,
119                "jpg" | "jpeg" => plotly::ImageFormat::JPEG,
120                "webp" => plotly::ImageFormat::WEBP,
121                "svg" => plotly::ImageFormat::SVG,
122                _ => return Err(format!("Unsupported image format: {extension}").into()),
123            };
124            result
125                .plot
126                .write_image(filename, format, width, height, scale)?;
127        } else {
128            return Err("No extension provided for image.".into());
129        }
130
131        Ok(())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use plotlars_core::components::orientation::Orientation;
139    use plotlars_core::components::Rgb;
140    use plotlars_core::plots::array2dplot::Array2dPlot;
141    use plotlars_core::plots::barplot::BarPlot;
142    use plotlars_core::plots::boxplot::BoxPlot;
143    use plotlars_core::plots::candlestick::CandlestickPlot;
144    use plotlars_core::plots::contourplot::ContourPlot;
145    use plotlars_core::plots::density_mapbox::DensityMapbox;
146    use plotlars_core::plots::heatmap::HeatMap;
147    use plotlars_core::plots::histogram::Histogram;
148    use plotlars_core::plots::lineplot::LinePlot;
149    use plotlars_core::plots::mesh3d::Mesh3D;
150    use plotlars_core::plots::ohlc::OhlcPlot;
151    use plotlars_core::plots::piechart::PieChart;
152    use plotlars_core::plots::sankeydiagram::SankeyDiagram;
153    use plotlars_core::plots::scatter3dplot::Scatter3dPlot;
154    use plotlars_core::plots::scattergeo::ScatterGeo;
155    use plotlars_core::plots::scattermap::ScatterMap;
156    use plotlars_core::plots::scatterplot::ScatterPlot;
157    use plotlars_core::plots::scatterpolar::ScatterPolar;
158    use plotlars_core::plots::surfaceplot::SurfacePlot;
159    use plotlars_core::plots::table::Table;
160    use plotlars_core::plots::timeseriesplot::TimeSeriesPlot;
161    use plotlars_core::Plot;
162    use polars::prelude::*;
163
164    fn to_json_value(plot: &impl Plot) -> serde_json::Value {
165        let json_str = ir_to_json(plot).unwrap();
166        serde_json::from_str(&json_str).unwrap()
167    }
168
169    #[test]
170    fn test_scatter_to_json_has_traces() {
171        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
172        let plot = ScatterPlot::builder().data(&df).x("x").y("y").build();
173        let json = to_json_value(&plot);
174        assert!(json["traces"].is_array());
175        assert_eq!(json["traces"].as_array().unwrap().len(), 1);
176    }
177
178    #[test]
179    fn test_scatter_to_json_has_layout() {
180        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
181        let plot = ScatterPlot::builder().data(&df).x("x").y("y").build();
182        let json = to_json_value(&plot);
183        assert!(json["layout"].is_object());
184    }
185
186    #[test]
187    fn test_scatter_to_json_with_title() {
188        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
189        let plot = ScatterPlot::builder()
190            .data(&df)
191            .x("x")
192            .y("y")
193            .plot_title("My Plot")
194            .build();
195        let json = to_json_value(&plot);
196        let layout_str = serde_json::to_string(&json["layout"]).unwrap();
197        assert!(layout_str.contains("My Plot"));
198    }
199
200    #[test]
201    fn test_bar_to_json_has_traces() {
202        let df = df!["labels" => ["a", "b", "c"], "values" => [10.0, 20.0, 30.0]].unwrap();
203        let plot = BarPlot::builder()
204            .data(&df)
205            .labels("labels")
206            .values("values")
207            .build();
208        let json = to_json_value(&plot);
209        assert_eq!(json["traces"].as_array().unwrap().len(), 1);
210    }
211
212    #[test]
213    fn test_bar_to_json_trace_type() {
214        let df = df!["labels" => ["a", "b", "c"], "values" => [10.0, 20.0, 30.0]].unwrap();
215        let plot = BarPlot::builder()
216            .data(&df)
217            .labels("labels")
218            .values("values")
219            .build();
220        let json = to_json_value(&plot);
221        assert_eq!(json["traces"][0]["type"], "bar");
222    }
223
224    #[test]
225    fn test_pie_to_json_trace_type() {
226        let df = df!["labels" => ["a", "b", "c", "a", "b"]].unwrap();
227        let plot = PieChart::builder().data(&df).labels("labels").build();
228        let json = to_json_value(&plot);
229        assert_eq!(json["traces"][0]["type"], "pie");
230    }
231
232    #[test]
233    fn test_scatter_grouped_to_json() {
234        let df = df![
235            "x" => [1.0, 2.0, 3.0, 4.0],
236            "y" => [10.0, 20.0, 30.0, 40.0],
237            "g" => ["a", "b", "a", "b"]
238        ]
239        .unwrap();
240        let plot = ScatterPlot::builder()
241            .data(&df)
242            .x("x")
243            .y("y")
244            .group("g")
245            .build();
246        let json = to_json_value(&plot);
247        assert_eq!(json["traces"].as_array().unwrap().len(), 2);
248    }
249
250    #[test]
251    fn test_scatter_trace_has_x_and_y() {
252        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
253        let plot = ScatterPlot::builder().data(&df).x("x").y("y").build();
254        let json = to_json_value(&plot);
255        let trace = &json["traces"][0];
256        assert!(trace["x"].is_array());
257        assert!(trace["y"].is_array());
258        assert_eq!(trace["x"].as_array().unwrap().len(), 3);
259    }
260
261    #[test]
262    fn test_histogram_to_json() {
263        let df = df!["x" => [1.0, 2.0, 2.0, 3.0, 3.0, 3.0]].unwrap();
264        let plot = Histogram::builder().data(&df).x("x").build();
265        let json = to_json_value(&plot);
266        assert_eq!(json["traces"].as_array().unwrap().len(), 1);
267        assert_eq!(json["traces"][0]["type"], "histogram");
268    }
269
270    #[test]
271    fn test_line_to_json() {
272        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
273        let plot = LinePlot::builder().data(&df).x("x").y("y").build();
274        let json = to_json_value(&plot);
275        assert_eq!(json["traces"].as_array().unwrap().len(), 1);
276        assert_eq!(json["traces"][0]["type"], "scatter");
277    }
278
279    // ---- E2E scatter tests ----
280
281    #[test]
282    fn test_e2e_scatter() {
283        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
284        let plot = ScatterPlot::builder().data(&df).x("x").y("y").build();
285        let json = to_json_value(&plot);
286        let trace = &json["traces"][0];
287        assert_eq!(trace["type"], "scatter");
288        assert_eq!(trace["mode"], "markers");
289        assert_eq!(trace["x"].as_array().unwrap().len(), 3);
290        assert_eq!(trace["y"].as_array().unwrap().len(), 3);
291    }
292
293    #[test]
294    fn test_e2e_scatter_styled() {
295        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
296        let plot = ScatterPlot::builder()
297            .data(&df)
298            .x("x")
299            .y("y")
300            .opacity(0.7)
301            .size(12)
302            .color(Rgb(255, 0, 0))
303            .build();
304        let json = to_json_value(&plot);
305        let marker = &json["traces"][0]["marker"];
306        assert_eq!(marker["opacity"], 0.7);
307        assert_eq!(marker["size"], 12);
308    }
309
310    #[test]
311    fn test_e2e_scatter_grouped() {
312        let df = df![
313            "x" => [1.0, 2.0, 3.0, 4.0],
314            "y" => [10.0, 20.0, 30.0, 40.0],
315            "g" => ["a", "b", "a", "b"]
316        ]
317        .unwrap();
318        let plot = ScatterPlot::builder()
319            .data(&df)
320            .x("x")
321            .y("y")
322            .group("g")
323            .build();
324        let json = to_json_value(&plot);
325        let traces = json["traces"].as_array().unwrap();
326        assert_eq!(traces.len(), 2);
327        assert!(traces[0]["name"].is_string());
328        assert!(traces[1]["name"].is_string());
329    }
330
331    // ---- E2E bar tests ----
332
333    #[test]
334    fn test_e2e_bar() {
335        let df = df!["labels" => ["a", "b", "c"], "values" => [10.0, 20.0, 30.0]].unwrap();
336        let plot = BarPlot::builder()
337            .data(&df)
338            .labels("labels")
339            .values("values")
340            .build();
341        let json = to_json_value(&plot);
342        let trace = &json["traces"][0];
343        assert_eq!(trace["type"], "bar");
344        assert!(trace["x"].is_array() || trace["y"].is_array());
345    }
346
347    #[test]
348    fn test_e2e_bar_horizontal() {
349        let df = df!["labels" => ["a", "b", "c"], "values" => [10.0, 20.0, 30.0]].unwrap();
350        let plot = BarPlot::builder()
351            .data(&df)
352            .labels("labels")
353            .values("values")
354            .orientation(Orientation::Horizontal)
355            .build();
356        let json = to_json_value(&plot);
357        let trace = &json["traces"][0];
358        assert_eq!(trace["type"], "bar");
359        assert_eq!(trace["orientation"], "h");
360    }
361
362    #[test]
363    fn test_e2e_bar_grouped() {
364        let df = df![
365            "labels" => ["a", "b", "a", "b"],
366            "values" => [10.0, 20.0, 30.0, 40.0],
367            "g" => ["x", "x", "y", "y"]
368        ]
369        .unwrap();
370        let plot = BarPlot::builder()
371            .data(&df)
372            .labels("labels")
373            .values("values")
374            .group("g")
375            .build();
376        let json = to_json_value(&plot);
377        assert_eq!(json["traces"].as_array().unwrap().len(), 2);
378    }
379
380    // ---- E2E boxplot test ----
381
382    #[test]
383    fn test_e2e_boxplot() {
384        let df = df![
385            "labels" => ["a", "a", "b", "b"],
386            "values" => [1.0, 2.0, 3.0, 4.0]
387        ]
388        .unwrap();
389        let plot = BoxPlot::builder()
390            .data(&df)
391            .labels("labels")
392            .values("values")
393            .build();
394        let json = to_json_value(&plot);
395        assert_eq!(json["traces"][0]["type"], "box");
396    }
397
398    // ---- E2E histogram test ----
399
400    #[test]
401    fn test_e2e_histogram() {
402        let df = df!["x" => [1.0, 2.0, 2.0, 3.0, 3.0, 3.0]].unwrap();
403        let plot = Histogram::builder().data(&df).x("x").build();
404        let json = to_json_value(&plot);
405        assert_eq!(json["traces"][0]["type"], "histogram");
406        assert!(json["traces"][0]["x"].is_array());
407    }
408
409    // ---- E2E line plot tests ----
410
411    #[test]
412    fn test_e2e_lineplot() {
413        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
414        let plot = LinePlot::builder().data(&df).x("x").y("y").build();
415        let json = to_json_value(&plot);
416        let trace = &json["traces"][0];
417        assert_eq!(trace["type"], "scatter");
418        assert!(trace["x"].is_array());
419        assert!(trace["y"].is_array());
420    }
421
422    #[test]
423    fn test_e2e_lineplot_additional_lines() {
424        let df = df![
425            "x" => [1.0, 2.0, 3.0],
426            "y1" => [4.0, 5.0, 6.0],
427            "y2" => [7.0, 8.0, 9.0]
428        ]
429        .unwrap();
430        let plot = LinePlot::builder()
431            .data(&df)
432            .x("x")
433            .y("y1")
434            .additional_lines(vec!["y2"])
435            .build();
436        let json = to_json_value(&plot);
437        assert!(json["traces"].as_array().unwrap().len() >= 2);
438    }
439
440    // ---- E2E time series test ----
441
442    #[test]
443    fn test_e2e_timeseries() {
444        let df = df![
445            "date" => ["2024-01", "2024-02", "2024-03"],
446            "val" => [10.0, 20.0, 30.0]
447        ]
448        .unwrap();
449        let plot = TimeSeriesPlot::builder()
450            .data(&df)
451            .x("date")
452            .y("val")
453            .build();
454        let json = to_json_value(&plot);
455        let trace = &json["traces"][0];
456        assert_eq!(trace["type"], "scatter");
457        assert_eq!(trace["x"].as_array().unwrap().len(), 3);
458    }
459
460    // ---- E2E heatmap test ----
461
462    #[test]
463    fn test_e2e_heatmap() {
464        let df = df![
465            "x" => ["a", "b", "c"],
466            "y" => ["d", "e", "f"],
467            "z" => [1.0, 2.0, 3.0]
468        ]
469        .unwrap();
470        let plot = HeatMap::builder().data(&df).x("x").y("y").z("z").build();
471        let json = to_json_value(&plot);
472        assert_eq!(json["traces"][0]["type"], "heatmap");
473    }
474
475    // ---- E2E contour test ----
476
477    #[test]
478    fn test_e2e_contour() {
479        let df = df![
480            "x" => ["a", "b", "c"],
481            "y" => ["d", "e", "f"],
482            "z" => [1.0, 2.0, 3.0]
483        ]
484        .unwrap();
485        let plot = ContourPlot::builder()
486            .data(&df)
487            .x("x")
488            .y("y")
489            .z("z")
490            .build();
491        let json = to_json_value(&plot);
492        assert_eq!(json["traces"][0]["type"], "contour");
493    }
494
495    // ---- E2E pie chart test ----
496
497    #[test]
498    fn test_e2e_piechart() {
499        let df = df!["labels" => ["a", "b", "c", "a", "b"]].unwrap();
500        let plot = PieChart::builder().data(&df).labels("labels").build();
501        let json = to_json_value(&plot);
502        assert_eq!(json["traces"][0]["type"], "pie");
503        assert!(json["traces"][0]["labels"].is_array());
504    }
505
506    // ---- E2E sankey test ----
507
508    #[test]
509    fn test_e2e_sankey() {
510        let df = df![
511            "source" => ["A", "A", "B"],
512            "target" => ["B", "C", "C"],
513            "value" => [10.0, 20.0, 30.0]
514        ]
515        .unwrap();
516        let plot = SankeyDiagram::builder()
517            .data(&df)
518            .sources("source")
519            .targets("target")
520            .values("value")
521            .build();
522        let json = to_json_value(&plot);
523        assert_eq!(json["traces"][0]["type"], "sankey");
524        assert!(json["traces"][0]["node"].is_object());
525        assert!(json["traces"][0]["link"].is_object());
526    }
527
528    // ---- E2E candlestick test ----
529
530    #[test]
531    fn test_e2e_candlestick() {
532        let df = df![
533            "date"  => ["2024-01-01", "2024-01-02", "2024-01-03"],
534            "open"  => [100.0, 105.0, 102.0],
535            "high"  => [110.0, 108.0, 107.0],
536            "low"   => [ 95.0, 100.0,  98.0],
537            "close" => [105.0, 102.0, 106.0]
538        ]
539        .unwrap();
540        let plot = CandlestickPlot::builder()
541            .data(&df)
542            .dates("date")
543            .open("open")
544            .high("high")
545            .low("low")
546            .close("close")
547            .build();
548        let json = to_json_value(&plot);
549        assert_eq!(json["traces"][0]["type"], "candlestick");
550    }
551
552    // ---- E2E OHLC test ----
553
554    #[test]
555    fn test_e2e_ohlc() {
556        let df = df![
557            "date"  => ["2024-01-01", "2024-01-02", "2024-01-03"],
558            "open"  => [100.0, 105.0, 102.0],
559            "high"  => [110.0, 108.0, 107.0],
560            "low"   => [ 95.0, 100.0,  98.0],
561            "close" => [105.0, 102.0, 106.0]
562        ]
563        .unwrap();
564        let plot = OhlcPlot::builder()
565            .data(&df)
566            .dates("date")
567            .open("open")
568            .high("high")
569            .low("low")
570            .close("close")
571            .build();
572        let json = to_json_value(&plot);
573        assert_eq!(json["traces"][0]["type"], "ohlc");
574    }
575
576    // ---- E2E scatter polar test ----
577
578    #[test]
579    fn test_e2e_scatter_polar() {
580        let df = df![
581            "angle" => [0.0, 90.0, 180.0, 270.0],
582            "radius" => [1.0, 2.0, 3.0, 4.0]
583        ]
584        .unwrap();
585        let plot = ScatterPolar::builder()
586            .data(&df)
587            .theta("angle")
588            .r("radius")
589            .build();
590        let json = to_json_value(&plot);
591        assert_eq!(json["traces"][0]["type"], "scatterpolar");
592        assert!(json["traces"][0]["theta"].is_array());
593        assert!(json["traces"][0]["r"].is_array());
594    }
595
596    // ---- E2E scatter 3d test ----
597
598    #[test]
599    fn test_e2e_scatter3d() {
600        let df = df![
601            "x" => [1.0, 2.0, 3.0],
602            "y" => [4.0, 5.0, 6.0],
603            "z" => [7.0, 8.0, 9.0]
604        ]
605        .unwrap();
606        let plot = Scatter3dPlot::builder()
607            .data(&df)
608            .x("x")
609            .y("y")
610            .z("z")
611            .build();
612        let json = to_json_value(&plot);
613        assert_eq!(json["traces"][0]["type"], "scatter3d");
614        assert!(json["traces"][0]["x"].is_array());
615        assert!(json["traces"][0]["y"].is_array());
616        assert!(json["traces"][0]["z"].is_array());
617    }
618
619    // ---- E2E surface test ----
620
621    #[test]
622    fn test_e2e_surface() {
623        let df = df![
624            "x" => [1.0, 1.0, 2.0, 2.0],
625            "y" => [1.0, 2.0, 1.0, 2.0],
626            "z" => [5.0, 6.0, 7.0, 8.0]
627        ]
628        .unwrap();
629        let plot = SurfacePlot::builder()
630            .data(&df)
631            .x("x")
632            .y("y")
633            .z("z")
634            .build();
635        let json = to_json_value(&plot);
636        assert_eq!(json["traces"][0]["type"], "surface");
637        assert!(json["traces"][0]["z"].is_array());
638    }
639
640    // ---- E2E mesh3d test ----
641
642    #[test]
643    fn test_e2e_mesh3d() {
644        let df = df![
645            "x" => [0.0, 1.0, 0.5, 0.5],
646            "y" => [0.0, 0.0, 1.0, 0.5],
647            "z" => [0.0, 0.0, 0.0, 1.0]
648        ]
649        .unwrap();
650        let plot = Mesh3D::builder().data(&df).x("x").y("y").z("z").build();
651        let json = to_json_value(&plot);
652        assert_eq!(json["traces"][0]["type"], "mesh3d");
653    }
654
655    // ---- E2E scatter geo test ----
656
657    #[test]
658    fn test_e2e_scattergeo() {
659        let df = df![
660            "lat" => [40.7, 34.0, 41.8],
661            "lon" => [-74.0, -118.2, -87.6]
662        ]
663        .unwrap();
664        let plot = ScatterGeo::builder()
665            .data(&df)
666            .lat("lat")
667            .lon("lon")
668            .build();
669        let json = to_json_value(&plot);
670        assert_eq!(json["traces"][0]["type"], "scattergeo");
671        assert!(json["traces"][0]["lat"].is_array());
672        assert!(json["traces"][0]["lon"].is_array());
673    }
674
675    // ---- E2E scatter map test ----
676
677    #[test]
678    fn test_e2e_scattermap() {
679        let df = df![
680            "latitude" => [48.8, 51.5, 40.7],
681            "longitude" => [2.3, -0.1, -74.0]
682        ]
683        .unwrap();
684        let plot = ScatterMap::builder()
685            .data(&df)
686            .latitude("latitude")
687            .longitude("longitude")
688            .build();
689        let json = to_json_value(&plot);
690        assert_eq!(json["traces"][0]["type"], "scattermapbox");
691        assert!(json["traces"][0]["lat"].is_array());
692        assert!(json["traces"][0]["lon"].is_array());
693        let layout_str = serde_json::to_string(&json["layout"]).unwrap();
694        assert!(layout_str.contains("mapbox"));
695    }
696
697    // ---- E2E density mapbox test ----
698
699    #[test]
700    fn test_e2e_density_mapbox() {
701        let df = df![
702            "lat" => [40.7, 34.0, 41.8],
703            "lon" => [-74.0, -118.2, -87.6],
704            "density" => [100.0, 200.0, 150.0]
705        ]
706        .unwrap();
707        let plot = DensityMapbox::builder()
708            .data(&df)
709            .lat("lat")
710            .lon("lon")
711            .z("density")
712            .build();
713        let json = to_json_value(&plot);
714        assert_eq!(json["traces"][0]["type"], "densitymapbox");
715        let layout_str = serde_json::to_string(&json["layout"]).unwrap();
716        assert!(layout_str.contains("mapbox"));
717    }
718
719    // ---- E2E table test ----
720
721    #[test]
722    fn test_e2e_table() {
723        let df = df![
724            "name" => ["Alice", "Bob", "Carol"],
725            "score" => [90, 85, 95]
726        ]
727        .unwrap();
728        let plot = Table::builder()
729            .data(&df)
730            .columns(vec!["name", "score"])
731            .build();
732        let json = to_json_value(&plot);
733        assert_eq!(json["traces"][0]["type"], "table");
734        assert!(json["traces"][0]["header"].is_object());
735        assert!(json["traces"][0]["cells"].is_object());
736    }
737
738    // ---- E2E array2d test ----
739
740    #[test]
741    fn test_e2e_array2d() {
742        let data = vec![
743            vec![[255, 0, 0], [0, 255, 0], [0, 0, 255]],
744            vec![[0, 0, 255], [255, 0, 0], [0, 255, 0]],
745        ];
746        let plot = Array2dPlot::builder().data(&data).build();
747        let json = to_json_value(&plot);
748        assert_eq!(json["traces"][0]["type"], "image");
749    }
750
751    // ---- E2E layout titles test ----
752
753    #[test]
754    fn test_e2e_with_all_titles() {
755        let df = df!["x" => [1.0, 2.0, 3.0], "y" => [4.0, 5.0, 6.0]].unwrap();
756        let plot = ScatterPlot::builder()
757            .data(&df)
758            .x("x")
759            .y("y")
760            .plot_title("Main Title")
761            .x_title("X Axis")
762            .y_title("Y Axis")
763            .legend_title("Groups")
764            .build();
765        let json = to_json_value(&plot);
766        let layout = &json["layout"];
767        let layout_str = serde_json::to_string(layout).unwrap();
768        assert!(layout_str.contains("Main Title"));
769        assert!(layout_str.contains("X Axis"));
770        assert!(layout_str.contains("Y Axis"));
771        assert!(layout_str.contains("Groups"));
772    }
773
774    // ---- E2E faceted test ----
775
776    #[test]
777    fn test_e2e_scatter_faceted() {
778        let df = df![
779            "x" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
780            "y" => [10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
781            "f" => ["a", "a", "b", "b", "c", "c"]
782        ]
783        .unwrap();
784        let plot = ScatterPlot::builder()
785            .data(&df)
786            .x("x")
787            .y("y")
788            .facet("f")
789            .build();
790        let json = to_json_value(&plot);
791        let traces = json["traces"].as_array().unwrap();
792        assert_eq!(traces.len(), 3);
793        let layout_str = serde_json::to_string(&json["layout"]).unwrap();
794        assert!(layout_str.contains("xaxis2") || layout_str.contains("yaxis2"));
795    }
796}