Skip to main content

mdbook_plotly/code_handler/plot_obj_parser/
layout_parser.rs

1use super::until::Color;
2use crate::code_handler::parse_context::ParseContext;
3use crate::{translate_enum_with_config, translate_with_config};
4use anyhow::{Result, anyhow};
5use plotly::{Configuration, Layout};
6use serde_json::Value;
7
8pub fn parse_config_obj(
9    config_obj: &mut Value,
10    context: &ParseContext<'_>,
11) -> Result<Configuration> {
12    use plotly::configuration::{DisplayModeBar, DoubleClick};
13
14    let config = translate_with_config! {
15        Configuration::new(),
16        config_obj,
17        context.map(),
18        context.map_eval(),
19        (static_plot, bool),
20        (typeset_math, bool),
21        (editable, bool),
22        (autosizable, bool),
23        (fill_frame, bool),
24        (frame_margins, f64),
25        (scroll_zoom, bool),
26        (show_axis_drag_handles, bool),
27        (show_axis_range_entry_boxes, bool),
28        (show_tips, bool),
29        (show_link, bool),
30        (send_data, bool),
31        (show_edit_in_chart_studio, bool),
32        (double_click_delay, usize),
33        (queue_length, usize),
34        (display_logo, bool),
35        (watermark, bool),
36    }?;
37
38    let config = translate_enum_with_config! {
39        config,
40        config_obj,
41        context.map(),
42        context.map_eval(),
43        (display_mode_bar, {
44            "hover"  => DisplayModeBar::Hover,
45            "true"   => DisplayModeBar::True,
46            "false"  => DisplayModeBar::False,
47        }),
48        (double_click, {
49            "false"         => DoubleClick::False,
50            "reset"         => DoubleClick::Reset,
51            "autosize"      => DoubleClick::AutoSize,
52            "reset+autosize"=> DoubleClick::ResetAutoSize,
53        }),
54    }?;
55
56    Ok(config)
57}
58
59pub fn parse_layout_obj(layout_obj: &mut Value, context: &ParseContext<'_>) -> Result<Layout> {
60    use plotly::layout::{
61        ClickMode, DragMode, GroupClick, HoverMode, ItemClick, ItemSizing, Legend, Margin,
62        TraceOrder, VAlign,
63    };
64
65    let layout = translate_with_config! {
66        Layout::new(),
67        layout_obj,
68        context.map(),
69        context.map_eval(),
70        (title, String),
71        (show_legend, bool),
72        (auto_size, bool),
73        (height, usize),
74        (width, usize),
75        (colorway, Vec<Color>),
76        (plot_background_color, Color),
77        (paper_background_color, Color),
78        (separators, String),
79        (bar_gap, f64),
80        (bar_group_gap, f64),
81        (box_gap, f64),
82        (box_group_gap, f64),
83    }?;
84
85    let layout = translate_enum_with_config! {
86        layout,
87        layout_obj,
88        context.map(),
89        context.map_eval(),
90        (hover_mode, {
91            "x"          => HoverMode::X,
92            "y"          => HoverMode::Y,
93            "closest"    => HoverMode::Closest,
94            "false"      => HoverMode::False,
95            "x unified"  => HoverMode::XUnified,
96            "y unified"  => HoverMode::YUnified,
97        }),
98        (drag_mode, {
99            "zoom"      => DragMode::Zoom,
100            "pan"       => DragMode::Pan,
101            "select"    => DragMode::Select,
102            "lasso"     => DragMode::Lasso,
103            "orbit"     => DragMode::Orbit,
104            "turntable" => DragMode::Turntable,
105            "false"     => DragMode::False,
106        }),
107        (click_mode, {
108            "event"        => ClickMode::Event,
109            "select"       => ClickMode::Select,
110            "none"         => ClickMode::None,
111        }),
112    }?;
113
114    let layout = if let Some(legend_obj) = layout_obj.get_mut("legend")
115        && legend_obj.is_object()
116    {
117        let legend = translate_with_config! {
118            Legend::new(),
119            legend_obj,
120            context.map(),
121            context.map_eval(),
122            (background_color, Color),
123            (border_color, Color),
124            (border_width, usize),
125            (x, f64),
126            (y, f64),
127            (trace_group_gap, usize),
128            (item_width, usize),
129            (title, String),
130        }?;
131
132        let legend = translate_enum_with_config! {
133            legend,
134            legend_obj,
135            context.map(),
136            context.map_eval(),
137            (trace_order, {
138                "reversed"        => TraceOrder::Reversed,
139                "grouped"         => TraceOrder::Grouped,
140                "reversed+grouped"=> TraceOrder::ReversedGrouped,
141                "normal"          => TraceOrder::Normal,
142            }),
143            (item_sizing, {
144                "trace"    => ItemSizing::Trace,
145                "constant" => ItemSizing::Constant,
146            }),
147            (item_click, {
148                "toggle"       => ItemClick::Toggle,
149                "toggleothers" => ItemClick::ToggleOthers,
150                "false"        => ItemClick::False,
151            }),
152            (item_double_click, {
153                "toggle"       => ItemClick::Toggle,
154                "toggleothers" => ItemClick::ToggleOthers,
155                "false"        => ItemClick::False,
156            }),
157            (valign, {
158                "top"    => VAlign::Top,
159                "middle" => VAlign::Middle,
160                "bottom" => VAlign::Bottom,
161            }),
162            (group_click, {
163                "toggleitem" => GroupClick::ToggleItem,
164                "togglegroup"=> GroupClick::ToggleGroup,
165            }),
166        }?;
167
168        layout.legend(legend)
169    } else {
170        layout
171    };
172
173    let layout = if let Some(margin_obj) = layout_obj.get_mut("margin")
174        && margin_obj.is_object()
175    {
176        let margin = translate_with_config! {
177            Margin::new(),
178            margin_obj,
179            context.map(),
180            context.map_eval(),
181            (left, usize),
182            (right, usize),
183            (top, usize),
184            (bottom, usize),
185            (pad, usize),
186            (auto_expand, bool)
187        }?;
188        layout.margin(margin)
189    } else {
190        layout
191    };
192
193    use plotly::common::Font;
194    let layout = if let Some(font_obj) = layout_obj.get_mut("font")
195        && font_obj.is_object()
196    {
197        let font = translate_with_config! {
198            Font::new(),
199            font_obj,
200            context.map(),
201            context.map_eval(),
202            (family, String),
203            (size, usize),
204            (color, Color),
205        }?;
206        layout.font(font)
207    } else {
208        layout
209    };
210
211    use plotly::layout::ColorAxis;
212    let layout = if let Some(ca_obj) = layout_obj.get_mut("coloraxis")
213        && ca_obj.is_object()
214    {
215        let ca = translate_with_config! {
216            ColorAxis::new(),
217            ca_obj,
218            context.map(),
219            context.map_eval(),
220            (cmin, f64),
221            (cmax, f64),
222            (cmid, f64),
223            (auto_color_scale, bool),
224            (reverse_scale, bool),
225            (show_scale, bool),
226        }?;
227        layout.color_axis(ca)
228    } else {
229        layout
230    };
231
232    let layout = if let Some(axis_obj) = layout_obj.get_mut("xaxis")
233        && axis_obj.is_object()
234    {
235        let axis = parse_axis_obj(axis_obj, context)?;
236        layout.x_axis(axis)
237    } else {
238        layout
239    };
240
241    let layout = if let Some(axis_obj) = layout_obj.get_mut("yaxis")
242        && axis_obj.is_object()
243    {
244        let axis = parse_axis_obj(axis_obj, context)?;
245        layout.y_axis(axis)
246    } else {
247        layout
248    };
249
250    let layout = parse_named_axes(layout, layout_obj, context, "x")?;
251    let layout = parse_named_axes(layout, layout_obj, context, "y")?;
252
253    Ok(layout)
254}
255
256fn parse_axis_obj(
257    axis_obj: &mut Value,
258    context: &ParseContext<'_>,
259) -> Result<plotly::layout::Axis> {
260    use crate::code_handler::until::DataPack;
261    use plotly::layout::{Axis, AxisType};
262
263    const AXIS_CONTEXT: &str = "layout axis";
264
265    let axis = translate_with_config! {
266        Axis::new(),
267        axis_obj,
268        context.map(),
269        context.map_eval(),
270        (title, String),
271        (show_grid, bool),
272        (show_line, bool),
273        (zero_line, bool),
274        (visible, bool),
275        (anchor, String),
276        (overlaying, String),
277        (range, Vec<Option<f64>>),
278        (color, Color),
279        (line_color, Color),
280        (grid_color, Color),
281        (tick_prefix, String),
282        (tick_suffix, String),
283        (tick_format, String),
284        (hover_format, String),
285        (category_array, Vec<String>),
286        (fixed_range, bool),
287        (scale_anchor, String),
288        (auto_margin, bool),
289        (show_tick_labels, bool),
290    }?;
291
292    let axis = translate_enum_with_config! {
293        axis,
294        axis_obj,
295        context.map(),
296        context.map_eval(),
297        (category_order, {
298            "trace"               => plotly::layout::CategoryOrder::Trace,
299            "category-ascending"  => plotly::layout::CategoryOrder::CategoryAscending,
300            "category-descending" => plotly::layout::CategoryOrder::CategoryDescending,
301            "array"               => plotly::layout::CategoryOrder::Array,
302            "total-ascending"     => plotly::layout::CategoryOrder::TotalAscending,
303            "total-descending"    => plotly::layout::CategoryOrder::TotalDescending,
304            "min-ascending"       => plotly::layout::CategoryOrder::MinAscending,
305            "min-descending"      => plotly::layout::CategoryOrder::MinDescending,
306            "max-ascending"       => plotly::layout::CategoryOrder::MaxAscending,
307            "max-descending"      => plotly::layout::CategoryOrder::MaxDescending,
308            "sum-ascending"       => plotly::layout::CategoryOrder::SumAscending,
309            "sum-descending"      => plotly::layout::CategoryOrder::SumDescending,
310            "mean-ascending"      => plotly::layout::CategoryOrder::MeanAscending,
311            "mean-descending"     => plotly::layout::CategoryOrder::MeanDescending,
312            "median-ascending"    => plotly::layout::CategoryOrder::MedianAscending,
313            "median-descending"   => plotly::layout::CategoryOrder::MedianDescending,
314        }),
315    }?;
316
317    let axis = if let Some(v) = axis_obj.get_mut("type") {
318        let data = serde_json::from_value::<DataPack<String>>(v.take()).map_err(|e| {
319            anyhow!(
320                "Failed to deserialize field 'type' in {}: {}",
321                AXIS_CONTEXT,
322                e
323            )
324        })?;
325        let s = data.unwrap_from_context(context).map_err(|e| {
326            anyhow!(
327                "Failed to resolve DataPack for field 'type' in {}: {}",
328                AXIS_CONTEXT,
329                e
330            )
331        })?;
332        let at = match s.as_str() {
333            "-" | "linear" => AxisType::Linear,
334            "log" => AxisType::Log,
335            "date" => AxisType::Date,
336            "category" => AxisType::Category,
337            "multicategory" => AxisType::MultiCategory,
338            other => {
339                return Err(anyhow!(
340                    "\"{}\" is not a valid value for `type` in {}",
341                    other,
342                    AXIS_CONTEXT,
343                ));
344            }
345        };
346        axis.type_(at)
347    } else {
348        axis
349    };
350
351    Ok(axis)
352}
353
354fn parse_named_axes(
355    layout: Layout,
356    layout_obj: &mut Value,
357    context: &ParseContext<'_>,
358    prefix: &str,
359) -> Result<Layout> {
360    let mut layout = layout;
361    for i in 2..=8 {
362        let json_key = format!("{}axis{}", prefix, i);
363        let Some(axis_obj) = layout_obj.get_mut(json_key.as_str()) else {
364            continue;
365        };
366        if !axis_obj.is_object() {
367            continue;
368        }
369        let axis = parse_axis_obj(axis_obj, context)?;
370        if prefix == "x" {
371            layout = match i {
372                2 => layout.x_axis2(axis),
373                3 => layout.x_axis3(axis),
374                4 => layout.x_axis4(axis),
375                5 => layout.x_axis5(axis),
376                6 => layout.x_axis6(axis),
377                7 => layout.x_axis7(axis),
378                8 => layout.x_axis8(axis),
379                _ => unreachable!(),
380            };
381        } else {
382            layout = match i {
383                2 => layout.y_axis2(axis),
384                3 => layout.y_axis3(axis),
385                4 => layout.y_axis4(axis),
386                5 => layout.y_axis5(axis),
387                6 => layout.y_axis6(axis),
388                7 => layout.y_axis7(axis),
389                8 => layout.y_axis8(axis),
390                _ => unreachable!(),
391            };
392        }
393    }
394    Ok(layout)
395}