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 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 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 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 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
307fn 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 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 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
389fn 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 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}