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