Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

1use runmat_builtins::{StringArray, StructValue, Tensor, Value};
2use runmat_plot::plots::{LegendStyle, TextStyle};
3
4use super::state::{
5    axes_handle_exists, axes_handles_for_figure, axes_metadata_snapshot, axes_state_snapshot,
6    current_axes_handle_for_figure, decode_axes_handle, decode_plot_object_handle,
7    figure_handle_exists, figure_has_sg_title, legend_entries_snapshot, select_axes_for_figure,
8    set_legend_for_axes, set_sg_title_properties_for_figure,
9    set_text_annotation_properties_for_axes, set_text_properties_for_axes, FigureHandle,
10    PlotObjectKind,
11};
12use super::style::{
13    parse_color_value, value_as_bool, value_as_f64, value_as_string, LineStyleParseOptions,
14};
15use super::{plotting_error, plotting_error_with_source};
16use crate::builtins::plotting::op_common::limits::limit_value;
17use crate::builtins::plotting::op_common::value_as_text_string;
18use crate::BuiltinResult;
19
20#[derive(Clone, Debug)]
21pub enum PlotHandle {
22    Figure(FigureHandle),
23    Axes(FigureHandle, usize),
24    Text(FigureHandle, usize, PlotObjectKind),
25    Legend(FigureHandle, usize),
26    PlotChild(super::state::PlotChildHandleState),
27}
28
29pub fn resolve_plot_handle(value: &Value, builtin: &'static str) -> BuiltinResult<PlotHandle> {
30    let scalar = handle_scalar(value, builtin)?;
31    if let Ok(state) = super::state::plot_child_handle_snapshot(scalar) {
32        return Ok(PlotHandle::PlotChild(state));
33    }
34    if let Ok((handle, axes_index, kind)) = decode_plot_object_handle(scalar) {
35        if axes_handle_exists(handle, axes_index) {
36            return Ok(match kind {
37                PlotObjectKind::Legend => PlotHandle::Legend(handle, axes_index),
38                _ => PlotHandle::Text(handle, axes_index, kind),
39            });
40        }
41    }
42    if let Ok((handle, axes_index)) = decode_axes_handle(scalar) {
43        if axes_handle_exists(handle, axes_index) {
44            return Ok(PlotHandle::Axes(handle, axes_index));
45        }
46        return Err(plotting_error(
47            builtin,
48            format!("{builtin}: invalid axes handle"),
49        ));
50    }
51    let figure = FigureHandle::from(scalar.round() as u32);
52    if figure_handle_exists(figure) {
53        return Ok(PlotHandle::Figure(figure));
54    }
55    Err(plotting_error(
56        builtin,
57        format!("{builtin}: unsupported or invalid plotting handle"),
58    ))
59}
60
61pub fn get_properties(
62    handle: PlotHandle,
63    property: Option<&str>,
64    builtin: &'static str,
65) -> BuiltinResult<Value> {
66    match handle {
67        PlotHandle::Axes(handle, axes_index) => {
68            get_axes_property(handle, axes_index, property, builtin)
69        }
70        PlotHandle::Text(handle, axes_index, kind) => {
71            get_text_property(handle, axes_index, kind, property, builtin)
72        }
73        PlotHandle::Legend(handle, axes_index) => {
74            get_legend_property(handle, axes_index, property, builtin)
75        }
76        PlotHandle::Figure(handle) => get_figure_property(handle, property, builtin),
77        PlotHandle::PlotChild(state) => get_plot_child_property(&state, property, builtin),
78    }
79}
80
81pub fn set_properties(
82    handle: PlotHandle,
83    args: &[Value],
84    builtin: &'static str,
85) -> BuiltinResult<()> {
86    if args.is_empty() || !args.len().is_multiple_of(2) {
87        return Err(plotting_error(
88            builtin,
89            format!("{builtin}: property/value arguments must come in pairs"),
90        ));
91    }
92    match handle {
93        PlotHandle::Figure(handle) => {
94            for pair in args.chunks_exact(2) {
95                let key = property_name(&pair[0], builtin)?;
96                apply_figure_property(handle, &key, &pair[1], builtin)?;
97            }
98            Ok(())
99        }
100        PlotHandle::Axes(handle, axes_index) => {
101            for pair in args.chunks_exact(2) {
102                let key = property_name(&pair[0], builtin)?;
103                apply_axes_property(handle, axes_index, &key, &pair[1], builtin)?;
104            }
105            Ok(())
106        }
107        PlotHandle::Text(handle, axes_index, kind) => {
108            let mut text: Option<String> = None;
109            let mut style = if matches!(kind, PlotObjectKind::SuperTitle) {
110                super::state::clone_figure(handle)
111                    .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?
112                    .sg_title_style
113            } else {
114                axes_metadata_snapshot(handle, axes_index)
115                    .map_err(|err| map_figure_error(builtin, err))?
116                    .text_style_for(kind)
117            };
118            for pair in args.chunks_exact(2) {
119                let key = property_name(&pair[0], builtin)?;
120                apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
121            }
122            if matches!(kind, PlotObjectKind::SuperTitle) {
123                set_sg_title_properties_for_figure(handle, text, Some(style))
124                    .map_err(|err| map_figure_error(builtin, err))?;
125            } else {
126                set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
127                    .map_err(|err| map_figure_error(builtin, err))?;
128            }
129            Ok(())
130        }
131        PlotHandle::Legend(handle, axes_index) => {
132            let snapshot = axes_metadata_snapshot(handle, axes_index)
133                .map_err(|err| map_figure_error(builtin, err))?;
134            let mut style = snapshot.legend_style;
135            let mut enabled = snapshot.legend_enabled;
136            let mut labels: Option<Vec<String>> = None;
137            for pair in args.chunks_exact(2) {
138                let key = property_name(&pair[0], builtin)?;
139                apply_legend_property(
140                    &mut style,
141                    &mut enabled,
142                    &mut labels,
143                    &key,
144                    &pair[1],
145                    builtin,
146                )?;
147            }
148            set_legend_for_axes(handle, axes_index, enabled, labels.as_deref(), Some(style))
149                .map_err(|err| map_figure_error(builtin, err))?;
150            Ok(())
151        }
152        PlotHandle::PlotChild(state) => {
153            for pair in args.chunks_exact(2) {
154                let key = property_name(&pair[0], builtin)?;
155                apply_plot_child_property(&state, &key, &pair[1], builtin)?;
156            }
157            Ok(())
158        }
159    }
160}
161
162pub fn parse_text_style_pairs(builtin: &'static str, args: &[Value]) -> BuiltinResult<TextStyle> {
163    if args.is_empty() {
164        return Ok(TextStyle::default());
165    }
166    if !args.len().is_multiple_of(2) {
167        return Err(plotting_error(
168            builtin,
169            format!("{builtin}: property/value arguments must come in pairs"),
170        ));
171    }
172    let mut style = TextStyle::default();
173    let mut text = None;
174    for pair in args.chunks_exact(2) {
175        let key = property_name(&pair[0], builtin)?;
176        apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
177    }
178    Ok(style)
179}
180
181pub fn validate_heatmap_property_pairs(
182    args: &[Value],
183    x_label_len: usize,
184    y_label_len: usize,
185    builtin: &'static str,
186) -> BuiltinResult<()> {
187    if args.is_empty() {
188        return Ok(());
189    }
190    if !args.len().is_multiple_of(2) {
191        return Err(plotting_error(
192            builtin,
193            format!("{builtin}: property/value arguments must come in pairs"),
194        ));
195    }
196    for pair in args.chunks_exact(2) {
197        let key = property_name(&pair[0], builtin)?;
198        match key.as_str() {
199            "title" => validate_axes_text_alias(PlotObjectKind::Title, &pair[1], builtin)?,
200            "xlabel" => validate_axes_text_alias(PlotObjectKind::XLabel, &pair[1], builtin)?,
201            "ylabel" => validate_axes_text_alias(PlotObjectKind::YLabel, &pair[1], builtin)?,
202            "colorbar" | "colorbarvisible" => {
203                value_as_bool(&pair[1]).ok_or_else(|| {
204                    plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
205                })?;
206            }
207            "colormap" => {
208                let name = value_as_string(&pair[1]).ok_or_else(|| {
209                    plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
210                })?;
211                parse_colormap_name(&name, builtin)?;
212            }
213            "xdisplaylabels" => {
214                let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
215                if labels.len() != x_label_len {
216                    return Err(plotting_error(
217                        builtin,
218                        format!("{builtin}: XDisplayLabels length must match heatmap columns"),
219                    ));
220                }
221            }
222            "ydisplaylabels" => {
223                let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
224                if labels.len() != y_label_len {
225                    return Err(plotting_error(
226                        builtin,
227                        format!("{builtin}: YDisplayLabels length must match heatmap rows"),
228                    ));
229                }
230            }
231            other => {
232                return Err(plotting_error(
233                    builtin,
234                    format!("{builtin}: unsupported heatmap property `{other}`"),
235                ));
236            }
237        }
238    }
239    Ok(())
240}
241
242pub fn split_legend_style_pairs<'a>(
243    builtin: &'static str,
244    args: &'a [Value],
245) -> BuiltinResult<(&'a [Value], LegendStyle)> {
246    let mut style = LegendStyle::default();
247    let mut enabled = true;
248    let mut labels = None;
249    let mut split = args.len();
250    while split >= 2 {
251        let key_idx = split - 2;
252        let Ok(key) = property_name(&args[key_idx], builtin) else {
253            break;
254        };
255        if !matches!(
256            key.as_str(),
257            "location"
258                | "fontsize"
259                | "fontweight"
260                | "fontangle"
261                | "interpreter"
262                | "textcolor"
263                | "color"
264                | "visible"
265                | "string"
266                | "box"
267                | "orientation"
268        ) {
269            break;
270        }
271        apply_legend_property(
272            &mut style,
273            &mut enabled,
274            &mut labels,
275            &key,
276            &args[key_idx + 1],
277            builtin,
278        )?;
279        split -= 2;
280    }
281    Ok((&args[..split], style))
282}
283
284pub fn map_figure_error(
285    builtin: &'static str,
286    err: impl std::error::Error + Send + Sync + 'static,
287) -> crate::RuntimeError {
288    let message = format!("{builtin}: {err}");
289    plotting_error_with_source(builtin, message, err)
290}
291
292fn get_figure_property(
293    handle: FigureHandle,
294    property: Option<&str>,
295    builtin: &'static str,
296) -> BuiltinResult<Value> {
297    let axes = axes_handles_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
298    let current_axes =
299        current_axes_handle_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
300    let sg_title_handle =
301        super::state::encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle);
302    let has_sg_title = figure_has_sg_title(handle);
303    match property.map(canonical_property_name) {
304        None => {
305            let mut st = StructValue::new();
306            st.insert("Handle", Value::Num(handle.as_u32() as f64));
307            st.insert("Number", Value::Num(handle.as_u32() as f64));
308            st.insert("Type", Value::String("figure".into()));
309            st.insert("CurrentAxes", Value::Num(current_axes));
310            let mut children = axes;
311            if has_sg_title {
312                children.push(sg_title_handle);
313            }
314            st.insert("Children", handles_value(children));
315            st.insert("Parent", Value::Num(f64::NAN));
316            st.insert("Name", Value::String(format!("Figure {}", handle.as_u32())));
317            st.insert("SGTitle", Value::Num(sg_title_handle));
318            Ok(Value::Struct(st))
319        }
320        Some("number") => Ok(Value::Num(handle.as_u32() as f64)),
321        Some("type") => Ok(Value::String("figure".into())),
322        Some("currentaxes") => Ok(Value::Num(current_axes)),
323        Some("children") => Ok(handles_value({
324            let mut children = axes;
325            if has_sg_title {
326                children.push(sg_title_handle);
327            }
328            children
329        })),
330        Some("parent") => Ok(Value::Num(f64::NAN)),
331        Some("name") => Ok(Value::String(format!("Figure {}", handle.as_u32()))),
332        Some("sgtitle") => Ok(Value::Num(sg_title_handle)),
333        Some(other) => Err(plotting_error(
334            builtin,
335            format!("{builtin}: unsupported figure property `{other}`"),
336        )),
337    }
338}
339
340fn get_axes_property(
341    handle: FigureHandle,
342    axes_index: usize,
343    property: Option<&str>,
344    builtin: &'static str,
345) -> BuiltinResult<Value> {
346    let meta =
347        axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
348    let axes =
349        axes_state_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
350    match property.map(canonical_property_name) {
351        None => {
352            let mut st = StructValue::new();
353            st.insert(
354                "Handle",
355                Value::Num(super::state::encode_axes_handle(handle, axes_index)),
356            );
357            st.insert("Figure", Value::Num(handle.as_u32() as f64));
358            st.insert("Rows", Value::Num(axes.rows as f64));
359            st.insert("Cols", Value::Num(axes.cols as f64));
360            st.insert("Index", Value::Num((axes_index + 1) as f64));
361            st.insert(
362                "Title",
363                Value::Num(super::state::encode_plot_object_handle(
364                    handle,
365                    axes_index,
366                    PlotObjectKind::Title,
367                )),
368            );
369            st.insert(
370                "XLabel",
371                Value::Num(super::state::encode_plot_object_handle(
372                    handle,
373                    axes_index,
374                    PlotObjectKind::XLabel,
375                )),
376            );
377            st.insert(
378                "YLabel",
379                Value::Num(super::state::encode_plot_object_handle(
380                    handle,
381                    axes_index,
382                    PlotObjectKind::YLabel,
383                )),
384            );
385            st.insert(
386                "ZLabel",
387                Value::Num(super::state::encode_plot_object_handle(
388                    handle,
389                    axes_index,
390                    PlotObjectKind::ZLabel,
391                )),
392            );
393            st.insert(
394                "Legend",
395                Value::Num(super::state::encode_plot_object_handle(
396                    handle,
397                    axes_index,
398                    PlotObjectKind::Legend,
399                )),
400            );
401            st.insert("LegendVisible", Value::Bool(meta.legend_enabled));
402            st.insert("Type", Value::String("axes".into()));
403            st.insert("Parent", Value::Num(handle.as_u32() as f64));
404            st.insert(
405                "Children",
406                handles_value(vec![
407                    super::state::encode_plot_object_handle(
408                        handle,
409                        axes_index,
410                        PlotObjectKind::Title,
411                    ),
412                    super::state::encode_plot_object_handle(
413                        handle,
414                        axes_index,
415                        PlotObjectKind::XLabel,
416                    ),
417                    super::state::encode_plot_object_handle(
418                        handle,
419                        axes_index,
420                        PlotObjectKind::YLabel,
421                    ),
422                    super::state::encode_plot_object_handle(
423                        handle,
424                        axes_index,
425                        PlotObjectKind::ZLabel,
426                    ),
427                    super::state::encode_plot_object_handle(
428                        handle,
429                        axes_index,
430                        PlotObjectKind::Legend,
431                    ),
432                ]),
433            );
434            st.insert("Grid", Value::Bool(meta.grid_enabled));
435            st.insert("Box", Value::Bool(meta.box_enabled));
436            st.insert("AxisEqual", Value::Bool(meta.axis_equal));
437            st.insert("Colorbar", Value::Bool(meta.colorbar_enabled));
438            st.insert(
439                "Colormap",
440                Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
441            );
442            st.insert("XLim", limit_value(meta.x_limits));
443            st.insert("YLim", limit_value(meta.y_limits));
444            st.insert("ZLim", limit_value(meta.z_limits));
445            st.insert("CLim", limit_value(meta.color_limits));
446            st.insert(
447                "XScale",
448                Value::String(if meta.x_log { "log" } else { "linear" }.into()),
449            );
450            st.insert(
451                "YScale",
452                Value::String(if meta.y_log { "log" } else { "linear" }.into()),
453            );
454            Ok(Value::Struct(st))
455        }
456        Some("title") => Ok(Value::Num(super::state::encode_plot_object_handle(
457            handle,
458            axes_index,
459            PlotObjectKind::Title,
460        ))),
461        Some("xlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
462            handle,
463            axes_index,
464            PlotObjectKind::XLabel,
465        ))),
466        Some("ylabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
467            handle,
468            axes_index,
469            PlotObjectKind::YLabel,
470        ))),
471        Some("zlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
472            handle,
473            axes_index,
474            PlotObjectKind::ZLabel,
475        ))),
476        Some("legend") => Ok(Value::Num(super::state::encode_plot_object_handle(
477            handle,
478            axes_index,
479            PlotObjectKind::Legend,
480        ))),
481        Some("view") => {
482            let az = meta.view_azimuth_deg.unwrap_or(-37.5) as f64;
483            let el = meta.view_elevation_deg.unwrap_or(30.0) as f64;
484            Ok(Value::Tensor(runmat_builtins::Tensor {
485                rows: 1,
486                cols: 2,
487                shape: vec![1, 2],
488                data: vec![az, el],
489                dtype: runmat_builtins::NumericDType::F64,
490            }))
491        }
492        Some("grid") => Ok(Value::Bool(meta.grid_enabled)),
493        Some("box") => Ok(Value::Bool(meta.box_enabled)),
494        Some("axisequal") => Ok(Value::Bool(meta.axis_equal)),
495        Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
496        Some("colormap") => Ok(Value::String(
497            format!("{:?}", meta.colormap).to_ascii_lowercase(),
498        )),
499        Some("xlim") => Ok(limit_value(meta.x_limits)),
500        Some("ylim") => Ok(limit_value(meta.y_limits)),
501        Some("zlim") => Ok(limit_value(meta.z_limits)),
502        Some("clim") => Ok(limit_value(meta.color_limits)),
503        Some("xscale") => Ok(Value::String(
504            if meta.x_log { "log" } else { "linear" }.into(),
505        )),
506        Some("yscale") => Ok(Value::String(
507            if meta.y_log { "log" } else { "linear" }.into(),
508        )),
509        Some("type") => Ok(Value::String("axes".into())),
510        Some("parent") => Ok(Value::Num(handle.as_u32() as f64)),
511        Some("legendvisible") => Ok(Value::Bool(meta.legend_enabled)),
512        Some("children") => Ok(handles_value(vec![
513            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title),
514            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel),
515            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel),
516            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel),
517            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend),
518        ])),
519        Some(other) => Err(plotting_error(
520            builtin,
521            format!("{builtin}: unsupported axes property `{other}`"),
522        )),
523    }
524}
525
526fn get_text_property(
527    handle: FigureHandle,
528    axes_index: usize,
529    kind: PlotObjectKind,
530    property: Option<&str>,
531    builtin: &'static str,
532) -> BuiltinResult<Value> {
533    let (text, style) = match kind {
534        PlotObjectKind::SuperTitle => {
535            let figure = super::state::clone_figure(handle)
536                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
537            (figure.sg_title, figure.sg_title_style)
538        }
539        PlotObjectKind::Title
540        | PlotObjectKind::XLabel
541        | PlotObjectKind::YLabel
542        | PlotObjectKind::ZLabel => {
543            let meta = axes_metadata_snapshot(handle, axes_index)
544                .map_err(|err| map_figure_error(builtin, err))?;
545            match kind {
546                PlotObjectKind::Title => (meta.title, meta.title_style),
547                PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
548                PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
549                PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
550                PlotObjectKind::Legend | PlotObjectKind::SuperTitle => unreachable!(),
551            }
552        }
553        PlotObjectKind::Legend => unreachable!(),
554    };
555    let parent = if matches!(kind, PlotObjectKind::SuperTitle) {
556        Value::Num(handle.as_u32() as f64)
557    } else {
558        Value::Num(super::state::encode_axes_handle(handle, axes_index))
559    };
560    match property.map(canonical_property_name) {
561        None => {
562            let mut st = StructValue::new();
563            st.insert("Type", Value::String("text".into()));
564            st.insert("Parent", parent.clone());
565            st.insert("Children", handles_value(Vec::new()));
566            st.insert("String", text_value(text));
567            st.insert("Visible", Value::Bool(style.visible));
568            if let Some(size) = style.font_size {
569                st.insert("FontSize", Value::Num(size as f64));
570            }
571            if let Some(weight) = style.font_weight {
572                st.insert("FontWeight", Value::String(weight));
573            }
574            if let Some(angle) = style.font_angle {
575                st.insert("FontAngle", Value::String(angle));
576            }
577            if let Some(interpreter) = style.interpreter {
578                st.insert("Interpreter", Value::String(interpreter));
579            }
580            if let Some(color) = style.color {
581                st.insert("Color", Value::String(color_to_short_name(color)));
582            }
583            Ok(Value::Struct(st))
584        }
585        Some("type") => Ok(Value::String("text".into())),
586        Some("parent") => Ok(parent),
587        Some("children") => Ok(handles_value(Vec::new())),
588        Some("string") => Ok(text_value(text)),
589        Some("visible") => Ok(Value::Bool(style.visible)),
590        Some("fontsize") => Ok(style
591            .font_size
592            .map(|v| Value::Num(v as f64))
593            .unwrap_or(Value::Num(f64::NAN))),
594        Some("fontweight") => Ok(style
595            .font_weight
596            .map(Value::String)
597            .unwrap_or_else(|| Value::String(String::new()))),
598        Some("fontangle") => Ok(style
599            .font_angle
600            .map(Value::String)
601            .unwrap_or_else(|| Value::String(String::new()))),
602        Some("interpreter") => Ok(style
603            .interpreter
604            .map(Value::String)
605            .unwrap_or_else(|| Value::String(String::new()))),
606        Some("color") => Ok(style
607            .color
608            .map(|c| Value::String(color_to_short_name(c)))
609            .unwrap_or_else(|| Value::String(String::new()))),
610        Some(other) => Err(plotting_error(
611            builtin,
612            format!("{builtin}: unsupported text property `{other}`"),
613        )),
614    }
615}
616
617fn get_legend_property(
618    handle: FigureHandle,
619    axes_index: usize,
620    property: Option<&str>,
621    builtin: &'static str,
622) -> BuiltinResult<Value> {
623    let meta =
624        axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
625    let entries = legend_entries_snapshot(handle, axes_index)
626        .map_err(|err| map_figure_error(builtin, err))?;
627    match property.map(canonical_property_name) {
628        None => {
629            let mut st = StructValue::new();
630            st.insert("Type", Value::String("legend".into()));
631            st.insert(
632                "Parent",
633                Value::Num(super::state::encode_axes_handle(handle, axes_index)),
634            );
635            st.insert("Children", handles_value(Vec::new()));
636            st.insert(
637                "Visible",
638                Value::Bool(meta.legend_enabled && meta.legend_style.visible),
639            );
640            st.insert(
641                "String",
642                legend_labels_value(entries.iter().map(|e| e.label.clone()).collect()),
643            );
644            if let Some(location) = meta.legend_style.location {
645                st.insert("Location", Value::String(location));
646            }
647            if let Some(size) = meta.legend_style.font_size {
648                st.insert("FontSize", Value::Num(size as f64));
649            }
650            if let Some(weight) = meta.legend_style.font_weight {
651                st.insert("FontWeight", Value::String(weight));
652            }
653            if let Some(angle) = meta.legend_style.font_angle {
654                st.insert("FontAngle", Value::String(angle));
655            }
656            if let Some(interpreter) = meta.legend_style.interpreter {
657                st.insert("Interpreter", Value::String(interpreter));
658            }
659            if let Some(box_visible) = meta.legend_style.box_visible {
660                st.insert("Box", Value::Bool(box_visible));
661            }
662            if let Some(orientation) = meta.legend_style.orientation {
663                st.insert("Orientation", Value::String(orientation));
664            }
665            if let Some(color) = meta.legend_style.text_color {
666                st.insert("TextColor", Value::String(color_to_short_name(color)));
667            }
668            Ok(Value::Struct(st))
669        }
670        Some("visible") => Ok(Value::Bool(
671            meta.legend_enabled && meta.legend_style.visible,
672        )),
673        Some("type") => Ok(Value::String("legend".into())),
674        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
675            handle, axes_index,
676        ))),
677        Some("children") => Ok(handles_value(Vec::new())),
678        Some("string") => Ok(legend_labels_value(
679            entries.into_iter().map(|e| e.label).collect(),
680        )),
681        Some("location") => Ok(meta
682            .legend_style
683            .location
684            .map(Value::String)
685            .unwrap_or_else(|| Value::String(String::new()))),
686        Some("fontsize") => Ok(meta
687            .legend_style
688            .font_size
689            .map(|v| Value::Num(v as f64))
690            .unwrap_or(Value::Num(f64::NAN))),
691        Some("fontweight") => Ok(meta
692            .legend_style
693            .font_weight
694            .map(Value::String)
695            .unwrap_or_else(|| Value::String(String::new()))),
696        Some("fontangle") => Ok(meta
697            .legend_style
698            .font_angle
699            .map(Value::String)
700            .unwrap_or_else(|| Value::String(String::new()))),
701        Some("interpreter") => Ok(meta
702            .legend_style
703            .interpreter
704            .map(Value::String)
705            .unwrap_or_else(|| Value::String(String::new()))),
706        Some("box") => Ok(meta
707            .legend_style
708            .box_visible
709            .map(Value::Bool)
710            .unwrap_or(Value::Bool(true))),
711        Some("orientation") => Ok(meta
712            .legend_style
713            .orientation
714            .map(Value::String)
715            .unwrap_or_else(|| Value::String(String::new()))),
716        Some("textcolor") | Some("color") => Ok(meta
717            .legend_style
718            .text_color
719            .map(|c| Value::String(color_to_short_name(c)))
720            .unwrap_or_else(|| Value::String(String::new()))),
721        Some(other) => Err(plotting_error(
722            builtin,
723            format!("{builtin}: unsupported legend property `{other}`"),
724        )),
725    }
726}
727
728fn property_name(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
729    value_as_string(value)
730        .map(|s| canonical_property_name(s.trim()).to_string())
731        .ok_or_else(|| {
732            plotting_error(
733                builtin,
734                format!("{builtin}: property names must be strings"),
735            )
736        })
737}
738
739fn canonical_property_name(name: &str) -> &str {
740    match name.to_ascii_lowercase().as_str() {
741        "textcolor" => "textcolor",
742        "color" => "color",
743        "fontsize" => "fontsize",
744        "fontweight" => "fontweight",
745        "fontangle" => "fontangle",
746        "interpreter" => "interpreter",
747        "visible" => "visible",
748        "location" => "location",
749        "box" => "box",
750        "orientation" => "orientation",
751        "string" => "string",
752        "title" => "title",
753        "xlabel" => "xlabel",
754        "ylabel" => "ylabel",
755        "zlabel" => "zlabel",
756        "view" => "view",
757        "grid" => "grid",
758        "axisequal" => "axisequal",
759        "colorbar" => "colorbar",
760        "colorbarvisible" => "colorbarvisible",
761        "colormap" => "colormap",
762        "xdisplaylabels" => "xdisplaylabels",
763        "ydisplaylabels" => "ydisplaylabels",
764        "colordata" | "cdata" => "colordata",
765        "xlim" => "xlim",
766        "ylim" => "ylim",
767        "zlim" => "zlim",
768        "clim" => "clim",
769        "caxis" => "clim",
770        "xscale" => "xscale",
771        "yscale" => "yscale",
772        "currentaxes" => "currentaxes",
773        "sgtitle" | "supertitle" => "sgtitle",
774        "children" => "children",
775        "parent" => "parent",
776        "type" => "type",
777        "number" => "number",
778        "name" => "name",
779        "legend" => "legend",
780        "legendvisible" => "legendvisible",
781        other => Box::leak(other.to_string().into_boxed_str()),
782    }
783}
784
785fn apply_text_property(
786    text: &mut Option<String>,
787    style: &mut TextStyle,
788    key: &str,
789    value: &Value,
790    builtin: &'static str,
791) -> BuiltinResult<()> {
792    let opts = LineStyleParseOptions::generic(builtin);
793    match key {
794        "string" => {
795            *text = Some(value_as_text_string(value).ok_or_else(|| {
796                plotting_error(builtin, format!("{builtin}: String must be text"))
797            })?);
798        }
799        "color" => style.color = Some(parse_color_value(&opts, value)?),
800        "fontsize" => {
801            style.font_size = Some(value_as_f64(value).ok_or_else(|| {
802                plotting_error(builtin, format!("{builtin}: FontSize must be numeric"))
803            })? as f32)
804        }
805        "fontweight" => {
806            style.font_weight = Some(value_as_string(value).ok_or_else(|| {
807                plotting_error(builtin, format!("{builtin}: FontWeight must be a string"))
808            })?)
809        }
810        "fontangle" => {
811            style.font_angle = Some(value_as_string(value).ok_or_else(|| {
812                plotting_error(builtin, format!("{builtin}: FontAngle must be a string"))
813            })?)
814        }
815        "interpreter" => {
816            style.interpreter = Some(value_as_string(value).ok_or_else(|| {
817                plotting_error(builtin, format!("{builtin}: Interpreter must be a string"))
818            })?)
819        }
820        "visible" => {
821            style.visible = value_as_bool(value).ok_or_else(|| {
822                plotting_error(builtin, format!("{builtin}: Visible must be logical"))
823            })?
824        }
825        other => {
826            return Err(plotting_error(
827                builtin,
828                format!("{builtin}: unsupported property `{other}`"),
829            ))
830        }
831    }
832    Ok(())
833}
834
835fn apply_legend_property(
836    style: &mut LegendStyle,
837    enabled: &mut bool,
838    labels: &mut Option<Vec<String>>,
839    key: &str,
840    value: &Value,
841    builtin: &'static str,
842) -> BuiltinResult<()> {
843    let opts = LineStyleParseOptions::generic(builtin);
844    match key {
845        "string" => *labels = Some(collect_label_strings(builtin, std::slice::from_ref(value))?),
846        "location" => {
847            style.location = Some(
848                value_as_string(value)
849                    .ok_or_else(|| plotting_error(builtin, "legend: Location must be a string"))?,
850            )
851        }
852        "fontsize" => {
853            style.font_size = Some(
854                value_as_f64(value)
855                    .ok_or_else(|| plotting_error(builtin, "legend: FontSize must be numeric"))?
856                    as f32,
857            )
858        }
859        "fontweight" => {
860            style.font_weight =
861                Some(value_as_string(value).ok_or_else(|| {
862                    plotting_error(builtin, "legend: FontWeight must be a string")
863                })?)
864        }
865        "fontangle" => {
866            style.font_angle = Some(
867                value_as_string(value)
868                    .ok_or_else(|| plotting_error(builtin, "legend: FontAngle must be a string"))?,
869            )
870        }
871        "interpreter" => {
872            style.interpreter =
873                Some(value_as_string(value).ok_or_else(|| {
874                    plotting_error(builtin, "legend: Interpreter must be a string")
875                })?)
876        }
877        "textcolor" | "color" => style.text_color = Some(parse_color_value(&opts, value)?),
878        "visible" => {
879            let visible = value_as_bool(value)
880                .ok_or_else(|| plotting_error(builtin, "legend: Visible must be logical"))?;
881            style.visible = visible;
882            *enabled = visible;
883        }
884        "box" => {
885            style.box_visible = Some(
886                value_as_bool(value)
887                    .ok_or_else(|| plotting_error(builtin, "legend: Box must be logical"))?,
888            )
889        }
890        "orientation" => {
891            style.orientation =
892                Some(value_as_string(value).ok_or_else(|| {
893                    plotting_error(builtin, "legend: Orientation must be a string")
894                })?)
895        }
896        other => {
897            return Err(plotting_error(
898                builtin,
899                format!("{builtin}: unsupported property `{other}`"),
900            ))
901        }
902    }
903    Ok(())
904}
905
906fn apply_axes_property(
907    handle: FigureHandle,
908    axes_index: usize,
909    key: &str,
910    value: &Value,
911    builtin: &'static str,
912) -> BuiltinResult<()> {
913    match key {
914        "legendvisible" => {
915            let visible = value_as_bool(value).ok_or_else(|| {
916                plotting_error(builtin, format!("{builtin}: LegendVisible must be logical"))
917            })?;
918            set_legend_for_axes(handle, axes_index, visible, None, None)
919                .map_err(|err| map_figure_error(builtin, err))?;
920            Ok(())
921        }
922        "title" => apply_axes_text_alias(handle, axes_index, PlotObjectKind::Title, value, builtin),
923        "xlabel" => {
924            apply_axes_text_alias(handle, axes_index, PlotObjectKind::XLabel, value, builtin)
925        }
926        "ylabel" => {
927            apply_axes_text_alias(handle, axes_index, PlotObjectKind::YLabel, value, builtin)
928        }
929        "zlabel" => {
930            apply_axes_text_alias(handle, axes_index, PlotObjectKind::ZLabel, value, builtin)
931        }
932        "view" => {
933            let tensor = runmat_builtins::Tensor::try_from(value)
934                .map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
935            if tensor.data.len() != 2 || !tensor.data[0].is_finite() || !tensor.data[1].is_finite()
936            {
937                return Err(plotting_error(
938                    builtin,
939                    format!("{builtin}: View must be a 2-element finite numeric vector"),
940                ));
941            }
942            crate::builtins::plotting::state::set_view_for_axes(
943                handle,
944                axes_index,
945                tensor.data[0] as f32,
946                tensor.data[1] as f32,
947            )
948            .map_err(|err| map_figure_error(builtin, err))?;
949            Ok(())
950        }
951        "grid" => {
952            let enabled = value_as_bool(value).ok_or_else(|| {
953                plotting_error(builtin, format!("{builtin}: Grid must be logical"))
954            })?;
955            crate::builtins::plotting::state::set_grid_enabled_for_axes(
956                handle, axes_index, enabled,
957            )
958            .map_err(|err| map_figure_error(builtin, err))?;
959            Ok(())
960        }
961        "box" => {
962            let enabled = value_as_bool(value).ok_or_else(|| {
963                plotting_error(builtin, format!("{builtin}: Box must be logical"))
964            })?;
965            crate::builtins::plotting::state::set_box_enabled_for_axes(handle, axes_index, enabled)
966                .map_err(|err| map_figure_error(builtin, err))?;
967            Ok(())
968        }
969        "axisequal" => {
970            let enabled = value_as_bool(value).ok_or_else(|| {
971                plotting_error(builtin, format!("{builtin}: AxisEqual must be logical"))
972            })?;
973            crate::builtins::plotting::state::set_axis_equal_for_axes(handle, axes_index, enabled)
974                .map_err(|err| map_figure_error(builtin, err))?;
975            Ok(())
976        }
977        "colorbar" => {
978            let enabled = value_as_bool(value).ok_or_else(|| {
979                plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
980            })?;
981            crate::builtins::plotting::state::set_colorbar_enabled_for_axes(
982                handle, axes_index, enabled,
983            )
984            .map_err(|err| map_figure_error(builtin, err))?;
985            Ok(())
986        }
987        "colormap" => {
988            let name = value_as_string(value).ok_or_else(|| {
989                plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
990            })?;
991            let cmap = parse_colormap_name(&name, builtin)?;
992            crate::builtins::plotting::state::set_colormap_for_axes(handle, axes_index, cmap)
993                .map_err(|err| map_figure_error(builtin, err))?;
994            Ok(())
995        }
996        "xlim" => {
997            let limits = limits_from_optional_value(value, builtin)?;
998            let meta = axes_metadata_snapshot(handle, axes_index)
999                .map_err(|err| map_figure_error(builtin, err))?;
1000            crate::builtins::plotting::state::set_axis_limits_for_axes(
1001                handle,
1002                axes_index,
1003                limits,
1004                meta.y_limits,
1005            )
1006            .map_err(|err| map_figure_error(builtin, err))?;
1007            Ok(())
1008        }
1009        "ylim" => {
1010            let limits = limits_from_optional_value(value, builtin)?;
1011            let meta = axes_metadata_snapshot(handle, axes_index)
1012                .map_err(|err| map_figure_error(builtin, err))?;
1013            crate::builtins::plotting::state::set_axis_limits_for_axes(
1014                handle,
1015                axes_index,
1016                meta.x_limits,
1017                limits,
1018            )
1019            .map_err(|err| map_figure_error(builtin, err))?;
1020            Ok(())
1021        }
1022        "zlim" => {
1023            let limits = limits_from_optional_value(value, builtin)?;
1024            crate::builtins::plotting::state::set_z_limits_for_axes(handle, axes_index, limits)
1025                .map_err(|err| map_figure_error(builtin, err))?;
1026            Ok(())
1027        }
1028        "clim" => {
1029            let limits = limits_from_optional_value(value, builtin)?;
1030            crate::builtins::plotting::state::set_color_limits_for_axes(handle, axes_index, limits)
1031                .map_err(|err| map_figure_error(builtin, err))?;
1032            Ok(())
1033        }
1034        "xscale" => {
1035            let mode = value_as_string(value).ok_or_else(|| {
1036                plotting_error(builtin, format!("{builtin}: XScale must be a string"))
1037            })?;
1038            let meta = axes_metadata_snapshot(handle, axes_index)
1039                .map_err(|err| map_figure_error(builtin, err))?;
1040            crate::builtins::plotting::state::set_log_modes_for_axes(
1041                handle,
1042                axes_index,
1043                mode.trim().eq_ignore_ascii_case("log"),
1044                meta.y_log,
1045            )
1046            .map_err(|err| map_figure_error(builtin, err))?;
1047            Ok(())
1048        }
1049        "yscale" => {
1050            let mode = value_as_string(value).ok_or_else(|| {
1051                plotting_error(builtin, format!("{builtin}: YScale must be a string"))
1052            })?;
1053            let meta = axes_metadata_snapshot(handle, axes_index)
1054                .map_err(|err| map_figure_error(builtin, err))?;
1055            crate::builtins::plotting::state::set_log_modes_for_axes(
1056                handle,
1057                axes_index,
1058                meta.x_log,
1059                mode.trim().eq_ignore_ascii_case("log"),
1060            )
1061            .map_err(|err| map_figure_error(builtin, err))?;
1062            Ok(())
1063        }
1064        other => Err(plotting_error(
1065            builtin,
1066            format!("{builtin}: unsupported axes property `{other}`"),
1067        )),
1068    }
1069}
1070
1071fn apply_figure_property(
1072    figure_handle: FigureHandle,
1073    key: &str,
1074    value: &Value,
1075    builtin: &'static str,
1076) -> BuiltinResult<()> {
1077    match key {
1078        "currentaxes" => {
1079            let resolved = resolve_plot_handle(value, builtin)?;
1080            let PlotHandle::Axes(fig, axes_index) = resolved else {
1081                return Err(plotting_error(
1082                    builtin,
1083                    format!("{builtin}: CurrentAxes must be an axes handle"),
1084                ));
1085            };
1086            if fig != figure_handle {
1087                return Err(plotting_error(
1088                    builtin,
1089                    format!("{builtin}: CurrentAxes must belong to the target figure"),
1090                ));
1091            }
1092            select_axes_for_figure(figure_handle, axes_index)
1093                .map_err(|err| map_figure_error(builtin, err))?;
1094            Ok(())
1095        }
1096        "sgtitle" => {
1097            apply_figure_text_alias(figure_handle, PlotObjectKind::SuperTitle, value, builtin)
1098        }
1099        other => Err(plotting_error(
1100            builtin,
1101            format!("{builtin}: unsupported figure property `{other}`"),
1102        )),
1103    }
1104}
1105
1106fn get_histogram_property(
1107    hist: &super::state::HistogramHandleState,
1108    property: Option<&str>,
1109    builtin: &'static str,
1110) -> BuiltinResult<Value> {
1111    let normalized =
1112        apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &hist.normalization);
1113    match property.map(canonical_property_name) {
1114        None => {
1115            let mut st = StructValue::new();
1116            st.insert("Type", Value::String("histogram".into()));
1117            st.insert(
1118                "Parent",
1119                Value::Num(super::state::encode_axes_handle(
1120                    hist.figure,
1121                    hist.axes_index,
1122                )),
1123            );
1124            st.insert("Children", handles_value(Vec::new()));
1125            st.insert("BinEdges", tensor_from_vec(hist.bin_edges.clone()));
1126            st.insert("BinCounts", tensor_from_vec(normalized));
1127            st.insert("Normalization", Value::String(hist.normalization.clone()));
1128            st.insert("NumBins", Value::Num(hist.raw_counts.len() as f64));
1129            Ok(Value::Struct(st))
1130        }
1131        Some("type") => Ok(Value::String("histogram".into())),
1132        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
1133            hist.figure,
1134            hist.axes_index,
1135        ))),
1136        Some("children") => Ok(handles_value(Vec::new())),
1137        Some("binedges") => Ok(tensor_from_vec(hist.bin_edges.clone())),
1138        Some("bincounts") => Ok(tensor_from_vec(normalized)),
1139        Some("normalization") => Ok(Value::String(hist.normalization.clone())),
1140        Some("numbins") => Ok(Value::Num(hist.raw_counts.len() as f64)),
1141        Some(other) => Err(plotting_error(
1142            builtin,
1143            format!("{builtin}: unsupported histogram property `{other}`"),
1144        )),
1145    }
1146}
1147
1148fn get_plot_child_property(
1149    state: &super::state::PlotChildHandleState,
1150    property: Option<&str>,
1151    builtin: &'static str,
1152) -> BuiltinResult<Value> {
1153    match state {
1154        super::state::PlotChildHandleState::Histogram(hist) => {
1155            get_histogram_property(hist, property, builtin)
1156        }
1157        super::state::PlotChildHandleState::Line(plot) => {
1158            get_line_property(plot, property, builtin)
1159        }
1160        super::state::PlotChildHandleState::Scatter(plot) => {
1161            get_scatter_property(plot, property, builtin)
1162        }
1163        super::state::PlotChildHandleState::Bar(plot) => get_bar_property(plot, property, builtin),
1164        super::state::PlotChildHandleState::Stem(stem) => {
1165            get_stem_property(stem, property, builtin)
1166        }
1167        super::state::PlotChildHandleState::ErrorBar(errorbar) => {
1168            get_errorbar_property(errorbar, property, builtin)
1169        }
1170        super::state::PlotChildHandleState::Stairs(plot) => {
1171            get_stairs_property(plot, property, builtin)
1172        }
1173        super::state::PlotChildHandleState::Quiver(quiver) => {
1174            get_quiver_property(quiver, property, builtin)
1175        }
1176        super::state::PlotChildHandleState::Image(image) => {
1177            get_image_property(image, property, builtin)
1178        }
1179        super::state::PlotChildHandleState::Heatmap(heatmap) => {
1180            get_heatmap_property(heatmap, property, builtin)
1181        }
1182        super::state::PlotChildHandleState::Area(area) => {
1183            get_area_property(area, property, builtin)
1184        }
1185        super::state::PlotChildHandleState::Surface(plot) => {
1186            get_surface_property(plot, property, builtin)
1187        }
1188        super::state::PlotChildHandleState::Patch(plot) => {
1189            get_patch_property(plot, property, builtin)
1190        }
1191        super::state::PlotChildHandleState::Line3(plot) => {
1192            get_line3_property(plot, property, builtin)
1193        }
1194        super::state::PlotChildHandleState::Scatter3(plot) => {
1195            get_scatter3_property(plot, property, builtin)
1196        }
1197        super::state::PlotChildHandleState::Contour(plot) => {
1198            get_contour_property(plot, property, builtin)
1199        }
1200        super::state::PlotChildHandleState::ContourFill(plot) => {
1201            get_contour_fill_property(plot, property, builtin)
1202        }
1203        super::state::PlotChildHandleState::ReferenceLine(plot) => {
1204            get_reference_line_property(plot, property, builtin)
1205        }
1206        super::state::PlotChildHandleState::Pie(plot) => get_pie_property(plot, property, builtin),
1207        super::state::PlotChildHandleState::Text(text) => {
1208            get_world_text_property(text, property, builtin)
1209        }
1210    }
1211}
1212
1213fn apply_plot_child_property(
1214    state: &super::state::PlotChildHandleState,
1215    key: &str,
1216    value: &Value,
1217    builtin: &'static str,
1218) -> BuiltinResult<()> {
1219    match state {
1220        super::state::PlotChildHandleState::Histogram(hist) => {
1221            apply_histogram_property(hist, key, value, builtin)
1222        }
1223        super::state::PlotChildHandleState::Line(plot) => {
1224            apply_line_property(plot, key, value, builtin)
1225        }
1226        super::state::PlotChildHandleState::Scatter(plot) => {
1227            apply_scatter_property(plot, key, value, builtin)
1228        }
1229        super::state::PlotChildHandleState::Bar(plot) => {
1230            apply_bar_property(plot, key, value, builtin)
1231        }
1232        super::state::PlotChildHandleState::Stem(stem) => {
1233            apply_stem_property(stem, key, value, builtin)
1234        }
1235        super::state::PlotChildHandleState::ErrorBar(errorbar) => {
1236            apply_errorbar_property(errorbar, key, value, builtin)
1237        }
1238        super::state::PlotChildHandleState::Stairs(plot) => {
1239            apply_stairs_property(plot, key, value, builtin)
1240        }
1241        super::state::PlotChildHandleState::Quiver(quiver) => {
1242            apply_quiver_property(quiver, key, value, builtin)
1243        }
1244        super::state::PlotChildHandleState::Image(image) => {
1245            apply_image_property(image, key, value, builtin)
1246        }
1247        super::state::PlotChildHandleState::Heatmap(heatmap) => {
1248            apply_heatmap_property(heatmap, key, value, builtin)
1249        }
1250        super::state::PlotChildHandleState::Area(area) => {
1251            apply_area_property(area, key, value, builtin)
1252        }
1253        super::state::PlotChildHandleState::Surface(plot) => {
1254            apply_surface_property(plot, key, value, builtin)
1255        }
1256        super::state::PlotChildHandleState::Patch(plot) => {
1257            apply_patch_property(plot, key, value, builtin)
1258        }
1259        super::state::PlotChildHandleState::Line3(plot) => {
1260            apply_line3_property(plot, key, value, builtin)
1261        }
1262        super::state::PlotChildHandleState::Scatter3(plot) => {
1263            apply_scatter3_property(plot, key, value, builtin)
1264        }
1265        super::state::PlotChildHandleState::Contour(plot) => {
1266            apply_contour_property(plot, key, value, builtin)
1267        }
1268        super::state::PlotChildHandleState::ContourFill(plot) => {
1269            apply_contour_fill_property(plot, key, value, builtin)
1270        }
1271        super::state::PlotChildHandleState::ReferenceLine(plot) => {
1272            apply_reference_line_property(plot, key, value, builtin)
1273        }
1274        super::state::PlotChildHandleState::Pie(plot) => {
1275            apply_pie_property(plot, key, value, builtin)
1276        }
1277        super::state::PlotChildHandleState::Text(text) => {
1278            apply_world_text_property(text, key, value, builtin)
1279        }
1280    }
1281}
1282
1283fn child_parent_handle(figure: FigureHandle, axes_index: usize) -> Value {
1284    Value::Num(super::state::encode_axes_handle(figure, axes_index))
1285}
1286
1287fn child_base_struct(kind: &str, figure: FigureHandle, axes_index: usize) -> StructValue {
1288    let mut st = StructValue::new();
1289    st.insert("Type", Value::String(kind.into()));
1290    st.insert("Parent", child_parent_handle(figure, axes_index));
1291    st.insert("Children", handles_value(Vec::new()));
1292    st
1293}
1294
1295fn text_position_value(position: glam::Vec3) -> Value {
1296    Value::Tensor(Tensor {
1297        rows: 1,
1298        cols: 3,
1299        shape: vec![1, 3],
1300        data: vec![position.x as f64, position.y as f64, position.z as f64],
1301        dtype: runmat_builtins::NumericDType::F64,
1302    })
1303}
1304
1305fn parse_text_position(value: &Value, builtin: &'static str) -> BuiltinResult<glam::Vec3> {
1306    match value {
1307        Value::Tensor(t) if t.data.len() == 2 || t.data.len() == 3 => Ok(glam::Vec3::new(
1308            t.data[0] as f32,
1309            t.data[1] as f32,
1310            t.data.get(2).copied().unwrap_or(0.0) as f32,
1311        )),
1312        _ => Err(plotting_error(
1313            builtin,
1314            format!("{builtin}: Position must be a 2-element or 3-element vector"),
1315        )),
1316    }
1317}
1318
1319fn get_world_text_property(
1320    handle: &super::state::TextAnnotationHandleState,
1321    property: Option<&str>,
1322    builtin: &'static str,
1323) -> BuiltinResult<Value> {
1324    let figure = super::state::clone_figure(handle.figure)
1325        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
1326    let annotation = figure
1327        .axes_text_annotation(handle.axes_index, handle.annotation_index)
1328        .cloned()
1329        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
1330    match property.map(canonical_property_name) {
1331        None => {
1332            let mut st = child_base_struct("text", handle.figure, handle.axes_index);
1333            st.insert("String", Value::String(annotation.text.clone()));
1334            st.insert("Position", text_position_value(annotation.position));
1335            if let Some(weight) = annotation.style.font_weight.clone() {
1336                st.insert("FontWeight", Value::String(weight));
1337            }
1338            if let Some(angle) = annotation.style.font_angle.clone() {
1339                st.insert("FontAngle", Value::String(angle));
1340            }
1341            if let Some(interpreter) = annotation.style.interpreter.clone() {
1342                st.insert("Interpreter", Value::String(interpreter));
1343            }
1344            if let Some(color) = annotation.style.color {
1345                st.insert("Color", Value::String(color_to_short_name(color)));
1346            }
1347            if let Some(font_size) = annotation.style.font_size {
1348                st.insert("FontSize", Value::Num(font_size as f64));
1349            }
1350            st.insert("Visible", Value::Bool(annotation.style.visible));
1351            Ok(Value::Struct(st))
1352        }
1353        Some("type") => Ok(Value::String("text".into())),
1354        Some("parent") => Ok(child_parent_handle(handle.figure, handle.axes_index)),
1355        Some("children") => Ok(handles_value(Vec::new())),
1356        Some("string") => Ok(Value::String(annotation.text)),
1357        Some("position") => Ok(text_position_value(annotation.position)),
1358        Some("fontweight") => Ok(annotation
1359            .style
1360            .font_weight
1361            .map(Value::String)
1362            .unwrap_or_else(|| Value::String(String::new()))),
1363        Some("fontangle") => Ok(annotation
1364            .style
1365            .font_angle
1366            .map(Value::String)
1367            .unwrap_or_else(|| Value::String(String::new()))),
1368        Some("interpreter") => Ok(annotation
1369            .style
1370            .interpreter
1371            .map(Value::String)
1372            .unwrap_or_else(|| Value::String(String::new()))),
1373        Some("color") => Ok(annotation
1374            .style
1375            .color
1376            .map(|c| Value::String(color_to_short_name(c)))
1377            .unwrap_or_else(|| Value::String(String::new()))),
1378        Some("fontsize") => Ok(Value::Num(
1379            annotation.style.font_size.unwrap_or_default() as f64
1380        )),
1381        Some("visible") => Ok(Value::Bool(annotation.style.visible)),
1382        Some(other) => Err(plotting_error(
1383            builtin,
1384            format!("{builtin}: unsupported text property `{other}`"),
1385        )),
1386    }
1387}
1388
1389fn apply_world_text_property(
1390    handle: &super::state::TextAnnotationHandleState,
1391    key: &str,
1392    value: &Value,
1393    builtin: &'static str,
1394) -> BuiltinResult<()> {
1395    let figure = super::state::clone_figure(handle.figure)
1396        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
1397    let annotation = figure
1398        .axes_text_annotation(handle.axes_index, handle.annotation_index)
1399        .cloned()
1400        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
1401    let mut text = None;
1402    let mut position = None;
1403    let mut style = annotation.style;
1404    match canonical_property_name(key) {
1405        "string" => {
1406            text = Some(value_as_text_string(value).ok_or_else(|| {
1407                plotting_error(builtin, format!("{builtin}: String must be text"))
1408            })?);
1409        }
1410        "position" => position = Some(parse_text_position(value, builtin)?),
1411        other => apply_text_property(&mut text, &mut style, other, value, builtin)?,
1412    }
1413    set_text_annotation_properties_for_axes(
1414        handle.figure,
1415        handle.axes_index,
1416        handle.annotation_index,
1417        text,
1418        position,
1419        Some(style),
1420    )
1421    .map_err(|err| map_figure_error(builtin, err))?;
1422    Ok(())
1423}
1424
1425fn get_simple_plot(
1426    plot: &super::state::SimplePlotHandleState,
1427    builtin: &'static str,
1428) -> BuiltinResult<runmat_plot::plots::figure::PlotElement> {
1429    let figure = super::state::clone_figure(plot.figure)
1430        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot figure")))?;
1431    let resolved = figure
1432        .plots()
1433        .nth(plot.plot_index)
1434        .cloned()
1435        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot handle")))?;
1436    Ok(resolved)
1437}
1438
1439fn get_line_property(
1440    line_handle: &super::state::SimplePlotHandleState,
1441    property: Option<&str>,
1442    builtin: &'static str,
1443) -> BuiltinResult<Value> {
1444    let plot = get_simple_plot(line_handle, builtin)?;
1445    let runmat_plot::plots::figure::PlotElement::Line(line) = plot else {
1446        return Err(plotting_error(
1447            builtin,
1448            format!("{builtin}: invalid line handle"),
1449        ));
1450    };
1451    match property.map(canonical_property_name) {
1452        None => {
1453            let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
1454            st.insert("XData", tensor_from_vec(line.x_data.clone()));
1455            st.insert("YData", tensor_from_vec(line.y_data.clone()));
1456            st.insert("Color", Value::String(color_to_short_name(line.color)));
1457            st.insert("LineWidth", Value::Num(line.line_width as f64));
1458            st.insert(
1459                "LineStyle",
1460                Value::String(line_style_name(line.line_style).into()),
1461            );
1462            if let Some(label) = line.label.clone() {
1463                st.insert("DisplayName", Value::String(label));
1464            }
1465            insert_line_marker_struct_props(&mut st, line.marker.as_ref());
1466            Ok(Value::Struct(st))
1467        }
1468        Some("type") => Ok(Value::String("line".into())),
1469        Some("parent") => Ok(child_parent_handle(
1470            line_handle.figure,
1471            line_handle.axes_index,
1472        )),
1473        Some("children") => Ok(handles_value(Vec::new())),
1474        Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
1475        Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
1476        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
1477        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
1478        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
1479        Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
1480        Some(name) => line_marker_property_value(&line.marker, name, builtin),
1481    }
1482}
1483
1484fn get_reference_line_property(
1485    line_handle: &super::state::SimplePlotHandleState,
1486    property: Option<&str>,
1487    builtin: &'static str,
1488) -> BuiltinResult<Value> {
1489    let plot = get_simple_plot(line_handle, builtin)?;
1490    let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot else {
1491        return Err(plotting_error(
1492            builtin,
1493            format!("{builtin}: invalid reference line handle"),
1494        ));
1495    };
1496    let orientation = match line.orientation {
1497        runmat_plot::plots::ReferenceLineOrientation::Vertical => "vertical",
1498        runmat_plot::plots::ReferenceLineOrientation::Horizontal => "horizontal",
1499    };
1500    match property.map(canonical_property_name) {
1501        None => {
1502            let mut st =
1503                child_base_struct("constantline", line_handle.figure, line_handle.axes_index);
1504            st.insert("Value", Value::Num(line.value));
1505            st.insert("Orientation", Value::String(orientation.into()));
1506            st.insert("Color", Value::String(color_to_short_name(line.color)));
1507            st.insert("LineWidth", Value::Num(line.line_width as f64));
1508            st.insert(
1509                "LineStyle",
1510                Value::String(line_style_name(line.line_style).into()),
1511            );
1512            st.insert(
1513                "Label",
1514                Value::String(line.label.clone().unwrap_or_default()),
1515            );
1516            st.insert(
1517                "LabelOrientation",
1518                Value::String(line.label_orientation.clone()),
1519            );
1520            st.insert(
1521                "DisplayName",
1522                Value::String(line.display_name.clone().unwrap_or_default()),
1523            );
1524            st.insert(
1525                "Visible",
1526                Value::String(if line.visible { "on" } else { "off" }.into()),
1527            );
1528            Ok(Value::Struct(st))
1529        }
1530        Some("type") => Ok(Value::String("constantline".into())),
1531        Some("parent") => Ok(child_parent_handle(
1532            line_handle.figure,
1533            line_handle.axes_index,
1534        )),
1535        Some("children") => Ok(handles_value(Vec::new())),
1536        Some("value") => Ok(Value::Num(line.value)),
1537        Some("orientation") => Ok(Value::String(orientation.into())),
1538        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
1539        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
1540        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
1541        Some("label") => Ok(Value::String(line.label.unwrap_or_default())),
1542        Some("labelorientation") => Ok(Value::String(line.label_orientation)),
1543        Some("displayname") => Ok(Value::String(line.display_name.unwrap_or_default())),
1544        Some("visible") => Ok(Value::String(
1545            if line.visible { "on" } else { "off" }.into(),
1546        )),
1547        Some(other) => Err(plotting_error(
1548            builtin,
1549            format!("{builtin}: unsupported reference line property `{other}`"),
1550        )),
1551    }
1552}
1553
1554fn get_stairs_property(
1555    stairs_handle: &super::state::SimplePlotHandleState,
1556    property: Option<&str>,
1557    builtin: &'static str,
1558) -> BuiltinResult<Value> {
1559    let plot = get_simple_plot(stairs_handle, builtin)?;
1560    let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot else {
1561        return Err(plotting_error(
1562            builtin,
1563            format!("{builtin}: invalid stairs handle"),
1564        ));
1565    };
1566    match property.map(canonical_property_name) {
1567        None => {
1568            let mut st =
1569                child_base_struct("stairs", stairs_handle.figure, stairs_handle.axes_index);
1570            st.insert("XData", tensor_from_vec(stairs.x.clone()));
1571            st.insert("YData", tensor_from_vec(stairs.y.clone()));
1572            st.insert("Color", Value::String(color_to_short_name(stairs.color)));
1573            st.insert("LineWidth", Value::Num(stairs.line_width as f64));
1574            if let Some(label) = stairs.label.clone() {
1575                st.insert("DisplayName", Value::String(label));
1576            }
1577            Ok(Value::Struct(st))
1578        }
1579        Some("type") => Ok(Value::String("stairs".into())),
1580        Some("parent") => Ok(child_parent_handle(
1581            stairs_handle.figure,
1582            stairs_handle.axes_index,
1583        )),
1584        Some("children") => Ok(handles_value(Vec::new())),
1585        Some("xdata") => Ok(tensor_from_vec(stairs.x.clone())),
1586        Some("ydata") => Ok(tensor_from_vec(stairs.y.clone())),
1587        Some("color") => Ok(Value::String(color_to_short_name(stairs.color))),
1588        Some("linewidth") => Ok(Value::Num(stairs.line_width as f64)),
1589        Some("displayname") => Ok(Value::String(stairs.label.unwrap_or_default())),
1590        Some(other) => Err(plotting_error(
1591            builtin,
1592            format!("{builtin}: unsupported stairs property `{other}`"),
1593        )),
1594    }
1595}
1596
1597fn get_scatter_property(
1598    scatter_handle: &super::state::SimplePlotHandleState,
1599    property: Option<&str>,
1600    builtin: &'static str,
1601) -> BuiltinResult<Value> {
1602    let plot = get_simple_plot(scatter_handle, builtin)?;
1603    let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot else {
1604        return Err(plotting_error(
1605            builtin,
1606            format!("{builtin}: invalid scatter handle"),
1607        ));
1608    };
1609    match property.map(canonical_property_name) {
1610        None => {
1611            let mut st =
1612                child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
1613            st.insert("XData", tensor_from_vec(scatter.x_data.clone()));
1614            st.insert("YData", tensor_from_vec(scatter.y_data.clone()));
1615            st.insert(
1616                "Marker",
1617                Value::String(marker_style_name(scatter.marker_style).into()),
1618            );
1619            st.insert("SizeData", Value::Num(scatter.marker_size as f64));
1620            st.insert(
1621                "MarkerFaceColor",
1622                Value::String(color_to_short_name(scatter.color)),
1623            );
1624            st.insert(
1625                "MarkerEdgeColor",
1626                Value::String(color_to_short_name(scatter.edge_color)),
1627            );
1628            st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
1629            if let Some(label) = scatter.label.clone() {
1630                st.insert("DisplayName", Value::String(label));
1631            }
1632            Ok(Value::Struct(st))
1633        }
1634        Some("type") => Ok(Value::String("scatter".into())),
1635        Some("parent") => Ok(child_parent_handle(
1636            scatter_handle.figure,
1637            scatter_handle.axes_index,
1638        )),
1639        Some("children") => Ok(handles_value(Vec::new())),
1640        Some("xdata") => Ok(tensor_from_vec(scatter.x_data.clone())),
1641        Some("ydata") => Ok(tensor_from_vec(scatter.y_data.clone())),
1642        Some("marker") => Ok(Value::String(
1643            marker_style_name(scatter.marker_style).into(),
1644        )),
1645        Some("sizedata") => Ok(Value::Num(scatter.marker_size as f64)),
1646        Some("markerfacecolor") => Ok(Value::String(color_to_short_name(scatter.color))),
1647        Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
1648        Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
1649        Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
1650        Some(other) => Err(plotting_error(
1651            builtin,
1652            format!("{builtin}: unsupported scatter property `{other}`"),
1653        )),
1654    }
1655}
1656
1657fn get_bar_property(
1658    bar_handle: &super::state::SimplePlotHandleState,
1659    property: Option<&str>,
1660    builtin: &'static str,
1661) -> BuiltinResult<Value> {
1662    let plot = get_simple_plot(bar_handle, builtin)?;
1663    let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot else {
1664        return Err(plotting_error(
1665            builtin,
1666            format!("{builtin}: invalid bar handle"),
1667        ));
1668    };
1669    match property.map(canonical_property_name) {
1670        None => {
1671            let mut st = child_base_struct("bar", bar_handle.figure, bar_handle.axes_index);
1672            st.insert("FaceColor", Value::String(color_to_short_name(bar.color)));
1673            st.insert("BarWidth", Value::Num(bar.bar_width as f64));
1674            if let Some(label) = bar.label.clone() {
1675                st.insert("DisplayName", Value::String(label));
1676            }
1677            Ok(Value::Struct(st))
1678        }
1679        Some("type") => Ok(Value::String("bar".into())),
1680        Some("parent") => Ok(child_parent_handle(
1681            bar_handle.figure,
1682            bar_handle.axes_index,
1683        )),
1684        Some("children") => Ok(handles_value(Vec::new())),
1685        Some("facecolor") | Some("color") => Ok(Value::String(color_to_short_name(bar.color))),
1686        Some("barwidth") => Ok(Value::Num(bar.bar_width as f64)),
1687        Some("displayname") => Ok(Value::String(bar.label.unwrap_or_default())),
1688        Some(other) => Err(plotting_error(
1689            builtin,
1690            format!("{builtin}: unsupported bar property `{other}`"),
1691        )),
1692    }
1693}
1694
1695fn get_surface_property(
1696    surface_handle: &super::state::SimplePlotHandleState,
1697    property: Option<&str>,
1698    builtin: &'static str,
1699) -> BuiltinResult<Value> {
1700    let plot = get_simple_plot(surface_handle, builtin)?;
1701    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
1702        return Err(plotting_error(
1703            builtin,
1704            format!("{builtin}: invalid surface handle"),
1705        ));
1706    };
1707    match property.map(canonical_property_name) {
1708        None => {
1709            let mut st =
1710                child_base_struct("surface", surface_handle.figure, surface_handle.axes_index);
1711            st.insert("XData", tensor_from_vec(surface.x_data.clone()));
1712            st.insert("YData", tensor_from_vec(surface.y_data.clone()));
1713            if let Some(z) = surface.z_data.clone() {
1714                st.insert("ZData", tensor_from_matrix(z));
1715            }
1716            st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
1717            if let Some(label) = surface.label.clone() {
1718                st.insert("DisplayName", Value::String(label));
1719            }
1720            Ok(Value::Struct(st))
1721        }
1722        Some("type") => Ok(Value::String("surface".into())),
1723        Some("parent") => Ok(child_parent_handle(
1724            surface_handle.figure,
1725            surface_handle.axes_index,
1726        )),
1727        Some("children") => Ok(handles_value(Vec::new())),
1728        Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
1729        Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
1730        Some("zdata") => Ok(surface
1731            .z_data
1732            .clone()
1733            .map(tensor_from_matrix)
1734            .unwrap_or_else(|| tensor_from_vec(Vec::new()))),
1735        Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
1736        Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
1737        Some(other) => Err(plotting_error(
1738            builtin,
1739            format!("{builtin}: unsupported surface property `{other}`"),
1740        )),
1741    }
1742}
1743
1744fn get_patch_property(
1745    patch_handle: &super::state::SimplePlotHandleState,
1746    property: Option<&str>,
1747    builtin: &'static str,
1748) -> BuiltinResult<Value> {
1749    let plot = get_simple_plot(patch_handle, builtin)?;
1750    let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot else {
1751        return Err(plotting_error(
1752            builtin,
1753            format!("{builtin}: invalid patch handle"),
1754        ));
1755    };
1756    match property.map(canonical_property_name) {
1757        None => {
1758            let mut st = child_base_struct("patch", patch_handle.figure, patch_handle.axes_index);
1759            st.insert("Faces", faces_tensor(patch.faces()));
1760            st.insert("Vertices", vertices_tensor(patch.vertices()));
1761            st.insert(
1762                "XData",
1763                tensor_from_vec(patch.vertices().iter().map(|p| p.x as f64).collect()),
1764            );
1765            st.insert(
1766                "YData",
1767                tensor_from_vec(patch.vertices().iter().map(|p| p.y as f64).collect()),
1768            );
1769            st.insert(
1770                "ZData",
1771                tensor_from_vec(patch.vertices().iter().map(|p| p.z as f64).collect()),
1772            );
1773            st.insert(
1774                "FaceColor",
1775                patch_color_property(patch.face_color_mode(), patch.face_color()),
1776            );
1777            st.insert(
1778                "EdgeColor",
1779                patch_edge_color_property(patch.edge_color_mode(), patch.edge_color()),
1780            );
1781            st.insert("FaceAlpha", Value::Num(patch.face_alpha() as f64));
1782            st.insert("EdgeAlpha", Value::Num(patch.edge_alpha() as f64));
1783            st.insert("LineWidth", Value::Num(patch.line_width() as f64));
1784            st.insert("Visible", Value::Bool(patch.is_visible()));
1785            if let Some(label) = patch.label() {
1786                st.insert("DisplayName", Value::String(label.to_string()));
1787            }
1788            Ok(Value::Struct(st))
1789        }
1790        Some("type") => Ok(Value::String("patch".into())),
1791        Some("parent") => Ok(child_parent_handle(
1792            patch_handle.figure,
1793            patch_handle.axes_index,
1794        )),
1795        Some("children") => Ok(handles_value(Vec::new())),
1796        Some("faces") => Ok(faces_tensor(patch.faces())),
1797        Some("vertices") => Ok(vertices_tensor(patch.vertices())),
1798        Some("xdata") => Ok(tensor_from_vec(
1799            patch.vertices().iter().map(|p| p.x as f64).collect(),
1800        )),
1801        Some("ydata") => Ok(tensor_from_vec(
1802            patch.vertices().iter().map(|p| p.y as f64).collect(),
1803        )),
1804        Some("zdata") => Ok(tensor_from_vec(
1805            patch.vertices().iter().map(|p| p.z as f64).collect(),
1806        )),
1807        Some("facecolor") | Some("color") => Ok(patch_color_property(
1808            patch.face_color_mode(),
1809            patch.face_color(),
1810        )),
1811        Some("edgecolor") => Ok(patch_edge_color_property(
1812            patch.edge_color_mode(),
1813            patch.edge_color(),
1814        )),
1815        Some("facealpha") => Ok(Value::Num(patch.face_alpha() as f64)),
1816        Some("edgealpha") => Ok(Value::Num(patch.edge_alpha() as f64)),
1817        Some("linewidth") => Ok(Value::Num(patch.line_width() as f64)),
1818        Some("displayname") => Ok(Value::String(patch.label().unwrap_or_default().to_string())),
1819        Some("visible") => Ok(Value::Bool(patch.is_visible())),
1820        Some(other) => Err(plotting_error(
1821            builtin,
1822            format!("{builtin}: unsupported patch property `{other}`"),
1823        )),
1824    }
1825}
1826
1827fn get_line3_property(
1828    line_handle: &super::state::SimplePlotHandleState,
1829    property: Option<&str>,
1830    builtin: &'static str,
1831) -> BuiltinResult<Value> {
1832    let plot = get_simple_plot(line_handle, builtin)?;
1833    let runmat_plot::plots::figure::PlotElement::Line3(line) = plot else {
1834        return Err(plotting_error(
1835            builtin,
1836            format!("{builtin}: invalid plot3 handle"),
1837        ));
1838    };
1839    match property.map(canonical_property_name) {
1840        None => {
1841            let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
1842            st.insert("XData", tensor_from_vec(line.x_data.clone()));
1843            st.insert("YData", tensor_from_vec(line.y_data.clone()));
1844            st.insert("ZData", tensor_from_vec(line.z_data.clone()));
1845            st.insert("Color", Value::String(color_to_short_name(line.color)));
1846            st.insert("LineWidth", Value::Num(line.line_width as f64));
1847            st.insert(
1848                "LineStyle",
1849                Value::String(line_style_name(line.line_style).into()),
1850            );
1851            if let Some(label) = line.label.clone() {
1852                st.insert("DisplayName", Value::String(label));
1853            }
1854            Ok(Value::Struct(st))
1855        }
1856        Some("type") => Ok(Value::String("line".into())),
1857        Some("parent") => Ok(child_parent_handle(
1858            line_handle.figure,
1859            line_handle.axes_index,
1860        )),
1861        Some("children") => Ok(handles_value(Vec::new())),
1862        Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
1863        Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
1864        Some("zdata") => Ok(tensor_from_vec(line.z_data.clone())),
1865        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
1866        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
1867        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
1868        Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
1869        Some(other) => Err(plotting_error(
1870            builtin,
1871            format!("{builtin}: unsupported plot3 property `{other}`"),
1872        )),
1873    }
1874}
1875
1876fn get_scatter3_property(
1877    scatter_handle: &super::state::SimplePlotHandleState,
1878    property: Option<&str>,
1879    builtin: &'static str,
1880) -> BuiltinResult<Value> {
1881    let plot = get_simple_plot(scatter_handle, builtin)?;
1882    let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot else {
1883        return Err(plotting_error(
1884            builtin,
1885            format!("{builtin}: invalid scatter3 handle"),
1886        ));
1887    };
1888    let (x, y, z): (Vec<f64>, Vec<f64>, Vec<f64>) = scatter
1889        .points
1890        .iter()
1891        .map(|p| (p.x as f64, p.y as f64, p.z as f64))
1892        .unzip_n_vec();
1893    match property.map(canonical_property_name) {
1894        None => {
1895            let mut st =
1896                child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
1897            st.insert("XData", tensor_from_vec(x));
1898            st.insert("YData", tensor_from_vec(y));
1899            st.insert("ZData", tensor_from_vec(z));
1900            st.insert("SizeData", Value::Num(scatter.point_size as f64));
1901            if let Some(label) = scatter.label.clone() {
1902                st.insert("DisplayName", Value::String(label));
1903            }
1904            Ok(Value::Struct(st))
1905        }
1906        Some("type") => Ok(Value::String("scatter".into())),
1907        Some("parent") => Ok(child_parent_handle(
1908            scatter_handle.figure,
1909            scatter_handle.axes_index,
1910        )),
1911        Some("children") => Ok(handles_value(Vec::new())),
1912        Some("sizedata") => Ok(Value::Num(scatter.point_size as f64)),
1913        Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
1914        Some(other) => Err(plotting_error(
1915            builtin,
1916            format!("{builtin}: unsupported scatter3 property `{other}`"),
1917        )),
1918    }
1919}
1920
1921fn get_pie_property(
1922    pie_handle: &super::state::SimplePlotHandleState,
1923    property: Option<&str>,
1924    builtin: &'static str,
1925) -> BuiltinResult<Value> {
1926    let plot = get_simple_plot(pie_handle, builtin)?;
1927    let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot else {
1928        return Err(plotting_error(
1929            builtin,
1930            format!("{builtin}: invalid pie handle"),
1931        ));
1932    };
1933    match property.map(canonical_property_name) {
1934        None => {
1935            let mut st = child_base_struct("pie", pie_handle.figure, pie_handle.axes_index);
1936            if let Some(label) = pie.label.clone() {
1937                st.insert("DisplayName", Value::String(label));
1938            }
1939            Ok(Value::Struct(st))
1940        }
1941        Some("type") => Ok(Value::String("pie".into())),
1942        Some("parent") => Ok(child_parent_handle(
1943            pie_handle.figure,
1944            pie_handle.axes_index,
1945        )),
1946        Some("children") => Ok(handles_value(Vec::new())),
1947        Some("displayname") => Ok(Value::String(pie.label.unwrap_or_default())),
1948        Some(other) => Err(plotting_error(
1949            builtin,
1950            format!("{builtin}: unsupported pie property `{other}`"),
1951        )),
1952    }
1953}
1954
1955fn get_contour_property(
1956    contour_handle: &super::state::SimplePlotHandleState,
1957    property: Option<&str>,
1958    builtin: &'static str,
1959) -> BuiltinResult<Value> {
1960    let plot = get_simple_plot(contour_handle, builtin)?;
1961    let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
1962        return Err(plotting_error(
1963            builtin,
1964            format!("{builtin}: invalid contour handle"),
1965        ));
1966    };
1967    match property.map(canonical_property_name) {
1968        None => {
1969            let mut st =
1970                child_base_struct("contour", contour_handle.figure, contour_handle.axes_index);
1971            st.insert("ZData", Value::Num(contour.base_z as f64));
1972            if let Some(label) = contour.label.clone() {
1973                st.insert("DisplayName", Value::String(label));
1974            }
1975            Ok(Value::Struct(st))
1976        }
1977        Some("type") => Ok(Value::String("contour".into())),
1978        Some("parent") => Ok(child_parent_handle(
1979            contour_handle.figure,
1980            contour_handle.axes_index,
1981        )),
1982        Some("children") => Ok(handles_value(Vec::new())),
1983        Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
1984        Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
1985        Some(other) => Err(plotting_error(
1986            builtin,
1987            format!("{builtin}: unsupported contour property `{other}`"),
1988        )),
1989    }
1990}
1991
1992fn get_contour_fill_property(
1993    fill_handle: &super::state::SimplePlotHandleState,
1994    property: Option<&str>,
1995    builtin: &'static str,
1996) -> BuiltinResult<Value> {
1997    let plot = get_simple_plot(fill_handle, builtin)?;
1998    let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot else {
1999        return Err(plotting_error(
2000            builtin,
2001            format!("{builtin}: invalid contourf handle"),
2002        ));
2003    };
2004    match property.map(canonical_property_name) {
2005        None => {
2006            let mut st = child_base_struct("contour", fill_handle.figure, fill_handle.axes_index);
2007            if let Some(label) = fill.label.clone() {
2008                st.insert("DisplayName", Value::String(label));
2009            }
2010            Ok(Value::Struct(st))
2011        }
2012        Some("type") => Ok(Value::String("contour".into())),
2013        Some("parent") => Ok(child_parent_handle(
2014            fill_handle.figure,
2015            fill_handle.axes_index,
2016        )),
2017        Some("children") => Ok(handles_value(Vec::new())),
2018        Some("displayname") => Ok(Value::String(fill.label.unwrap_or_default())),
2019        Some(other) => Err(plotting_error(
2020            builtin,
2021            format!("{builtin}: unsupported contourf property `{other}`"),
2022        )),
2023    }
2024}
2025
2026fn get_stem_property(
2027    stem_handle: &super::state::StemHandleState,
2028    property: Option<&str>,
2029    builtin: &'static str,
2030) -> BuiltinResult<Value> {
2031    let figure = super::state::clone_figure(stem_handle.figure)
2032        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem figure")))?;
2033    let plot = figure
2034        .plots()
2035        .nth(stem_handle.plot_index)
2036        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem handle")))?;
2037    let runmat_plot::plots::figure::PlotElement::Stem(stem) = plot else {
2038        return Err(plotting_error(
2039            builtin,
2040            format!("{builtin}: invalid stem handle"),
2041        ));
2042    };
2043    match property.map(canonical_property_name) {
2044        None => {
2045            let mut st = StructValue::new();
2046            st.insert("Type", Value::String("stem".into()));
2047            st.insert(
2048                "Parent",
2049                Value::Num(super::state::encode_axes_handle(
2050                    stem_handle.figure,
2051                    stem_handle.axes_index,
2052                )),
2053            );
2054            st.insert("Children", handles_value(Vec::new()));
2055            st.insert("BaseValue", Value::Num(stem.baseline));
2056            st.insert("BaseLine", Value::Bool(stem.baseline_visible));
2057            st.insert("LineWidth", Value::Num(stem.line_width as f64));
2058            st.insert(
2059                "LineStyle",
2060                Value::String(line_style_name(stem.line_style).into()),
2061            );
2062            st.insert("Color", Value::String(color_to_short_name(stem.color)));
2063            if let Some(marker) = &stem.marker {
2064                st.insert(
2065                    "Marker",
2066                    Value::String(marker_style_name(marker.kind).into()),
2067                );
2068                st.insert("MarkerSize", Value::Num(marker.size as f64));
2069                st.insert(
2070                    "MarkerFaceColor",
2071                    Value::String(color_to_short_name(marker.face_color)),
2072                );
2073                st.insert(
2074                    "MarkerEdgeColor",
2075                    Value::String(color_to_short_name(marker.edge_color)),
2076                );
2077                st.insert("Filled", Value::Bool(marker.filled));
2078            }
2079            Ok(Value::Struct(st))
2080        }
2081        Some("type") => Ok(Value::String("stem".into())),
2082        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2083            stem_handle.figure,
2084            stem_handle.axes_index,
2085        ))),
2086        Some("children") => Ok(handles_value(Vec::new())),
2087        Some("basevalue") => Ok(Value::Num(stem.baseline)),
2088        Some("baseline") => Ok(Value::Bool(stem.baseline_visible)),
2089        Some("linewidth") => Ok(Value::Num(stem.line_width as f64)),
2090        Some("linestyle") => Ok(Value::String(line_style_name(stem.line_style).into())),
2091        Some("color") => Ok(Value::String(color_to_short_name(stem.color))),
2092        Some("marker") => Ok(Value::String(
2093            stem.marker
2094                .as_ref()
2095                .map(|m| marker_style_name(m.kind).to_string())
2096                .unwrap_or("none".into()),
2097        )),
2098        Some("markersize") => Ok(Value::Num(
2099            stem.marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
2100        )),
2101        Some("markerfacecolor") => Ok(Value::String(
2102            stem.marker
2103                .as_ref()
2104                .map(|m| color_to_short_name(m.face_color))
2105                .unwrap_or("none".into()),
2106        )),
2107        Some("markeredgecolor") => Ok(Value::String(
2108            stem.marker
2109                .as_ref()
2110                .map(|m| color_to_short_name(m.edge_color))
2111                .unwrap_or("none".into()),
2112        )),
2113        Some("filled") => Ok(Value::Bool(
2114            stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
2115        )),
2116        Some(other) => Err(plotting_error(
2117            builtin,
2118            format!("{builtin}: unsupported stem property `{other}`"),
2119        )),
2120    }
2121}
2122
2123fn get_errorbar_property(
2124    error_handle: &super::state::ErrorBarHandleState,
2125    property: Option<&str>,
2126    builtin: &'static str,
2127) -> BuiltinResult<Value> {
2128    let figure = super::state::clone_figure(error_handle.figure)
2129        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar figure")))?;
2130    let plot = figure
2131        .plots()
2132        .nth(error_handle.plot_index)
2133        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar handle")))?;
2134    let runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) = plot else {
2135        return Err(plotting_error(
2136            builtin,
2137            format!("{builtin}: invalid errorbar handle"),
2138        ));
2139    };
2140    match property.map(canonical_property_name) {
2141        None => {
2142            let mut st = StructValue::new();
2143            st.insert("Type", Value::String("errorbar".into()));
2144            st.insert(
2145                "Parent",
2146                Value::Num(super::state::encode_axes_handle(
2147                    error_handle.figure,
2148                    error_handle.axes_index,
2149                )),
2150            );
2151            st.insert("Children", handles_value(Vec::new()));
2152            st.insert("LineWidth", Value::Num(errorbar.line_width as f64));
2153            st.insert(
2154                "LineStyle",
2155                Value::String(line_style_name(errorbar.line_style).into()),
2156            );
2157            st.insert("Color", Value::String(color_to_short_name(errorbar.color)));
2158            st.insert("CapSize", Value::Num(errorbar.cap_size as f64));
2159            if let Some(marker) = &errorbar.marker {
2160                st.insert(
2161                    "Marker",
2162                    Value::String(marker_style_name(marker.kind).into()),
2163                );
2164                st.insert("MarkerSize", Value::Num(marker.size as f64));
2165            }
2166            Ok(Value::Struct(st))
2167        }
2168        Some("type") => Ok(Value::String("errorbar".into())),
2169        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2170            error_handle.figure,
2171            error_handle.axes_index,
2172        ))),
2173        Some("children") => Ok(handles_value(Vec::new())),
2174        Some("linewidth") => Ok(Value::Num(errorbar.line_width as f64)),
2175        Some("linestyle") => Ok(Value::String(line_style_name(errorbar.line_style).into())),
2176        Some("color") => Ok(Value::String(color_to_short_name(errorbar.color))),
2177        Some("capsize") => Ok(Value::Num(errorbar.cap_size as f64)),
2178        Some("marker") => Ok(Value::String(
2179            errorbar
2180                .marker
2181                .as_ref()
2182                .map(|m| marker_style_name(m.kind).to_string())
2183                .unwrap_or("none".into()),
2184        )),
2185        Some("markersize") => Ok(Value::Num(
2186            errorbar
2187                .marker
2188                .as_ref()
2189                .map(|m| m.size as f64)
2190                .unwrap_or(0.0),
2191        )),
2192        Some(other) => Err(plotting_error(
2193            builtin,
2194            format!("{builtin}: unsupported errorbar property `{other}`"),
2195        )),
2196    }
2197}
2198
2199fn get_quiver_property(
2200    quiver_handle: &super::state::QuiverHandleState,
2201    property: Option<&str>,
2202    builtin: &'static str,
2203) -> BuiltinResult<Value> {
2204    let figure = super::state::clone_figure(quiver_handle.figure)
2205        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
2206    let plot = figure
2207        .plots()
2208        .nth(quiver_handle.plot_index)
2209        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
2210    let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
2211        return Err(plotting_error(
2212            builtin,
2213            format!("{builtin}: invalid quiver handle"),
2214        ));
2215    };
2216    match property.map(canonical_property_name) {
2217        None => {
2218            let mut st = StructValue::new();
2219            st.insert("Type", Value::String("quiver".into()));
2220            st.insert(
2221                "Parent",
2222                Value::Num(super::state::encode_axes_handle(
2223                    quiver_handle.figure,
2224                    quiver_handle.axes_index,
2225                )),
2226            );
2227            st.insert("Children", handles_value(Vec::new()));
2228            st.insert("Color", Value::String(color_to_short_name(quiver.color)));
2229            st.insert("LineWidth", Value::Num(quiver.line_width as f64));
2230            st.insert("AutoScaleFactor", Value::Num(quiver.scale as f64));
2231            st.insert("MaxHeadSize", Value::Num(quiver.head_size as f64));
2232            Ok(Value::Struct(st))
2233        }
2234        Some("type") => Ok(Value::String("quiver".into())),
2235        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2236            quiver_handle.figure,
2237            quiver_handle.axes_index,
2238        ))),
2239        Some("children") => Ok(handles_value(Vec::new())),
2240        Some("color") => Ok(Value::String(color_to_short_name(quiver.color))),
2241        Some("linewidth") => Ok(Value::Num(quiver.line_width as f64)),
2242        Some("autoscalefactor") => Ok(Value::Num(quiver.scale as f64)),
2243        Some("maxheadsize") => Ok(Value::Num(quiver.head_size as f64)),
2244        Some(other) => Err(plotting_error(
2245            builtin,
2246            format!("{builtin}: unsupported quiver property `{other}`"),
2247        )),
2248    }
2249}
2250
2251fn get_image_property(
2252    image_handle: &super::state::ImageHandleState,
2253    property: Option<&str>,
2254    builtin: &'static str,
2255) -> BuiltinResult<Value> {
2256    let figure = super::state::clone_figure(image_handle.figure)
2257        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image figure")))?;
2258    let plot = figure
2259        .plots()
2260        .nth(image_handle.plot_index)
2261        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image handle")))?;
2262    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
2263        return Err(plotting_error(
2264            builtin,
2265            format!("{builtin}: invalid image handle"),
2266        ));
2267    };
2268    if !surface.image_mode {
2269        return Err(plotting_error(
2270            builtin,
2271            format!("{builtin}: handle does not reference an image plot"),
2272        ));
2273    }
2274    match property.map(canonical_property_name) {
2275        None => {
2276            let mut st = StructValue::new();
2277            st.insert("Type", Value::String("image".into()));
2278            st.insert(
2279                "Parent",
2280                Value::Num(super::state::encode_axes_handle(
2281                    image_handle.figure,
2282                    image_handle.axes_index,
2283                )),
2284            );
2285            st.insert("Children", handles_value(Vec::new()));
2286            st.insert("XData", tensor_from_vec(surface.x_data.clone()));
2287            st.insert("YData", tensor_from_vec(surface.y_data.clone()));
2288            st.insert(
2289                "CDataMapping",
2290                Value::String(if surface.color_grid.is_some() {
2291                    "direct".into()
2292                } else {
2293                    "scaled".into()
2294                }),
2295            );
2296            Ok(Value::Struct(st))
2297        }
2298        Some("type") => Ok(Value::String("image".into())),
2299        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2300            image_handle.figure,
2301            image_handle.axes_index,
2302        ))),
2303        Some("children") => Ok(handles_value(Vec::new())),
2304        Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
2305        Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
2306        Some("cdatamapping") => Ok(Value::String(if surface.color_grid.is_some() {
2307            "direct".into()
2308        } else {
2309            "scaled".into()
2310        })),
2311        Some(other) => Err(plotting_error(
2312            builtin,
2313            format!("{builtin}: unsupported image property `{other}`"),
2314        )),
2315    }
2316}
2317
2318fn get_heatmap_property(
2319    heatmap_handle: &super::state::HeatmapHandleState,
2320    property: Option<&str>,
2321    builtin: &'static str,
2322) -> BuiltinResult<Value> {
2323    let figure = super::state::clone_figure(heatmap_handle.figure)
2324        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap figure")))?;
2325    let plot = figure
2326        .plots()
2327        .nth(heatmap_handle.plot_index)
2328        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap handle")))?;
2329    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
2330        return Err(plotting_error(
2331            builtin,
2332            format!("{builtin}: invalid heatmap handle"),
2333        ));
2334    };
2335    if !surface.image_mode {
2336        return Err(plotting_error(
2337            builtin,
2338            format!("{builtin}: handle does not reference a heatmap plot"),
2339        ));
2340    }
2341    let meta = figure
2342        .axes_metadata(heatmap_handle.axes_index)
2343        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap axes")))?;
2344    match property.map(canonical_property_name) {
2345        None => {
2346            let mut st = StructValue::new();
2347            st.insert("Type", Value::String("heatmap".into()));
2348            st.insert(
2349                "Parent",
2350                Value::Num(super::state::encode_axes_handle(
2351                    heatmap_handle.figure,
2352                    heatmap_handle.axes_index,
2353                )),
2354            );
2355            st.insert("Children", handles_value(Vec::new()));
2356            st.insert(
2357                "Title",
2358                Value::String(meta.title.clone().unwrap_or_default()),
2359            );
2360            st.insert(
2361                "XLabel",
2362                Value::String(meta.x_label.clone().unwrap_or_default()),
2363            );
2364            st.insert(
2365                "YLabel",
2366                Value::String(meta.y_label.clone().unwrap_or_default()),
2367            );
2368            st.insert(
2369                "XDisplayLabels",
2370                string_array_from_vec(heatmap_handle.x_labels.clone())?,
2371            );
2372            st.insert(
2373                "YDisplayLabels",
2374                string_array_from_vec(heatmap_handle.y_labels.clone())?,
2375            );
2376            st.insert(
2377                "ColorData",
2378                Value::Tensor(heatmap_handle.color_data.clone()),
2379            );
2380            st.insert("ColorbarVisible", Value::Bool(meta.colorbar_enabled));
2381            st.insert(
2382                "Colormap",
2383                Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
2384            );
2385            Ok(Value::Struct(st))
2386        }
2387        Some("type") => Ok(Value::String("heatmap".into())),
2388        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2389            heatmap_handle.figure,
2390            heatmap_handle.axes_index,
2391        ))),
2392        Some("children") => Ok(handles_value(Vec::new())),
2393        Some("title") => Ok(Value::String(meta.title.clone().unwrap_or_default())),
2394        Some("xlabel") => Ok(Value::String(meta.x_label.clone().unwrap_or_default())),
2395        Some("ylabel") => Ok(Value::String(meta.y_label.clone().unwrap_or_default())),
2396        Some("xdisplaylabels") => string_array_from_vec(heatmap_handle.x_labels.clone()),
2397        Some("ydisplaylabels") => string_array_from_vec(heatmap_handle.y_labels.clone()),
2398        Some("colordata") => Ok(Value::Tensor(heatmap_handle.color_data.clone())),
2399        Some("colorbarvisible") | Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
2400        Some("colormap") => Ok(Value::String(
2401            format!("{:?}", meta.colormap).to_ascii_lowercase(),
2402        )),
2403        Some(other) => Err(plotting_error(
2404            builtin,
2405            format!("{builtin}: unsupported heatmap property `{other}`"),
2406        )),
2407    }
2408}
2409
2410fn get_area_property(
2411    area_handle: &super::state::AreaHandleState,
2412    property: Option<&str>,
2413    builtin: &'static str,
2414) -> BuiltinResult<Value> {
2415    let figure = super::state::clone_figure(area_handle.figure)
2416        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area figure")))?;
2417    let plot = figure
2418        .plots()
2419        .nth(area_handle.plot_index)
2420        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area handle")))?;
2421    let runmat_plot::plots::figure::PlotElement::Area(area) = plot else {
2422        return Err(plotting_error(
2423            builtin,
2424            format!("{builtin}: invalid area handle"),
2425        ));
2426    };
2427    match property.map(canonical_property_name) {
2428        None => {
2429            let mut st = StructValue::new();
2430            st.insert("Type", Value::String("area".into()));
2431            st.insert(
2432                "Parent",
2433                Value::Num(super::state::encode_axes_handle(
2434                    area_handle.figure,
2435                    area_handle.axes_index,
2436                )),
2437            );
2438            st.insert("Children", handles_value(Vec::new()));
2439            st.insert("XData", tensor_from_vec(area.x.clone()));
2440            st.insert("YData", tensor_from_vec(area.y.clone()));
2441            st.insert("BaseValue", Value::Num(area.baseline));
2442            st.insert("Color", Value::String(color_to_short_name(area.color)));
2443            Ok(Value::Struct(st))
2444        }
2445        Some("type") => Ok(Value::String("area".into())),
2446        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2447            area_handle.figure,
2448            area_handle.axes_index,
2449        ))),
2450        Some("children") => Ok(handles_value(Vec::new())),
2451        Some("xdata") => Ok(tensor_from_vec(area.x.clone())),
2452        Some("ydata") => Ok(tensor_from_vec(area.y.clone())),
2453        Some("basevalue") => Ok(Value::Num(area.baseline)),
2454        Some("color") => Ok(Value::String(color_to_short_name(area.color))),
2455        Some(other) => Err(plotting_error(
2456            builtin,
2457            format!("{builtin}: unsupported area property `{other}`"),
2458        )),
2459    }
2460}
2461
2462fn apply_histogram_property(
2463    hist: &super::state::HistogramHandleState,
2464    key: &str,
2465    value: &Value,
2466    builtin: &'static str,
2467) -> BuiltinResult<()> {
2468    match key {
2469        "normalization" => {
2470            let norm = value_as_string(value)
2471                .ok_or_else(|| {
2472                    plotting_error(
2473                        builtin,
2474                        format!("{builtin}: Normalization must be a string"),
2475                    )
2476                })?
2477                .trim()
2478                .to_ascii_lowercase();
2479            validate_histogram_normalization(&norm, builtin)?;
2480            let normalized =
2481                apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &norm);
2482            let labels = histogram_labels_from_edges(&hist.bin_edges);
2483            super::state::update_histogram_plot_data(
2484                hist.figure,
2485                hist.plot_index,
2486                labels,
2487                normalized,
2488            )
2489            .map_err(|err| map_figure_error(builtin, err))?;
2490            super::state::update_histogram_handle_for_plot(
2491                hist.figure,
2492                hist.axes_index,
2493                hist.plot_index,
2494                norm,
2495                hist.raw_counts.clone(),
2496            )
2497            .map_err(|err| map_figure_error(builtin, err))?;
2498            Ok(())
2499        }
2500        other => Err(plotting_error(
2501            builtin,
2502            format!("{builtin}: unsupported histogram property `{other}`"),
2503        )),
2504    }
2505}
2506
2507fn apply_stem_property(
2508    stem_handle: &super::state::StemHandleState,
2509    key: &str,
2510    value: &Value,
2511    builtin: &'static str,
2512) -> BuiltinResult<()> {
2513    super::state::update_stem_plot(
2514        stem_handle.figure,
2515        stem_handle.plot_index,
2516        |stem| match key {
2517            "basevalue" => {
2518                if let Some(v) = value_as_f64(value) {
2519                    stem.baseline = v;
2520                }
2521            }
2522            "baseline" => {
2523                if let Some(v) = value_as_bool(value) {
2524                    stem.baseline_visible = v;
2525                }
2526            }
2527            "linewidth" => {
2528                if let Some(v) = value_as_f64(value) {
2529                    stem.line_width = v as f32;
2530                }
2531            }
2532            "linestyle" => {
2533                if let Some(s) = value_as_string(value) {
2534                    stem.line_style = parse_line_style_name_for_props(&s);
2535                }
2536            }
2537            "color" => {
2538                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2539                    stem.color = c;
2540                }
2541            }
2542            "marker" => {
2543                if let Some(s) = value_as_string(value) {
2544                    stem.marker = marker_from_name(&s, stem.marker.clone());
2545                }
2546            }
2547            "markersize" => {
2548                if let Some(v) = value_as_f64(value) {
2549                    if let Some(marker) = &mut stem.marker {
2550                        marker.size = v as f32;
2551                    }
2552                }
2553            }
2554            "markerfacecolor" => {
2555                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2556                    if let Some(marker) = &mut stem.marker {
2557                        marker.face_color = c;
2558                    }
2559                }
2560            }
2561            "markeredgecolor" => {
2562                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2563                    if let Some(marker) = &mut stem.marker {
2564                        marker.edge_color = c;
2565                    }
2566                }
2567            }
2568            "filled" => {
2569                if let Some(v) = value_as_bool(value) {
2570                    if let Some(marker) = &mut stem.marker {
2571                        marker.filled = v;
2572                    }
2573                }
2574            }
2575            _ => {}
2576        },
2577    )
2578    .map_err(|err| map_figure_error(builtin, err))?;
2579    Ok(())
2580}
2581
2582fn apply_errorbar_property(
2583    error_handle: &super::state::ErrorBarHandleState,
2584    key: &str,
2585    value: &Value,
2586    builtin: &'static str,
2587) -> BuiltinResult<()> {
2588    super::state::update_errorbar_plot(error_handle.figure, error_handle.plot_index, |errorbar| {
2589        match key {
2590            "linewidth" => {
2591                if let Some(v) = value_as_f64(value) {
2592                    errorbar.line_width = v as f32;
2593                }
2594            }
2595            "linestyle" => {
2596                if let Some(s) = value_as_string(value) {
2597                    errorbar.line_style = parse_line_style_name_for_props(&s);
2598                }
2599            }
2600            "color" => {
2601                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2602                    errorbar.color = c;
2603                }
2604            }
2605            "capsize" => {
2606                if let Some(v) = value_as_f64(value) {
2607                    errorbar.cap_size = v as f32;
2608                }
2609            }
2610            "marker" => {
2611                if let Some(s) = value_as_string(value) {
2612                    errorbar.marker = marker_from_name(&s, errorbar.marker.clone());
2613                }
2614            }
2615            "markersize" => {
2616                if let Some(v) = value_as_f64(value) {
2617                    if let Some(marker) = &mut errorbar.marker {
2618                        marker.size = v as f32;
2619                    }
2620                }
2621            }
2622            _ => {}
2623        }
2624    })
2625    .map_err(|err| map_figure_error(builtin, err))?;
2626    Ok(())
2627}
2628
2629fn apply_quiver_property(
2630    quiver_handle: &super::state::QuiverHandleState,
2631    key: &str,
2632    value: &Value,
2633    builtin: &'static str,
2634) -> BuiltinResult<()> {
2635    super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
2636        match key {
2637            "color" => {
2638                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2639                    quiver.color = c;
2640                }
2641            }
2642            "linewidth" => {
2643                if let Some(v) = value_as_f64(value) {
2644                    quiver.line_width = v as f32;
2645                }
2646            }
2647            "autoscalefactor" => {
2648                if let Some(v) = value_as_f64(value) {
2649                    quiver.scale = v as f32;
2650                }
2651            }
2652            "maxheadsize" => {
2653                if let Some(v) = value_as_f64(value) {
2654                    quiver.head_size = v as f32;
2655                }
2656            }
2657            _ => {}
2658        }
2659    })
2660    .map_err(|err| map_figure_error(builtin, err))?;
2661    Ok(())
2662}
2663
2664fn apply_image_property(
2665    image_handle: &super::state::ImageHandleState,
2666    key: &str,
2667    value: &Value,
2668    builtin: &'static str,
2669) -> BuiltinResult<()> {
2670    super::state::update_image_plot(image_handle.figure, image_handle.plot_index, |surface| {
2671        match key {
2672            "xdata" => {
2673                if let Ok(tensor) = Tensor::try_from(value) {
2674                    surface.x_data = tensor.data;
2675                }
2676            }
2677            "ydata" => {
2678                if let Ok(tensor) = Tensor::try_from(value) {
2679                    surface.y_data = tensor.data;
2680                }
2681            }
2682            "cdatamapping" => {
2683                if let Some(text) = value_as_string(value) {
2684                    if text.trim().eq_ignore_ascii_case("direct") {
2685                        surface.image_mode = true;
2686                    }
2687                }
2688            }
2689            _ => {}
2690        }
2691    })
2692    .map_err(|err| map_figure_error(builtin, err))?;
2693    Ok(())
2694}
2695
2696fn apply_heatmap_property(
2697    heatmap_handle: &super::state::HeatmapHandleState,
2698    key: &str,
2699    value: &Value,
2700    builtin: &'static str,
2701) -> BuiltinResult<()> {
2702    match key {
2703        "title" => apply_axes_property(
2704            heatmap_handle.figure,
2705            heatmap_handle.axes_index,
2706            "title",
2707            value,
2708            builtin,
2709        ),
2710        "xlabel" => apply_axes_property(
2711            heatmap_handle.figure,
2712            heatmap_handle.axes_index,
2713            "xlabel",
2714            value,
2715            builtin,
2716        ),
2717        "ylabel" => apply_axes_property(
2718            heatmap_handle.figure,
2719            heatmap_handle.axes_index,
2720            "ylabel",
2721            value,
2722            builtin,
2723        ),
2724        "colorbar" | "colorbarvisible" => apply_axes_property(
2725            heatmap_handle.figure,
2726            heatmap_handle.axes_index,
2727            "colorbar",
2728            value,
2729            builtin,
2730        ),
2731        "colormap" => apply_axes_property(
2732            heatmap_handle.figure,
2733            heatmap_handle.axes_index,
2734            "colormap",
2735            value,
2736            builtin,
2737        ),
2738        "xdisplaylabels" => {
2739            let labels = label_strings_from_value(value, builtin, "labels")?;
2740            if labels.len() != heatmap_handle.x_labels.len() {
2741                return Err(plotting_error(
2742                    builtin,
2743                    format!("{builtin}: XDisplayLabels length must match heatmap columns"),
2744                ));
2745            }
2746            super::state::set_heatmap_display_labels(
2747                heatmap_handle.figure,
2748                heatmap_handle.axes_index,
2749                heatmap_handle.plot_index,
2750                Some(labels),
2751                None,
2752            )
2753            .map_err(|err| map_figure_error(builtin, err))
2754        }
2755        "ydisplaylabels" => {
2756            let labels = label_strings_from_value(value, builtin, "labels")?;
2757            if labels.len() != heatmap_handle.y_labels.len() {
2758                return Err(plotting_error(
2759                    builtin,
2760                    format!("{builtin}: YDisplayLabels length must match heatmap rows"),
2761                ));
2762            }
2763            super::state::set_heatmap_display_labels(
2764                heatmap_handle.figure,
2765                heatmap_handle.axes_index,
2766                heatmap_handle.plot_index,
2767                None,
2768                Some(labels),
2769            )
2770            .map_err(|err| map_figure_error(builtin, err))
2771        }
2772        other => Err(plotting_error(
2773            builtin,
2774            format!("{builtin}: unsupported heatmap property `{other}`"),
2775        )),
2776    }
2777}
2778
2779fn apply_area_property(
2780    area_handle: &super::state::AreaHandleState,
2781    key: &str,
2782    value: &Value,
2783    builtin: &'static str,
2784) -> BuiltinResult<()> {
2785    super::state::update_area_plot(
2786        area_handle.figure,
2787        area_handle.plot_index,
2788        |area| match key {
2789            "color" => {
2790                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2791                    area.color = c;
2792                }
2793            }
2794            "basevalue" => {
2795                if let Some(v) = value_as_f64(value) {
2796                    area.baseline = v;
2797                    area.lower_y = None;
2798                }
2799            }
2800            _ => {}
2801        },
2802    )
2803    .map_err(|err| map_figure_error(builtin, err))?;
2804    Ok(())
2805}
2806
2807fn apply_line_property(
2808    line_handle: &super::state::SimplePlotHandleState,
2809    key: &str,
2810    value: &Value,
2811    builtin: &'static str,
2812) -> BuiltinResult<()> {
2813    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
2814        if let runmat_plot::plots::figure::PlotElement::Line(line) = plot {
2815            apply_line_plot_properties(line, key, value, builtin);
2816        }
2817    })
2818    .map_err(|err| map_figure_error(builtin, err))?;
2819    Ok(())
2820}
2821
2822fn apply_reference_line_property(
2823    line_handle: &super::state::SimplePlotHandleState,
2824    key: &str,
2825    value: &Value,
2826    builtin: &'static str,
2827) -> BuiltinResult<()> {
2828    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
2829        if let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot {
2830            match key {
2831                "value" => {
2832                    if let Some(v) = value_as_f64(value) {
2833                        if v.is_finite() {
2834                            line.value = v;
2835                        }
2836                    }
2837                }
2838                "color" => {
2839                    if let Ok(c) =
2840                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2841                    {
2842                        line.color = c;
2843                    }
2844                }
2845                "linewidth" => {
2846                    if let Some(v) = value_as_f64(value) {
2847                        if v > 0.0 {
2848                            line.line_width = v as f32;
2849                        }
2850                    }
2851                }
2852                "linestyle" => {
2853                    if let Some(s) = value_as_string(value) {
2854                        line.line_style = parse_line_style_name_for_props(&s);
2855                    }
2856                }
2857                "label" => {
2858                    line.label = value_as_string(value).map(|s| s.to_string());
2859                }
2860                "labelorientation" => {
2861                    if let Some(s) = value_as_string(value) {
2862                        line.label_orientation = s.to_ascii_lowercase();
2863                    }
2864                }
2865                "displayname" => {
2866                    line.display_name = value_as_string(value).map(|s| s.to_string());
2867                }
2868                "visible" => {
2869                    if let Some(v) = value_as_bool(value) {
2870                        line.visible = v;
2871                    } else if let Some(s) = value_as_string(value) {
2872                        line.visible =
2873                            !matches!(s.trim().to_ascii_lowercase().as_str(), "off" | "false");
2874                    }
2875                }
2876                _ => {}
2877            }
2878        }
2879    })
2880    .map_err(|err| map_figure_error(builtin, err))?;
2881    Ok(())
2882}
2883
2884fn apply_stairs_property(
2885    stairs_handle: &super::state::SimplePlotHandleState,
2886    key: &str,
2887    value: &Value,
2888    builtin: &'static str,
2889) -> BuiltinResult<()> {
2890    super::state::update_plot_element(stairs_handle.figure, stairs_handle.plot_index, |plot| {
2891        if let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot {
2892            match key {
2893                "color" => {
2894                    if let Ok(c) =
2895                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2896                    {
2897                        stairs.color = c;
2898                    }
2899                }
2900                "linewidth" => {
2901                    if let Some(v) = value_as_f64(value) {
2902                        stairs.line_width = v as f32;
2903                    }
2904                }
2905                "displayname" => {
2906                    stairs.label = value_as_string(value).map(|s| s.to_string());
2907                }
2908                _ => {}
2909            }
2910        }
2911    })
2912    .map_err(|err| map_figure_error(builtin, err))?;
2913    Ok(())
2914}
2915
2916fn apply_scatter_property(
2917    scatter_handle: &super::state::SimplePlotHandleState,
2918    key: &str,
2919    value: &Value,
2920    builtin: &'static str,
2921) -> BuiltinResult<()> {
2922    super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
2923        if let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot {
2924            match key {
2925                "marker" => {
2926                    if let Some(s) = value_as_string(value) {
2927                        scatter.marker_style = scatter_marker_from_name(&s, scatter.marker_style);
2928                    }
2929                }
2930                "sizedata" => {
2931                    if let Some(v) = value_as_f64(value) {
2932                        scatter.marker_size = v as f32;
2933                    }
2934                }
2935                "markerfacecolor" => {
2936                    if let Ok(c) =
2937                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2938                    {
2939                        scatter.set_face_color(c);
2940                    }
2941                }
2942                "markeredgecolor" => {
2943                    if let Ok(c) =
2944                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2945                    {
2946                        scatter.set_edge_color(c);
2947                    }
2948                }
2949                "linewidth" => {
2950                    if let Some(v) = value_as_f64(value) {
2951                        scatter.set_edge_thickness(v as f32);
2952                    }
2953                }
2954                "displayname" => {
2955                    scatter.label = value_as_string(value).map(|s| s.to_string());
2956                }
2957                _ => {}
2958            }
2959        }
2960    })
2961    .map_err(|err| map_figure_error(builtin, err))?;
2962    Ok(())
2963}
2964
2965fn apply_bar_property(
2966    bar_handle: &super::state::SimplePlotHandleState,
2967    key: &str,
2968    value: &Value,
2969    builtin: &'static str,
2970) -> BuiltinResult<()> {
2971    super::state::update_plot_element(bar_handle.figure, bar_handle.plot_index, |plot| {
2972        if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
2973            match key {
2974                "facecolor" | "color" => {
2975                    if let Ok(c) =
2976                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2977                    {
2978                        bar.color = c;
2979                    }
2980                }
2981                "barwidth" => {
2982                    if let Some(v) = value_as_f64(value) {
2983                        bar.bar_width = v as f32;
2984                    }
2985                }
2986                "displayname" => {
2987                    bar.label = value_as_string(value).map(|s| s.to_string());
2988                }
2989                _ => {}
2990            }
2991        }
2992    })
2993    .map_err(|err| map_figure_error(builtin, err))?;
2994    Ok(())
2995}
2996
2997fn apply_surface_property(
2998    surface_handle: &super::state::SimplePlotHandleState,
2999    key: &str,
3000    value: &Value,
3001    builtin: &'static str,
3002) -> BuiltinResult<()> {
3003    super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
3004        if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
3005            match key {
3006                "facealpha" => {
3007                    if let Some(v) = value_as_f64(value) {
3008                        surface.alpha = v as f32;
3009                    }
3010                }
3011                "displayname" => {
3012                    surface.label = value_as_string(value).map(|s| s.to_string());
3013                }
3014                _ => {}
3015            }
3016        }
3017    })
3018    .map_err(|err| map_figure_error(builtin, err))?;
3019    Ok(())
3020}
3021
3022fn apply_patch_property(
3023    patch_handle: &super::state::SimplePlotHandleState,
3024    key: &str,
3025    value: &Value,
3026    builtin: &'static str,
3027) -> BuiltinResult<()> {
3028    super::state::update_plot_element(patch_handle.figure, patch_handle.plot_index, |plot| {
3029        if let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot {
3030            match key {
3031                "facecolor" | "color" => {
3032                    if let Some(text) = value_as_string(value) {
3033                        match text.trim().to_ascii_lowercase().as_str() {
3034                            "none" => patch
3035                                .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::None),
3036                            "flat" => patch
3037                                .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Flat),
3038                            _ => {
3039                                if let Ok(c) = parse_color_value(
3040                                    &LineStyleParseOptions::generic(builtin),
3041                                    value,
3042                                ) {
3043                                    patch.set_face_color(c);
3044                                    patch.set_face_color_mode(
3045                                        runmat_plot::plots::PatchFaceColorMode::Color,
3046                                    );
3047                                }
3048                            }
3049                        }
3050                    } else if let Ok(c) =
3051                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
3052                    {
3053                        patch.set_face_color(c);
3054                        patch.set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Color);
3055                    }
3056                }
3057                "edgecolor" => {
3058                    if let Some(text) = value_as_string(value) {
3059                        if text.trim().eq_ignore_ascii_case("none") {
3060                            patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::None);
3061                        } else if let Ok(c) =
3062                            parse_color_value(&LineStyleParseOptions::generic(builtin), value)
3063                        {
3064                            patch.set_edge_color(c);
3065                            patch
3066                                .set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
3067                        }
3068                    } else if let Ok(c) =
3069                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
3070                    {
3071                        patch.set_edge_color(c);
3072                        patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
3073                    }
3074                }
3075                "facealpha" => {
3076                    if let Some(v) = value_as_f64(value) {
3077                        patch.set_face_alpha(v as f32);
3078                    }
3079                }
3080                "edgealpha" => {
3081                    if let Some(v) = value_as_f64(value) {
3082                        patch.set_edge_alpha(v as f32);
3083                    }
3084                }
3085                "linewidth" => {
3086                    if let Some(v) = value_as_f64(value) {
3087                        patch.set_line_width(v as f32);
3088                    }
3089                }
3090                "displayname" => {
3091                    patch.set_label(value_as_string(value).map(|s| s.to_string()));
3092                }
3093                "visible" => {
3094                    if let Some(v) = value_as_bool(value) {
3095                        patch.set_visible(v);
3096                    }
3097                }
3098                _ => {}
3099            }
3100        }
3101    })
3102    .map_err(|err| map_figure_error(builtin, err))?;
3103    Ok(())
3104}
3105
3106fn apply_line3_property(
3107    line_handle: &super::state::SimplePlotHandleState,
3108    key: &str,
3109    value: &Value,
3110    builtin: &'static str,
3111) -> BuiltinResult<()> {
3112    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
3113        if let runmat_plot::plots::figure::PlotElement::Line3(line) = plot {
3114            match key {
3115                "color" => {
3116                    if let Ok(c) =
3117                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
3118                    {
3119                        line.color = c;
3120                    }
3121                }
3122                "linewidth" => {
3123                    if let Some(v) = value_as_f64(value) {
3124                        line.line_width = v as f32;
3125                    }
3126                }
3127                "linestyle" => {
3128                    if let Some(s) = value_as_string(value) {
3129                        line.line_style = parse_line_style_name_for_props(&s);
3130                    }
3131                }
3132                "displayname" => {
3133                    line.label = value_as_string(value).map(|s| s.to_string());
3134                }
3135                _ => {}
3136            }
3137        }
3138    })
3139    .map_err(|err| map_figure_error(builtin, err))?;
3140    Ok(())
3141}
3142
3143fn apply_scatter3_property(
3144    scatter_handle: &super::state::SimplePlotHandleState,
3145    key: &str,
3146    value: &Value,
3147    builtin: &'static str,
3148) -> BuiltinResult<()> {
3149    super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
3150        if let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot {
3151            match key {
3152                "sizedata" => {
3153                    if let Some(v) = value_as_f64(value) {
3154                        scatter.point_size = v as f32;
3155                    }
3156                }
3157                "displayname" => {
3158                    scatter.label = value_as_string(value).map(|s| s.to_string());
3159                }
3160                _ => {}
3161            }
3162        }
3163    })
3164    .map_err(|err| map_figure_error(builtin, err))?;
3165    Ok(())
3166}
3167
3168fn apply_pie_property(
3169    pie_handle: &super::state::SimplePlotHandleState,
3170    key: &str,
3171    value: &Value,
3172    builtin: &'static str,
3173) -> BuiltinResult<()> {
3174    super::state::update_plot_element(pie_handle.figure, pie_handle.plot_index, |plot| {
3175        if let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot {
3176            if key == "displayname" {
3177                pie.label = value_as_string(value).map(|s| s.to_string());
3178            }
3179        }
3180    })
3181    .map_err(|err| map_figure_error(builtin, err))?;
3182    Ok(())
3183}
3184
3185fn apply_contour_property(
3186    contour_handle: &super::state::SimplePlotHandleState,
3187    key: &str,
3188    value: &Value,
3189    builtin: &'static str,
3190) -> BuiltinResult<()> {
3191    super::state::update_plot_element(contour_handle.figure, contour_handle.plot_index, |plot| {
3192        if let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot {
3193            if key == "displayname" {
3194                contour.label = value_as_string(value).map(|s| s.to_string());
3195            }
3196        }
3197    })
3198    .map_err(|err| map_figure_error(builtin, err))?;
3199    Ok(())
3200}
3201
3202fn apply_contour_fill_property(
3203    fill_handle: &super::state::SimplePlotHandleState,
3204    key: &str,
3205    value: &Value,
3206    builtin: &'static str,
3207) -> BuiltinResult<()> {
3208    super::state::update_plot_element(fill_handle.figure, fill_handle.plot_index, |plot| {
3209        if let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot {
3210            if key == "displayname" {
3211                fill.label = value_as_string(value).map(|s| s.to_string());
3212            }
3213        }
3214    })
3215    .map_err(|err| map_figure_error(builtin, err))?;
3216    Ok(())
3217}
3218
3219fn apply_line_plot_properties(
3220    line: &mut runmat_plot::plots::LinePlot,
3221    key: &str,
3222    value: &Value,
3223    builtin: &'static str,
3224) {
3225    match key {
3226        "color" => {
3227            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
3228                line.color = c;
3229            }
3230        }
3231        "linewidth" => {
3232            if let Some(v) = value_as_f64(value) {
3233                line.line_width = v as f32;
3234            }
3235        }
3236        "linestyle" => {
3237            if let Some(s) = value_as_string(value) {
3238                line.line_style = parse_line_style_name_for_props(&s);
3239            }
3240        }
3241        "displayname" => {
3242            line.label = value_as_string(value).map(|s| s.to_string());
3243        }
3244        "marker" => {
3245            if let Some(s) = value_as_string(value) {
3246                line.marker = marker_from_name(&s, line.marker.clone());
3247            }
3248        }
3249        "markersize" => {
3250            if let Some(v) = value_as_f64(value) {
3251                if let Some(marker) = &mut line.marker {
3252                    marker.size = v as f32;
3253                }
3254            }
3255        }
3256        "markerfacecolor" => {
3257            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
3258                if let Some(marker) = &mut line.marker {
3259                    marker.face_color = c;
3260                }
3261            }
3262        }
3263        "markeredgecolor" => {
3264            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
3265                if let Some(marker) = &mut line.marker {
3266                    marker.edge_color = c;
3267                }
3268            }
3269        }
3270        "filled" => {
3271            if let Some(v) = value_as_bool(value) {
3272                if let Some(marker) = &mut line.marker {
3273                    marker.filled = v;
3274                }
3275            }
3276        }
3277        _ => {}
3278    }
3279}
3280
3281fn limits_from_optional_value(
3282    value: &Value,
3283    builtin: &'static str,
3284) -> BuiltinResult<Option<(f64, f64)>> {
3285    if let Some(text) = value_as_string(value) {
3286        let norm = text.trim().to_ascii_lowercase();
3287        if matches!(norm.as_str(), "auto" | "tight") {
3288            return Ok(None);
3289        }
3290    }
3291    Ok(Some(
3292        crate::builtins::plotting::op_common::limits::limits_from_value(value, builtin)?,
3293    ))
3294}
3295
3296fn parse_colormap_name(
3297    name: &str,
3298    builtin: &'static str,
3299) -> BuiltinResult<runmat_plot::plots::surface::ColorMap> {
3300    match name.trim().to_ascii_lowercase().as_str() {
3301        "parula" => Ok(runmat_plot::plots::surface::ColorMap::Parula),
3302        "viridis" => Ok(runmat_plot::plots::surface::ColorMap::Viridis),
3303        "plasma" => Ok(runmat_plot::plots::surface::ColorMap::Plasma),
3304        "inferno" => Ok(runmat_plot::plots::surface::ColorMap::Inferno),
3305        "magma" => Ok(runmat_plot::plots::surface::ColorMap::Magma),
3306        "turbo" => Ok(runmat_plot::plots::surface::ColorMap::Turbo),
3307        "jet" => Ok(runmat_plot::plots::surface::ColorMap::Jet),
3308        "hot" => Ok(runmat_plot::plots::surface::ColorMap::Hot),
3309        "cool" => Ok(runmat_plot::plots::surface::ColorMap::Cool),
3310        "spring" => Ok(runmat_plot::plots::surface::ColorMap::Spring),
3311        "summer" => Ok(runmat_plot::plots::surface::ColorMap::Summer),
3312        "autumn" => Ok(runmat_plot::plots::surface::ColorMap::Autumn),
3313        "winter" => Ok(runmat_plot::plots::surface::ColorMap::Winter),
3314        "gray" | "grey" => Ok(runmat_plot::plots::surface::ColorMap::Gray),
3315        "bone" => Ok(runmat_plot::plots::surface::ColorMap::Bone),
3316        "copper" => Ok(runmat_plot::plots::surface::ColorMap::Copper),
3317        "pink" => Ok(runmat_plot::plots::surface::ColorMap::Pink),
3318        "lines" => Ok(runmat_plot::plots::surface::ColorMap::Lines),
3319        other => Err(plotting_error(
3320            builtin,
3321            format!("{builtin}: unknown colormap '{other}'"),
3322        )),
3323    }
3324}
3325
3326fn apply_axes_text_alias(
3327    handle: FigureHandle,
3328    axes_index: usize,
3329    kind: PlotObjectKind,
3330    value: &Value,
3331    builtin: &'static str,
3332) -> BuiltinResult<()> {
3333    if let Some(text) = value_as_string(value) {
3334        set_text_properties_for_axes(handle, axes_index, kind, Some(text), None)
3335            .map_err(|err| map_figure_error(builtin, err))?;
3336        return Ok(());
3337    }
3338
3339    let scalar = handle_scalar(value, builtin)?;
3340    let (src_handle, src_axes, src_kind) =
3341        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3342    if src_kind != kind {
3343        return Err(plotting_error(
3344            builtin,
3345            format!(
3346                "{builtin}: expected a matching text handle for `{}`",
3347                key_name(kind)
3348            ),
3349        ));
3350    }
3351    let meta = axes_metadata_snapshot(src_handle, src_axes)
3352        .map_err(|err| map_figure_error(builtin, err))?;
3353    let (text, style) = match kind {
3354        PlotObjectKind::Title => (meta.title, meta.title_style),
3355        PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
3356        PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
3357        PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
3358        PlotObjectKind::Legend => unreachable!(),
3359        PlotObjectKind::SuperTitle => unreachable!(),
3360    };
3361    set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
3362        .map_err(|err| map_figure_error(builtin, err))?;
3363    Ok(())
3364}
3365
3366fn validate_axes_text_alias(
3367    kind: PlotObjectKind,
3368    value: &Value,
3369    builtin: &'static str,
3370) -> BuiltinResult<()> {
3371    if value_as_string(value).is_some() {
3372        return Ok(());
3373    }
3374
3375    let scalar = handle_scalar(value, builtin)?;
3376    let (src_handle, src_axes, src_kind) =
3377        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3378    if src_kind != kind {
3379        return Err(plotting_error(
3380            builtin,
3381            format!(
3382                "{builtin}: expected a matching text handle for `{}`",
3383                key_name(kind)
3384            ),
3385        ));
3386    }
3387    axes_metadata_snapshot(src_handle, src_axes).map_err(|err| map_figure_error(builtin, err))?;
3388    Ok(())
3389}
3390
3391fn apply_figure_text_alias(
3392    handle: FigureHandle,
3393    kind: PlotObjectKind,
3394    value: &Value,
3395    builtin: &'static str,
3396) -> BuiltinResult<()> {
3397    if let Some(text) = value_as_text_string(value) {
3398        match kind {
3399            PlotObjectKind::SuperTitle => {
3400                set_sg_title_properties_for_figure(handle, Some(text), None)
3401                    .map_err(|err| map_figure_error(builtin, err))?;
3402            }
3403            _ => unreachable!(),
3404        }
3405        return Ok(());
3406    }
3407
3408    let scalar = handle_scalar(value, builtin)?;
3409    let (src_handle, _src_axes, src_kind) =
3410        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3411    if src_kind != kind {
3412        return Err(plotting_error(
3413            builtin,
3414            format!(
3415                "{builtin}: expected a matching text handle for `{}`",
3416                key_name(kind)
3417            ),
3418        ));
3419    }
3420
3421    let figure = super::state::clone_figure(src_handle)
3422        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
3423    let (text, style) = match kind {
3424        PlotObjectKind::SuperTitle => (figure.sg_title, figure.sg_title_style),
3425        _ => unreachable!(),
3426    };
3427    set_sg_title_properties_for_figure(handle, text, Some(style))
3428        .map_err(|err| map_figure_error(builtin, err))?;
3429    Ok(())
3430}
3431
3432fn collect_label_strings(builtin: &'static str, args: &[Value]) -> BuiltinResult<Vec<String>> {
3433    let mut labels = Vec::new();
3434    for arg in args {
3435        match arg {
3436            Value::StringArray(arr) => labels.extend(arr.data.iter().cloned()),
3437            Value::Cell(cell) => {
3438                for row in 0..cell.rows {
3439                    for col in 0..cell.cols {
3440                        let value = cell.get(row, col).map_err(|err| {
3441                            plotting_error(builtin, format!("legend: invalid label cell: {err}"))
3442                        })?;
3443                        labels.push(value_as_string(&value).ok_or_else(|| {
3444                            plotting_error(builtin, "legend: labels must be strings or char arrays")
3445                        })?);
3446                    }
3447                }
3448            }
3449            _ => labels.push(value_as_string(arg).ok_or_else(|| {
3450                plotting_error(builtin, "legend: labels must be strings or char arrays")
3451            })?),
3452        }
3453    }
3454    Ok(labels)
3455}
3456
3457fn handle_scalar(value: &Value, builtin: &'static str) -> BuiltinResult<f64> {
3458    match value {
3459        Value::Num(v) => Ok(*v),
3460        Value::Int(i) => Ok(i.to_f64()),
3461        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
3462        _ => Err(plotting_error(
3463            builtin,
3464            format!("{builtin}: expected plotting handle"),
3465        )),
3466    }
3467}
3468
3469fn legend_labels_value(labels: Vec<String>) -> Value {
3470    Value::StringArray(StringArray {
3471        rows: 1,
3472        cols: labels.len().max(1),
3473        shape: vec![1, labels.len().max(1)],
3474        data: labels,
3475    })
3476}
3477
3478fn text_value(text: Option<String>) -> Value {
3479    match text {
3480        Some(text) if text.contains('\n') => {
3481            let lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
3482            Value::StringArray(StringArray {
3483                rows: 1,
3484                cols: lines.len().max(1),
3485                shape: vec![1, lines.len().max(1)],
3486                data: lines,
3487            })
3488        }
3489        Some(text) => Value::String(text),
3490        None => Value::String(String::new()),
3491    }
3492}
3493
3494fn handles_value(handles: Vec<f64>) -> Value {
3495    Value::Tensor(runmat_builtins::Tensor {
3496        rows: 1,
3497        cols: handles.len(),
3498        shape: vec![1, handles.len()],
3499        data: handles,
3500        dtype: runmat_builtins::NumericDType::F64,
3501    })
3502}
3503
3504fn tensor_from_vec(data: Vec<f64>) -> Value {
3505    Value::Tensor(runmat_builtins::Tensor {
3506        rows: 1,
3507        cols: data.len(),
3508        shape: vec![1, data.len()],
3509        data,
3510        dtype: runmat_builtins::NumericDType::F64,
3511    })
3512}
3513
3514fn string_array_from_vec(data: Vec<String>) -> BuiltinResult<Value> {
3515    let cols = data.len();
3516    let array = StringArray::new(data, vec![1, cols])
3517        .map_err(|e| plotting_error("get", format!("get: {e}")))?;
3518    Ok(Value::StringArray(array))
3519}
3520
3521pub(crate) fn label_strings_from_value(
3522    value: &Value,
3523    builtin: &'static str,
3524    label_context: &str,
3525) -> BuiltinResult<Vec<String>> {
3526    match value {
3527        Value::StringArray(array) => Ok(array.data.clone()),
3528        Value::Cell(cell) => cell
3529            .data
3530            .iter()
3531            .map(|item| {
3532                value_as_text_string(item).ok_or_else(|| {
3533                    plotting_error(
3534                        builtin,
3535                        format!("{builtin}: {label_context} must contain text values"),
3536                    )
3537                })
3538            })
3539            .collect(),
3540        Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
3541        Value::String(text) => Ok(vec![text.clone()]),
3542        Value::Tensor(tensor) => Ok(tensor.data.iter().map(|v| v.to_string()).collect()),
3543        Value::Int(i) => Ok(vec![i.to_i64().to_string()]),
3544        Value::Num(v) => Ok(vec![v.to_string()]),
3545        other => Err(plotting_error(
3546            builtin,
3547            format!("{builtin}: unsupported {label_context} value {other:?}"),
3548        )),
3549    }
3550}
3551
3552fn tensor_from_matrix(data: Vec<Vec<f64>>) -> Value {
3553    let rows = data.len();
3554    let cols = data.first().map(|row| row.len()).unwrap_or(0);
3555    let flat = data.into_iter().flat_map(|row| row.into_iter()).collect();
3556    Value::Tensor(runmat_builtins::Tensor {
3557        rows,
3558        cols,
3559        shape: vec![rows, cols],
3560        data: flat,
3561        dtype: runmat_builtins::NumericDType::F64,
3562    })
3563}
3564
3565fn vertices_tensor(vertices: &[glam::Vec3]) -> Value {
3566    let rows = vertices.len();
3567    let cols = 3;
3568    let mut data = Vec::with_capacity(rows * cols);
3569    for col in 0..cols {
3570        for vertex in vertices {
3571            data.push(match col {
3572                0 => vertex.x as f64,
3573                1 => vertex.y as f64,
3574                _ => vertex.z as f64,
3575            });
3576        }
3577    }
3578    Value::Tensor(runmat_builtins::Tensor {
3579        rows,
3580        cols,
3581        shape: vec![rows, cols],
3582        data,
3583        dtype: runmat_builtins::NumericDType::F64,
3584    })
3585}
3586
3587fn faces_tensor(faces: &[Vec<usize>]) -> Value {
3588    let rows = faces.len();
3589    let cols = faces.iter().map(|face| face.len()).max().unwrap_or(0);
3590    let mut data = Vec::with_capacity(rows * cols);
3591    for col in 0..cols {
3592        for face in faces {
3593            data.push(
3594                face.get(col)
3595                    .map(|idx| *idx as f64 + 1.0)
3596                    .unwrap_or(f64::NAN),
3597            );
3598        }
3599    }
3600    Value::Tensor(runmat_builtins::Tensor {
3601        rows,
3602        cols,
3603        shape: vec![rows, cols],
3604        data,
3605        dtype: runmat_builtins::NumericDType::F64,
3606    })
3607}
3608
3609fn patch_color_property(mode: runmat_plot::plots::PatchFaceColorMode, color: glam::Vec4) -> Value {
3610    match mode {
3611        runmat_plot::plots::PatchFaceColorMode::None => Value::String("none".into()),
3612        runmat_plot::plots::PatchFaceColorMode::Flat => Value::String("flat".into()),
3613        runmat_plot::plots::PatchFaceColorMode::Color => Value::String(color_to_short_name(color)),
3614    }
3615}
3616
3617fn patch_edge_color_property(
3618    mode: runmat_plot::plots::PatchEdgeColorMode,
3619    color: glam::Vec4,
3620) -> Value {
3621    match mode {
3622        runmat_plot::plots::PatchEdgeColorMode::None => Value::String("none".into()),
3623        runmat_plot::plots::PatchEdgeColorMode::Color => Value::String(color_to_short_name(color)),
3624    }
3625}
3626
3627fn insert_line_marker_struct_props(
3628    st: &mut StructValue,
3629    marker: Option<&runmat_plot::plots::line::LineMarkerAppearance>,
3630) {
3631    if let Some(marker) = marker {
3632        st.insert(
3633            "Marker",
3634            Value::String(marker_style_name(marker.kind).into()),
3635        );
3636        st.insert("MarkerSize", Value::Num(marker.size as f64));
3637        st.insert(
3638            "MarkerFaceColor",
3639            Value::String(color_to_short_name(marker.face_color)),
3640        );
3641        st.insert(
3642            "MarkerEdgeColor",
3643            Value::String(color_to_short_name(marker.edge_color)),
3644        );
3645        st.insert("Filled", Value::Bool(marker.filled));
3646    }
3647}
3648
3649fn line_marker_property_value(
3650    marker: &Option<runmat_plot::plots::line::LineMarkerAppearance>,
3651    name: &str,
3652    builtin: &'static str,
3653) -> BuiltinResult<Value> {
3654    match name {
3655        "marker" => Ok(Value::String(
3656            marker
3657                .as_ref()
3658                .map(|m| marker_style_name(m.kind).to_string())
3659                .unwrap_or_else(|| "none".into()),
3660        )),
3661        "markersize" => Ok(Value::Num(
3662            marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
3663        )),
3664        "markerfacecolor" => Ok(Value::String(
3665            marker
3666                .as_ref()
3667                .map(|m| color_to_short_name(m.face_color))
3668                .unwrap_or_else(|| "none".into()),
3669        )),
3670        "markeredgecolor" => Ok(Value::String(
3671            marker
3672                .as_ref()
3673                .map(|m| color_to_short_name(m.edge_color))
3674                .unwrap_or_else(|| "none".into()),
3675        )),
3676        "filled" => Ok(Value::Bool(
3677            marker.as_ref().map(|m| m.filled).unwrap_or(false),
3678        )),
3679        other => Err(plotting_error(
3680            builtin,
3681            format!("{builtin}: unsupported line property `{other}`"),
3682        )),
3683    }
3684}
3685
3686fn histogram_labels_from_edges(edges: &[f64]) -> Vec<String> {
3687    edges
3688        .windows(2)
3689        .map(|pair| format!("[{:.3}, {:.3})", pair[0], pair[1]))
3690        .collect()
3691}
3692
3693fn validate_histogram_normalization(norm: &str, builtin: &'static str) -> BuiltinResult<()> {
3694    match norm {
3695        "count" | "probability" | "countdensity" | "pdf" | "cumcount" | "cdf" => Ok(()),
3696        other => Err(plotting_error(
3697            builtin,
3698            format!("{builtin}: unsupported histogram normalization `{other}`"),
3699        )),
3700    }
3701}
3702
3703fn apply_histogram_normalization(raw_counts: &[f64], edges: &[f64], norm: &str) -> Vec<f64> {
3704    let widths: Vec<f64> = edges.windows(2).map(|pair| pair[1] - pair[0]).collect();
3705    let total: f64 = raw_counts.iter().sum();
3706    match norm {
3707        "count" => raw_counts.to_vec(),
3708        "probability" => {
3709            if total > 0.0 {
3710                raw_counts.iter().map(|&c| c / total).collect()
3711            } else {
3712                vec![0.0; raw_counts.len()]
3713            }
3714        }
3715        "countdensity" => raw_counts
3716            .iter()
3717            .zip(widths.iter())
3718            .map(|(&c, &w)| if w > 0.0 { c / w } else { 0.0 })
3719            .collect(),
3720        "pdf" => {
3721            if total > 0.0 {
3722                raw_counts
3723                    .iter()
3724                    .zip(widths.iter())
3725                    .map(|(&c, &w)| if w > 0.0 { c / (total * w) } else { 0.0 })
3726                    .collect()
3727            } else {
3728                vec![0.0; raw_counts.len()]
3729            }
3730        }
3731        "cumcount" => {
3732            let mut acc = 0.0;
3733            raw_counts
3734                .iter()
3735                .map(|&c| {
3736                    acc += c;
3737                    acc
3738                })
3739                .collect()
3740        }
3741        "cdf" => {
3742            if total > 0.0 {
3743                let mut acc = 0.0;
3744                raw_counts
3745                    .iter()
3746                    .map(|&c| {
3747                        acc += c;
3748                        acc / total
3749                    })
3750                    .collect()
3751            } else {
3752                vec![0.0; raw_counts.len()]
3753            }
3754        }
3755        _ => raw_counts.to_vec(),
3756    }
3757}
3758
3759fn line_style_name(style: runmat_plot::plots::line::LineStyle) -> &'static str {
3760    match style {
3761        runmat_plot::plots::line::LineStyle::Solid => "-",
3762        runmat_plot::plots::line::LineStyle::Dashed => "--",
3763        runmat_plot::plots::line::LineStyle::Dotted => ":",
3764        runmat_plot::plots::line::LineStyle::DashDot => "-.",
3765    }
3766}
3767
3768fn parse_line_style_name_for_props(name: &str) -> runmat_plot::plots::line::LineStyle {
3769    match name.trim() {
3770        "--" | "dashed" => runmat_plot::plots::line::LineStyle::Dashed,
3771        ":" | "dotted" => runmat_plot::plots::line::LineStyle::Dotted,
3772        "-." | "dashdot" => runmat_plot::plots::line::LineStyle::DashDot,
3773        _ => runmat_plot::plots::line::LineStyle::Solid,
3774    }
3775}
3776
3777fn marker_style_name(style: runmat_plot::plots::scatter::MarkerStyle) -> &'static str {
3778    match style {
3779        runmat_plot::plots::scatter::MarkerStyle::Circle => "o",
3780        runmat_plot::plots::scatter::MarkerStyle::Square => "s",
3781        runmat_plot::plots::scatter::MarkerStyle::Triangle => "^",
3782        runmat_plot::plots::scatter::MarkerStyle::Diamond => "d",
3783        runmat_plot::plots::scatter::MarkerStyle::Plus => "+",
3784        runmat_plot::plots::scatter::MarkerStyle::Cross => "x",
3785        runmat_plot::plots::scatter::MarkerStyle::Star => "*",
3786        runmat_plot::plots::scatter::MarkerStyle::Hexagon => "h",
3787    }
3788}
3789
3790fn marker_from_name(
3791    name: &str,
3792    current: Option<runmat_plot::plots::line::LineMarkerAppearance>,
3793) -> Option<runmat_plot::plots::line::LineMarkerAppearance> {
3794    let mut marker = current.unwrap_or(runmat_plot::plots::line::LineMarkerAppearance {
3795        kind: runmat_plot::plots::scatter::MarkerStyle::Circle,
3796        size: 6.0,
3797        edge_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
3798        face_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
3799        filled: false,
3800    });
3801    marker.kind = match name.trim() {
3802        "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
3803        "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
3804        "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
3805        "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
3806        "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
3807        "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
3808        "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
3809        "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
3810        "none" => return None,
3811        _ => marker.kind,
3812    };
3813    Some(marker)
3814}
3815
3816fn scatter_marker_from_name(
3817    name: &str,
3818    current: runmat_plot::plots::scatter::MarkerStyle,
3819) -> runmat_plot::plots::scatter::MarkerStyle {
3820    match name.trim() {
3821        "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
3822        "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
3823        "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
3824        "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
3825        "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
3826        "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
3827        "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
3828        "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
3829        _ => current,
3830    }
3831}
3832
3833trait Unzip3Vec<A, B, C> {
3834    fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>);
3835}
3836
3837impl<I, A, B, C> Unzip3Vec<A, B, C> for I
3838where
3839    I: Iterator<Item = (A, B, C)>,
3840{
3841    fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>) {
3842        let mut a = Vec::new();
3843        let mut b = Vec::new();
3844        let mut c = Vec::new();
3845        for (va, vb, vc) in self {
3846            a.push(va);
3847            b.push(vb);
3848            c.push(vc);
3849        }
3850        (a, b, c)
3851    }
3852}
3853
3854fn color_to_short_name(color: glam::Vec4) -> String {
3855    let candidates = [
3856        (glam::Vec4::new(1.0, 0.0, 0.0, 1.0), "r"),
3857        (glam::Vec4::new(0.0, 1.0, 0.0, 1.0), "g"),
3858        (glam::Vec4::new(0.0, 0.0, 1.0, 1.0), "b"),
3859        (glam::Vec4::new(0.0, 0.0, 0.0, 1.0), "k"),
3860        (glam::Vec4::new(1.0, 1.0, 1.0, 1.0), "w"),
3861        (glam::Vec4::new(1.0, 1.0, 0.0, 1.0), "y"),
3862        (glam::Vec4::new(1.0, 0.0, 1.0, 1.0), "m"),
3863        (glam::Vec4::new(0.0, 1.0, 1.0, 1.0), "c"),
3864    ];
3865    for (candidate, name) in candidates {
3866        if (candidate - color).abs().max_element() < 1e-6 {
3867            return name.to_string();
3868        }
3869    }
3870    format!("[{:.3},{:.3},{:.3}]", color.x, color.y, color.z)
3871}
3872
3873fn key_name(kind: PlotObjectKind) -> &'static str {
3874    match kind {
3875        PlotObjectKind::Title => "Title",
3876        PlotObjectKind::XLabel => "XLabel",
3877        PlotObjectKind::YLabel => "YLabel",
3878        PlotObjectKind::ZLabel => "ZLabel",
3879        PlotObjectKind::Legend => "Legend",
3880        PlotObjectKind::SuperTitle => "SGTitle",
3881    }
3882}
3883
3884trait AxesMetadataExt {
3885    fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle;
3886}
3887
3888impl AxesMetadataExt for runmat_plot::plots::AxesMetadata {
3889    fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle {
3890        match kind {
3891            PlotObjectKind::Title => self.title_style.clone(),
3892            PlotObjectKind::XLabel => self.x_label_style.clone(),
3893            PlotObjectKind::YLabel => self.y_label_style.clone(),
3894            PlotObjectKind::ZLabel => self.z_label_style.clone(),
3895            PlotObjectKind::Legend => TextStyle::default(),
3896            PlotObjectKind::SuperTitle => TextStyle::default(),
3897        }
3898    }
3899}