Skip to main content

mdbook_plotly/code_handler/
plot_obj_parser.rs

1pub use super::until;
2use super::until::{Color, Map};
3use crate::{translate, translate_enum};
4use anyhow::{Result, anyhow};
5use plotly::{Configuration, Layout, Plot, Trace};
6use serde_json::Value;
7
8pub mod bar_parser;
9pub mod box_plot_parser;
10pub mod candlestick_parser;
11pub mod contour_parser;
12pub mod density_mapbox_parser;
13pub mod heat_map_parser;
14pub mod histogram_parser;
15pub mod image_parser;
16pub mod mesh3d_parser;
17pub mod ohlc_parser;
18pub mod pie_parser;
19pub mod sankey_parser;
20pub mod scatter3d_parser;
21pub mod scatter_geo_parser;
22pub mod scatter_mapbox_parser;
23pub mod scatter_parser;
24pub mod scatter_polar_parser;
25pub mod surface_parser;
26pub mod table_parser;
27
28pub fn parse(plot_obj: &mut Value) -> Result<Plot> {
29    let mut plot = Plot::new();
30
31    let map = if let Some(map_obj) = plot_obj.get_mut("map") {
32        serde_json::from_value::<Map>(map_obj.take())?
33    } else {
34        Map::new()
35    };
36
37    if let Some(config_obj) = plot_obj.get_mut("config")
38        && config_obj.is_object()
39    {
40        let config = parse_config_obj(config_obj, &map)?;
41        plot.set_configuration(config);
42    }
43
44    if let Some(layout_obj) = plot_obj.get_mut("layout")
45        && layout_obj.is_object()
46    {
47        let layout = parse_layout_obj(layout_obj, &map)?;
48        plot.set_layout(layout);
49    }
50
51    if let Some(data_list) = plot_obj.get_mut("data")
52        && data_list.is_array()
53    {
54        for data in data_list.as_array_mut().unwrap_or_else(|| unreachable!()) {
55            let trace = parse_data_obj(data, &map)?;
56            plot.add_trace(trace);
57        }
58    }
59
60    Ok(plot)
61}
62
63fn parse_config_obj(config_obj: &mut Value, map: &Map) -> Result<Configuration> {
64    use plotly::configuration::{DisplayModeBar, DoubleClick};
65
66    let config = translate! {
67        Configuration::new(),
68        config_obj,
69        map,
70        (static_plot, bool),
71        (typeset_math, bool),
72        (editable, bool),
73        (autosizable, bool),
74        (fill_frame, bool),
75        (frame_margins, f64),
76        (scroll_zoom, bool),
77        (show_axis_drag_handles, bool),
78        (show_axis_range_entry_boxes, bool),
79        (show_tips, bool),
80        (show_link, bool),
81        (send_data, bool),
82        (show_edit_in_chart_studio, bool),
83        (double_click_delay, usize),
84        (queue_length, usize),
85        (display_logo, bool),
86        (watermark, bool),
87    }?;
88
89    let config = translate_enum! {
90        config,
91        config_obj,
92        map,
93        (display_mode_bar, {
94            "hover"  => DisplayModeBar::Hover,
95            "true"   => DisplayModeBar::True,
96            "false"  => DisplayModeBar::False,
97        }),
98        (double_click, {
99            "false"         => DoubleClick::False,
100            "reset"         => DoubleClick::Reset,
101            "autosize"      => DoubleClick::AutoSize,
102            "reset+autosize"=> DoubleClick::ResetAutoSize,
103        }),
104    }?;
105
106    Ok(config)
107}
108
109fn parse_layout_obj(layout_obj: &mut Value, map: &Map) -> Result<Layout> {
110    use plotly::layout::{
111        ClickMode, DragMode, GroupClick, HoverMode, ItemClick, ItemSizing, Legend, Margin,
112        TraceOrder, VAlign,
113    };
114
115    let layout = translate! {
116        Layout::new(),
117        layout_obj,
118        map,
119        (title, String),
120        (show_legend, bool),
121        (auto_size, bool),
122        (height, usize),
123        (width, usize),
124        (colorway, Vec<Color>),
125        (plot_background_color, Color),
126        (paper_background_color, Color),
127        (separators, String),
128        (bar_gap, f64),
129        (bar_group_gap, f64),
130        (box_gap, f64),
131        (box_group_gap, f64),
132    }?;
133
134    let layout = translate_enum! {
135        layout,
136        layout_obj,
137        map,
138        (hover_mode, {
139            "x"          => HoverMode::X,
140            "y"          => HoverMode::Y,
141            "closest"    => HoverMode::Closest,
142            "false"      => HoverMode::False,
143            "x unified"  => HoverMode::XUnified,
144            "y unified"  => HoverMode::YUnified,
145        }),
146        (drag_mode, {
147            "zoom"      => DragMode::Zoom,
148            "pan"       => DragMode::Pan,
149            "select"    => DragMode::Select,
150            "lasso"     => DragMode::Lasso,
151            "orbit"     => DragMode::Orbit,
152            "turntable" => DragMode::Turntable,
153            "false"     => DragMode::False,
154        }),
155        (click_mode, {
156            "event"        => ClickMode::Event,
157            "select"       => ClickMode::Select,
158            "none"         => ClickMode::None,
159        }),
160    }?;
161
162    let layout = if let Some(legend_obj) = layout_obj.get_mut("legend")
163        && legend_obj.is_object()
164    {
165        let legend = translate! {
166            Legend::new(),
167            legend_obj,
168            map,
169            (background_color, Color),
170            (border_color, Color),
171            (border_width, usize),
172            (x, f64),
173            (y, f64),
174            (trace_group_gap, usize),
175            (item_width, usize),
176            (title, String),
177        }?;
178
179        let legend = translate_enum! {
180            legend,
181            legend_obj,
182            map,
183            (trace_order, {
184                "reversed"        => TraceOrder::Reversed,
185                "grouped"         => TraceOrder::Grouped,
186                "reversed+grouped"=> TraceOrder::ReversedGrouped,
187                "normal"          => TraceOrder::Normal,
188            }),
189            (item_sizing, {
190                "trace"    => ItemSizing::Trace,
191                "constant" => ItemSizing::Constant,
192            }),
193            (item_click, {
194                "toggle"       => ItemClick::Toggle,
195                "toggleothers" => ItemClick::ToggleOthers,
196                "false"        => ItemClick::False,
197            }),
198            (item_double_click, {
199                "toggle"       => ItemClick::Toggle,
200                "toggleothers" => ItemClick::ToggleOthers,
201                "false"        => ItemClick::False,
202            }),
203            (valign, {
204                "top"    => VAlign::Top,
205                "middle" => VAlign::Middle,
206                "bottom" => VAlign::Bottom,
207            }),
208            (group_click, {
209                "toggleitem" => GroupClick::ToggleItem,
210                "togglegroup"=> GroupClick::ToggleGroup,
211            }),
212        }?;
213
214        layout.legend(legend)
215    } else {
216        layout
217    };
218
219    let layout = if let Some(margin_obj) = layout_obj.get_mut("margin")
220        && margin_obj.is_object()
221    {
222        let margin = translate! {
223            Margin::new(),
224            margin_obj,
225            map,
226            (left, usize),
227            (right, usize),
228            (top, usize),
229            (bottom, usize),
230            (pad, usize),
231            (auto_expand, bool)
232        }?;
233        layout.margin(margin)
234    } else {
235        layout
236    };
237
238    // ── Phase 1: Basic fields & sub-objects ──
239
240    // Font sub-object
241    use plotly::common::Font;
242    let layout = if let Some(font_obj) = layout_obj.get_mut("font")
243        && font_obj.is_object()
244    {
245        let font = translate! {
246            Font::new(),
247            font_obj,
248            map,
249            (family, String),
250            (size, usize),
251            (color, Color),
252        }?;
253        layout.font(font)
254    } else {
255        layout
256    };
257
258    // ColorAxis sub-object
259    use plotly::layout::ColorAxis;
260    let layout = if let Some(ca_obj) = layout_obj.get_mut("coloraxis")
261        && ca_obj.is_object()
262    {
263        let ca = translate! {
264            ColorAxis::new(),
265            ca_obj,
266            map,
267            (cmin, f64),
268            (cmax, f64),
269            (cmid, f64),
270            (auto_color_scale, bool),
271            (reverse_scale, bool),
272            (show_scale, bool),
273        }?;
274        layout.color_axis(ca)
275    } else {
276        layout
277    };
278
279    // ── Phase 2: Axis support ──
280    // Default axes: `xaxis` & `yaxis`
281    let layout = if let Some(axis_obj) = layout_obj.get_mut("xaxis")
282        && axis_obj.is_object()
283    {
284        let axis = parse_axis_obj(axis_obj, map)?;
285        layout.x_axis(axis)
286    } else {
287        layout
288    };
289
290    let layout = if let Some(axis_obj) = layout_obj.get_mut("yaxis")
291        && axis_obj.is_object()
292    {
293        let axis = parse_axis_obj(axis_obj, map)?;
294        layout.y_axis(axis)
295    } else {
296        layout
297    };
298
299    // Named axes: `xaxis2`, `xaxis3`, … / `yaxis2`, `yaxis3`, …
300    // Layout methods: x_axis2(), x_axis3(), … / y_axis2(), y_axis3(), …
301    let layout = parse_named_axes(layout, layout_obj, map, "x")?;
302    let layout = parse_named_axes(layout, layout_obj, map, "y")?;
303
304    Ok(layout)
305}
306
307/// Parse an `Axis` object from a JSON value.
308fn parse_axis_obj(axis_obj: &mut Value, map: &Map) -> Result<plotly::layout::Axis> {
309    use crate::code_handler::until::DataPack;
310    use plotly::layout::{Axis, AxisType};
311
312    // translate! for simple fields.
313    // Note: builder methods whose parameter types cannot be directly deserialized
314    // from JSON (e.g. &[f64], enums) must be handled manually below.
315    let axis = translate! {
316        Axis::new(),
317        axis_obj,
318        map,
319        (title, String),
320        (show_grid, bool),
321        (show_line, bool),
322        (zero_line, bool),
323        (visible, bool),
324        (anchor, String),
325        (overlaying, String),
326        (range, Vec<Option<f64>>),
327        (color, Color),
328        (line_color, Color),
329        (grid_color, Color),
330        (tick_prefix, String),
331        (tick_suffix, String),
332        (tick_format, String),
333        (hover_format, String),
334        (category_array, Vec<String>),
335        (fixed_range, bool),
336        (scale_anchor, String),
337        (auto_margin, bool),
338        (show_tick_labels, bool),
339    }?;
340
341    let axis = translate_enum! {
342        axis,
343        axis_obj,
344        map,
345        (category_order, {
346            "trace"               => plotly::layout::CategoryOrder::Trace,
347            "category-ascending"  => plotly::layout::CategoryOrder::CategoryAscending,
348            "category-descending" => plotly::layout::CategoryOrder::CategoryDescending,
349            "array"               => plotly::layout::CategoryOrder::Array,
350            "total-ascending"     => plotly::layout::CategoryOrder::TotalAscending,
351            "total-descending"    => plotly::layout::CategoryOrder::TotalDescending,
352            "min-ascending"       => plotly::layout::CategoryOrder::MinAscending,
353            "min-descending"      => plotly::layout::CategoryOrder::MinDescending,
354            "max-ascending"       => plotly::layout::CategoryOrder::MaxAscending,
355            "max-descending"      => plotly::layout::CategoryOrder::MaxDescending,
356            "sum-ascending"       => plotly::layout::CategoryOrder::SumAscending,
357            "sum-descending"      => plotly::layout::CategoryOrder::SumDescending,
358            "mean-ascending"      => plotly::layout::CategoryOrder::MeanAscending,
359            "mean-descending"     => plotly::layout::CategoryOrder::MeanDescending,
360            "median-ascending"    => plotly::layout::CategoryOrder::MedianAscending,
361            "median-descending"   => plotly::layout::CategoryOrder::MedianDescending,
362        }),
363    }?;
364
365    // Handle `type` field separately — `type` is a Rust keyword,
366    // the plotly crate exposes it as `type_()` which takes an `AxisType` enum.
367    let axis = if let Some(v) = axis_obj.get_mut("type") {
368        let data = serde_json::from_value::<DataPack<String>>(v.take())
369            .map_err(|e| anyhow!("Failed to deserialize axis `type`: {}", e))?;
370        let s = data
371            .unwrap(map)
372            .map_err(|e| anyhow!("Failed to unwrap DataPack for axis `type`: {}", e))?;
373        let at = match s.as_str() {
374            "-" | "linear" => AxisType::Linear,
375            "log" => AxisType::Log,
376            "date" => AxisType::Date,
377            "category" => AxisType::Category,
378            "multicategory" => AxisType::MultiCategory,
379            other => return Err(anyhow!("Invalid axis type: '{}'", other)),
380        };
381        axis.type_(at)
382    } else {
383        axis
384    };
385
386    Ok(axis)
387}
388
389/// Parse named axes (xaxis2..xaxis8, yaxis2..yaxis8) and chain them onto the layout.
390/// JSON `xaxisN` → `Layout::x_axisN()`, JSON `yaxisN` → `Layout::y_axisN()`.
391fn parse_named_axes(
392    layout: Layout,
393    layout_obj: &mut Value,
394    map: &Map,
395    prefix: &str,
396) -> Result<Layout> {
397    let mut layout = layout;
398    // plotly.rs 0.14 supports up to 8 additional axes (xaxis2..xaxis8, yaxis2..yaxis8)
399    for i in 2..=8 {
400        let json_key = format!("{}axis{}", prefix, i);
401        let Some(axis_obj) = layout_obj.get_mut(json_key.as_str()) else {
402            continue;
403        };
404        if !axis_obj.is_object() {
405            continue;
406        }
407        let axis = parse_axis_obj(axis_obj, map)?;
408        if prefix == "x" {
409            layout = match i {
410                2 => layout.x_axis2(axis),
411                3 => layout.x_axis3(axis),
412                4 => layout.x_axis4(axis),
413                5 => layout.x_axis5(axis),
414                6 => layout.x_axis6(axis),
415                7 => layout.x_axis7(axis),
416                8 => layout.x_axis8(axis),
417                _ => unreachable!(),
418            };
419        } else {
420            layout = match i {
421                2 => layout.y_axis2(axis),
422                3 => layout.y_axis3(axis),
423                4 => layout.y_axis4(axis),
424                5 => layout.y_axis5(axis),
425                6 => layout.y_axis6(axis),
426                7 => layout.y_axis7(axis),
427                8 => layout.y_axis8(axis),
428                _ => unreachable!(),
429            };
430        }
431    }
432    Ok(layout)
433}
434
435pub fn parse_data_obj(data_obj: &mut Value, map: &Map) -> Result<Box<dyn Trace>> {
436    let data_type = data_obj
437        .get("type")
438        .and_then(|v| v.as_str())
439        .ok_or_else(|| anyhow!("`type` must be a string"))?;
440    match data_type {
441        "bar" => bar_parser::parse_bar_data(data_obj, map).map(|v| v as Box<dyn Trace>),
442        "box" => box_plot_parser::parse_box_plot_data(data_obj, map).map(|v| v as Box<dyn Trace>),
443        "candlestick" => {
444            candlestick_parser::parse_candlestick_data(data_obj, map).map(|v| v as Box<dyn Trace>)
445        }
446        "contour" => contour_parser::parse_contour_data(data_obj, map).map(|v| v as Box<dyn Trace>),
447        "density_mapbox" => density_mapbox_parser::parse_density_mapbox_data(data_obj, map)
448            .map(|v| v as Box<dyn Trace>),
449        "heatmap" => {
450            heat_map_parser::parse_heat_map_data(data_obj, map).map(|v| v as Box<dyn Trace>)
451        }
452        "histogram" => {
453            histogram_parser::parse_histogram_data(data_obj, map).map(|v| v as Box<dyn Trace>)
454        }
455        "ohlc" => ohlc_parser::parse_ohlc_data(data_obj, map).map(|v| v as Box<dyn Trace>),
456        "image" => image_parser::parse_image_data(data_obj, map).map(|v| v as Box<dyn Trace>),
457        "mesh3d" => mesh3d_parser::parse_mesh3d_data(data_obj, map).map(|v| v as Box<dyn Trace>),
458        "pie" => pie_parser::parse_pie_data(data_obj, map).map(|v| v as Box<dyn Trace>),
459        "sankey" => sankey_parser::parse_sankey_data(data_obj, map).map(|v| v as Box<dyn Trace>),
460        "scatter" => scatter_parser::parse_scatter_data(data_obj, map).map(|v| v as Box<dyn Trace>),
461        "scatter3d" => {
462            scatter3d_parser::parse_scatter3d_data(data_obj, map).map(|v| v as Box<dyn Trace>)
463        }
464        "scatter_geo" => {
465            scatter_geo_parser::parse_scatter_geo_data(data_obj, map).map(|v| v as Box<dyn Trace>)
466        }
467        "scatter_mapbox" => scatter_mapbox_parser::parse_scatter_mapbox_data(data_obj, map)
468            .map(|v| v as Box<dyn Trace>),
469        "scatter_polar" => scatter_polar_parser::parse_scatter_polar_data(data_obj, map)
470            .map(|v| v as Box<dyn Trace>),
471        "surface" => surface_parser::parse_surface_data(data_obj, map).map(|v| v as Box<dyn Trace>),
472        "table" => table_parser::parse_table_data(data_obj, map).map(|v| v as Box<dyn Trace>),
473        unexpected => Err(anyhow!("{} isn't a type in data", unexpected)),
474    }
475}