Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

1use runmat_builtins::{CellArray, CharArray, StringArray, StructValue, Tensor, Value};
2use runmat_plot::plots::{
3    ColorMap, LegendStyle, PolarHistogramDisplayStyle, ShadingMode, TextStyle,
4};
5use std::borrow::Cow;
6
7use super::point::{marker_area_points2_to_diameter_px, marker_diameter_px_to_area_points2};
8use super::state::{
9    axes_handle_exists, axes_handles_for_figure, axes_metadata_snapshot, axes_state_snapshot,
10    axis_display_bounds_snapshot_for_axes, current_axes_handle_for_figure,
11    current_figure_handle_if_exists, decode_axes_handle, decode_plot_object_handle,
12    figure_handle_exists, figure_has_sg_title, figure_tag, legend_entries_snapshot,
13    present_figure_update, root_default_properties, root_default_property, root_figure_handles,
14    root_show_hidden_handles, root_units, select_axes_for_figure, select_current_figure_if_exists,
15    set_axes_position_for_axes, set_axes_style_for_axes, set_axes_units_for_axes,
16    set_axis_tick_angles_for_axes, set_axis_tick_formats_for_axes, set_axis_tick_labels_for_axes,
17    set_axis_ticks_for_axes, set_figure_background_color, set_figure_name, set_figure_number_title,
18    set_figure_position, set_figure_tag, set_figure_visible, set_legend_for_axes,
19    set_root_default_property, set_root_show_hidden_handles, set_root_units,
20    set_sg_title_properties_for_figure, set_text_annotation_properties_for_axes,
21    set_text_properties_for_axes, FigureHandle, PlotObjectKind, RootPropertyValue,
22};
23use super::style::{
24    color_from_name_or_token, parse_color_value, parse_handle_visibility, value_as_bool,
25    value_as_f64, value_as_string, LineStyleParseOptions,
26};
27use super::{plotting_error, plotting_error_with_source};
28use crate::builtins::common::tensor;
29use crate::builtins::plotting::axis_scale::scale_mode_from_value;
30use crate::builtins::plotting::histogram2;
31use crate::builtins::plotting::op_common::limits::limit_value;
32use crate::builtins::plotting::op_common::value_as_text_string;
33use crate::builtins::stats::summary::binscatter;
34use crate::BuiltinResult;
35
36const MAX_AXES_FONT_SIZE_POINTS: f64 = 512.0;
37
38#[derive(Clone, Debug)]
39pub enum PlotHandle {
40    Root,
41    Figure(FigureHandle),
42    Axes(FigureHandle, usize),
43    Ruler(FigureHandle, usize, PlotObjectKind),
44    Text(FigureHandle, usize, PlotObjectKind),
45    Legend(FigureHandle, usize),
46    PlotChild(f64, Box<super::state::PlotChildHandleState>),
47}
48
49pub fn resolve_plot_handle(value: &Value, builtin: &'static str) -> BuiltinResult<PlotHandle> {
50    let scalar = handle_scalar(value, builtin)?;
51    if scalar == 0.0 {
52        return Ok(PlotHandle::Root);
53    }
54    if !scalar.is_finite() || scalar < 0.0 {
55        return Err(plotting_error(
56            builtin,
57            format!("{builtin}: unsupported or invalid plotting handle"),
58        ));
59    }
60    if let Ok(state) = super::state::plot_child_handle_snapshot(scalar) {
61        return Ok(PlotHandle::PlotChild(scalar.round(), Box::new(state)));
62    }
63    if let Ok((handle, axes_index, kind)) = decode_plot_object_handle(scalar) {
64        if axes_handle_exists(handle, axes_index) {
65            return Ok(match kind {
66                PlotObjectKind::Legend => PlotHandle::Legend(handle, axes_index),
67                PlotObjectKind::XAxis | PlotObjectKind::YAxis => {
68                    PlotHandle::Ruler(handle, axes_index, kind)
69                }
70                _ => PlotHandle::Text(handle, axes_index, kind),
71            });
72        }
73    }
74    if let Ok((handle, axes_index)) = decode_axes_handle(scalar) {
75        if axes_handle_exists(handle, axes_index) {
76            return Ok(PlotHandle::Axes(handle, axes_index));
77        }
78        return Err(plotting_error(
79            builtin,
80            format!("{builtin}: invalid axes handle"),
81        ));
82    }
83    let figure = FigureHandle::from(scalar.round() as u32);
84    if figure_handle_exists(figure) {
85        return Ok(PlotHandle::Figure(figure));
86    }
87    Err(plotting_error(
88        builtin,
89        format!("{builtin}: unsupported or invalid plotting handle"),
90    ))
91}
92
93pub fn get_properties(
94    handle: PlotHandle,
95    property: Option<&str>,
96    builtin: &'static str,
97) -> BuiltinResult<Value> {
98    match handle {
99        PlotHandle::Root => get_root_property(property, builtin),
100        PlotHandle::Axes(handle, axes_index) => {
101            get_axes_property(handle, axes_index, property, builtin)
102        }
103        PlotHandle::Ruler(handle, axes_index, kind) => {
104            get_ruler_property(handle, axes_index, kind, property, builtin)
105        }
106        PlotHandle::Text(handle, axes_index, kind) => {
107            get_text_property(handle, axes_index, kind, property, builtin)
108        }
109        PlotHandle::Legend(handle, axes_index) => {
110            get_legend_property(handle, axes_index, property, builtin)
111        }
112        PlotHandle::Figure(handle) => get_figure_property(handle, property, builtin),
113        PlotHandle::PlotChild(_, state) => get_plot_child_property(&state, property, builtin),
114    }
115}
116
117pub fn set_properties(
118    handle: PlotHandle,
119    args: &[Value],
120    builtin: &'static str,
121) -> BuiltinResult<()> {
122    if args.is_empty() || !args.len().is_multiple_of(2) {
123        return Err(plotting_error(
124            builtin,
125            format!("{builtin}: property/value arguments must come in pairs"),
126        ));
127    }
128    match handle {
129        PlotHandle::Root => {
130            for pair in args.chunks_exact(2) {
131                let raw_key = property_name_text(&pair[0], builtin)?;
132                let key = canonical_property_name(raw_key.trim()).into_owned();
133                apply_root_property(&key, raw_key.trim(), &pair[1], builtin)?;
134            }
135            Ok(())
136        }
137        PlotHandle::Figure(handle) => {
138            let mut needs_present = false;
139            for pair in args.chunks_exact(2) {
140                validate_figure_property_value(&pair[0], &pair[1], Some(handle), builtin)?;
141            }
142            for pair in args.chunks_exact(2) {
143                let key = property_name(&pair[0], builtin)?;
144                needs_present |= apply_figure_property(handle, &key, &pair[1], builtin)?;
145            }
146            if needs_present {
147                let figure = super::state::clone_figure(handle)
148                    .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
149                let _ = present_figure_update(builtin, handle, figure)?;
150            }
151            Ok(())
152        }
153        PlotHandle::Axes(handle, axes_index) => {
154            for pair in args.chunks_exact(2) {
155                let key = property_name(&pair[0], builtin)?;
156                apply_axes_property(handle, axes_index, &key, &pair[1], builtin)?;
157            }
158            Ok(())
159        }
160        PlotHandle::Ruler(handle, axes_index, kind) => {
161            for pair in args.chunks_exact(2) {
162                let key = property_name(&pair[0], builtin)?;
163                apply_ruler_property(handle, axes_index, kind, &key, &pair[1], builtin)?;
164            }
165            Ok(())
166        }
167        PlotHandle::Text(handle, axes_index, kind) => {
168            let mut text: Option<String> = None;
169            let mut style = if matches!(kind, PlotObjectKind::SuperTitle) {
170                super::state::clone_figure(handle)
171                    .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?
172                    .sg_title_style
173            } else {
174                axes_metadata_snapshot(handle, axes_index)
175                    .map_err(|err| map_figure_error(builtin, err))?
176                    .text_style_for(kind)
177            };
178            for pair in args.chunks_exact(2) {
179                let key = property_name(&pair[0], builtin)?;
180                apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
181            }
182            if matches!(kind, PlotObjectKind::SuperTitle) {
183                set_sg_title_properties_for_figure(handle, text, Some(style))
184                    .map_err(|err| map_figure_error(builtin, err))?;
185            } else {
186                set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
187                    .map_err(|err| map_figure_error(builtin, err))?;
188            }
189            Ok(())
190        }
191        PlotHandle::Legend(handle, axes_index) => {
192            let snapshot = axes_metadata_snapshot(handle, axes_index)
193                .map_err(|err| map_figure_error(builtin, err))?;
194            let mut style = snapshot.legend_style;
195            let mut enabled = snapshot.legend_enabled;
196            let mut labels: Option<Vec<String>> = None;
197            for pair in args.chunks_exact(2) {
198                let key = property_name(&pair[0], builtin)?;
199                apply_legend_property(
200                    &mut style,
201                    &mut enabled,
202                    &mut labels,
203                    &key,
204                    &pair[1],
205                    builtin,
206                )?;
207            }
208            set_legend_for_axes(handle, axes_index, enabled, labels.as_deref(), Some(style))
209                .map_err(|err| map_figure_error(builtin, err))?;
210            Ok(())
211        }
212        PlotHandle::PlotChild(handle, state) => {
213            apply_plot_child_properties(handle, &state, args, builtin)
214        }
215    }
216}
217
218pub fn parse_text_style_pairs(builtin: &'static str, args: &[Value]) -> BuiltinResult<TextStyle> {
219    if args.is_empty() {
220        return Ok(TextStyle::default());
221    }
222    if !args.len().is_multiple_of(2) {
223        return Err(plotting_error(
224            builtin,
225            format!("{builtin}: property/value arguments must come in pairs"),
226        ));
227    }
228    let mut style = TextStyle::default();
229    let mut text = None;
230    for pair in args.chunks_exact(2) {
231        let key = property_name(&pair[0], builtin)?;
232        apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
233    }
234    Ok(style)
235}
236
237pub fn validate_heatmap_property_pairs(
238    args: &[Value],
239    x_label_len: usize,
240    y_label_len: usize,
241    builtin: &'static str,
242) -> BuiltinResult<()> {
243    if args.is_empty() {
244        return Ok(());
245    }
246    if !args.len().is_multiple_of(2) {
247        return Err(plotting_error(
248            builtin,
249            format!("{builtin}: property/value arguments must come in pairs"),
250        ));
251    }
252    for pair in args.chunks_exact(2) {
253        let key = property_name(&pair[0], builtin)?;
254        match key.as_str() {
255            "title" => validate_axes_text_alias(PlotObjectKind::Title, &pair[1], builtin)?,
256            "subtitle" => validate_axes_text_alias(PlotObjectKind::Subtitle, &pair[1], builtin)?,
257            "xlabel" => validate_axes_text_alias(PlotObjectKind::XLabel, &pair[1], builtin)?,
258            "ylabel" => validate_axes_text_alias(PlotObjectKind::YLabel, &pair[1], builtin)?,
259            "colorbar" | "colorbarvisible" => {
260                value_as_bool(&pair[1]).ok_or_else(|| {
261                    plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
262                })?;
263            }
264            "colormap" => {
265                let name = value_as_string(&pair[1]).ok_or_else(|| {
266                    plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
267                })?;
268                parse_colormap_name(&name, builtin)?;
269            }
270            "xdisplaylabels" => {
271                let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
272                if labels.len() != x_label_len {
273                    return Err(plotting_error(
274                        builtin,
275                        format!("{builtin}: XDisplayLabels length must match heatmap columns"),
276                    ));
277                }
278            }
279            "ydisplaylabels" => {
280                let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
281                if labels.len() != y_label_len {
282                    return Err(plotting_error(
283                        builtin,
284                        format!("{builtin}: YDisplayLabels length must match heatmap rows"),
285                    ));
286                }
287            }
288            other => {
289                return Err(plotting_error(
290                    builtin,
291                    format!("{builtin}: unsupported heatmap property `{other}`"),
292                ));
293            }
294        }
295    }
296    Ok(())
297}
298
299pub fn split_legend_style_pairs<'a>(
300    builtin: &'static str,
301    args: &'a [Value],
302) -> BuiltinResult<(&'a [Value], LegendStyle)> {
303    let mut style = LegendStyle::default();
304    let mut enabled = true;
305    let mut labels = None;
306    let mut split = args.len();
307    while split >= 2 {
308        let key_idx = split - 2;
309        let Ok(key) = property_name(&args[key_idx], builtin) else {
310            break;
311        };
312        if !matches!(
313            key.as_str(),
314            "location"
315                | "fontsize"
316                | "fontweight"
317                | "fontangle"
318                | "interpreter"
319                | "textcolor"
320                | "color"
321                | "visible"
322                | "string"
323                | "box"
324                | "orientation"
325        ) {
326            break;
327        }
328        apply_legend_property(
329            &mut style,
330            &mut enabled,
331            &mut labels,
332            &key,
333            &args[key_idx + 1],
334            builtin,
335        )?;
336        split -= 2;
337    }
338    Ok((&args[..split], style))
339}
340
341pub fn map_figure_error(
342    builtin: &'static str,
343    err: impl std::error::Error + Send + Sync + 'static,
344) -> crate::RuntimeError {
345    let message = format!("{builtin}: {err}");
346    plotting_error_with_source(builtin, message, err)
347}
348
349fn get_figure_property(
350    handle: FigureHandle,
351    property: Option<&str>,
352    builtin: &'static str,
353) -> BuiltinResult<Value> {
354    let figure = super::state::clone_figure(handle)
355        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
356    let axes = axes_handles_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
357    let current_axes =
358        current_axes_handle_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
359    let sg_title_handle =
360        super::state::encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle);
361    let has_sg_title = figure_has_sg_title(handle);
362    match property.map(canonical_property_name).as_deref() {
363        None => {
364            let mut st = StructValue::new();
365            st.insert("Handle", Value::Num(handle.as_u32() as f64));
366            st.insert("Number", Value::Num(handle.as_u32() as f64));
367            st.insert("Type", Value::String("figure".into()));
368            st.insert("CurrentAxes", Value::Num(current_axes));
369            let mut children = axes;
370            if has_sg_title {
371                children.push(sg_title_handle);
372            }
373            st.insert("Children", handles_value(children));
374            st.insert("Parent", Value::Num(f64::NAN));
375            st.insert(
376                "Name",
377                Value::String(figure.name.clone().unwrap_or_default()),
378            );
379            st.insert("NumberTitle", Value::Bool(figure.number_title));
380            st.insert("Visible", Value::Bool(figure.visible));
381            st.insert("Position", figure_position_value(figure.position));
382            st.insert(
383                "Color",
384                Value::String(color_to_short_name(figure.background_color)),
385            );
386            st.insert("Tag", Value::String(figure_tag(handle).unwrap_or_default()));
387            if let Some(waitbar) = super::state::waitbar_state_snapshot(handle)
388                .map_err(|err| map_figure_error(builtin, err))?
389            {
390                st.insert("WaitbarProgress", Value::Num(waitbar.progress));
391                st.insert("WaitbarMessage", Value::String(waitbar.message));
392            }
393            st.insert("SGTitle", Value::Num(sg_title_handle));
394            Ok(Value::Struct(st))
395        }
396        Some("number") => Ok(Value::Num(handle.as_u32() as f64)),
397        Some("type") => Ok(Value::String("figure".into())),
398        Some("currentaxes") => Ok(Value::Num(current_axes)),
399        Some("children") => Ok(handles_value({
400            let mut children = axes;
401            if has_sg_title {
402                children.push(sg_title_handle);
403            }
404            children
405        })),
406        Some("parent") => Ok(Value::Num(f64::NAN)),
407        Some("name") => Ok(Value::String(figure.name.unwrap_or_default())),
408        Some("numbertitle") => Ok(Value::Bool(figure.number_title)),
409        Some("visible") => Ok(Value::Bool(figure.visible)),
410        Some("position") => Ok(figure_position_value(figure.position)),
411        Some("color") => Ok(Value::String(color_to_short_name(figure.background_color))),
412        Some("waitbarprogress") => Ok(Value::Num(
413            super::state::waitbar_state_snapshot(handle)
414                .map_err(|err| map_figure_error(builtin, err))?
415                .map(|state| state.progress)
416                .unwrap_or(f64::NAN),
417        )),
418        Some("waitbarmessage") => Ok(Value::String(
419            super::state::waitbar_state_snapshot(handle)
420                .map_err(|err| map_figure_error(builtin, err))?
421                .map(|state| state.message)
422                .unwrap_or_default(),
423        )),
424        Some("tag") => Ok(Value::String(figure_tag(handle).unwrap_or_default())),
425        Some("sgtitle") => Ok(Value::Num(sg_title_handle)),
426        Some(other) => Err(plotting_error(
427            builtin,
428            format!("{builtin}: unsupported figure property `{other}`"),
429        )),
430    }
431}
432
433fn get_root_property(property: Option<&str>, builtin: &'static str) -> BuiltinResult<Value> {
434    match property.map(canonical_property_name).as_deref() {
435        None => {
436            let mut st = StructValue::new();
437            st.insert("Handle", Value::Num(0.0));
438            st.insert("Type", Value::String("root".into()));
439            st.insert("CurrentFigure", current_root_figure_value());
440            st.insert("Children", root_children_value());
441            st.insert("Parent", empty_handle_value());
442            st.insert("ScreenSize", root_screen_size_value());
443            st.insert("MonitorPositions", root_screen_size_value());
444            st.insert("Units", Value::String(root_units()));
445            st.insert(
446                "ShowHiddenHandles",
447                Value::String(on_off(root_show_hidden_handles()).into()),
448            );
449            for (name, value) in root_default_properties() {
450                st.insert(name, root_property_value_to_value(value));
451            }
452            Ok(Value::Struct(st))
453        }
454        Some("handle") => Ok(Value::Num(0.0)),
455        Some("type") => Ok(Value::String("root".into())),
456        Some("currentfigure") => Ok(current_root_figure_value()),
457        Some("children") => Ok(root_children_value()),
458        Some("parent") => Ok(empty_handle_value()),
459        Some("screensize") => Ok(root_screen_size_value()),
460        Some("monitorpositions") => Ok(root_screen_size_value()),
461        Some("units") => Ok(Value::String(root_units())),
462        Some("showhiddenhandles") => Ok(Value::String(on_off(root_show_hidden_handles()).into())),
463        Some(default) if is_root_default_property(default) => Ok(root_default_property(default)
464            .map(root_property_value_to_value)
465            .unwrap_or_else(|| Value::String(String::new()))),
466        Some(other) => Err(plotting_error(
467            builtin,
468            format!("{builtin}: unsupported root property `{other}`"),
469        )),
470    }
471}
472
473fn apply_root_property(
474    key: &str,
475    display_key: &str,
476    value: &Value,
477    builtin: &'static str,
478) -> BuiltinResult<()> {
479    match key {
480        "currentfigure" => {
481            let resolved = resolve_plot_handle(value, builtin)?;
482            let PlotHandle::Figure(handle) = resolved else {
483                return Err(plotting_error(
484                    builtin,
485                    format!("{builtin}: CurrentFigure must be a figure handle"),
486                ));
487            };
488            select_current_figure_if_exists(handle)
489                .map_err(|err| map_figure_error(builtin, err))?;
490            Ok(())
491        }
492        "units" => {
493            let units = value_as_text_string(value)
494                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Units must be text")))?
495                .trim()
496                .to_ascii_lowercase();
497            if !matches!(
498                units.as_str(),
499                "pixels" | "normalized" | "inches" | "centimeters" | "points" | "characters"
500            ) {
501                return Err(plotting_error(
502                    builtin,
503                    format!("{builtin}: unsupported root Units `{units}`"),
504                ));
505            }
506            set_root_units(units);
507            Ok(())
508        }
509        "showhiddenhandles" => {
510            let enabled = value_as_bool(value).ok_or_else(|| {
511                plotting_error(
512                    builtin,
513                    format!("{builtin}: ShowHiddenHandles must be 'on' or 'off'"),
514                )
515            })?;
516            set_root_show_hidden_handles(enabled);
517            Ok(())
518        }
519        "handle" | "type" | "children" | "parent" | "screensize" | "monitorpositions" => {
520            Err(plotting_error(
521                builtin,
522                format!("{builtin}: root property `{key}` is read-only"),
523            ))
524        }
525        default if is_root_default_property(default) => {
526            let stored = root_property_value_from_value(value, builtin)?;
527            set_root_default_property(default.to_string(), display_key.to_string(), stored);
528            Ok(())
529        }
530        other => Err(plotting_error(
531            builtin,
532            format!("{builtin}: unsupported root property `{other}`"),
533        )),
534    }
535}
536
537fn current_root_figure_value() -> Value {
538    current_figure_handle_if_exists()
539        .map(|handle| Value::Num(handle.as_u32() as f64))
540        .unwrap_or_else(empty_handle_value)
541}
542
543fn root_children_value() -> Value {
544    handles_value(
545        root_figure_handles()
546            .into_iter()
547            .map(|handle| handle.as_u32() as f64)
548            .collect(),
549    )
550}
551
552fn root_screen_size_value() -> Value {
553    tensor_from_vec(vec![1.0, 1.0, 1920.0, 1080.0])
554}
555
556fn empty_handle_value() -> Value {
557    Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty tensor shape is valid"))
558}
559
560fn on_off(value: bool) -> &'static str {
561    if value {
562        "on"
563    } else {
564        "off"
565    }
566}
567
568fn is_root_default_property(name: &str) -> bool {
569    name.starts_with("default") && name.len() > "default".len()
570}
571
572fn root_property_value_from_value(
573    value: &Value,
574    builtin: &'static str,
575) -> BuiltinResult<RootPropertyValue> {
576    match value {
577        Value::Bool(value) => Ok(RootPropertyValue::Bool(*value)),
578        Value::Num(value) => Ok(RootPropertyValue::Num(*value)),
579        Value::Int(value) => Ok(RootPropertyValue::Num(value.to_f64())),
580        Value::String(value) => Ok(RootPropertyValue::String(value.clone())),
581        Value::CharArray(value) => Ok(RootPropertyValue::String(value.data.iter().collect())),
582        Value::Tensor(value) => Ok(RootPropertyValue::Tensor(value.clone())),
583        Value::StringArray(value) => Ok(RootPropertyValue::StringArray {
584            rows: value.rows,
585            cols: value.cols,
586            shape: value.shape.clone(),
587            data: value.data.clone(),
588        }),
589        _ => Err(plotting_error(
590            builtin,
591            format!("{builtin}: unsupported root default property value"),
592        )),
593    }
594}
595
596fn root_property_value_to_value(value: RootPropertyValue) -> Value {
597    match value {
598        RootPropertyValue::Bool(value) => Value::Bool(value),
599        RootPropertyValue::Num(value) => Value::Num(value),
600        RootPropertyValue::String(value) => Value::String(value),
601        RootPropertyValue::Tensor(value) => Value::Tensor(value),
602        RootPropertyValue::StringArray {
603            rows,
604            cols,
605            shape,
606            data,
607        } => Value::StringArray(StringArray {
608            rows,
609            cols,
610            shape,
611            data,
612        }),
613    }
614}
615
616fn get_axes_property(
617    handle: FigureHandle,
618    axes_index: usize,
619    property: Option<&str>,
620    builtin: &'static str,
621) -> BuiltinResult<Value> {
622    let meta =
623        axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
624    let axes =
625        axes_state_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
626    let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
627        .map_err(|err| map_figure_error(builtin, err))?;
628    match property.map(canonical_property_name).as_deref() {
629        None => {
630            let mut st = StructValue::new();
631            st.insert(
632                "Handle",
633                Value::Num(super::state::encode_axes_handle(handle, axes_index)),
634            );
635            st.insert("Figure", Value::Num(handle.as_u32() as f64));
636            st.insert("Rows", Value::Num(axes.rows as f64));
637            st.insert("Cols", Value::Num(axes.cols as f64));
638            st.insert("Index", Value::Num((axes_index + 1) as f64));
639            st.insert(
640                "Title",
641                Value::Num(super::state::encode_plot_object_handle(
642                    handle,
643                    axes_index,
644                    PlotObjectKind::Title,
645                )),
646            );
647            st.insert(
648                "Subtitle",
649                Value::Num(super::state::encode_plot_object_handle(
650                    handle,
651                    axes_index,
652                    PlotObjectKind::Subtitle,
653                )),
654            );
655            st.insert(
656                "XLabel",
657                Value::Num(super::state::encode_plot_object_handle(
658                    handle,
659                    axes_index,
660                    PlotObjectKind::XLabel,
661                )),
662            );
663            st.insert(
664                "YLabel",
665                Value::Num(super::state::encode_plot_object_handle(
666                    handle,
667                    axes_index,
668                    PlotObjectKind::YLabel,
669                )),
670            );
671            st.insert(
672                "ZLabel",
673                Value::Num(super::state::encode_plot_object_handle(
674                    handle,
675                    axes_index,
676                    PlotObjectKind::ZLabel,
677                )),
678            );
679            st.insert(
680                "Legend",
681                Value::Num(super::state::encode_plot_object_handle(
682                    handle,
683                    axes_index,
684                    PlotObjectKind::Legend,
685                )),
686            );
687            st.insert(
688                "XAxis",
689                Value::Num(super::state::encode_plot_object_handle(
690                    handle,
691                    axes_index,
692                    PlotObjectKind::XAxis,
693                )),
694            );
695            st.insert(
696                "YAxis",
697                Value::Num(super::state::encode_plot_object_handle(
698                    handle,
699                    axes_index,
700                    PlotObjectKind::YAxis,
701                )),
702            );
703            st.insert("LegendVisible", Value::Bool(meta.legend_enabled));
704            st.insert("Type", Value::String("axes".into()));
705            st.insert("Parent", Value::Num(handle.as_u32() as f64));
706            st.insert("Position", figure_position_value(meta.position));
707            st.insert("Units", Value::String(meta.units.clone()));
708            st.insert(
709                "Children",
710                handles_value(vec![
711                    super::state::encode_plot_object_handle(
712                        handle,
713                        axes_index,
714                        PlotObjectKind::Title,
715                    ),
716                    super::state::encode_plot_object_handle(
717                        handle,
718                        axes_index,
719                        PlotObjectKind::Subtitle,
720                    ),
721                    super::state::encode_plot_object_handle(
722                        handle,
723                        axes_index,
724                        PlotObjectKind::XLabel,
725                    ),
726                    super::state::encode_plot_object_handle(
727                        handle,
728                        axes_index,
729                        PlotObjectKind::YLabel,
730                    ),
731                    super::state::encode_plot_object_handle(
732                        handle,
733                        axes_index,
734                        PlotObjectKind::ZLabel,
735                    ),
736                    super::state::encode_plot_object_handle(
737                        handle,
738                        axes_index,
739                        PlotObjectKind::Legend,
740                    ),
741                ]),
742            );
743            st.insert("Grid", Value::Bool(meta.grid_enabled));
744            st.insert("MinorGrid", Value::Bool(meta.minor_grid_enabled));
745            st.insert(
746                "HiddenLineRemoval",
747                Value::String(on_off(meta.hidden_line_removal).into()),
748            );
749            st.insert("Box", Value::Bool(meta.box_enabled));
750            st.insert("AxisEqual", Value::Bool(meta.axis_equal));
751            st.insert(
752                "DataAspectRatio",
753                tensor_from_vec(meta.data_aspect_ratio.to_vec()),
754            );
755            st.insert(
756                "DataAspectRatioMode",
757                Value::String(meta.data_aspect_ratio_mode.clone()),
758            );
759            st.insert("Colorbar", Value::Bool(meta.colorbar_enabled));
760            st.insert(
761                "Colormap",
762                Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
763            );
764            st.insert("XLim", limit_value(meta.x_limits));
765            st.insert("YLim", limit_value(meta.y_limits));
766            st.insert("ZLim", limit_value(meta.z_limits));
767            st.insert("CLim", limit_value(meta.color_limits));
768            st.insert(
769                "XTick",
770                tick_value(ticks_or_auto(
771                    meta.x_ticks.as_deref(),
772                    x_bounds(display_bounds),
773                )),
774            );
775            st.insert(
776                "YTick",
777                tick_value(ticks_or_auto(
778                    meta.y_ticks.as_deref(),
779                    y_bounds(display_bounds),
780                )),
781            );
782            st.insert("XTickMode", tick_mode_value(meta.x_ticks.as_ref()));
783            st.insert("YTickMode", tick_mode_value(meta.y_ticks.as_ref()));
784            st.insert(
785                "XTickLabel",
786                tick_label_value(tick_labels_or_auto(
787                    meta.x_tick_labels.as_deref(),
788                    meta.x_ticks.as_deref(),
789                    x_bounds(display_bounds),
790                    meta.x_tick_format.as_deref(),
791                )),
792            );
793            st.insert(
794                "YTickLabel",
795                tick_label_value(tick_labels_or_auto(
796                    meta.y_tick_labels.as_deref(),
797                    meta.y_ticks.as_deref(),
798                    y_bounds(display_bounds),
799                    meta.y_tick_format.as_deref(),
800                )),
801            );
802            st.insert(
803                "XTickLabelMode",
804                tick_mode_value(meta.x_tick_labels.as_ref()),
805            );
806            st.insert(
807                "YTickLabelMode",
808                tick_mode_value(meta.y_tick_labels.as_ref()),
809            );
810            st.insert(
811                "XTickLabelFormat",
812                Value::String(tick_format_or_default(meta.x_tick_format.as_deref())),
813            );
814            st.insert(
815                "YTickLabelFormat",
816                Value::String(tick_format_or_default(meta.y_tick_format.as_deref())),
817            );
818            st.insert(
819                "XTickLabelRotation",
820                Value::Num(meta.x_tick_label_rotation.unwrap_or(0.0)),
821            );
822            st.insert(
823                "YTickLabelRotation",
824                Value::Num(meta.y_tick_label_rotation.unwrap_or(0.0)),
825            );
826            st.insert(
827                "FontSize",
828                Value::Num(meta.axes_style.font_size.unwrap_or(10.0) as f64),
829            );
830            st.insert(
831                "XScale",
832                Value::String(if meta.x_log { "log" } else { "linear" }.into()),
833            );
834            st.insert(
835                "YScale",
836                Value::String(if meta.y_log { "log" } else { "linear" }.into()),
837            );
838            st.insert("YAxisLocation", Value::String(meta.y_axis_location.clone()));
839            Ok(Value::Struct(st))
840        }
841        Some("title") => Ok(Value::Num(super::state::encode_plot_object_handle(
842            handle,
843            axes_index,
844            PlotObjectKind::Title,
845        ))),
846        Some("subtitle") => Ok(Value::Num(super::state::encode_plot_object_handle(
847            handle,
848            axes_index,
849            PlotObjectKind::Subtitle,
850        ))),
851        Some("xlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
852            handle,
853            axes_index,
854            PlotObjectKind::XLabel,
855        ))),
856        Some("ylabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
857            handle,
858            axes_index,
859            PlotObjectKind::YLabel,
860        ))),
861        Some("zlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
862            handle,
863            axes_index,
864            PlotObjectKind::ZLabel,
865        ))),
866        Some("legend") => Ok(Value::Num(super::state::encode_plot_object_handle(
867            handle,
868            axes_index,
869            PlotObjectKind::Legend,
870        ))),
871        Some("xaxis") => Ok(Value::Num(super::state::encode_plot_object_handle(
872            handle,
873            axes_index,
874            PlotObjectKind::XAxis,
875        ))),
876        Some("yaxis") => Ok(Value::Num(super::state::encode_plot_object_handle(
877            handle,
878            axes_index,
879            PlotObjectKind::YAxis,
880        ))),
881        Some("view") => {
882            let az = meta.view_azimuth_deg.unwrap_or(-37.5) as f64;
883            let el = meta.view_elevation_deg.unwrap_or(30.0) as f64;
884            Ok(Value::Tensor(runmat_builtins::Tensor {
885                rows: 1,
886                cols: 2,
887                shape: vec![1, 2],
888                data: vec![az, el],
889                integer_data: None,
890                dtype: runmat_builtins::NumericDType::F64,
891            }))
892        }
893        Some("grid") => Ok(Value::Bool(meta.grid_enabled)),
894        Some("minorgrid") => Ok(Value::Bool(meta.minor_grid_enabled)),
895        Some("hiddenlineremoval") => Ok(Value::String(on_off(meta.hidden_line_removal).into())),
896        Some("box") => Ok(Value::Bool(meta.box_enabled)),
897        Some("axisequal") => Ok(Value::Bool(meta.axis_equal)),
898        Some("dataaspectratio") => Ok(tensor_from_vec(meta.data_aspect_ratio.to_vec())),
899        Some("dataaspectratiomode") => Ok(Value::String(meta.data_aspect_ratio_mode.clone())),
900        Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
901        Some("colormap") => Ok(Value::String(
902            format!("{:?}", meta.colormap).to_ascii_lowercase(),
903        )),
904        Some("xlim") => Ok(limit_value(meta.x_limits)),
905        Some("ylim") => Ok(limit_value(meta.y_limits)),
906        Some("zlim") => Ok(limit_value(meta.z_limits)),
907        Some("clim") => Ok(limit_value(meta.color_limits)),
908        Some("xtick") => Ok(tick_value(ticks_or_auto(
909            meta.x_ticks.as_deref(),
910            x_bounds(display_bounds),
911        ))),
912        Some("ytick") => Ok(tick_value(ticks_or_auto(
913            meta.y_ticks.as_deref(),
914            y_bounds(display_bounds),
915        ))),
916        Some("xtickmode") => Ok(tick_mode_value(meta.x_ticks.as_ref())),
917        Some("ytickmode") => Ok(tick_mode_value(meta.y_ticks.as_ref())),
918        Some("xticklabel") => Ok(tick_label_value(tick_labels_or_auto(
919            meta.x_tick_labels.as_deref(),
920            meta.x_ticks.as_deref(),
921            x_bounds(display_bounds),
922            meta.x_tick_format.as_deref(),
923        ))),
924        Some("yticklabel") => Ok(tick_label_value(tick_labels_or_auto(
925            meta.y_tick_labels.as_deref(),
926            meta.y_ticks.as_deref(),
927            y_bounds(display_bounds),
928            meta.y_tick_format.as_deref(),
929        ))),
930        Some("xticklabelmode") => Ok(tick_mode_value(meta.x_tick_labels.as_ref())),
931        Some("yticklabelmode") => Ok(tick_mode_value(meta.y_tick_labels.as_ref())),
932        Some("xticklabelformat") => Ok(Value::String(tick_format_or_default(
933            meta.x_tick_format.as_deref(),
934        ))),
935        Some("yticklabelformat") => Ok(Value::String(tick_format_or_default(
936            meta.y_tick_format.as_deref(),
937        ))),
938        Some("xticklabelrotation") => Ok(Value::Num(meta.x_tick_label_rotation.unwrap_or(0.0))),
939        Some("yticklabelrotation") => Ok(Value::Num(meta.y_tick_label_rotation.unwrap_or(0.0))),
940        Some("fontsize") => Ok(Value::Num(meta.axes_style.font_size.unwrap_or(10.0) as f64)),
941        Some("xscale") => Ok(Value::String(
942            if meta.x_log { "log" } else { "linear" }.into(),
943        )),
944        Some("yscale") => Ok(Value::String(
945            if meta.y_log { "log" } else { "linear" }.into(),
946        )),
947        Some("yaxislocation") => Ok(Value::String(meta.y_axis_location)),
948        Some("type") => Ok(Value::String("axes".into())),
949        Some("parent") => Ok(Value::Num(handle.as_u32() as f64)),
950        Some("position") => Ok(figure_position_value(meta.position)),
951        Some("units") => Ok(Value::String(meta.units)),
952        Some("legendvisible") => Ok(Value::Bool(meta.legend_enabled)),
953        Some("children") => Ok(handles_value(vec![
954            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title),
955            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Subtitle),
956            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel),
957            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel),
958            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel),
959            super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend),
960        ])),
961        Some(other) => Err(plotting_error(
962            builtin,
963            format!("{builtin}: unsupported axes property `{other}`"),
964        )),
965    }
966}
967
968fn get_text_property(
969    handle: FigureHandle,
970    axes_index: usize,
971    kind: PlotObjectKind,
972    property: Option<&str>,
973    builtin: &'static str,
974) -> BuiltinResult<Value> {
975    let (text, style) = match kind {
976        PlotObjectKind::SuperTitle => {
977            let figure = super::state::clone_figure(handle)
978                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
979            (figure.sg_title, figure.sg_title_style)
980        }
981        PlotObjectKind::Title
982        | PlotObjectKind::Subtitle
983        | PlotObjectKind::XLabel
984        | PlotObjectKind::YLabel
985        | PlotObjectKind::ZLabel => {
986            let meta = axes_metadata_snapshot(handle, axes_index)
987                .map_err(|err| map_figure_error(builtin, err))?;
988            match kind {
989                PlotObjectKind::Title => (meta.title, meta.title_style),
990                PlotObjectKind::Subtitle => (meta.subtitle, meta.subtitle_style),
991                PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
992                PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
993                PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
994                PlotObjectKind::Legend
995                | PlotObjectKind::SuperTitle
996                | PlotObjectKind::XAxis
997                | PlotObjectKind::YAxis => unreachable!(),
998            }
999        }
1000        PlotObjectKind::Legend | PlotObjectKind::XAxis | PlotObjectKind::YAxis => unreachable!(),
1001    };
1002    let parent = if matches!(kind, PlotObjectKind::SuperTitle) {
1003        Value::Num(handle.as_u32() as f64)
1004    } else {
1005        Value::Num(super::state::encode_axes_handle(handle, axes_index))
1006    };
1007    match property.map(canonical_property_name).as_deref() {
1008        None => {
1009            let mut st = StructValue::new();
1010            st.insert("Type", Value::String("text".into()));
1011            st.insert("Parent", parent.clone());
1012            st.insert("Children", handles_value(Vec::new()));
1013            st.insert("String", text_value(text));
1014            st.insert("Visible", Value::Bool(style.visible));
1015            if let Some(size) = style.font_size {
1016                st.insert("FontSize", Value::Num(size as f64));
1017            }
1018            if let Some(weight) = style.font_weight {
1019                st.insert("FontWeight", Value::String(weight));
1020            }
1021            if let Some(angle) = style.font_angle {
1022                st.insert("FontAngle", Value::String(angle));
1023            }
1024            if let Some(interpreter) = style.interpreter {
1025                st.insert("Interpreter", Value::String(interpreter));
1026            }
1027            if let Some(color) = style.color {
1028                st.insert("Color", Value::String(color_to_short_name(color)));
1029            }
1030            Ok(Value::Struct(st))
1031        }
1032        Some("type") => Ok(Value::String("text".into())),
1033        Some("parent") => Ok(parent),
1034        Some("children") => Ok(handles_value(Vec::new())),
1035        Some("string") => Ok(text_value(text)),
1036        Some("visible") => Ok(Value::Bool(style.visible)),
1037        Some("fontsize") => Ok(style
1038            .font_size
1039            .map(|v| Value::Num(v as f64))
1040            .unwrap_or(Value::Num(f64::NAN))),
1041        Some("fontweight") => Ok(style
1042            .font_weight
1043            .map(Value::String)
1044            .unwrap_or_else(|| Value::String(String::new()))),
1045        Some("fontangle") => Ok(style
1046            .font_angle
1047            .map(Value::String)
1048            .unwrap_or_else(|| Value::String(String::new()))),
1049        Some("interpreter") => Ok(style
1050            .interpreter
1051            .map(Value::String)
1052            .unwrap_or_else(|| Value::String(String::new()))),
1053        Some("color") => Ok(style
1054            .color
1055            .map(|c| Value::String(color_to_short_name(c)))
1056            .unwrap_or_else(|| Value::String(String::new()))),
1057        Some(other) => Err(plotting_error(
1058            builtin,
1059            format!("{builtin}: unsupported text property `{other}`"),
1060        )),
1061    }
1062}
1063
1064fn get_ruler_property(
1065    handle: FigureHandle,
1066    axes_index: usize,
1067    kind: PlotObjectKind,
1068    property: Option<&str>,
1069    builtin: &'static str,
1070) -> BuiltinResult<Value> {
1071    let meta =
1072        axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
1073    let (axis_name, format, rotation) = match kind {
1074        PlotObjectKind::XAxis => (
1075            "x",
1076            meta.x_tick_format.as_deref(),
1077            meta.x_tick_label_rotation,
1078        ),
1079        PlotObjectKind::YAxis => (
1080            "y",
1081            meta.y_tick_format.as_deref(),
1082            meta.y_tick_label_rotation,
1083        ),
1084        _ => {
1085            return Err(plotting_error(
1086                builtin,
1087                format!("{builtin}: invalid ruler handle"),
1088            ))
1089        }
1090    };
1091    let parent = Value::Num(super::state::encode_axes_handle(handle, axes_index));
1092    match property.map(canonical_property_name).as_deref() {
1093        None => {
1094            let mut st = StructValue::new();
1095            st.insert("Type", Value::String("numericruler".into()));
1096            st.insert("Parent", parent);
1097            st.insert("Children", handles_value(Vec::new()));
1098            st.insert("Axis", Value::String(axis_name.into()));
1099            st.insert(
1100                "TickLabelFormat",
1101                Value::String(tick_format_or_default(format)),
1102            );
1103            st.insert("TickLabelRotation", Value::Num(rotation.unwrap_or(0.0)));
1104            Ok(Value::Struct(st))
1105        }
1106        Some("type") => Ok(Value::String("numericruler".into())),
1107        Some("parent") => Ok(parent),
1108        Some("children") => Ok(handles_value(Vec::new())),
1109        Some("axis") => Ok(Value::String(axis_name.into())),
1110        Some("ticklabelformat") => Ok(Value::String(tick_format_or_default(format))),
1111        Some("ticklabelrotation") => Ok(Value::Num(rotation.unwrap_or(0.0))),
1112        Some(other) => Err(plotting_error(
1113            builtin,
1114            format!("{builtin}: unsupported ruler property `{other}`"),
1115        )),
1116    }
1117}
1118
1119fn get_legend_property(
1120    handle: FigureHandle,
1121    axes_index: usize,
1122    property: Option<&str>,
1123    builtin: &'static str,
1124) -> BuiltinResult<Value> {
1125    let meta =
1126        axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
1127    let entries = legend_entries_snapshot(handle, axes_index)
1128        .map_err(|err| map_figure_error(builtin, err))?;
1129    match property.map(canonical_property_name).as_deref() {
1130        None => {
1131            let mut st = StructValue::new();
1132            st.insert("Type", Value::String("legend".into()));
1133            st.insert(
1134                "Parent",
1135                Value::Num(super::state::encode_axes_handle(handle, axes_index)),
1136            );
1137            st.insert("Children", handles_value(Vec::new()));
1138            st.insert(
1139                "Visible",
1140                Value::Bool(meta.legend_enabled && meta.legend_style.visible),
1141            );
1142            st.insert(
1143                "String",
1144                legend_labels_value(entries.iter().map(|e| e.label.clone()).collect()),
1145            );
1146            if let Some(location) = meta.legend_style.location {
1147                st.insert("Location", Value::String(location));
1148            }
1149            if let Some(size) = meta.legend_style.font_size {
1150                st.insert("FontSize", Value::Num(size as f64));
1151            }
1152            if let Some(weight) = meta.legend_style.font_weight {
1153                st.insert("FontWeight", Value::String(weight));
1154            }
1155            if let Some(angle) = meta.legend_style.font_angle {
1156                st.insert("FontAngle", Value::String(angle));
1157            }
1158            if let Some(interpreter) = meta.legend_style.interpreter {
1159                st.insert("Interpreter", Value::String(interpreter));
1160            }
1161            if let Some(box_visible) = meta.legend_style.box_visible {
1162                st.insert("Box", Value::Bool(box_visible));
1163            }
1164            if let Some(orientation) = meta.legend_style.orientation {
1165                st.insert("Orientation", Value::String(orientation));
1166            }
1167            if let Some(color) = meta.legend_style.text_color {
1168                st.insert("TextColor", Value::String(color_to_short_name(color)));
1169            }
1170            Ok(Value::Struct(st))
1171        }
1172        Some("visible") => Ok(Value::Bool(
1173            meta.legend_enabled && meta.legend_style.visible,
1174        )),
1175        Some("type") => Ok(Value::String("legend".into())),
1176        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
1177            handle, axes_index,
1178        ))),
1179        Some("children") => Ok(handles_value(Vec::new())),
1180        Some("string") => Ok(legend_labels_value(
1181            entries.into_iter().map(|e| e.label).collect(),
1182        )),
1183        Some("location") => Ok(meta
1184            .legend_style
1185            .location
1186            .map(Value::String)
1187            .unwrap_or_else(|| Value::String(String::new()))),
1188        Some("fontsize") => Ok(meta
1189            .legend_style
1190            .font_size
1191            .map(|v| Value::Num(v as f64))
1192            .unwrap_or(Value::Num(f64::NAN))),
1193        Some("fontweight") => Ok(meta
1194            .legend_style
1195            .font_weight
1196            .map(Value::String)
1197            .unwrap_or_else(|| Value::String(String::new()))),
1198        Some("fontangle") => Ok(meta
1199            .legend_style
1200            .font_angle
1201            .map(Value::String)
1202            .unwrap_or_else(|| Value::String(String::new()))),
1203        Some("interpreter") => Ok(meta
1204            .legend_style
1205            .interpreter
1206            .map(Value::String)
1207            .unwrap_or_else(|| Value::String(String::new()))),
1208        Some("box") => Ok(meta
1209            .legend_style
1210            .box_visible
1211            .map(Value::Bool)
1212            .unwrap_or(Value::Bool(true))),
1213        Some("orientation") => Ok(meta
1214            .legend_style
1215            .orientation
1216            .map(Value::String)
1217            .unwrap_or_else(|| Value::String(String::new()))),
1218        Some("textcolor") | Some("color") => Ok(meta
1219            .legend_style
1220            .text_color
1221            .map(|c| Value::String(color_to_short_name(c)))
1222            .unwrap_or_else(|| Value::String(String::new()))),
1223        Some(other) => Err(plotting_error(
1224            builtin,
1225            format!("{builtin}: unsupported legend property `{other}`"),
1226        )),
1227    }
1228}
1229
1230fn property_name(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
1231    property_name_text(value, builtin).map(|s| canonical_property_name(s.trim()).into_owned())
1232}
1233
1234fn property_name_text(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
1235    value_as_string(value)
1236        .map(|s| s.trim().to_string())
1237        .ok_or_else(|| {
1238            plotting_error(
1239                builtin,
1240                format!("{builtin}: property names must be strings"),
1241            )
1242        })
1243}
1244
1245pub(crate) fn data_aspect_ratio_from_value(
1246    value: &Value,
1247    builtin: &'static str,
1248) -> BuiltinResult<[f64; 3]> {
1249    let tensor =
1250        Tensor::try_from(value).map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
1251    if tensor.data.len() != 3
1252        || tensor
1253            .data
1254            .iter()
1255            .any(|value| !value.is_finite() || *value <= 0.0)
1256    {
1257        return Err(plotting_error(
1258            builtin,
1259            format!(
1260                "{builtin}: DataAspectRatio must be a 3-element positive finite numeric vector"
1261            ),
1262        ));
1263    }
1264    Ok([tensor.data[0], tensor.data[1], tensor.data[2]])
1265}
1266
1267pub(crate) fn data_aspect_ratio_mode_from_value(
1268    value: &Value,
1269    builtin: &'static str,
1270) -> BuiltinResult<&'static str> {
1271    let mode = value_as_string(value).ok_or_else(|| {
1272        plotting_error(
1273            builtin,
1274            format!("{builtin}: DataAspectRatioMode must be text"),
1275        )
1276    })?;
1277    match mode.trim().to_ascii_lowercase().as_str() {
1278        "auto" => Ok("auto"),
1279        "manual" => Ok("manual"),
1280        other => Err(plotting_error(
1281            builtin,
1282            format!("{builtin}: unsupported DataAspectRatioMode `{other}`"),
1283        )),
1284    }
1285}
1286
1287fn canonical_property_name(name: &str) -> Cow<'_, str> {
1288    match name.to_ascii_lowercase().as_str() {
1289        "textcolor" => Cow::Borrowed("textcolor"),
1290        "color" | "backgroundcolor" => Cow::Borrowed("color"),
1291        "fontsize" => Cow::Borrowed("fontsize"),
1292        "fontweight" => Cow::Borrowed("fontweight"),
1293        "fontangle" => Cow::Borrowed("fontangle"),
1294        "interpreter" => Cow::Borrowed("interpreter"),
1295        "visible" => Cow::Borrowed("visible"),
1296        "location" => Cow::Borrowed("location"),
1297        "box" => Cow::Borrowed("box"),
1298        "orientation" => Cow::Borrowed("orientation"),
1299        "string" => Cow::Borrowed("string"),
1300        "title" => Cow::Borrowed("title"),
1301        "xlabel" => Cow::Borrowed("xlabel"),
1302        "ylabel" => Cow::Borrowed("ylabel"),
1303        "zlabel" => Cow::Borrowed("zlabel"),
1304        "xaxis" => Cow::Borrowed("xaxis"),
1305        "yaxis" => Cow::Borrowed("yaxis"),
1306        "axis" => Cow::Borrowed("axis"),
1307        "view" => Cow::Borrowed("view"),
1308        "grid" | "xgrid" | "ygrid" | "zgrid" => Cow::Borrowed("grid"),
1309        "minorgrid" | "xminorgrid" | "yminorgrid" | "zminorgrid" => Cow::Borrowed("minorgrid"),
1310        "axisequal" => Cow::Borrowed("axisequal"),
1311        "colorbar" => Cow::Borrowed("colorbar"),
1312        "colorbarvisible" => Cow::Borrowed("colorbarvisible"),
1313        "colormap" => Cow::Borrowed("colormap"),
1314        "xdisplaylabels" => Cow::Borrowed("xdisplaylabels"),
1315        "ydisplaylabels" => Cow::Borrowed("ydisplaylabels"),
1316        "colordata" | "cdata" => Cow::Borrowed("colordata"),
1317        "xlim" => Cow::Borrowed("xlim"),
1318        "ylim" => Cow::Borrowed("ylim"),
1319        "zlim" => Cow::Borrowed("zlim"),
1320        "clim" | "caxis" => Cow::Borrowed("clim"),
1321        "xtick" => Cow::Borrowed("xtick"),
1322        "ytick" => Cow::Borrowed("ytick"),
1323        "xtickmode" => Cow::Borrowed("xtickmode"),
1324        "ytickmode" => Cow::Borrowed("ytickmode"),
1325        "xticklabel" => Cow::Borrowed("xticklabel"),
1326        "yticklabel" => Cow::Borrowed("yticklabel"),
1327        "xticklabelmode" => Cow::Borrowed("xticklabelmode"),
1328        "yticklabelmode" => Cow::Borrowed("yticklabelmode"),
1329        "xticklabelformat" => Cow::Borrowed("xticklabelformat"),
1330        "yticklabelformat" => Cow::Borrowed("yticklabelformat"),
1331        "ticklabelformat" => Cow::Borrowed("ticklabelformat"),
1332        "xticklabelrotation" => Cow::Borrowed("xticklabelrotation"),
1333        "yticklabelrotation" => Cow::Borrowed("yticklabelrotation"),
1334        "ticklabelrotation" => Cow::Borrowed("ticklabelrotation"),
1335        "hiddenlineremoval" => Cow::Borrowed("hiddenlineremoval"),
1336        "xscale" => Cow::Borrowed("xscale"),
1337        "yscale" => Cow::Borrowed("yscale"),
1338        "yaxislocation" => Cow::Borrowed("yaxislocation"),
1339        "currentaxes" => Cow::Borrowed("currentaxes"),
1340        "currentfigure" => Cow::Borrowed("currentfigure"),
1341        "sgtitle" | "supertitle" => Cow::Borrowed("sgtitle"),
1342        "children" => Cow::Borrowed("children"),
1343        "handle" => Cow::Borrowed("handle"),
1344        "parent" => Cow::Borrowed("parent"),
1345        "type" => Cow::Borrowed("type"),
1346        "screensize" => Cow::Borrowed("screensize"),
1347        "monitorpositions" => Cow::Borrowed("monitorpositions"),
1348        "units" => Cow::Borrowed("units"),
1349        "showhiddenhandles" => Cow::Borrowed("showhiddenhandles"),
1350        "number" => Cow::Borrowed("number"),
1351        "name" => Cow::Borrowed("name"),
1352        "numbertitle" => Cow::Borrowed("numbertitle"),
1353        "legend" => Cow::Borrowed("legend"),
1354        "legendvisible" => Cow::Borrowed("legendvisible"),
1355        "autoscalefactor" => Cow::Borrowed("autoscalefactor"),
1356        "barwidth" => Cow::Borrowed("barwidth"),
1357        "baseline" => Cow::Borrowed("baseline"),
1358        "basevalue" => Cow::Borrowed("basevalue"),
1359        "bincounts" => Cow::Borrowed("bincounts"),
1360        "binedges" => Cow::Borrowed("binedges"),
1361        "capsize" => Cow::Borrowed("capsize"),
1362        "cdatamapping" => Cow::Borrowed("cdatamapping"),
1363        "displayname" => Cow::Borrowed("displayname"),
1364        "edgealpha" => Cow::Borrowed("edgealpha"),
1365        "edgecolor" => Cow::Borrowed("edgecolor"),
1366        "facealpha" => Cow::Borrowed("facealpha"),
1367        "facecolor" => Cow::Borrowed("facecolor"),
1368        "faces" => Cow::Borrowed("faces"),
1369        "filled" => Cow::Borrowed("filled"),
1370        "label" => Cow::Borrowed("label"),
1371        "labelorientation" => Cow::Borrowed("labelorientation"),
1372        "linestyle" => Cow::Borrowed("linestyle"),
1373        "linewidth" => Cow::Borrowed("linewidth"),
1374        "marker" => Cow::Borrowed("marker"),
1375        "markeredgecolor" => Cow::Borrowed("markeredgecolor"),
1376        "markerfacecolor" => Cow::Borrowed("markerfacecolor"),
1377        "markersize" => Cow::Borrowed("markersize"),
1378        "maxheadsize" => Cow::Borrowed("maxheadsize"),
1379        "normalization" => Cow::Borrowed("normalization"),
1380        "numbins" => Cow::Borrowed("numbins"),
1381        "position" => Cow::Borrowed("position"),
1382        "sizedata" => Cow::Borrowed("sizedata"),
1383        "value" => Cow::Borrowed("value"),
1384        "vertices" => Cow::Borrowed("vertices"),
1385        "xdata" => Cow::Borrowed("xdata"),
1386        "ydata" => Cow::Borrowed("ydata"),
1387        "zdata" => Cow::Borrowed("zdata"),
1388        other => Cow::Owned(other.to_string()),
1389    }
1390}
1391
1392fn apply_text_property(
1393    text: &mut Option<String>,
1394    style: &mut TextStyle,
1395    key: &str,
1396    value: &Value,
1397    builtin: &'static str,
1398) -> BuiltinResult<()> {
1399    let opts = LineStyleParseOptions::generic(builtin);
1400    match key {
1401        "string" => {
1402            *text = Some(value_as_text_string(value).ok_or_else(|| {
1403                plotting_error(builtin, format!("{builtin}: String must be text"))
1404            })?);
1405        }
1406        "color" => style.color = Some(parse_color_value(&opts, value)?),
1407        "fontsize" => {
1408            style.font_size = Some(value_as_f64(value).ok_or_else(|| {
1409                plotting_error(builtin, format!("{builtin}: FontSize must be numeric"))
1410            })? as f32)
1411        }
1412        "fontweight" => {
1413            style.font_weight = Some(value_as_string(value).ok_or_else(|| {
1414                plotting_error(builtin, format!("{builtin}: FontWeight must be a string"))
1415            })?)
1416        }
1417        "fontangle" => {
1418            style.font_angle = Some(value_as_string(value).ok_or_else(|| {
1419                plotting_error(builtin, format!("{builtin}: FontAngle must be a string"))
1420            })?)
1421        }
1422        "interpreter" => {
1423            style.interpreter = Some(value_as_string(value).ok_or_else(|| {
1424                plotting_error(builtin, format!("{builtin}: Interpreter must be a string"))
1425            })?)
1426        }
1427        "visible" => {
1428            style.visible = value_as_bool(value).ok_or_else(|| {
1429                plotting_error(builtin, format!("{builtin}: Visible must be logical"))
1430            })?
1431        }
1432        other => {
1433            return Err(plotting_error(
1434                builtin,
1435                format!("{builtin}: unsupported property `{other}`"),
1436            ))
1437        }
1438    }
1439    Ok(())
1440}
1441
1442fn apply_legend_property(
1443    style: &mut LegendStyle,
1444    enabled: &mut bool,
1445    labels: &mut Option<Vec<String>>,
1446    key: &str,
1447    value: &Value,
1448    builtin: &'static str,
1449) -> BuiltinResult<()> {
1450    let opts = LineStyleParseOptions::generic(builtin);
1451    match key {
1452        "string" => *labels = Some(collect_label_strings(builtin, std::slice::from_ref(value))?),
1453        "location" => {
1454            style.location = Some(
1455                value_as_string(value)
1456                    .ok_or_else(|| plotting_error(builtin, "legend: Location must be a string"))?,
1457            )
1458        }
1459        "fontsize" => {
1460            style.font_size = Some(
1461                value_as_f64(value)
1462                    .ok_or_else(|| plotting_error(builtin, "legend: FontSize must be numeric"))?
1463                    as f32,
1464            )
1465        }
1466        "fontweight" => {
1467            style.font_weight =
1468                Some(value_as_string(value).ok_or_else(|| {
1469                    plotting_error(builtin, "legend: FontWeight must be a string")
1470                })?)
1471        }
1472        "fontangle" => {
1473            style.font_angle = Some(
1474                value_as_string(value)
1475                    .ok_or_else(|| plotting_error(builtin, "legend: FontAngle must be a string"))?,
1476            )
1477        }
1478        "interpreter" => {
1479            style.interpreter =
1480                Some(value_as_string(value).ok_or_else(|| {
1481                    plotting_error(builtin, "legend: Interpreter must be a string")
1482                })?)
1483        }
1484        "textcolor" | "color" => style.text_color = Some(parse_color_value(&opts, value)?),
1485        "visible" => {
1486            let visible = value_as_bool(value)
1487                .ok_or_else(|| plotting_error(builtin, "legend: Visible must be logical"))?;
1488            style.visible = visible;
1489            *enabled = visible;
1490        }
1491        "box" => {
1492            style.box_visible = Some(
1493                value_as_bool(value)
1494                    .ok_or_else(|| plotting_error(builtin, "legend: Box must be logical"))?,
1495            )
1496        }
1497        "orientation" => {
1498            style.orientation =
1499                Some(value_as_string(value).ok_or_else(|| {
1500                    plotting_error(builtin, "legend: Orientation must be a string")
1501                })?)
1502        }
1503        other => {
1504            return Err(plotting_error(
1505                builtin,
1506                format!("{builtin}: unsupported property `{other}`"),
1507            ))
1508        }
1509    }
1510    Ok(())
1511}
1512
1513fn apply_axes_property(
1514    handle: FigureHandle,
1515    axes_index: usize,
1516    key: &str,
1517    value: &Value,
1518    builtin: &'static str,
1519) -> BuiltinResult<()> {
1520    match key {
1521        "legendvisible" => {
1522            let visible = value_as_bool(value).ok_or_else(|| {
1523                plotting_error(builtin, format!("{builtin}: LegendVisible must be logical"))
1524            })?;
1525            set_legend_for_axes(handle, axes_index, visible, None, None)
1526                .map_err(|err| map_figure_error(builtin, err))?;
1527            Ok(())
1528        }
1529        "title" => apply_axes_text_alias(handle, axes_index, PlotObjectKind::Title, value, builtin),
1530        "subtitle" => {
1531            apply_axes_text_alias(handle, axes_index, PlotObjectKind::Subtitle, value, builtin)
1532        }
1533        "xlabel" => {
1534            apply_axes_text_alias(handle, axes_index, PlotObjectKind::XLabel, value, builtin)
1535        }
1536        "ylabel" => {
1537            apply_axes_text_alias(handle, axes_index, PlotObjectKind::YLabel, value, builtin)
1538        }
1539        "zlabel" => {
1540            apply_axes_text_alias(handle, axes_index, PlotObjectKind::ZLabel, value, builtin)
1541        }
1542        "view" => {
1543            let tensor = runmat_builtins::Tensor::try_from(value)
1544                .map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
1545            if tensor.data.len() != 2 || !tensor.data[0].is_finite() || !tensor.data[1].is_finite()
1546            {
1547                return Err(plotting_error(
1548                    builtin,
1549                    format!("{builtin}: View must be a 2-element finite numeric vector"),
1550                ));
1551            }
1552            crate::builtins::plotting::state::set_view_for_axes(
1553                handle,
1554                axes_index,
1555                tensor.data[0] as f32,
1556                tensor.data[1] as f32,
1557            )
1558            .map_err(|err| map_figure_error(builtin, err))?;
1559            Ok(())
1560        }
1561        "grid" => {
1562            let enabled = value_as_bool(value).ok_or_else(|| {
1563                plotting_error(builtin, format!("{builtin}: Grid must be logical"))
1564            })?;
1565            crate::builtins::plotting::state::set_grid_enabled_for_axes(
1566                handle, axes_index, enabled,
1567            )
1568            .map_err(|err| map_figure_error(builtin, err))?;
1569            Ok(())
1570        }
1571        "minorgrid" => {
1572            let enabled = value_as_bool(value).ok_or_else(|| {
1573                plotting_error(builtin, format!("{builtin}: MinorGrid must be logical"))
1574            })?;
1575            crate::builtins::plotting::state::set_minor_grid_enabled_for_axes(
1576                handle, axes_index, enabled,
1577            )
1578            .map_err(|err| map_figure_error(builtin, err))?;
1579            Ok(())
1580        }
1581        "box" => {
1582            let enabled = value_as_bool(value).ok_or_else(|| {
1583                plotting_error(builtin, format!("{builtin}: Box must be logical"))
1584            })?;
1585            crate::builtins::plotting::state::set_box_enabled_for_axes(handle, axes_index, enabled)
1586                .map_err(|err| map_figure_error(builtin, err))?;
1587            Ok(())
1588        }
1589        "hiddenlineremoval" => {
1590            let enabled = value_as_bool(value).ok_or_else(|| {
1591                plotting_error(
1592                    builtin,
1593                    format!("{builtin}: HiddenLineRemoval must be 'on' or 'off'"),
1594                )
1595            })?;
1596            crate::builtins::plotting::state::set_hidden_line_removal_for_axes(
1597                handle, axes_index, enabled,
1598            )
1599            .map_err(|err| map_figure_error(builtin, err))?;
1600            Ok(())
1601        }
1602        "axisequal" => {
1603            let enabled = value_as_bool(value).ok_or_else(|| {
1604                plotting_error(builtin, format!("{builtin}: AxisEqual must be logical"))
1605            })?;
1606            crate::builtins::plotting::state::set_axis_equal_for_axes(handle, axes_index, enabled)
1607                .map_err(|err| map_figure_error(builtin, err))?;
1608            Ok(())
1609        }
1610        "dataaspectratio" => {
1611            let ratio = data_aspect_ratio_from_value(value, builtin)?;
1612            crate::builtins::plotting::state::set_data_aspect_ratio_for_axes(
1613                handle, axes_index, ratio, "manual",
1614            )
1615            .map_err(|err| map_figure_error(builtin, err))?;
1616            Ok(())
1617        }
1618        "dataaspectratiomode" => {
1619            let mode = data_aspect_ratio_mode_from_value(value, builtin)?;
1620            let (ratio, _) = crate::builtins::plotting::state::data_aspect_ratio_snapshot_for_axes(
1621                handle, axes_index,
1622            )
1623            .map_err(|err| map_figure_error(builtin, err))?;
1624            crate::builtins::plotting::state::set_data_aspect_ratio_for_axes(
1625                handle, axes_index, ratio, mode,
1626            )
1627            .map_err(|err| map_figure_error(builtin, err))?;
1628            Ok(())
1629        }
1630        "colorbar" => {
1631            let enabled = value_as_bool(value).ok_or_else(|| {
1632                plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
1633            })?;
1634            crate::builtins::plotting::state::set_colorbar_enabled_for_axes(
1635                handle, axes_index, enabled,
1636            )
1637            .map_err(|err| map_figure_error(builtin, err))?;
1638            Ok(())
1639        }
1640        "colormap" => {
1641            let name = value_as_string(value).ok_or_else(|| {
1642                plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
1643            })?;
1644            let cmap = parse_colormap_name(&name, builtin)?;
1645            crate::builtins::plotting::state::set_colormap_for_axes(handle, axes_index, cmap)
1646                .map_err(|err| map_figure_error(builtin, err))?;
1647            Ok(())
1648        }
1649        "fontsize" => {
1650            let font_size = value_as_f64(value).ok_or_else(|| {
1651                plotting_error(builtin, format!("{builtin}: FontSize must be numeric"))
1652            })?;
1653            if !font_size.is_finite() || font_size <= 0.0 || font_size > MAX_AXES_FONT_SIZE_POINTS {
1654                return Err(plotting_error(
1655                    builtin,
1656                    format!(
1657                        "{builtin}: FontSize must be a positive finite value no larger than {MAX_AXES_FONT_SIZE_POINTS}"
1658                    ),
1659                ));
1660            }
1661            let meta = axes_metadata_snapshot(handle, axes_index)
1662                .map_err(|err| map_figure_error(builtin, err))?;
1663            let mut style = meta.axes_style;
1664            style.font_size = Some(font_size as f32);
1665            set_axes_style_for_axes(handle, axes_index, style)
1666                .map_err(|err| map_figure_error(builtin, err))?;
1667            Ok(())
1668        }
1669        "xlim" => {
1670            let limits = limits_from_optional_value(value, builtin)?;
1671            let meta = axes_metadata_snapshot(handle, axes_index)
1672                .map_err(|err| map_figure_error(builtin, err))?;
1673            crate::builtins::plotting::state::set_axis_limits_for_axes(
1674                handle,
1675                axes_index,
1676                limits,
1677                meta.y_limits,
1678            )
1679            .map_err(|err| map_figure_error(builtin, err))?;
1680            Ok(())
1681        }
1682        "ylim" => {
1683            let limits = limits_from_optional_value(value, builtin)?;
1684            let meta = axes_metadata_snapshot(handle, axes_index)
1685                .map_err(|err| map_figure_error(builtin, err))?;
1686            crate::builtins::plotting::state::set_axis_limits_for_axes(
1687                handle,
1688                axes_index,
1689                meta.x_limits,
1690                limits,
1691            )
1692            .map_err(|err| map_figure_error(builtin, err))?;
1693            Ok(())
1694        }
1695        "zlim" => {
1696            let limits = limits_from_optional_value(value, builtin)?;
1697            crate::builtins::plotting::state::set_z_limits_for_axes(handle, axes_index, limits)
1698                .map_err(|err| map_figure_error(builtin, err))?;
1699            Ok(())
1700        }
1701        "clim" => {
1702            let limits = limits_from_optional_value(value, builtin)?;
1703            crate::builtins::plotting::state::set_color_limits_for_axes(handle, axes_index, limits)
1704                .map_err(|err| map_figure_error(builtin, err))?;
1705            Ok(())
1706        }
1707        "xtick" => {
1708            let ticks = ticks_from_value(value, builtin)?;
1709            let meta = axes_metadata_snapshot(handle, axes_index)
1710                .map_err(|err| map_figure_error(builtin, err))?;
1711            set_axis_ticks_for_axes(handle, axes_index, Some(ticks), meta.y_ticks)
1712                .map_err(|err| map_figure_error(builtin, err))?;
1713            Ok(())
1714        }
1715        "ytick" => {
1716            let ticks = ticks_from_value(value, builtin)?;
1717            let meta = axes_metadata_snapshot(handle, axes_index)
1718                .map_err(|err| map_figure_error(builtin, err))?;
1719            set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks))
1720                .map_err(|err| map_figure_error(builtin, err))?;
1721            Ok(())
1722        }
1723        "xtickmode" => {
1724            let mode = tick_mode_from_value(value, builtin, "XTickMode")?;
1725            let meta = axes_metadata_snapshot(handle, axes_index)
1726                .map_err(|err| map_figure_error(builtin, err))?;
1727            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1728                .map_err(|err| map_figure_error(builtin, err))?;
1729            let x = match mode {
1730                TickMode::Auto => None,
1731                TickMode::Manual => Some(ticks_or_auto(
1732                    meta.x_ticks.as_deref(),
1733                    x_bounds(display_bounds),
1734                )),
1735            };
1736            set_axis_ticks_for_axes(handle, axes_index, x, meta.y_ticks)
1737                .map_err(|err| map_figure_error(builtin, err))?;
1738            Ok(())
1739        }
1740        "ytickmode" => {
1741            let mode = tick_mode_from_value(value, builtin, "YTickMode")?;
1742            let meta = axes_metadata_snapshot(handle, axes_index)
1743                .map_err(|err| map_figure_error(builtin, err))?;
1744            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1745                .map_err(|err| map_figure_error(builtin, err))?;
1746            let y = match mode {
1747                TickMode::Auto => None,
1748                TickMode::Manual => Some(ticks_or_auto(
1749                    meta.y_ticks.as_deref(),
1750                    y_bounds(display_bounds),
1751                )),
1752            };
1753            set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, y)
1754                .map_err(|err| map_figure_error(builtin, err))?;
1755            Ok(())
1756        }
1757        "xticklabel" => {
1758            let labels = tick_labels_from_value(value, builtin)?;
1759            let meta = axes_metadata_snapshot(handle, axes_index)
1760                .map_err(|err| map_figure_error(builtin, err))?;
1761            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1762                .map_err(|err| map_figure_error(builtin, err))?;
1763            let ticks = ticks_or_auto(meta.x_ticks.as_deref(), x_bounds(display_bounds));
1764            set_axis_ticks_for_axes(handle, axes_index, Some(ticks.clone()), meta.y_ticks)
1765                .map_err(|err| map_figure_error(builtin, err))?;
1766            set_axis_tick_labels_for_axes(
1767                handle,
1768                axes_index,
1769                Some(labels_padded_to_ticks(labels, ticks.len())),
1770                meta.y_tick_labels,
1771            )
1772            .map_err(|err| map_figure_error(builtin, err))?;
1773            Ok(())
1774        }
1775        "yticklabel" => {
1776            let labels = tick_labels_from_value(value, builtin)?;
1777            let meta = axes_metadata_snapshot(handle, axes_index)
1778                .map_err(|err| map_figure_error(builtin, err))?;
1779            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1780                .map_err(|err| map_figure_error(builtin, err))?;
1781            let ticks = ticks_or_auto(meta.y_ticks.as_deref(), y_bounds(display_bounds));
1782            set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks.clone()))
1783                .map_err(|err| map_figure_error(builtin, err))?;
1784            set_axis_tick_labels_for_axes(
1785                handle,
1786                axes_index,
1787                meta.x_tick_labels,
1788                Some(labels_padded_to_ticks(labels, ticks.len())),
1789            )
1790            .map_err(|err| map_figure_error(builtin, err))?;
1791            Ok(())
1792        }
1793        "xticklabelmode" => {
1794            let mode = tick_mode_from_value(value, builtin, "XTickLabelMode")?;
1795            let meta = axes_metadata_snapshot(handle, axes_index)
1796                .map_err(|err| map_figure_error(builtin, err))?;
1797            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1798                .map_err(|err| map_figure_error(builtin, err))?;
1799            let x_labels = match mode {
1800                TickMode::Auto => None,
1801                TickMode::Manual => Some(tick_labels_or_auto(
1802                    meta.x_tick_labels.as_deref(),
1803                    meta.x_ticks.as_deref(),
1804                    x_bounds(display_bounds),
1805                    meta.x_tick_format.as_deref(),
1806                )),
1807            };
1808            if matches!(mode, TickMode::Manual) {
1809                let ticks = ticks_or_auto(meta.x_ticks.as_deref(), x_bounds(display_bounds));
1810                set_axis_ticks_for_axes(handle, axes_index, Some(ticks), meta.y_ticks)
1811                    .map_err(|err| map_figure_error(builtin, err))?;
1812            }
1813            set_axis_tick_labels_for_axes(handle, axes_index, x_labels, meta.y_tick_labels)
1814                .map_err(|err| map_figure_error(builtin, err))?;
1815            Ok(())
1816        }
1817        "yticklabelmode" => {
1818            let mode = tick_mode_from_value(value, builtin, "YTickLabelMode")?;
1819            let meta = axes_metadata_snapshot(handle, axes_index)
1820                .map_err(|err| map_figure_error(builtin, err))?;
1821            let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1822                .map_err(|err| map_figure_error(builtin, err))?;
1823            let y_labels = match mode {
1824                TickMode::Auto => None,
1825                TickMode::Manual => Some(tick_labels_or_auto(
1826                    meta.y_tick_labels.as_deref(),
1827                    meta.y_ticks.as_deref(),
1828                    y_bounds(display_bounds),
1829                    meta.y_tick_format.as_deref(),
1830                )),
1831            };
1832            if matches!(mode, TickMode::Manual) {
1833                let ticks = ticks_or_auto(meta.y_ticks.as_deref(), y_bounds(display_bounds));
1834                set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks))
1835                    .map_err(|err| map_figure_error(builtin, err))?;
1836            }
1837            set_axis_tick_labels_for_axes(handle, axes_index, meta.x_tick_labels, y_labels)
1838                .map_err(|err| map_figure_error(builtin, err))?;
1839            Ok(())
1840        }
1841        "xticklabelformat" => {
1842            let format = tick_format_from_value(value, builtin, "XTickLabelFormat")?;
1843            let meta = axes_metadata_snapshot(handle, axes_index)
1844                .map_err(|err| map_figure_error(builtin, err))?;
1845            set_axis_tick_formats_for_axes(handle, axes_index, Some(format), meta.y_tick_format)
1846                .map_err(|err| map_figure_error(builtin, err))?;
1847            Ok(())
1848        }
1849        "yticklabelformat" => {
1850            let format = tick_format_from_value(value, builtin, "YTickLabelFormat")?;
1851            let meta = axes_metadata_snapshot(handle, axes_index)
1852                .map_err(|err| map_figure_error(builtin, err))?;
1853            set_axis_tick_formats_for_axes(handle, axes_index, meta.x_tick_format, Some(format))
1854                .map_err(|err| map_figure_error(builtin, err))?;
1855            Ok(())
1856        }
1857        "xticklabelrotation" => {
1858            let angle = tick_angle_from_value(value, builtin, "XTickLabelRotation")?;
1859            let meta = axes_metadata_snapshot(handle, axes_index)
1860                .map_err(|err| map_figure_error(builtin, err))?;
1861            set_axis_tick_angles_for_axes(
1862                handle,
1863                axes_index,
1864                Some(angle),
1865                meta.y_tick_label_rotation,
1866            )
1867            .map_err(|err| map_figure_error(builtin, err))?;
1868            Ok(())
1869        }
1870        "yticklabelrotation" => {
1871            let angle = tick_angle_from_value(value, builtin, "YTickLabelRotation")?;
1872            let meta = axes_metadata_snapshot(handle, axes_index)
1873                .map_err(|err| map_figure_error(builtin, err))?;
1874            set_axis_tick_angles_for_axes(
1875                handle,
1876                axes_index,
1877                meta.x_tick_label_rotation,
1878                Some(angle),
1879            )
1880            .map_err(|err| map_figure_error(builtin, err))?;
1881            Ok(())
1882        }
1883        "xscale" => {
1884            let mode = scale_mode_from_value(value, builtin)?;
1885            let meta = axes_metadata_snapshot(handle, axes_index)
1886                .map_err(|err| map_figure_error(builtin, err))?;
1887            crate::builtins::plotting::state::set_log_modes_for_axes(
1888                handle,
1889                axes_index,
1890                mode.is_log(),
1891                meta.y_log,
1892            )
1893            .map_err(|err| map_figure_error(builtin, err))?;
1894            Ok(())
1895        }
1896        "yscale" => {
1897            let mode = scale_mode_from_value(value, builtin)?;
1898            let meta = axes_metadata_snapshot(handle, axes_index)
1899                .map_err(|err| map_figure_error(builtin, err))?;
1900            crate::builtins::plotting::state::set_log_modes_for_axes(
1901                handle,
1902                axes_index,
1903                meta.x_log,
1904                mode.is_log(),
1905            )
1906            .map_err(|err| map_figure_error(builtin, err))?;
1907            Ok(())
1908        }
1909        "yaxislocation" => {
1910            let location = value_as_string(value)
1911                .ok_or_else(|| {
1912                    plotting_error(builtin, format!("{builtin}: YAxisLocation must be text"))
1913                })?
1914                .trim()
1915                .to_ascii_lowercase();
1916            if !matches!(location.as_str(), "left" | "right") {
1917                return Err(plotting_error(
1918                    builtin,
1919                    format!("{builtin}: YAxisLocation must be 'left' or 'right'"),
1920                ));
1921            }
1922            crate::builtins::plotting::state::set_y_axis_location_for_axes(
1923                handle, axes_index, location,
1924            )
1925            .map_err(|err| map_figure_error(builtin, err))?;
1926            Ok(())
1927        }
1928        "position" => {
1929            let position = parse_figure_position(value, builtin)?;
1930            set_axes_position_for_axes(handle, axes_index, position)
1931                .map_err(|err| map_figure_error(builtin, err))?;
1932            Ok(())
1933        }
1934        "units" => {
1935            let units = value_as_string(value)
1936                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Units must be text")))?
1937                .trim()
1938                .to_ascii_lowercase();
1939            if !matches!(
1940                units.as_str(),
1941                "normalized" | "pixels" | "inches" | "centimeters" | "points" | "characters"
1942            ) {
1943                return Err(plotting_error(
1944                    builtin,
1945                    format!("{builtin}: unsupported axes Units `{units}`"),
1946                ));
1947            }
1948            set_axes_units_for_axes(handle, axes_index, units)
1949                .map_err(|err| map_figure_error(builtin, err))?;
1950            Ok(())
1951        }
1952        other => Err(plotting_error(
1953            builtin,
1954            format!("{builtin}: unsupported axes property `{other}`"),
1955        )),
1956    }
1957}
1958
1959fn apply_ruler_property(
1960    handle: FigureHandle,
1961    axes_index: usize,
1962    kind: PlotObjectKind,
1963    key: &str,
1964    value: &Value,
1965    builtin: &'static str,
1966) -> BuiltinResult<()> {
1967    match key {
1968        "ticklabelformat" => {
1969            let format = tick_format_from_value(value, builtin, "TickLabelFormat")?;
1970            let meta = axes_metadata_snapshot(handle, axes_index)
1971                .map_err(|err| map_figure_error(builtin, err))?;
1972            let (x_format, y_format) = match kind {
1973                PlotObjectKind::XAxis => (Some(format), meta.y_tick_format),
1974                PlotObjectKind::YAxis => (meta.x_tick_format, Some(format)),
1975                _ => {
1976                    return Err(plotting_error(
1977                        builtin,
1978                        format!("{builtin}: invalid ruler handle"),
1979                    ))
1980                }
1981            };
1982            set_axis_tick_formats_for_axes(handle, axes_index, x_format, y_format)
1983                .map_err(|err| map_figure_error(builtin, err))?;
1984            Ok(())
1985        }
1986        "ticklabelrotation" => {
1987            let angle = tick_angle_from_value(value, builtin, "TickLabelRotation")?;
1988            let meta = axes_metadata_snapshot(handle, axes_index)
1989                .map_err(|err| map_figure_error(builtin, err))?;
1990            let (x_angle, y_angle) = match kind {
1991                PlotObjectKind::XAxis => (Some(angle), meta.y_tick_label_rotation),
1992                PlotObjectKind::YAxis => (meta.x_tick_label_rotation, Some(angle)),
1993                _ => {
1994                    return Err(plotting_error(
1995                        builtin,
1996                        format!("{builtin}: invalid ruler handle"),
1997                    ))
1998                }
1999            };
2000            set_axis_tick_angles_for_axes(handle, axes_index, x_angle, y_angle)
2001                .map_err(|err| map_figure_error(builtin, err))?;
2002            Ok(())
2003        }
2004        other => Err(plotting_error(
2005            builtin,
2006            format!("{builtin}: unsupported ruler property `{other}`"),
2007        )),
2008    }
2009}
2010
2011fn apply_figure_property(
2012    figure_handle: FigureHandle,
2013    key: &str,
2014    value: &Value,
2015    builtin: &'static str,
2016) -> BuiltinResult<bool> {
2017    let opts = LineStyleParseOptions::generic(builtin);
2018    match key {
2019        "name" => {
2020            let name = value_as_text_string(value)
2021                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Name must be text")))?;
2022            set_figure_name(figure_handle, name).map_err(|err| map_figure_error(builtin, err))?;
2023            Ok(true)
2024        }
2025        "numbertitle" => {
2026            let enabled = value_as_bool(value).ok_or_else(|| {
2027                plotting_error(builtin, format!("{builtin}: NumberTitle must be logical"))
2028            })?;
2029            set_figure_number_title(figure_handle, enabled)
2030                .map_err(|err| map_figure_error(builtin, err))?;
2031            Ok(true)
2032        }
2033        "visible" => {
2034            let visible = value_as_bool(value).ok_or_else(|| {
2035                plotting_error(builtin, format!("{builtin}: Visible must be logical"))
2036            })?;
2037            let _ = set_figure_visible(figure_handle, visible)
2038                .map_err(|err| map_figure_error(builtin, err))?;
2039            Ok(true)
2040        }
2041        "position" => {
2042            let position = parse_figure_position(value, builtin)?;
2043            set_figure_position(figure_handle, position)
2044                .map_err(|err| map_figure_error(builtin, err))?;
2045            Ok(true)
2046        }
2047        "color" => {
2048            let color = parse_color_value(&opts, value)?;
2049            set_figure_background_color(figure_handle, color)
2050                .map_err(|err| map_figure_error(builtin, err))?;
2051            Ok(true)
2052        }
2053        "tag" => {
2054            let tag = value_as_text_string(value)
2055                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Tag must be text")))?;
2056            set_figure_tag(figure_handle, tag).map_err(|err| map_figure_error(builtin, err))?;
2057            Ok(true)
2058        }
2059        "currentaxes" => {
2060            let resolved = resolve_plot_handle(value, builtin)?;
2061            let PlotHandle::Axes(fig, axes_index) = resolved else {
2062                return Err(plotting_error(
2063                    builtin,
2064                    format!("{builtin}: CurrentAxes must be an axes handle"),
2065                ));
2066            };
2067            if fig != figure_handle {
2068                return Err(plotting_error(
2069                    builtin,
2070                    format!("{builtin}: CurrentAxes must belong to the target figure"),
2071                ));
2072            }
2073            select_axes_for_figure(figure_handle, axes_index)
2074                .map_err(|err| map_figure_error(builtin, err))?;
2075            Ok(true)
2076        }
2077        "sgtitle" => {
2078            apply_figure_text_alias(figure_handle, PlotObjectKind::SuperTitle, value, builtin)?;
2079            Ok(true)
2080        }
2081        other => Err(plotting_error(
2082            builtin,
2083            format!("{builtin}: unsupported figure property `{other}`"),
2084        )),
2085    }
2086}
2087
2088pub(crate) fn validate_figure_property_value(
2089    key_value: &Value,
2090    property_value: &Value,
2091    target_figure: Option<FigureHandle>,
2092    builtin: &'static str,
2093) -> BuiltinResult<()> {
2094    let key = property_name(key_value, builtin)?;
2095    let opts = LineStyleParseOptions::generic(builtin);
2096    match key.as_str() {
2097        "name" => {
2098            value_as_text_string(property_value)
2099                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Name must be text")))?;
2100        }
2101        "numbertitle" => {
2102            value_as_bool(property_value).ok_or_else(|| {
2103                plotting_error(builtin, format!("{builtin}: NumberTitle must be logical"))
2104            })?;
2105        }
2106        "visible" => {
2107            value_as_bool(property_value).ok_or_else(|| {
2108                plotting_error(builtin, format!("{builtin}: Visible must be logical"))
2109            })?;
2110        }
2111        "position" => {
2112            let _ = parse_figure_position(property_value, builtin)?;
2113        }
2114        "color" => {
2115            let _ = parse_color_value(&opts, property_value)?;
2116        }
2117        "tag" => {
2118            value_as_text_string(property_value)
2119                .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Tag must be text")))?;
2120        }
2121        "currentaxes" => {
2122            let resolved = resolve_plot_handle(property_value, builtin)?;
2123            let PlotHandle::Axes(fig, _) = resolved else {
2124                return Err(plotting_error(
2125                    builtin,
2126                    format!("{builtin}: CurrentAxes must be an axes handle"),
2127                ));
2128            };
2129            let Some(target) = target_figure else {
2130                return Err(plotting_error(
2131                    builtin,
2132                    format!("{builtin}: CurrentAxes requires an existing target figure"),
2133                ));
2134            };
2135            if fig != target {
2136                return Err(plotting_error(
2137                    builtin,
2138                    format!("{builtin}: CurrentAxes must belong to the target figure"),
2139                ));
2140            }
2141        }
2142        "sgtitle" => {
2143            validate_figure_text_alias(PlotObjectKind::SuperTitle, property_value, builtin)?
2144        }
2145        other => {
2146            return Err(plotting_error(
2147                builtin,
2148                format!("{builtin}: unsupported figure property `{other}`"),
2149            ));
2150        }
2151    }
2152    Ok(())
2153}
2154
2155fn get_histogram_property(
2156    hist: &super::state::HistogramHandleState,
2157    property: Option<&str>,
2158    builtin: &'static str,
2159) -> BuiltinResult<Value> {
2160    let normalized =
2161        apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &hist.normalization);
2162    match property.map(canonical_property_name).as_deref() {
2163        None => {
2164            let mut st = StructValue::new();
2165            st.insert("Type", Value::String("histogram".into()));
2166            st.insert(
2167                "Parent",
2168                Value::Num(super::state::encode_axes_handle(
2169                    hist.figure,
2170                    hist.axes_index,
2171                )),
2172            );
2173            st.insert("Children", handles_value(Vec::new()));
2174            st.insert("BinEdges", tensor_from_vec(hist.bin_edges.clone()));
2175            st.insert("BinCounts", tensor_from_vec(normalized));
2176            st.insert(
2177                "Values",
2178                tensor_from_vec(apply_histogram_normalization(
2179                    &hist.raw_counts,
2180                    &hist.bin_edges,
2181                    &hist.normalization,
2182                )),
2183            );
2184            if let Some(data) = &hist.metadata.data {
2185                st.insert("Data", tensor_from_vec(data.clone()));
2186            }
2187            st.insert("Normalization", Value::String(hist.normalization.clone()));
2188            st.insert("NumBins", Value::Num(hist.raw_counts.len() as f64));
2189            st.insert("BinWidth", Value::Num(hist.metadata.bin_width));
2190            st.insert(
2191                "BinLimits",
2192                tensor_from_vec(vec![hist.metadata.bin_limits.0, hist.metadata.bin_limits.1]),
2193            );
2194            st.insert("FaceColor", Value::String(hist.metadata.face_color.clone()));
2195            st.insert("FaceAlpha", Value::Num(hist.metadata.face_alpha));
2196            st.insert("EdgeColor", Value::String(hist.metadata.edge_color.clone()));
2197            st.insert(
2198                "DisplayStyle",
2199                Value::String(hist.metadata.display_style.clone()),
2200            );
2201            st.insert(
2202                "DisplayName",
2203                Value::String(hist.display_name.clone().unwrap_or_default()),
2204            );
2205            Ok(Value::Struct(st))
2206        }
2207        Some("type") => Ok(Value::String("histogram".into())),
2208        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2209            hist.figure,
2210            hist.axes_index,
2211        ))),
2212        Some("children") => Ok(handles_value(Vec::new())),
2213        Some("binedges") => Ok(tensor_from_vec(hist.bin_edges.clone())),
2214        Some("bincounts") => Ok(tensor_from_vec(normalized)),
2215        Some("values") => Ok(tensor_from_vec(apply_histogram_normalization(
2216            &hist.raw_counts,
2217            &hist.bin_edges,
2218            &hist.normalization,
2219        ))),
2220        Some("data") => Ok(tensor_from_vec(
2221            hist.metadata.data.clone().unwrap_or_default(),
2222        )),
2223        Some("normalization") => Ok(Value::String(hist.normalization.clone())),
2224        Some("numbins") => Ok(Value::Num(hist.raw_counts.len() as f64)),
2225        Some("binwidth") => Ok(Value::Num(hist.metadata.bin_width)),
2226        Some("binlimits") => Ok(tensor_from_vec(vec![
2227            hist.metadata.bin_limits.0,
2228            hist.metadata.bin_limits.1,
2229        ])),
2230        Some("facecolor") => Ok(Value::String(hist.metadata.face_color.clone())),
2231        Some("facealpha") => Ok(Value::Num(hist.metadata.face_alpha)),
2232        Some("edgecolor") => Ok(Value::String(hist.metadata.edge_color.clone())),
2233        Some("displaystyle") => Ok(Value::String(hist.metadata.display_style.clone())),
2234        Some("displayname") => Ok(Value::String(hist.display_name.clone().unwrap_or_default())),
2235        Some(other) => Err(plotting_error(
2236            builtin,
2237            format!("{builtin}: unsupported histogram property `{other}`"),
2238        )),
2239    }
2240}
2241
2242fn get_histogram2_property(
2243    hist: &super::state::Histogram2HandleState,
2244    property: Option<&str>,
2245    builtin: &'static str,
2246) -> BuiltinResult<Value> {
2247    match property.map(canonical_property_name).as_deref() {
2248        None => {
2249            let mut st = child_base_struct("histogram2", hist.figure, hist.axes_index);
2250            st.insert("Values", Value::Tensor(hist.values.clone()));
2251            st.insert("BinCounts", Value::Tensor(hist.values.clone()));
2252            st.insert("XBinEdges", tensor_from_vec(hist.x_bin_edges.clone()));
2253            st.insert("YBinEdges", tensor_from_vec(hist.y_bin_edges.clone()));
2254            st.insert("NumBins", tensor_from_vec(histogram2_num_bins(hist)));
2255            st.insert("Normalization", Value::String(hist.normalization.clone()));
2256            st.insert(
2257                "DisplayStyle",
2258                Value::String(hist.display_style.as_str().into()),
2259            );
2260            st.insert("ShowEmptyBins", Value::Bool(hist.show_empty_bins));
2261            st.insert("FaceAlpha", Value::Num(hist.face_alpha));
2262            st.insert(
2263                "DisplayName",
2264                Value::String(hist.display_name.clone().unwrap_or_default()),
2265            );
2266            Ok(Value::Struct(st))
2267        }
2268        Some("type") => Ok(Value::String("histogram2".into())),
2269        Some("parent") => Ok(child_parent_handle(hist.figure, hist.axes_index)),
2270        Some("children") => Ok(handles_value(Vec::new())),
2271        Some("values") | Some("bincounts") | Some("bindata") => {
2272            Ok(Value::Tensor(hist.values.clone()))
2273        }
2274        Some("xbinedges") => Ok(tensor_from_vec(hist.x_bin_edges.clone())),
2275        Some("ybinedges") => Ok(tensor_from_vec(hist.y_bin_edges.clone())),
2276        Some("numbins") => Ok(tensor_from_vec(histogram2_num_bins(hist))),
2277        Some("normalization") => Ok(Value::String(hist.normalization.clone())),
2278        Some("displaystyle") => Ok(Value::String(hist.display_style.as_str().into())),
2279        Some("showemptybins") => Ok(Value::Bool(hist.show_empty_bins)),
2280        Some("facealpha") => Ok(Value::Num(hist.face_alpha)),
2281        Some("displayname") => Ok(Value::String(hist.display_name.clone().unwrap_or_default())),
2282        Some(other) => Err(plotting_error(
2283            builtin,
2284            format!("{builtin}: unsupported histogram2 property `{other}`"),
2285        )),
2286    }
2287}
2288
2289fn histogram2_num_bins(hist: &super::state::Histogram2HandleState) -> Vec<f64> {
2290    vec![
2291        hist.x_bin_edges.len().saturating_sub(1) as f64,
2292        hist.y_bin_edges.len().saturating_sub(1) as f64,
2293    ]
2294}
2295
2296fn get_binscatter_property(
2297    binscatter: &super::state::BinscatterHandleState,
2298    property: Option<&str>,
2299    builtin: &'static str,
2300) -> BuiltinResult<Value> {
2301    match property.map(canonical_property_name).as_deref() {
2302        None => {
2303            let mut st = child_base_struct("binscatter", binscatter.figure, binscatter.axes_index);
2304            st.insert("Values", Value::Tensor(binscatter.values.clone()));
2305            st.insert("XData", tensor_from_vec(binscatter.x_data.clone()));
2306            st.insert("YData", tensor_from_vec(binscatter.y_data.clone()));
2307            st.insert("XBinEdges", tensor_from_vec(binscatter.x_bin_edges.clone()));
2308            st.insert("YBinEdges", tensor_from_vec(binscatter.y_bin_edges.clone()));
2309            st.insert("NumBins", tensor_from_vec(num_bins_value(binscatter)));
2310            st.insert("XLimits", tensor_from_vec(binscatter_x_limits(binscatter)));
2311            st.insert("YLimits", tensor_from_vec(binscatter_y_limits(binscatter)));
2312            st.insert("ShowEmptyBins", Value::Bool(binscatter.show_empty_bins));
2313            st.insert("FaceAlpha", Value::Num(binscatter.face_alpha));
2314            st.insert(
2315                "DisplayName",
2316                Value::String(binscatter.display_name.clone().unwrap_or_default()),
2317            );
2318            Ok(Value::Struct(st))
2319        }
2320        Some("type") => Ok(Value::String("binscatter".into())),
2321        Some("parent") => Ok(child_parent_handle(
2322            binscatter.figure,
2323            binscatter.axes_index,
2324        )),
2325        Some("children") => Ok(handles_value(Vec::new())),
2326        Some("values") | Some("bindata") => Ok(Value::Tensor(binscatter.values.clone())),
2327        Some("xdata") => Ok(tensor_from_vec(binscatter.x_data.clone())),
2328        Some("ydata") => Ok(tensor_from_vec(binscatter.y_data.clone())),
2329        Some("xbinedges") => Ok(tensor_from_vec(binscatter.x_bin_edges.clone())),
2330        Some("ybinedges") => Ok(tensor_from_vec(binscatter.y_bin_edges.clone())),
2331        Some("numbins") => Ok(tensor_from_vec(num_bins_value(binscatter))),
2332        Some("xlimits") => Ok(tensor_from_vec(binscatter_x_limits(binscatter))),
2333        Some("ylimits") => Ok(tensor_from_vec(binscatter_y_limits(binscatter))),
2334        Some("showemptybins") => Ok(Value::Bool(binscatter.show_empty_bins)),
2335        Some("facealpha") => Ok(Value::Num(binscatter.face_alpha)),
2336        Some("displayname") => Ok(Value::String(
2337            binscatter.display_name.clone().unwrap_or_default(),
2338        )),
2339        Some(other) => Err(plotting_error(
2340            builtin,
2341            format!("{builtin}: unsupported binscatter property `{other}`"),
2342        )),
2343    }
2344}
2345
2346fn num_bins_value(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2347    vec![binscatter.num_bins[0] as f64, binscatter.num_bins[1] as f64]
2348}
2349
2350fn binscatter_x_limits(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2351    vec![binscatter.x_limits.0, binscatter.x_limits.1]
2352}
2353
2354fn binscatter_y_limits(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2355    vec![binscatter.y_limits.0, binscatter.y_limits.1]
2356}
2357
2358fn get_plot_child_property(
2359    state: &super::state::PlotChildHandleState,
2360    property: Option<&str>,
2361    builtin: &'static str,
2362) -> BuiltinResult<Value> {
2363    match state {
2364        super::state::PlotChildHandleState::Histogram(hist) => {
2365            get_histogram_property(hist, property, builtin)
2366        }
2367        super::state::PlotChildHandleState::Histogram2(hist) => {
2368            get_histogram2_property(hist, property, builtin)
2369        }
2370        super::state::PlotChildHandleState::Line(plot) => {
2371            get_line_property(plot, property, builtin)
2372        }
2373        super::state::PlotChildHandleState::AnimatedLine(plot) => {
2374            get_animated_line_property(plot, property, builtin)
2375        }
2376        super::state::PlotChildHandleState::Scatter(plot) => {
2377            get_scatter_property(plot, property, builtin)
2378        }
2379        super::state::PlotChildHandleState::Bar(plot) => get_bar_property(plot, property, builtin),
2380        super::state::PlotChildHandleState::Stem(stem) => {
2381            get_stem_property(stem, property, builtin)
2382        }
2383        super::state::PlotChildHandleState::ErrorBar(errorbar) => {
2384            get_errorbar_property(errorbar, property, builtin)
2385        }
2386        super::state::PlotChildHandleState::Stairs(plot) => {
2387            get_stairs_property(plot, property, builtin)
2388        }
2389        super::state::PlotChildHandleState::Quiver(quiver) => {
2390            get_quiver_property(quiver, property, builtin)
2391        }
2392        super::state::PlotChildHandleState::Image(image) => {
2393            get_image_property(image, property, builtin)
2394        }
2395        super::state::PlotChildHandleState::Heatmap(heatmap) => {
2396            get_heatmap_property(heatmap, property, builtin)
2397        }
2398        super::state::PlotChildHandleState::Binscatter(binscatter) => {
2399            get_binscatter_property(binscatter, property, builtin)
2400        }
2401        super::state::PlotChildHandleState::FunctionSurface(function_surface) => {
2402            get_function_surface_property(function_surface, property, builtin)
2403        }
2404        super::state::PlotChildHandleState::FunctionContour(function_contour) => {
2405            get_function_contour_property(function_contour, property, builtin)
2406        }
2407        super::state::PlotChildHandleState::Area(area) => {
2408            get_area_property(area, property, builtin)
2409        }
2410        super::state::PlotChildHandleState::Surface(plot) => {
2411            get_surface_property(plot, property, builtin)
2412        }
2413        super::state::PlotChildHandleState::Patch(plot) => {
2414            get_patch_property(plot, property, builtin)
2415        }
2416        super::state::PlotChildHandleState::Line3(plot) => {
2417            get_line3_property(plot, property, builtin)
2418        }
2419        super::state::PlotChildHandleState::Scatter3(plot) => {
2420            get_scatter3_property(plot, property, builtin)
2421        }
2422        super::state::PlotChildHandleState::Contour(plot) => {
2423            get_contour_property(plot, property, builtin)
2424        }
2425        super::state::PlotChildHandleState::ContourFill(plot) => {
2426            get_contour_fill_property(plot, property, builtin)
2427        }
2428        super::state::PlotChildHandleState::ReferenceLine(plot) => {
2429            get_reference_line_property(plot, property, builtin)
2430        }
2431        super::state::PlotChildHandleState::Pie(plot) => get_pie_property(plot, property, builtin),
2432        super::state::PlotChildHandleState::Text(text) => {
2433            get_world_text_property(text, property, builtin)
2434        }
2435        super::state::PlotChildHandleState::TextScatter(textscatter) => {
2436            crate::builtins::plotting::textscatter::get_textscatter_property(
2437                textscatter,
2438                property,
2439                builtin,
2440            )
2441        }
2442        super::state::PlotChildHandleState::WordCloud(wordcloud) => {
2443            crate::builtins::plotting::wordcloud::get_wordcloud_property(
2444                wordcloud, property, builtin,
2445            )
2446        }
2447        super::state::PlotChildHandleState::StackedPlot(stacked) => {
2448            crate::builtins::plotting::stackedplot::get_stackedplot_property(
2449                stacked, property, builtin,
2450            )
2451        }
2452    }
2453}
2454
2455fn apply_plot_child_property(
2456    handle: f64,
2457    state: &super::state::PlotChildHandleState,
2458    key: &str,
2459    value: &Value,
2460    builtin: &'static str,
2461) -> BuiltinResult<()> {
2462    match state {
2463        super::state::PlotChildHandleState::Histogram(hist) => {
2464            apply_histogram_property(hist, key, value, builtin)
2465        }
2466        super::state::PlotChildHandleState::Histogram2(hist) => {
2467            apply_histogram2_property(hist, key, value, builtin)
2468        }
2469        super::state::PlotChildHandleState::Line(plot) => {
2470            apply_line_property(plot, key, value, builtin)
2471        }
2472        super::state::PlotChildHandleState::AnimatedLine(plot) => {
2473            apply_animated_line_property(plot, key, value, builtin)
2474        }
2475        super::state::PlotChildHandleState::Scatter(plot) => {
2476            apply_scatter_property(plot, key, value, builtin)
2477        }
2478        super::state::PlotChildHandleState::Bar(plot) => {
2479            apply_bar_property(plot, key, value, builtin)
2480        }
2481        super::state::PlotChildHandleState::Stem(stem) => {
2482            apply_stem_property(stem, key, value, builtin)
2483        }
2484        super::state::PlotChildHandleState::ErrorBar(errorbar) => {
2485            apply_errorbar_property(errorbar, key, value, builtin)
2486        }
2487        super::state::PlotChildHandleState::Stairs(plot) => {
2488            apply_stairs_property(plot, key, value, builtin)
2489        }
2490        super::state::PlotChildHandleState::Quiver(quiver) => {
2491            apply_quiver_property(quiver, key, value, builtin)
2492        }
2493        super::state::PlotChildHandleState::Image(image) => {
2494            apply_image_property(image, key, value, builtin)
2495        }
2496        super::state::PlotChildHandleState::Heatmap(heatmap) => {
2497            apply_heatmap_property(heatmap, key, value, builtin)
2498        }
2499        super::state::PlotChildHandleState::Binscatter(binscatter) => {
2500            apply_binscatter_property(binscatter, key, value, builtin)
2501        }
2502        super::state::PlotChildHandleState::FunctionSurface(function_surface) => {
2503            apply_function_surface_property(function_surface, key, value, builtin)
2504        }
2505        super::state::PlotChildHandleState::FunctionContour(function_contour) => {
2506            apply_function_contour_property(function_contour, key, value, builtin)
2507        }
2508        super::state::PlotChildHandleState::Area(area) => {
2509            apply_area_property(area, key, value, builtin)
2510        }
2511        super::state::PlotChildHandleState::Surface(plot) => {
2512            apply_surface_property(plot, key, value, builtin)
2513        }
2514        super::state::PlotChildHandleState::Patch(plot) => {
2515            apply_patch_property(plot, key, value, builtin)
2516        }
2517        super::state::PlotChildHandleState::Line3(plot) => {
2518            apply_line3_property(plot, key, value, builtin)
2519        }
2520        super::state::PlotChildHandleState::Scatter3(plot) => {
2521            apply_scatter3_property(plot, key, value, builtin)
2522        }
2523        super::state::PlotChildHandleState::Contour(plot) => {
2524            apply_contour_property(plot, key, value, builtin)
2525        }
2526        super::state::PlotChildHandleState::ContourFill(plot) => {
2527            apply_contour_fill_property(plot, key, value, builtin)
2528        }
2529        super::state::PlotChildHandleState::ReferenceLine(plot) => {
2530            apply_reference_line_property(plot, key, value, builtin)
2531        }
2532        super::state::PlotChildHandleState::Pie(plot) => {
2533            apply_pie_property(plot, key, value, builtin)
2534        }
2535        super::state::PlotChildHandleState::Text(text) => {
2536            apply_world_text_property(text, key, value, builtin)
2537        }
2538        super::state::PlotChildHandleState::TextScatter(textscatter) => {
2539            crate::builtins::plotting::textscatter::apply_textscatter_property(
2540                handle,
2541                textscatter,
2542                key,
2543                value,
2544                builtin,
2545            )
2546        }
2547        super::state::PlotChildHandleState::WordCloud(wordcloud) => {
2548            crate::builtins::plotting::wordcloud::apply_wordcloud_property(
2549                handle, wordcloud, key, value, builtin,
2550            )
2551        }
2552        super::state::PlotChildHandleState::StackedPlot(stacked) => {
2553            crate::builtins::plotting::stackedplot::apply_stackedplot_property(
2554                handle, stacked, key, value, builtin,
2555            )
2556        }
2557    }
2558}
2559
2560fn apply_plot_child_properties(
2561    handle: f64,
2562    state: &super::state::PlotChildHandleState,
2563    args: &[Value],
2564    builtin: &'static str,
2565) -> BuiltinResult<()> {
2566    let mut pairs = Vec::with_capacity(args.len() / 2);
2567    for pair in args.chunks_exact(2) {
2568        pairs.push((property_name(&pair[0], builtin)?, &pair[1]));
2569    }
2570    match state {
2571        super::state::PlotChildHandleState::Line(plot) => {
2572            apply_line_properties(plot, &pairs, builtin)
2573        }
2574        super::state::PlotChildHandleState::AnimatedLine(plot) => {
2575            apply_animated_line_properties(plot, &pairs, builtin)
2576        }
2577        super::state::PlotChildHandleState::Line3(plot) => {
2578            apply_line3_properties(plot, &pairs, builtin)
2579        }
2580        super::state::PlotChildHandleState::Histogram2(hist) => {
2581            apply_histogram2_properties(hist, &pairs, builtin)
2582        }
2583        super::state::PlotChildHandleState::Scatter(plot) => {
2584            apply_scatter_properties(plot, &pairs, builtin)
2585        }
2586        _ => {
2587            for (key, value) in pairs {
2588                apply_plot_child_property(handle, state, &key, value, builtin)?;
2589            }
2590            Ok(())
2591        }
2592    }
2593}
2594
2595fn child_parent_handle(figure: FigureHandle, axes_index: usize) -> Value {
2596    Value::Num(super::state::encode_axes_handle(figure, axes_index))
2597}
2598
2599fn child_base_struct(kind: &str, figure: FigureHandle, axes_index: usize) -> StructValue {
2600    let mut st = StructValue::new();
2601    st.insert("Type", Value::String(kind.into()));
2602    st.insert("Parent", child_parent_handle(figure, axes_index));
2603    st.insert("Children", handles_value(Vec::new()));
2604    st
2605}
2606
2607fn figure_position_value(position: [f64; 4]) -> Value {
2608    Value::Tensor(Tensor {
2609        rows: 1,
2610        cols: 4,
2611        shape: vec![1, 4],
2612        data: position.to_vec(),
2613        integer_data: None,
2614        dtype: runmat_builtins::NumericDType::F64,
2615    })
2616}
2617
2618fn parse_figure_position(value: &Value, builtin: &'static str) -> BuiltinResult<[f64; 4]> {
2619    let values = match value {
2620        Value::Tensor(t) if t.data.len() == 4 && is_figure_position_vector_shape(t) => &t.data,
2621        _ => {
2622            return Err(plotting_error(
2623                builtin,
2624                format!("{builtin}: Position must be a 4-element numeric vector"),
2625            ))
2626        }
2627    };
2628    let mut position = [0.0; 4];
2629    position.copy_from_slice(values);
2630    if !position.iter().all(|value| value.is_finite()) {
2631        return Err(plotting_error(
2632            builtin,
2633            format!("{builtin}: Position values must be finite"),
2634        ));
2635    }
2636    if position[2] <= 0.0 || position[3] <= 0.0 {
2637        return Err(plotting_error(
2638            builtin,
2639            format!("{builtin}: Position width and height must be positive"),
2640        ));
2641    }
2642    Ok(position)
2643}
2644
2645fn is_figure_position_vector_shape(tensor: &Tensor) -> bool {
2646    tensor.shape.len() <= 1 || (tensor.shape.len() == 2 && (tensor.rows == 1 || tensor.cols == 1))
2647}
2648
2649fn text_position_value(position: glam::Vec3) -> Value {
2650    Value::Tensor(Tensor {
2651        rows: 1,
2652        cols: 3,
2653        shape: vec![1, 3],
2654        data: vec![position.x as f64, position.y as f64, position.z as f64],
2655        integer_data: None,
2656        dtype: runmat_builtins::NumericDType::F64,
2657    })
2658}
2659
2660fn parse_text_position(value: &Value, builtin: &'static str) -> BuiltinResult<glam::Vec3> {
2661    match value {
2662        Value::Tensor(t) if t.data.len() == 2 || t.data.len() == 3 => Ok(glam::Vec3::new(
2663            t.data[0] as f32,
2664            t.data[1] as f32,
2665            t.data.get(2).copied().unwrap_or(0.0) as f32,
2666        )),
2667        _ => Err(plotting_error(
2668            builtin,
2669            format!("{builtin}: Position must be a 2-element or 3-element vector"),
2670        )),
2671    }
2672}
2673
2674fn get_world_text_property(
2675    handle: &super::state::TextAnnotationHandleState,
2676    property: Option<&str>,
2677    builtin: &'static str,
2678) -> BuiltinResult<Value> {
2679    let figure = super::state::clone_figure(handle.figure)
2680        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
2681    let annotation = figure
2682        .axes_text_annotation(handle.axes_index, handle.annotation_index)
2683        .cloned()
2684        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
2685    match property.map(canonical_property_name).as_deref() {
2686        None => {
2687            let mut st = child_base_struct("text", handle.figure, handle.axes_index);
2688            st.insert("String", Value::String(annotation.text.clone()));
2689            st.insert("Position", text_position_value(annotation.position));
2690            if let Some(weight) = annotation.style.font_weight.clone() {
2691                st.insert("FontWeight", Value::String(weight));
2692            }
2693            if let Some(angle) = annotation.style.font_angle.clone() {
2694                st.insert("FontAngle", Value::String(angle));
2695            }
2696            if let Some(interpreter) = annotation.style.interpreter.clone() {
2697                st.insert("Interpreter", Value::String(interpreter));
2698            }
2699            if let Some(color) = annotation.style.color {
2700                st.insert("Color", Value::String(color_to_short_name(color)));
2701            }
2702            if let Some(font_size) = annotation.style.font_size {
2703                st.insert("FontSize", Value::Num(font_size as f64));
2704            }
2705            st.insert("Visible", Value::Bool(annotation.style.visible));
2706            Ok(Value::Struct(st))
2707        }
2708        Some("type") => Ok(Value::String("text".into())),
2709        Some("parent") => Ok(child_parent_handle(handle.figure, handle.axes_index)),
2710        Some("children") => Ok(handles_value(Vec::new())),
2711        Some("string") => Ok(Value::String(annotation.text)),
2712        Some("position") => Ok(text_position_value(annotation.position)),
2713        Some("fontweight") => Ok(annotation
2714            .style
2715            .font_weight
2716            .map(Value::String)
2717            .unwrap_or_else(|| Value::String(String::new()))),
2718        Some("fontangle") => Ok(annotation
2719            .style
2720            .font_angle
2721            .map(Value::String)
2722            .unwrap_or_else(|| Value::String(String::new()))),
2723        Some("interpreter") => Ok(annotation
2724            .style
2725            .interpreter
2726            .map(Value::String)
2727            .unwrap_or_else(|| Value::String(String::new()))),
2728        Some("color") => Ok(annotation
2729            .style
2730            .color
2731            .map(|c| Value::String(color_to_short_name(c)))
2732            .unwrap_or_else(|| Value::String(String::new()))),
2733        Some("fontsize") => Ok(Value::Num(
2734            annotation.style.font_size.unwrap_or_default() as f64
2735        )),
2736        Some("visible") => Ok(Value::Bool(annotation.style.visible)),
2737        Some(other) => Err(plotting_error(
2738            builtin,
2739            format!("{builtin}: unsupported text property `{other}`"),
2740        )),
2741    }
2742}
2743
2744fn apply_world_text_property(
2745    handle: &super::state::TextAnnotationHandleState,
2746    key: &str,
2747    value: &Value,
2748    builtin: &'static str,
2749) -> BuiltinResult<()> {
2750    let figure = super::state::clone_figure(handle.figure)
2751        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
2752    let annotation = figure
2753        .axes_text_annotation(handle.axes_index, handle.annotation_index)
2754        .cloned()
2755        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
2756    let mut text = None;
2757    let mut position = None;
2758    let mut style = annotation.style;
2759    match canonical_property_name(key).as_ref() {
2760        "string" => {
2761            text = Some(value_as_text_string(value).ok_or_else(|| {
2762                plotting_error(builtin, format!("{builtin}: String must be text"))
2763            })?);
2764        }
2765        "position" => position = Some(parse_text_position(value, builtin)?),
2766        other => apply_text_property(&mut text, &mut style, other, value, builtin)?,
2767    }
2768    set_text_annotation_properties_for_axes(
2769        handle.figure,
2770        handle.axes_index,
2771        handle.annotation_index,
2772        text,
2773        position,
2774        Some(style),
2775    )
2776    .map_err(|err| map_figure_error(builtin, err))?;
2777    Ok(())
2778}
2779
2780fn get_simple_plot(
2781    plot: &super::state::SimplePlotHandleState,
2782    builtin: &'static str,
2783) -> BuiltinResult<runmat_plot::plots::figure::PlotElement> {
2784    let figure = super::state::clone_figure(plot.figure)
2785        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot figure")))?;
2786    let resolved = figure
2787        .plots()
2788        .nth(plot.plot_index)
2789        .cloned()
2790        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot handle")))?;
2791    Ok(resolved)
2792}
2793
2794fn get_line_property(
2795    line_handle: &super::state::SimplePlotHandleState,
2796    property: Option<&str>,
2797    builtin: &'static str,
2798) -> BuiltinResult<Value> {
2799    let plot = get_simple_plot(line_handle, builtin)?;
2800    let runmat_plot::plots::figure::PlotElement::Line(line) = plot else {
2801        return Err(plotting_error(
2802            builtin,
2803            format!("{builtin}: invalid line handle"),
2804        ));
2805    };
2806    let (x_data, y_data) = line_xy_data_for_properties(&line, builtin)?;
2807    match property.map(canonical_property_name).as_deref() {
2808        None => {
2809            let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
2810            st.insert("XData", tensor_from_vec(x_data.clone()));
2811            st.insert("YData", tensor_from_vec(y_data.clone()));
2812            st.insert("Color", Value::String(color_to_short_name(line.color)));
2813            st.insert("LineWidth", Value::Num(line.line_width as f64));
2814            st.insert(
2815                "LineStyle",
2816                Value::String(line_style_name(line.line_style).into()),
2817            );
2818            st.insert(
2819                "DisplayName",
2820                Value::String(line.label.clone().unwrap_or_default()),
2821            );
2822            st.insert(
2823                "HandleVisibility",
2824                Value::String(line.handle_visibility.clone()),
2825            );
2826            st.insert(
2827                "Visible",
2828                Value::String(if line.visible { "on" } else { "off" }.into()),
2829            );
2830            insert_line_marker_struct_props(&mut st, line.marker.as_ref());
2831            Ok(Value::Struct(st))
2832        }
2833        Some("type") => Ok(Value::String("line".into())),
2834        Some("parent") => Ok(child_parent_handle(
2835            line_handle.figure,
2836            line_handle.axes_index,
2837        )),
2838        Some("children") => Ok(handles_value(Vec::new())),
2839        Some("xdata") => Ok(tensor_from_vec(x_data)),
2840        Some("ydata") => Ok(tensor_from_vec(y_data)),
2841        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2842        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2843        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2844        Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2845        Some("handlevisibility") => Ok(Value::String(line.handle_visibility)),
2846        Some("visible") => Ok(Value::String(
2847            if line.visible { "on" } else { "off" }.into(),
2848        )),
2849        Some(name) => line_marker_property_value(&line.marker, name, builtin),
2850    }
2851}
2852
2853fn get_animated_line_property(
2854    line_handle: &super::state::AnimatedLineHandleState,
2855    property: Option<&str>,
2856    builtin: &'static str,
2857) -> BuiltinResult<Value> {
2858    let simple = animated_line_simple_state(line_handle);
2859    let plot = get_simple_plot(&simple, builtin)?;
2860    let property = property.map(canonical_property_name);
2861    match plot {
2862        runmat_plot::plots::figure::PlotElement::Line(line) => {
2863            let (x_data, y_data) = line_xy_data_for_properties(&line, builtin)?;
2864            match property.as_deref() {
2865                None => {
2866                    let mut st = child_base_struct(
2867                        "animatedline",
2868                        line_handle.figure,
2869                        line_handle.axes_index,
2870                    );
2871                    st.insert("XData", tensor_from_vec(x_data));
2872                    st.insert("YData", tensor_from_vec(y_data));
2873                    st.insert("Color", Value::String(color_to_short_name(line.color)));
2874                    st.insert("LineWidth", Value::Num(line.line_width as f64));
2875                    st.insert(
2876                        "LineStyle",
2877                        Value::String(line_style_name(line.line_style).into()),
2878                    );
2879                    st.insert(
2880                        "DisplayName",
2881                        Value::String(line.label.clone().unwrap_or_default()),
2882                    );
2883                    st.insert(
2884                        "Visible",
2885                        Value::String(if line.visible { "on" } else { "off" }.into()),
2886                    );
2887                    st.insert(
2888                        "MaximumNumPoints",
2889                        animated_line_maximum_value(line_handle.maximum_num_points),
2890                    );
2891                    insert_line_marker_struct_props(&mut st, line.marker.as_ref());
2892                    Ok(Value::Struct(st))
2893                }
2894                Some("type") => Ok(Value::String("animatedline".into())),
2895                Some("parent") => Ok(child_parent_handle(
2896                    line_handle.figure,
2897                    line_handle.axes_index,
2898                )),
2899                Some("children") => Ok(handles_value(Vec::new())),
2900                Some("xdata") => Ok(tensor_from_vec(x_data)),
2901                Some("ydata") => Ok(tensor_from_vec(y_data)),
2902                Some("zdata") => Ok(tensor_from_vec(Vec::new())),
2903                Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2904                Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2905                Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2906                Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2907                Some("visible") => Ok(Value::String(
2908                    if line.visible { "on" } else { "off" }.into(),
2909                )),
2910                Some("maximumnumpoints") => {
2911                    Ok(animated_line_maximum_value(line_handle.maximum_num_points))
2912                }
2913                Some(name) => line_marker_property_value(&line.marker, name, builtin),
2914            }
2915        }
2916        runmat_plot::plots::figure::PlotElement::Line3(line) => match property.as_deref() {
2917            None => {
2918                let mut st =
2919                    child_base_struct("animatedline", line_handle.figure, line_handle.axes_index);
2920                st.insert("XData", tensor_from_vec(line.x_data.clone()));
2921                st.insert("YData", tensor_from_vec(line.y_data.clone()));
2922                st.insert("ZData", tensor_from_vec(line.z_data.clone()));
2923                st.insert("Color", Value::String(color_to_short_name(line.color)));
2924                st.insert("LineWidth", Value::Num(line.line_width as f64));
2925                st.insert(
2926                    "LineStyle",
2927                    Value::String(line_style_name(line.line_style).into()),
2928                );
2929                st.insert(
2930                    "DisplayName",
2931                    Value::String(line.label.clone().unwrap_or_default()),
2932                );
2933                st.insert(
2934                    "Visible",
2935                    Value::String(if line.visible { "on" } else { "off" }.into()),
2936                );
2937                st.insert(
2938                    "MaximumNumPoints",
2939                    animated_line_maximum_value(line_handle.maximum_num_points),
2940                );
2941                Ok(Value::Struct(st))
2942            }
2943            Some("type") => Ok(Value::String("animatedline".into())),
2944            Some("parent") => Ok(child_parent_handle(
2945                line_handle.figure,
2946                line_handle.axes_index,
2947            )),
2948            Some("children") => Ok(handles_value(Vec::new())),
2949            Some("xdata") => Ok(tensor_from_vec(line.x_data)),
2950            Some("ydata") => Ok(tensor_from_vec(line.y_data)),
2951            Some("zdata") => Ok(tensor_from_vec(line.z_data)),
2952            Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2953            Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2954            Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2955            Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2956            Some("visible") => Ok(Value::String(
2957                if line.visible { "on" } else { "off" }.into(),
2958            )),
2959            Some("maximumnumpoints") => {
2960                Ok(animated_line_maximum_value(line_handle.maximum_num_points))
2961            }
2962            Some(other) => Err(plotting_error(
2963                builtin,
2964                format!("{builtin}: unsupported animatedline property `{other}`"),
2965            )),
2966        },
2967        _ => Err(plotting_error(
2968            builtin,
2969            format!("{builtin}: invalid animatedline handle"),
2970        )),
2971    }
2972}
2973
2974fn animated_line_maximum_value(maximum: Option<usize>) -> Value {
2975    match maximum {
2976        Some(value) => Value::Num(value as f64),
2977        None => Value::Num(f64::INFINITY),
2978    }
2979}
2980
2981fn animated_line_simple_state(
2982    line_handle: &super::state::AnimatedLineHandleState,
2983) -> super::state::SimplePlotHandleState {
2984    super::state::SimplePlotHandleState {
2985        figure: line_handle.figure,
2986        axes_index: line_handle.axes_index,
2987        plot_index: line_handle.plot_index,
2988    }
2989}
2990
2991fn get_reference_line_property(
2992    line_handle: &super::state::SimplePlotHandleState,
2993    property: Option<&str>,
2994    builtin: &'static str,
2995) -> BuiltinResult<Value> {
2996    let plot = get_simple_plot(line_handle, builtin)?;
2997    let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot else {
2998        return Err(plotting_error(
2999            builtin,
3000            format!("{builtin}: invalid reference line handle"),
3001        ));
3002    };
3003    let orientation = match line.orientation {
3004        runmat_plot::plots::ReferenceLineOrientation::Vertical => "vertical",
3005        runmat_plot::plots::ReferenceLineOrientation::Horizontal => "horizontal",
3006    };
3007    match property.map(canonical_property_name).as_deref() {
3008        None => {
3009            let mut st =
3010                child_base_struct("constantline", line_handle.figure, line_handle.axes_index);
3011            st.insert("Value", Value::Num(line.value));
3012            st.insert("Orientation", Value::String(orientation.into()));
3013            st.insert("Color", Value::String(color_to_short_name(line.color)));
3014            st.insert("LineWidth", Value::Num(line.line_width as f64));
3015            st.insert(
3016                "LineStyle",
3017                Value::String(line_style_name(line.line_style).into()),
3018            );
3019            st.insert(
3020                "Label",
3021                Value::String(line.label.clone().unwrap_or_default()),
3022            );
3023            st.insert(
3024                "LabelOrientation",
3025                Value::String(line.label_orientation.clone()),
3026            );
3027            st.insert(
3028                "DisplayName",
3029                Value::String(line.display_name.clone().unwrap_or_default()),
3030            );
3031            st.insert(
3032                "Visible",
3033                Value::String(if line.visible { "on" } else { "off" }.into()),
3034            );
3035            Ok(Value::Struct(st))
3036        }
3037        Some("type") => Ok(Value::String("constantline".into())),
3038        Some("parent") => Ok(child_parent_handle(
3039            line_handle.figure,
3040            line_handle.axes_index,
3041        )),
3042        Some("children") => Ok(handles_value(Vec::new())),
3043        Some("value") => Ok(Value::Num(line.value)),
3044        Some("orientation") => Ok(Value::String(orientation.into())),
3045        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
3046        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
3047        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
3048        Some("label") => Ok(Value::String(line.label.unwrap_or_default())),
3049        Some("labelorientation") => Ok(Value::String(line.label_orientation)),
3050        Some("displayname") => Ok(Value::String(line.display_name.unwrap_or_default())),
3051        Some("visible") => Ok(Value::String(
3052            if line.visible { "on" } else { "off" }.into(),
3053        )),
3054        Some(other) => Err(plotting_error(
3055            builtin,
3056            format!("{builtin}: unsupported reference line property `{other}`"),
3057        )),
3058    }
3059}
3060
3061fn get_stairs_property(
3062    stairs_handle: &super::state::SimplePlotHandleState,
3063    property: Option<&str>,
3064    builtin: &'static str,
3065) -> BuiltinResult<Value> {
3066    let plot = get_simple_plot(stairs_handle, builtin)?;
3067    let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot else {
3068        return Err(plotting_error(
3069            builtin,
3070            format!("{builtin}: invalid stairs handle"),
3071        ));
3072    };
3073    match property.map(canonical_property_name).as_deref() {
3074        None => {
3075            let mut st =
3076                child_base_struct("stairs", stairs_handle.figure, stairs_handle.axes_index);
3077            st.insert("XData", tensor_from_vec(stairs.x.clone()));
3078            st.insert("YData", tensor_from_vec(stairs.y.clone()));
3079            st.insert("Color", Value::String(color_to_short_name(stairs.color)));
3080            st.insert("LineWidth", Value::Num(stairs.line_width as f64));
3081            if let Some(label) = stairs.label.clone() {
3082                st.insert("DisplayName", Value::String(label));
3083            }
3084            Ok(Value::Struct(st))
3085        }
3086        Some("type") => Ok(Value::String("stairs".into())),
3087        Some("parent") => Ok(child_parent_handle(
3088            stairs_handle.figure,
3089            stairs_handle.axes_index,
3090        )),
3091        Some("children") => Ok(handles_value(Vec::new())),
3092        Some("xdata") => Ok(tensor_from_vec(stairs.x.clone())),
3093        Some("ydata") => Ok(tensor_from_vec(stairs.y.clone())),
3094        Some("color") => Ok(Value::String(color_to_short_name(stairs.color))),
3095        Some("linewidth") => Ok(Value::Num(stairs.line_width as f64)),
3096        Some("displayname") => Ok(Value::String(stairs.label.unwrap_or_default())),
3097        Some(other) => Err(plotting_error(
3098            builtin,
3099            format!("{builtin}: unsupported stairs property `{other}`"),
3100        )),
3101    }
3102}
3103
3104fn get_scatter_property(
3105    scatter_handle: &super::state::SimplePlotHandleState,
3106    property: Option<&str>,
3107    builtin: &'static str,
3108) -> BuiltinResult<Value> {
3109    let plot = get_simple_plot(scatter_handle, builtin)?;
3110    let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot else {
3111        return Err(plotting_error(
3112            builtin,
3113            format!("{builtin}: invalid scatter handle"),
3114        ));
3115    };
3116    match property.map(canonical_property_name).as_deref() {
3117        None => {
3118            let mut st =
3119                child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
3120            st.insert("XData", tensor_from_vec(scatter.x_data.clone()));
3121            st.insert("YData", tensor_from_vec(scatter.y_data.clone()));
3122            if let Some(theta) = scatter.theta_data.clone() {
3123                st.insert("ThetaData", tensor_from_vec(theta));
3124            }
3125            if let Some(r) = scatter.r_data.clone() {
3126                st.insert("RData", tensor_from_vec(r));
3127            }
3128            st.insert(
3129                "Marker",
3130                Value::String(marker_style_name(scatter.marker_style).into()),
3131            );
3132            st.insert("SizeData", scatter_size_data_value(&scatter));
3133            st.insert(
3134                "MarkerFaceColor",
3135                Value::String(color_to_short_name(scatter.color)),
3136            );
3137            st.insert(
3138                "MarkerEdgeColor",
3139                Value::String(color_to_short_name(scatter.edge_color)),
3140            );
3141            st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
3142            if let Some(label) = scatter.label.clone() {
3143                st.insert("DisplayName", Value::String(label));
3144            }
3145            Ok(Value::Struct(st))
3146        }
3147        Some("type") => Ok(Value::String("scatter".into())),
3148        Some("parent") => Ok(child_parent_handle(
3149            scatter_handle.figure,
3150            scatter_handle.axes_index,
3151        )),
3152        Some("children") => Ok(handles_value(Vec::new())),
3153        Some("xdata") => Ok(tensor_from_vec(scatter.x_data.clone())),
3154        Some("ydata") => Ok(tensor_from_vec(scatter.y_data.clone())),
3155        Some("thetadata") => {
3156            Ok(tensor_from_vec(scatter.theta_data.clone().unwrap_or_else(
3157                || cartesian_theta(&scatter.x_data, &scatter.y_data),
3158            )))
3159        }
3160        Some("rdata") => {
3161            Ok(tensor_from_vec(scatter.r_data.clone().unwrap_or_else(
3162                || cartesian_radius(&scatter.x_data, &scatter.y_data),
3163            )))
3164        }
3165        Some("marker") => Ok(Value::String(
3166            marker_style_name(scatter.marker_style).into(),
3167        )),
3168        Some("sizedata") => Ok(scatter_size_data_value(&scatter)),
3169        Some("markerfacecolor") => Ok(Value::String(color_to_short_name(scatter.color))),
3170        Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
3171        Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
3172        Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
3173        Some(other) => Err(plotting_error(
3174            builtin,
3175            format!("{builtin}: unsupported scatter property `{other}`"),
3176        )),
3177    }
3178}
3179
3180fn get_bar_property(
3181    bar_handle: &super::state::SimplePlotHandleState,
3182    property: Option<&str>,
3183    builtin: &'static str,
3184) -> BuiltinResult<Value> {
3185    let plot = get_simple_plot(bar_handle, builtin)?;
3186    let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot else {
3187        return Err(plotting_error(
3188            builtin,
3189            format!("{builtin}: invalid bar handle"),
3190        ));
3191    };
3192    match property.map(canonical_property_name).as_deref() {
3193        None => {
3194            let mut st = child_base_struct("bar", bar_handle.figure, bar_handle.axes_index);
3195            st.insert("FaceColor", Value::String(color_to_short_name(bar.color)));
3196            st.insert("BarWidth", Value::Num(bar.bar_width as f64));
3197            if let Some(label) = bar.label.clone() {
3198                st.insert("DisplayName", Value::String(label));
3199            }
3200            Ok(Value::Struct(st))
3201        }
3202        Some("type") => Ok(Value::String("bar".into())),
3203        Some("parent") => Ok(child_parent_handle(
3204            bar_handle.figure,
3205            bar_handle.axes_index,
3206        )),
3207        Some("children") => Ok(handles_value(Vec::new())),
3208        Some("facecolor") | Some("color") => Ok(Value::String(color_to_short_name(bar.color))),
3209        Some("barwidth") => Ok(Value::Num(bar.bar_width as f64)),
3210        Some("displayname") => Ok(Value::String(bar.label.unwrap_or_default())),
3211        Some(other) => Err(plotting_error(
3212            builtin,
3213            format!("{builtin}: unsupported bar property `{other}`"),
3214        )),
3215    }
3216}
3217
3218fn get_surface_property(
3219    surface_handle: &super::state::SimplePlotHandleState,
3220    property: Option<&str>,
3221    builtin: &'static str,
3222) -> BuiltinResult<Value> {
3223    let plot = get_simple_plot(surface_handle, builtin)?;
3224    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
3225        return Err(plotting_error(
3226            builtin,
3227            format!("{builtin}: invalid surface handle"),
3228        ));
3229    };
3230    match property.map(canonical_property_name).as_deref() {
3231        None => {
3232            let mut st =
3233                child_base_struct("surface", surface_handle.figure, surface_handle.axes_index);
3234            st.insert("XData", surface_x_data_value(&surface));
3235            st.insert("YData", surface_y_data_value(&surface));
3236            st.insert("ZData", surface_z_data_value(&surface));
3237            st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
3238            st.insert("FaceColor", surface_face_color_value(&surface));
3239            st.insert("EdgeColor", surface_edge_color_value(&surface));
3240            st.insert(
3241                "Shading",
3242                Value::String(surface_shading_name(surface.shading_mode).into()),
3243            );
3244            st.insert(
3245                "Lighting",
3246                Value::String(surface_lighting_name(surface.lighting_enabled).into()),
3247            );
3248            st.insert("Visible", Value::Bool(surface.visible));
3249            st.insert(
3250                "DisplayName",
3251                Value::String(surface.label.clone().unwrap_or_default()),
3252            );
3253            Ok(Value::Struct(st))
3254        }
3255        Some("type") => Ok(Value::String("surface".into())),
3256        Some("parent") => Ok(child_parent_handle(
3257            surface_handle.figure,
3258            surface_handle.axes_index,
3259        )),
3260        Some("children") => Ok(handles_value(Vec::new())),
3261        Some("xdata") => Ok(surface_x_data_value(&surface)),
3262        Some("ydata") => Ok(surface_y_data_value(&surface)),
3263        Some("zdata") => Ok(surface_z_data_value(&surface)),
3264        Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
3265        Some("facecolor") | Some("color") => Ok(surface_face_color_value(&surface)),
3266        Some("edgecolor") => Ok(surface_edge_color_value(&surface)),
3267        Some("shading") => Ok(Value::String(
3268            surface_shading_name(surface.shading_mode).into(),
3269        )),
3270        Some("lighting") => Ok(Value::String(
3271            surface_lighting_name(surface.lighting_enabled).into(),
3272        )),
3273        Some("visible") => Ok(Value::Bool(surface.visible)),
3274        Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
3275        Some(other) => Err(plotting_error(
3276            builtin,
3277            format!("{builtin}: unsupported surface property `{other}`"),
3278        )),
3279    }
3280}
3281
3282fn get_function_surface_property(
3283    function_surface: &super::state::FunctionSurfaceHandleState,
3284    property: Option<&str>,
3285    builtin: &'static str,
3286) -> BuiltinResult<Value> {
3287    let surface_handle = super::state::SimplePlotHandleState {
3288        figure: function_surface.figure,
3289        axes_index: function_surface.axes_index,
3290        plot_index: function_surface.plot_index,
3291    };
3292    let plot = get_simple_plot(&surface_handle, builtin)?;
3293    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
3294        return Err(plotting_error(
3295            builtin,
3296            format!("{builtin}: invalid function surface handle"),
3297        ));
3298    };
3299    match property.map(canonical_property_name).as_deref() {
3300        None => {
3301            let mut st = child_base_struct(
3302                "functionsurface",
3303                function_surface.figure,
3304                function_surface.axes_index,
3305            );
3306            insert_function_surface_metadata(&mut st, function_surface);
3307            st.insert("XData", surface_x_data_value(&surface));
3308            st.insert("YData", surface_y_data_value(&surface));
3309            st.insert("ZData", surface_z_data_value(&surface));
3310            st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
3311            st.insert("FaceColor", surface_face_color_value(&surface));
3312            st.insert("EdgeColor", surface_edge_color_value(&surface));
3313            st.insert(
3314                "Shading",
3315                Value::String(surface_shading_name(surface.shading_mode).into()),
3316            );
3317            st.insert(
3318                "Lighting",
3319                Value::String(surface_lighting_name(surface.lighting_enabled).into()),
3320            );
3321            st.insert("Visible", Value::Bool(surface.visible));
3322            st.insert(
3323                "DisplayName",
3324                Value::String(surface.label.clone().unwrap_or_default()),
3325            );
3326            Ok(Value::Struct(st))
3327        }
3328        Some("type") => Ok(Value::String("functionsurface".into())),
3329        Some("parent") => Ok(child_parent_handle(
3330            function_surface.figure,
3331            function_surface.axes_index,
3332        )),
3333        Some("children") => Ok(handles_value(Vec::new())),
3334        Some("function") => match &function_surface.function {
3335            super::state::FunctionSurfaceFunctionState::Explicit(function) => {
3336                Ok(function_surface_function_value(function))
3337            }
3338            super::state::FunctionSurfaceFunctionState::Parametric { .. } => Err(plotting_error(
3339                builtin,
3340                format!(
3341                    "{builtin}: parametric function surfaces use XFunction/YFunction/ZFunction"
3342                ),
3343            )),
3344        },
3345        Some("xfunction") => match &function_surface.function {
3346            super::state::FunctionSurfaceFunctionState::Parametric { x, .. } => {
3347                Ok(function_surface_function_value(x))
3348            }
3349            super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3350                builtin,
3351                format!("{builtin}: non-parametric function surfaces use Function"),
3352            )),
3353        },
3354        Some("yfunction") => match &function_surface.function {
3355            super::state::FunctionSurfaceFunctionState::Parametric { y, .. } => {
3356                Ok(function_surface_function_value(y))
3357            }
3358            super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3359                builtin,
3360                format!("{builtin}: non-parametric function surfaces use Function"),
3361            )),
3362        },
3363        Some("zfunction") => match &function_surface.function {
3364            super::state::FunctionSurfaceFunctionState::Parametric { z, .. } => {
3365                Ok(function_surface_function_value(z))
3366            }
3367            super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3368                builtin,
3369                format!("{builtin}: non-parametric function surfaces use Function"),
3370            )),
3371        },
3372        Some("meshdensity") => Ok(Value::Num(function_surface.mesh_density as f64)),
3373        Some("xrange") => Ok(tensor_from_vec(vec![
3374            function_surface.x_range.0,
3375            function_surface.x_range.1,
3376        ])),
3377        Some("yrange") => Ok(tensor_from_vec(vec![
3378            function_surface.y_range.0,
3379            function_surface.y_range.1,
3380        ])),
3381        Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
3382        Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
3383        Some("zdata") => Ok(surface
3384            .z_data
3385            .clone()
3386            .map(tensor_from_matrix)
3387            .unwrap_or_else(|| tensor_from_vec(Vec::new()))),
3388        Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
3389        Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
3390        Some(other) => Err(plotting_error(
3391            builtin,
3392            format!("{builtin}: unsupported functionsurface property `{other}`"),
3393        )),
3394    }
3395}
3396
3397fn insert_function_surface_metadata(
3398    st: &mut StructValue,
3399    function_surface: &super::state::FunctionSurfaceHandleState,
3400) {
3401    st.insert(
3402        "MeshDensity",
3403        Value::Num(function_surface.mesh_density as f64),
3404    );
3405    st.insert(
3406        "XRange",
3407        tensor_from_vec(vec![function_surface.x_range.0, function_surface.x_range.1]),
3408    );
3409    st.insert(
3410        "YRange",
3411        tensor_from_vec(vec![function_surface.y_range.0, function_surface.y_range.1]),
3412    );
3413    match &function_surface.function {
3414        super::state::FunctionSurfaceFunctionState::Explicit(function) => {
3415            st.insert("Function", function_surface_function_value(function));
3416        }
3417        super::state::FunctionSurfaceFunctionState::Parametric { x, y, z } => {
3418            st.insert("XFunction", function_surface_function_value(x));
3419            st.insert("YFunction", function_surface_function_value(y));
3420            st.insert("ZFunction", function_surface_function_value(z));
3421        }
3422    }
3423}
3424
3425fn function_surface_function_value(function: &super::state::FunctionSurfaceFunctionRef) -> Value {
3426    match function {
3427        super::state::FunctionSurfaceFunctionRef::FunctionHandle(name) => {
3428            Value::FunctionHandle(name.clone())
3429        }
3430        super::state::FunctionSurfaceFunctionRef::ExternalFunctionHandle(name) => {
3431            Value::ExternalFunctionHandle(name.clone())
3432        }
3433        super::state::FunctionSurfaceFunctionRef::MethodFunctionHandle(name) => {
3434            Value::MethodFunctionHandle(name.clone())
3435        }
3436        super::state::FunctionSurfaceFunctionRef::BoundFunctionHandle { name, function } => {
3437            Value::BoundFunctionHandle {
3438                name: name.clone(),
3439                function: *function,
3440            }
3441        }
3442        super::state::FunctionSurfaceFunctionRef::ClosureSummary {
3443            function_name,
3444            bound_function,
3445        } => match bound_function {
3446            Some(function) => Value::BoundFunctionHandle {
3447                name: function_name.clone(),
3448                function: *function,
3449            },
3450            None => Value::String(function_name.clone()),
3451        },
3452    }
3453}
3454
3455fn get_function_contour_property(
3456    function_contour: &super::state::FunctionContourHandleState,
3457    property: Option<&str>,
3458    builtin: &'static str,
3459) -> BuiltinResult<Value> {
3460    let contour_handle = super::state::SimplePlotHandleState {
3461        figure: function_contour.figure,
3462        axes_index: function_contour.axes_index,
3463        plot_index: function_contour.plot_index,
3464    };
3465    let plot = get_simple_plot(&contour_handle, builtin)?;
3466    let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
3467        return Err(plotting_error(
3468            builtin,
3469            format!("{builtin}: invalid function contour handle"),
3470        ));
3471    };
3472    match property.map(canonical_property_name).as_deref() {
3473        None => {
3474            let mut st = child_base_struct(
3475                "functioncontour",
3476                function_contour.figure,
3477                function_contour.axes_index,
3478            );
3479            st.insert(
3480                "Function",
3481                function_surface_function_value(&function_contour.function),
3482            );
3483            st.insert(
3484                "MeshDensity",
3485                Value::Num(function_contour.mesh_density as f64),
3486            );
3487            st.insert(
3488                "XRange",
3489                tensor_from_vec(vec![function_contour.x_range.0, function_contour.x_range.1]),
3490            );
3491            st.insert(
3492                "YRange",
3493                tensor_from_vec(vec![function_contour.y_range.0, function_contour.y_range.1]),
3494            );
3495            st.insert("LineWidth", Value::Num(contour.line_width as f64));
3496            st.insert(
3497                "DisplayName",
3498                Value::String(contour.label.clone().unwrap_or_default()),
3499            );
3500            Ok(Value::Struct(st))
3501        }
3502        Some("type") => Ok(Value::String("functioncontour".into())),
3503        Some("parent") => Ok(child_parent_handle(
3504            function_contour.figure,
3505            function_contour.axes_index,
3506        )),
3507        Some("children") => Ok(handles_value(Vec::new())),
3508        Some("function") => Ok(function_surface_function_value(&function_contour.function)),
3509        Some("meshdensity") => Ok(Value::Num(function_contour.mesh_density as f64)),
3510        Some("xrange") => Ok(tensor_from_vec(vec![
3511            function_contour.x_range.0,
3512            function_contour.x_range.1,
3513        ])),
3514        Some("yrange") => Ok(tensor_from_vec(vec![
3515            function_contour.y_range.0,
3516            function_contour.y_range.1,
3517        ])),
3518        Some("linewidth") => Ok(Value::Num(contour.line_width as f64)),
3519        Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
3520        Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
3521        Some(other) => Err(plotting_error(
3522            builtin,
3523            format!("{builtin}: unsupported functioncontour property `{other}`"),
3524        )),
3525    }
3526}
3527
3528fn get_patch_property(
3529    patch_handle: &super::state::SimplePlotHandleState,
3530    property: Option<&str>,
3531    builtin: &'static str,
3532) -> BuiltinResult<Value> {
3533    let plot = get_simple_plot(patch_handle, builtin)?;
3534    let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot else {
3535        return Err(plotting_error(
3536            builtin,
3537            format!("{builtin}: invalid patch handle"),
3538        ));
3539    };
3540    match property.map(canonical_property_name).as_deref() {
3541        None => {
3542            let mut st = child_base_struct("patch", patch_handle.figure, patch_handle.axes_index);
3543            st.insert("Faces", faces_tensor(patch.faces()));
3544            st.insert("Vertices", vertices_tensor(patch.vertices()));
3545            st.insert(
3546                "XData",
3547                tensor_from_vec(patch.vertices().iter().map(|p| p.x as f64).collect()),
3548            );
3549            st.insert(
3550                "YData",
3551                tensor_from_vec(patch.vertices().iter().map(|p| p.y as f64).collect()),
3552            );
3553            st.insert(
3554                "ZData",
3555                tensor_from_vec(patch.vertices().iter().map(|p| p.z as f64).collect()),
3556            );
3557            st.insert(
3558                "FaceColor",
3559                patch_color_property(patch.face_color_mode(), patch.face_color()),
3560            );
3561            st.insert(
3562                "EdgeColor",
3563                patch_edge_color_property(patch.edge_color_mode(), patch.edge_color()),
3564            );
3565            st.insert("FaceAlpha", Value::Num(patch.face_alpha() as f64));
3566            st.insert("EdgeAlpha", Value::Num(patch.edge_alpha() as f64));
3567            st.insert("LineWidth", Value::Num(patch.line_width() as f64));
3568            st.insert("Visible", Value::Bool(patch.is_visible()));
3569            if let Some(label) = patch.label() {
3570                st.insert("DisplayName", Value::String(label.to_string()));
3571            }
3572            Ok(Value::Struct(st))
3573        }
3574        Some("type") => Ok(Value::String("patch".into())),
3575        Some("parent") => Ok(child_parent_handle(
3576            patch_handle.figure,
3577            patch_handle.axes_index,
3578        )),
3579        Some("children") => Ok(handles_value(Vec::new())),
3580        Some("faces") => Ok(faces_tensor(patch.faces())),
3581        Some("vertices") => Ok(vertices_tensor(patch.vertices())),
3582        Some("xdata") => Ok(tensor_from_vec(
3583            patch.vertices().iter().map(|p| p.x as f64).collect(),
3584        )),
3585        Some("ydata") => Ok(tensor_from_vec(
3586            patch.vertices().iter().map(|p| p.y as f64).collect(),
3587        )),
3588        Some("zdata") => Ok(tensor_from_vec(
3589            patch.vertices().iter().map(|p| p.z as f64).collect(),
3590        )),
3591        Some("facecolor") | Some("color") => Ok(patch_color_property(
3592            patch.face_color_mode(),
3593            patch.face_color(),
3594        )),
3595        Some("edgecolor") => Ok(patch_edge_color_property(
3596            patch.edge_color_mode(),
3597            patch.edge_color(),
3598        )),
3599        Some("facealpha") => Ok(Value::Num(patch.face_alpha() as f64)),
3600        Some("edgealpha") => Ok(Value::Num(patch.edge_alpha() as f64)),
3601        Some("linewidth") => Ok(Value::Num(patch.line_width() as f64)),
3602        Some("displayname") => Ok(Value::String(patch.label().unwrap_or_default().to_string())),
3603        Some("visible") => Ok(Value::Bool(patch.is_visible())),
3604        Some(other) => Err(plotting_error(
3605            builtin,
3606            format!("{builtin}: unsupported patch property `{other}`"),
3607        )),
3608    }
3609}
3610
3611fn get_line3_property(
3612    line_handle: &super::state::SimplePlotHandleState,
3613    property: Option<&str>,
3614    builtin: &'static str,
3615) -> BuiltinResult<Value> {
3616    let plot = get_simple_plot(line_handle, builtin)?;
3617    let runmat_plot::plots::figure::PlotElement::Line3(line) = plot else {
3618        return Err(plotting_error(
3619            builtin,
3620            format!("{builtin}: invalid plot3 handle"),
3621        ));
3622    };
3623    match property.map(canonical_property_name).as_deref() {
3624        None => {
3625            let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
3626            st.insert("XData", tensor_from_vec(line.x_data.clone()));
3627            st.insert("YData", tensor_from_vec(line.y_data.clone()));
3628            st.insert("ZData", tensor_from_vec(line.z_data.clone()));
3629            st.insert("Color", Value::String(color_to_short_name(line.color)));
3630            st.insert("LineWidth", Value::Num(line.line_width as f64));
3631            st.insert(
3632                "LineStyle",
3633                Value::String(line_style_name(line.line_style).into()),
3634            );
3635            st.insert(
3636                "DisplayName",
3637                Value::String(line.label.clone().unwrap_or_default()),
3638            );
3639            st.insert(
3640                "Visible",
3641                Value::String(if line.visible { "on" } else { "off" }.into()),
3642            );
3643            Ok(Value::Struct(st))
3644        }
3645        Some("type") => Ok(Value::String("line".into())),
3646        Some("parent") => Ok(child_parent_handle(
3647            line_handle.figure,
3648            line_handle.axes_index,
3649        )),
3650        Some("children") => Ok(handles_value(Vec::new())),
3651        Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
3652        Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
3653        Some("zdata") => Ok(tensor_from_vec(line.z_data.clone())),
3654        Some("color") => Ok(Value::String(color_to_short_name(line.color))),
3655        Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
3656        Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
3657        Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
3658        Some("visible") => Ok(Value::String(
3659            if line.visible { "on" } else { "off" }.into(),
3660        )),
3661        Some(other) => Err(plotting_error(
3662            builtin,
3663            format!("{builtin}: unsupported plot3 property `{other}`"),
3664        )),
3665    }
3666}
3667
3668fn get_scatter3_property(
3669    scatter_handle: &super::state::SimplePlotHandleState,
3670    property: Option<&str>,
3671    builtin: &'static str,
3672) -> BuiltinResult<Value> {
3673    let plot = get_simple_plot(scatter_handle, builtin)?;
3674    let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot else {
3675        return Err(plotting_error(
3676            builtin,
3677            format!("{builtin}: invalid scatter3 handle"),
3678        ));
3679    };
3680    let (x, y, z): (Vec<f64>, Vec<f64>, Vec<f64>) = scatter
3681        .points
3682        .iter()
3683        .map(|p| (p.x as f64, p.y as f64, p.z as f64))
3684        .unzip_n_vec();
3685    match property.map(canonical_property_name).as_deref() {
3686        None => {
3687            let mut st =
3688                child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
3689            st.insert("XData", tensor_from_vec(x));
3690            st.insert("YData", tensor_from_vec(y));
3691            st.insert("ZData", tensor_from_vec(z));
3692            st.insert(
3693                "Marker",
3694                Value::String(marker_style_name(scatter.marker_style).into()),
3695            );
3696            st.insert(
3697                "SizeData",
3698                Value::Num(marker_diameter_px_to_area_points2(scatter.point_size)),
3699            );
3700            st.insert(
3701                "MarkerFaceColor",
3702                Value::String(color_to_short_name(
3703                    scatter.colors.first().copied().unwrap_or_default(),
3704                )),
3705            );
3706            st.insert(
3707                "MarkerEdgeColor",
3708                Value::String(color_to_short_name(scatter.edge_color)),
3709            );
3710            st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
3711            if let Some(label) = scatter.label.clone() {
3712                st.insert("DisplayName", Value::String(label));
3713            }
3714            Ok(Value::Struct(st))
3715        }
3716        Some("type") => Ok(Value::String("scatter".into())),
3717        Some("parent") => Ok(child_parent_handle(
3718            scatter_handle.figure,
3719            scatter_handle.axes_index,
3720        )),
3721        Some("children") => Ok(handles_value(Vec::new())),
3722        Some("marker") => Ok(Value::String(
3723            marker_style_name(scatter.marker_style).into(),
3724        )),
3725        Some("sizedata") => Ok(Value::Num(marker_diameter_px_to_area_points2(
3726            scatter.point_size,
3727        ))),
3728        Some("markerfacecolor") => Ok(Value::String(color_to_short_name(
3729            scatter.colors.first().copied().unwrap_or_default(),
3730        ))),
3731        Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
3732        Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
3733        Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
3734        Some(other) => Err(plotting_error(
3735            builtin,
3736            format!("{builtin}: unsupported scatter3 property `{other}`"),
3737        )),
3738    }
3739}
3740
3741fn get_pie_property(
3742    pie_handle: &super::state::SimplePlotHandleState,
3743    property: Option<&str>,
3744    builtin: &'static str,
3745) -> BuiltinResult<Value> {
3746    let plot = get_simple_plot(pie_handle, builtin)?;
3747    let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot else {
3748        return Err(plotting_error(
3749            builtin,
3750            format!("{builtin}: invalid pie handle"),
3751        ));
3752    };
3753    match property.map(canonical_property_name).as_deref() {
3754        None => {
3755            let mut st = child_base_struct("pie", pie_handle.figure, pie_handle.axes_index);
3756            if let Some(label) = pie.label.clone() {
3757                st.insert("DisplayName", Value::String(label));
3758            }
3759            Ok(Value::Struct(st))
3760        }
3761        Some("type") => Ok(Value::String("pie".into())),
3762        Some("parent") => Ok(child_parent_handle(
3763            pie_handle.figure,
3764            pie_handle.axes_index,
3765        )),
3766        Some("children") => Ok(handles_value(Vec::new())),
3767        Some("displayname") => Ok(Value::String(pie.label.unwrap_or_default())),
3768        Some(other) => Err(plotting_error(
3769            builtin,
3770            format!("{builtin}: unsupported pie property `{other}`"),
3771        )),
3772    }
3773}
3774
3775fn get_contour_property(
3776    contour_handle: &super::state::SimplePlotHandleState,
3777    property: Option<&str>,
3778    builtin: &'static str,
3779) -> BuiltinResult<Value> {
3780    let plot = get_simple_plot(contour_handle, builtin)?;
3781    let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
3782        return Err(plotting_error(
3783            builtin,
3784            format!("{builtin}: invalid contour handle"),
3785        ));
3786    };
3787    match property.map(canonical_property_name).as_deref() {
3788        None => {
3789            let mut st =
3790                child_base_struct("contour", contour_handle.figure, contour_handle.axes_index);
3791            st.insert("ZData", Value::Num(contour.base_z as f64));
3792            if let Some(label) = contour.label.clone() {
3793                st.insert("DisplayName", Value::String(label));
3794            }
3795            Ok(Value::Struct(st))
3796        }
3797        Some("type") => Ok(Value::String("contour".into())),
3798        Some("parent") => Ok(child_parent_handle(
3799            contour_handle.figure,
3800            contour_handle.axes_index,
3801        )),
3802        Some("children") => Ok(handles_value(Vec::new())),
3803        Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
3804        Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
3805        Some(other) => Err(plotting_error(
3806            builtin,
3807            format!("{builtin}: unsupported contour property `{other}`"),
3808        )),
3809    }
3810}
3811
3812fn get_contour_fill_property(
3813    fill_handle: &super::state::SimplePlotHandleState,
3814    property: Option<&str>,
3815    builtin: &'static str,
3816) -> BuiltinResult<Value> {
3817    let plot = get_simple_plot(fill_handle, builtin)?;
3818    let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot else {
3819        return Err(plotting_error(
3820            builtin,
3821            format!("{builtin}: invalid contourf handle"),
3822        ));
3823    };
3824    match property.map(canonical_property_name).as_deref() {
3825        None => {
3826            let mut st = child_base_struct("contour", fill_handle.figure, fill_handle.axes_index);
3827            if let Some(label) = fill.label.clone() {
3828                st.insert("DisplayName", Value::String(label));
3829            }
3830            Ok(Value::Struct(st))
3831        }
3832        Some("type") => Ok(Value::String("contour".into())),
3833        Some("parent") => Ok(child_parent_handle(
3834            fill_handle.figure,
3835            fill_handle.axes_index,
3836        )),
3837        Some("children") => Ok(handles_value(Vec::new())),
3838        Some("displayname") => Ok(Value::String(fill.label.unwrap_or_default())),
3839        Some(other) => Err(plotting_error(
3840            builtin,
3841            format!("{builtin}: unsupported contourf property `{other}`"),
3842        )),
3843    }
3844}
3845
3846fn get_stem_property(
3847    stem_handle: &super::state::StemHandleState,
3848    property: Option<&str>,
3849    builtin: &'static str,
3850) -> BuiltinResult<Value> {
3851    let figure = super::state::clone_figure(stem_handle.figure)
3852        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem figure")))?;
3853    let plot = figure
3854        .plots()
3855        .nth(stem_handle.plot_index)
3856        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem handle")))?;
3857    let runmat_plot::plots::figure::PlotElement::Stem(stem) = plot else {
3858        return Err(plotting_error(
3859            builtin,
3860            format!("{builtin}: invalid stem handle"),
3861        ));
3862    };
3863    match property.map(canonical_property_name).as_deref() {
3864        None => {
3865            let mut st = StructValue::new();
3866            st.insert("Type", Value::String("stem".into()));
3867            st.insert(
3868                "Parent",
3869                Value::Num(super::state::encode_axes_handle(
3870                    stem_handle.figure,
3871                    stem_handle.axes_index,
3872                )),
3873            );
3874            st.insert("Children", handles_value(Vec::new()));
3875            st.insert("BaseValue", Value::Num(stem.baseline));
3876            st.insert("BaseLine", Value::Bool(stem.baseline_visible));
3877            st.insert("LineWidth", Value::Num(stem.line_width as f64));
3878            st.insert(
3879                "LineStyle",
3880                Value::String(line_style_name(stem.line_style).into()),
3881            );
3882            st.insert("Color", Value::String(color_to_short_name(stem.color)));
3883            if let Some(marker) = &stem.marker {
3884                st.insert(
3885                    "Marker",
3886                    Value::String(marker_style_name(marker.kind).into()),
3887                );
3888                st.insert("MarkerSize", Value::Num(marker.size as f64));
3889                st.insert(
3890                    "MarkerFaceColor",
3891                    Value::String(color_to_short_name(marker.face_color)),
3892                );
3893                st.insert(
3894                    "MarkerEdgeColor",
3895                    Value::String(color_to_short_name(marker.edge_color)),
3896                );
3897                st.insert("Filled", Value::Bool(marker.filled));
3898            }
3899            Ok(Value::Struct(st))
3900        }
3901        Some("type") => Ok(Value::String("stem".into())),
3902        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
3903            stem_handle.figure,
3904            stem_handle.axes_index,
3905        ))),
3906        Some("children") => Ok(handles_value(Vec::new())),
3907        Some("basevalue") => Ok(Value::Num(stem.baseline)),
3908        Some("baseline") => Ok(Value::Bool(stem.baseline_visible)),
3909        Some("linewidth") => Ok(Value::Num(stem.line_width as f64)),
3910        Some("linestyle") => Ok(Value::String(line_style_name(stem.line_style).into())),
3911        Some("color") => Ok(Value::String(color_to_short_name(stem.color))),
3912        Some("marker") => Ok(Value::String(
3913            stem.marker
3914                .as_ref()
3915                .map(|m| marker_style_name(m.kind).to_string())
3916                .unwrap_or("none".into()),
3917        )),
3918        Some("markersize") => Ok(Value::Num(
3919            stem.marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
3920        )),
3921        Some("markerfacecolor") => Ok(Value::String(
3922            stem.marker
3923                .as_ref()
3924                .map(|m| color_to_short_name(m.face_color))
3925                .unwrap_or("none".into()),
3926        )),
3927        Some("markeredgecolor") => Ok(Value::String(
3928            stem.marker
3929                .as_ref()
3930                .map(|m| color_to_short_name(m.edge_color))
3931                .unwrap_or("none".into()),
3932        )),
3933        Some("filled") => Ok(Value::Bool(
3934            stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
3935        )),
3936        Some(other) => Err(plotting_error(
3937            builtin,
3938            format!("{builtin}: unsupported stem property `{other}`"),
3939        )),
3940    }
3941}
3942
3943fn get_errorbar_property(
3944    error_handle: &super::state::ErrorBarHandleState,
3945    property: Option<&str>,
3946    builtin: &'static str,
3947) -> BuiltinResult<Value> {
3948    let figure = super::state::clone_figure(error_handle.figure)
3949        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar figure")))?;
3950    let plot = figure
3951        .plots()
3952        .nth(error_handle.plot_index)
3953        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar handle")))?;
3954    let runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) = plot else {
3955        return Err(plotting_error(
3956            builtin,
3957            format!("{builtin}: invalid errorbar handle"),
3958        ));
3959    };
3960    match property.map(canonical_property_name).as_deref() {
3961        None => {
3962            let mut st = StructValue::new();
3963            st.insert("Type", Value::String("errorbar".into()));
3964            st.insert(
3965                "Parent",
3966                Value::Num(super::state::encode_axes_handle(
3967                    error_handle.figure,
3968                    error_handle.axes_index,
3969                )),
3970            );
3971            st.insert("Children", handles_value(Vec::new()));
3972            st.insert("LineWidth", Value::Num(errorbar.line_width as f64));
3973            st.insert(
3974                "LineStyle",
3975                Value::String(line_style_name(errorbar.line_style).into()),
3976            );
3977            st.insert("Color", Value::String(color_to_short_name(errorbar.color)));
3978            st.insert("CapSize", Value::Num(errorbar.cap_size as f64));
3979            if let Some(marker) = &errorbar.marker {
3980                st.insert(
3981                    "Marker",
3982                    Value::String(marker_style_name(marker.kind).into()),
3983                );
3984                st.insert("MarkerSize", Value::Num(marker.size as f64));
3985            }
3986            Ok(Value::Struct(st))
3987        }
3988        Some("type") => Ok(Value::String("errorbar".into())),
3989        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
3990            error_handle.figure,
3991            error_handle.axes_index,
3992        ))),
3993        Some("children") => Ok(handles_value(Vec::new())),
3994        Some("linewidth") => Ok(Value::Num(errorbar.line_width as f64)),
3995        Some("linestyle") => Ok(Value::String(line_style_name(errorbar.line_style).into())),
3996        Some("color") => Ok(Value::String(color_to_short_name(errorbar.color))),
3997        Some("capsize") => Ok(Value::Num(errorbar.cap_size as f64)),
3998        Some("marker") => Ok(Value::String(
3999            errorbar
4000                .marker
4001                .as_ref()
4002                .map(|m| marker_style_name(m.kind).to_string())
4003                .unwrap_or("none".into()),
4004        )),
4005        Some("markersize") => Ok(Value::Num(
4006            errorbar
4007                .marker
4008                .as_ref()
4009                .map(|m| m.size as f64)
4010                .unwrap_or(0.0),
4011        )),
4012        Some(other) => Err(plotting_error(
4013            builtin,
4014            format!("{builtin}: unsupported errorbar property `{other}`"),
4015        )),
4016    }
4017}
4018
4019fn get_quiver_property(
4020    quiver_handle: &super::state::QuiverHandleState,
4021    property: Option<&str>,
4022    builtin: &'static str,
4023) -> BuiltinResult<Value> {
4024    let figure = super::state::clone_figure(quiver_handle.figure)
4025        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
4026    let plot = figure
4027        .plots()
4028        .nth(quiver_handle.plot_index)
4029        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
4030    let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
4031        return Err(plotting_error(
4032            builtin,
4033            format!("{builtin}: invalid quiver handle"),
4034        ));
4035    };
4036    let has_cpu_data = quiver.has_cpu_vector_data();
4037    match property.map(canonical_property_name).as_deref() {
4038        None => {
4039            let mut st = StructValue::new();
4040            st.insert("Type", Value::String("quiver".into()));
4041            st.insert(
4042                "Parent",
4043                Value::Num(super::state::encode_axes_handle(
4044                    quiver_handle.figure,
4045                    quiver_handle.axes_index,
4046                )),
4047            );
4048            st.insert("Children", handles_value(Vec::new()));
4049            st.insert("Color", Value::String(color_to_short_name(quiver.color)));
4050            st.insert("LineWidth", Value::Num(quiver.line_width as f64));
4051            st.insert("AutoScaleFactor", Value::Num(quiver.scale as f64));
4052            st.insert("MaxHeadSize", Value::Num(quiver.head_size as f64));
4053            if has_cpu_data {
4054                st.insert("XData", tensor_from_vec(quiver.x.clone()));
4055                st.insert("YData", tensor_from_vec(quiver.y.clone()));
4056                if quiver_handle.is_3d {
4057                    st.insert(
4058                        "ZData",
4059                        tensor_from_vec(quiver.z.clone().unwrap_or_default()),
4060                    );
4061                    st.insert(
4062                        "WData",
4063                        tensor_from_vec(quiver.w.clone().unwrap_or_default()),
4064                    );
4065                }
4066                st.insert("UData", tensor_from_vec(quiver.u.clone()));
4067                st.insert("VData", tensor_from_vec(quiver.v.clone()));
4068            }
4069            Ok(Value::Struct(st))
4070        }
4071        Some("type") => Ok(Value::String("quiver".into())),
4072        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4073            quiver_handle.figure,
4074            quiver_handle.axes_index,
4075        ))),
4076        Some("children") => Ok(handles_value(Vec::new())),
4077        Some("color") => Ok(Value::String(color_to_short_name(quiver.color))),
4078        Some("linewidth") => Ok(Value::Num(quiver.line_width as f64)),
4079        Some("autoscalefactor") => Ok(Value::Num(quiver.scale as f64)),
4080        Some("maxheadsize") => Ok(Value::Num(quiver.head_size as f64)),
4081        Some("xdata") if has_cpu_data => Ok(tensor_from_vec(quiver.x.clone())),
4082        Some("ydata") if has_cpu_data => Ok(tensor_from_vec(quiver.y.clone())),
4083        Some("zdata") if quiver_handle.is_3d => {
4084            if has_cpu_data {
4085                Ok(tensor_from_vec(quiver.z.clone().unwrap_or_default()))
4086            } else {
4087                Err(quiver_data_unavailable_error(builtin))
4088            }
4089        }
4090        Some("udata") if has_cpu_data => Ok(tensor_from_vec(quiver.u.clone())),
4091        Some("vdata") if has_cpu_data => Ok(tensor_from_vec(quiver.v.clone())),
4092        Some("wdata") if quiver_handle.is_3d => {
4093            if has_cpu_data {
4094                Ok(tensor_from_vec(quiver.w.clone().unwrap_or_default()))
4095            } else {
4096                Err(quiver_data_unavailable_error(builtin))
4097            }
4098        }
4099        Some("xdata") | Some("ydata") | Some("udata") | Some("vdata") => {
4100            Err(quiver_data_unavailable_error(builtin))
4101        }
4102        Some(other) => Err(plotting_error(
4103            builtin,
4104            format!("{builtin}: unsupported quiver property `{other}`"),
4105        )),
4106    }
4107}
4108
4109fn quiver_data_unavailable_error(builtin: &'static str) -> crate::RuntimeError {
4110    plotting_error(
4111        builtin,
4112        format!("{builtin}: quiver data properties are unavailable for GPU-backed quiver handles"),
4113    )
4114}
4115
4116fn get_image_property(
4117    image_handle: &super::state::ImageHandleState,
4118    property: Option<&str>,
4119    builtin: &'static str,
4120) -> BuiltinResult<Value> {
4121    let figure = super::state::clone_figure(image_handle.figure)
4122        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image figure")))?;
4123    let plot = figure
4124        .plots()
4125        .nth(image_handle.plot_index)
4126        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image handle")))?;
4127    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4128        return Err(plotting_error(
4129            builtin,
4130            format!("{builtin}: invalid image handle"),
4131        ));
4132    };
4133    if !surface.image_mode {
4134        return Err(plotting_error(
4135            builtin,
4136            format!("{builtin}: handle does not reference an image plot"),
4137        ));
4138    }
4139    match property.map(canonical_property_name).as_deref() {
4140        None => {
4141            let mut st = StructValue::new();
4142            st.insert("Type", Value::String("image".into()));
4143            st.insert(
4144                "Parent",
4145                Value::Num(super::state::encode_axes_handle(
4146                    image_handle.figure,
4147                    image_handle.axes_index,
4148                )),
4149            );
4150            st.insert("Children", handles_value(Vec::new()));
4151            st.insert("XData", tensor_from_vec(surface.x_data.clone()));
4152            st.insert("YData", tensor_from_vec(surface.y_data.clone()));
4153            st.insert(
4154                "CDataMapping",
4155                Value::String(if surface.color_grid.is_some() {
4156                    "direct".into()
4157                } else {
4158                    "scaled".into()
4159                }),
4160            );
4161            Ok(Value::Struct(st))
4162        }
4163        Some("type") => Ok(Value::String("image".into())),
4164        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4165            image_handle.figure,
4166            image_handle.axes_index,
4167        ))),
4168        Some("children") => Ok(handles_value(Vec::new())),
4169        Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
4170        Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
4171        Some("cdatamapping") => Ok(Value::String(if surface.color_grid.is_some() {
4172            "direct".into()
4173        } else {
4174            "scaled".into()
4175        })),
4176        Some(other) => Err(plotting_error(
4177            builtin,
4178            format!("{builtin}: unsupported image property `{other}`"),
4179        )),
4180    }
4181}
4182
4183fn get_heatmap_property(
4184    heatmap_handle: &super::state::HeatmapHandleState,
4185    property: Option<&str>,
4186    builtin: &'static str,
4187) -> BuiltinResult<Value> {
4188    let figure = super::state::clone_figure(heatmap_handle.figure)
4189        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap figure")))?;
4190    let plot = figure
4191        .plots()
4192        .nth(heatmap_handle.plot_index)
4193        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap handle")))?;
4194    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4195        return Err(plotting_error(
4196            builtin,
4197            format!("{builtin}: invalid heatmap handle"),
4198        ));
4199    };
4200    if !surface.image_mode {
4201        return Err(plotting_error(
4202            builtin,
4203            format!("{builtin}: handle does not reference a heatmap plot"),
4204        ));
4205    }
4206    let meta = figure
4207        .axes_metadata(heatmap_handle.axes_index)
4208        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap axes")))?;
4209    match property.map(canonical_property_name).as_deref() {
4210        None => {
4211            let mut st = StructValue::new();
4212            st.insert("Type", Value::String("heatmap".into()));
4213            st.insert(
4214                "Parent",
4215                Value::Num(super::state::encode_axes_handle(
4216                    heatmap_handle.figure,
4217                    heatmap_handle.axes_index,
4218                )),
4219            );
4220            st.insert("Children", handles_value(Vec::new()));
4221            st.insert(
4222                "Title",
4223                Value::String(meta.title.clone().unwrap_or_default()),
4224            );
4225            st.insert(
4226                "XLabel",
4227                Value::String(meta.x_label.clone().unwrap_or_default()),
4228            );
4229            st.insert(
4230                "YLabel",
4231                Value::String(meta.y_label.clone().unwrap_or_default()),
4232            );
4233            st.insert(
4234                "XDisplayLabels",
4235                string_array_from_vec(heatmap_handle.x_labels.clone())?,
4236            );
4237            st.insert(
4238                "YDisplayLabels",
4239                string_array_from_vec(heatmap_handle.y_labels.clone())?,
4240            );
4241            st.insert(
4242                "ColorData",
4243                Value::Tensor(heatmap_handle.color_data.clone()),
4244            );
4245            st.insert("ColorbarVisible", Value::Bool(meta.colorbar_enabled));
4246            st.insert(
4247                "Colormap",
4248                Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
4249            );
4250            Ok(Value::Struct(st))
4251        }
4252        Some("type") => Ok(Value::String("heatmap".into())),
4253        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4254            heatmap_handle.figure,
4255            heatmap_handle.axes_index,
4256        ))),
4257        Some("children") => Ok(handles_value(Vec::new())),
4258        Some("title") => Ok(Value::String(meta.title.clone().unwrap_or_default())),
4259        Some("xlabel") => Ok(Value::String(meta.x_label.clone().unwrap_or_default())),
4260        Some("ylabel") => Ok(Value::String(meta.y_label.clone().unwrap_or_default())),
4261        Some("xdisplaylabels") => string_array_from_vec(heatmap_handle.x_labels.clone()),
4262        Some("ydisplaylabels") => string_array_from_vec(heatmap_handle.y_labels.clone()),
4263        Some("colordata") => Ok(Value::Tensor(heatmap_handle.color_data.clone())),
4264        Some("colorbarvisible") | Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
4265        Some("colormap") => Ok(Value::String(
4266            format!("{:?}", meta.colormap).to_ascii_lowercase(),
4267        )),
4268        Some(other) => Err(plotting_error(
4269            builtin,
4270            format!("{builtin}: unsupported heatmap property `{other}`"),
4271        )),
4272    }
4273}
4274
4275fn get_area_property(
4276    area_handle: &super::state::AreaHandleState,
4277    property: Option<&str>,
4278    builtin: &'static str,
4279) -> BuiltinResult<Value> {
4280    let figure = super::state::clone_figure(area_handle.figure)
4281        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area figure")))?;
4282    let plot = figure
4283        .plots()
4284        .nth(area_handle.plot_index)
4285        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area handle")))?;
4286    let runmat_plot::plots::figure::PlotElement::Area(area) = plot else {
4287        return Err(plotting_error(
4288            builtin,
4289            format!("{builtin}: invalid area handle"),
4290        ));
4291    };
4292    match property.map(canonical_property_name).as_deref() {
4293        None => {
4294            let mut st = StructValue::new();
4295            st.insert("Type", Value::String("area".into()));
4296            st.insert(
4297                "Parent",
4298                Value::Num(super::state::encode_axes_handle(
4299                    area_handle.figure,
4300                    area_handle.axes_index,
4301                )),
4302            );
4303            st.insert("Children", handles_value(Vec::new()));
4304            st.insert("XData", tensor_from_vec(area.x.clone()));
4305            st.insert("YData", tensor_from_vec(area.y.clone()));
4306            st.insert("BaseValue", Value::Num(area.baseline));
4307            st.insert("Color", Value::String(color_to_short_name(area.color)));
4308            Ok(Value::Struct(st))
4309        }
4310        Some("type") => Ok(Value::String("area".into())),
4311        Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4312            area_handle.figure,
4313            area_handle.axes_index,
4314        ))),
4315        Some("children") => Ok(handles_value(Vec::new())),
4316        Some("xdata") => Ok(tensor_from_vec(area.x.clone())),
4317        Some("ydata") => Ok(tensor_from_vec(area.y.clone())),
4318        Some("basevalue") => Ok(Value::Num(area.baseline)),
4319        Some("color") => Ok(Value::String(color_to_short_name(area.color))),
4320        Some(other) => Err(plotting_error(
4321            builtin,
4322            format!("{builtin}: unsupported area property `{other}`"),
4323        )),
4324    }
4325}
4326
4327fn apply_histogram_property(
4328    hist: &super::state::HistogramHandleState,
4329    key: &str,
4330    value: &Value,
4331    builtin: &'static str,
4332) -> BuiltinResult<()> {
4333    match key {
4334        "normalization" => {
4335            let norm = value_as_string(value)
4336                .ok_or_else(|| {
4337                    plotting_error(
4338                        builtin,
4339                        format!("{builtin}: Normalization must be a string"),
4340                    )
4341                })?
4342                .trim()
4343                .to_ascii_lowercase();
4344            validate_histogram_normalization(&norm, builtin)?;
4345            let normalized =
4346                apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &norm);
4347            let labels = histogram_labels_from_edges(&hist.bin_edges);
4348            super::state::update_histogram_plot_data(
4349                hist.figure,
4350                hist.plot_index,
4351                labels,
4352                normalized,
4353            )
4354            .map_err(|err| map_figure_error(builtin, err))?;
4355            super::state::update_histogram_handle_for_plot(
4356                hist.figure,
4357                hist.axes_index,
4358                hist.plot_index,
4359                norm,
4360                hist.raw_counts.clone(),
4361            )
4362            .map_err(|err| map_figure_error(builtin, err))?;
4363            Ok(())
4364        }
4365        "displayname" => {
4366            let display_name = value_as_string(value).map(|s| s.to_string());
4367            super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4368                if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4369                    bar.label = display_name.clone();
4370                }
4371            })
4372            .map_err(|err| map_figure_error(builtin, err))?;
4373            super::state::set_histogram_handle_display_name(
4374                hist.figure,
4375                hist.axes_index,
4376                hist.plot_index,
4377                display_name,
4378            )
4379            .map_err(|err| map_figure_error(builtin, err))?;
4380            Ok(())
4381        }
4382        "displaystyle" => {
4383            let display_style = value_as_string(value)
4384                .ok_or_else(|| {
4385                    plotting_error(builtin, format!("{builtin}: DisplayStyle must be a string"))
4386                })?
4387                .trim()
4388                .to_ascii_lowercase();
4389            let style = match display_style.as_str() {
4390                "bar" => PolarHistogramDisplayStyle::Bar,
4391                "stairs" => PolarHistogramDisplayStyle::Stairs,
4392                other => {
4393                    return Err(plotting_error(
4394                        builtin,
4395                        format!("{builtin}: unsupported histogram DisplayStyle `{other}`"),
4396                    ));
4397                }
4398            };
4399            if !hist.metadata.is_polar && display_style == "stairs" {
4400                return Err(plotting_error(
4401                    builtin,
4402                    format!(
4403                        "{builtin}: DisplayStyle 'stairs' is only supported for polar histograms"
4404                    ),
4405                ));
4406            }
4407            super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4408                if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4409                    if bar.is_polar_histogram() {
4410                        bar.set_polar_histogram_display_style(style);
4411                    }
4412                }
4413            })
4414            .map_err(|err| map_figure_error(builtin, err))?;
4415            super::state::update_histogram_handle_metadata_for_plot(
4416                hist.figure,
4417                hist.axes_index,
4418                hist.plot_index,
4419                |metadata| metadata.display_style = display_style.clone(),
4420            )
4421            .map_err(|err| map_figure_error(builtin, err))?;
4422            Ok(())
4423        }
4424        "facealpha" => {
4425            let alpha = value_as_f64(value).ok_or_else(|| {
4426                plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
4427            })?;
4428            let alpha = alpha.clamp(0.0, 1.0);
4429            super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4430                if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4431                    let mut color = bar.color;
4432                    color.w = alpha as f32;
4433                    bar.apply_face_style(color, bar.bar_width);
4434                }
4435            })
4436            .map_err(|err| map_figure_error(builtin, err))?;
4437            super::state::update_histogram_handle_metadata_for_plot(
4438                hist.figure,
4439                hist.axes_index,
4440                hist.plot_index,
4441                |metadata| metadata.face_alpha = alpha,
4442            )
4443            .map_err(|err| map_figure_error(builtin, err))?;
4444            Ok(())
4445        }
4446        "facecolor" | "edgecolor" => {
4447            let Some(color_text) = value_as_string(value) else {
4448                return Err(plotting_error(
4449                    builtin,
4450                    format!("{builtin}: color property must be a string"),
4451                ));
4452            };
4453            let lower = color_text.trim().to_ascii_lowercase();
4454            let color = match lower.as_str() {
4455                "auto" => None,
4456                "none" if key == "edgecolor" => None,
4457                _ => Some(color_from_name_or_token(&lower).ok_or_else(|| {
4458                    plotting_error(builtin, format!("{builtin}: unsupported color `{lower}`"))
4459                })?),
4460            };
4461            super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4462                if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4463                    if key == "facecolor" {
4464                        if let Some(mut color) = color {
4465                            color.w = hist.metadata.face_alpha as f32;
4466                            bar.apply_face_style(color, bar.bar_width);
4467                        }
4468                    } else {
4469                        bar.apply_outline_style(color, bar.outline_width);
4470                    }
4471                }
4472            })
4473            .map_err(|err| map_figure_error(builtin, err))?;
4474            super::state::update_histogram_handle_metadata_for_plot(
4475                hist.figure,
4476                hist.axes_index,
4477                hist.plot_index,
4478                |metadata| {
4479                    if key == "facecolor" {
4480                        metadata.face_color = lower.clone();
4481                    } else {
4482                        metadata.edge_color = lower.clone();
4483                    }
4484                },
4485            )
4486            .map_err(|err| map_figure_error(builtin, err))?;
4487            Ok(())
4488        }
4489        other => Err(plotting_error(
4490            builtin,
4491            format!("{builtin}: unsupported histogram property `{other}`"),
4492        )),
4493    }
4494}
4495
4496fn apply_histogram2_property(
4497    hist: &super::state::Histogram2HandleState,
4498    key: &str,
4499    value: &Value,
4500    builtin: &'static str,
4501) -> BuiltinResult<()> {
4502    let mut next = hist.clone();
4503    apply_histogram2_property_to_state(&mut next, key, value, builtin)?;
4504    refresh_histogram2_chart(next, builtin)
4505}
4506
4507fn apply_histogram2_properties(
4508    hist: &super::state::Histogram2HandleState,
4509    pairs: &[(String, &Value)],
4510    builtin: &'static str,
4511) -> BuiltinResult<()> {
4512    let mut next = hist.clone();
4513    for (key, value) in pairs {
4514        apply_histogram2_property_to_state(&mut next, key, value, builtin)?;
4515    }
4516    refresh_histogram2_chart(next, builtin)
4517}
4518
4519fn apply_histogram2_property_to_state(
4520    next: &mut super::state::Histogram2HandleState,
4521    key: &str,
4522    value: &Value,
4523    builtin: &'static str,
4524) -> BuiltinResult<()> {
4525    match key {
4526        "normalization" => {
4527            next.normalization = value_as_string(value)
4528                .ok_or_else(|| {
4529                    plotting_error(
4530                        builtin,
4531                        format!("{builtin}: Normalization must be a string"),
4532                    )
4533                })?
4534                .trim()
4535                .to_ascii_lowercase();
4536            next.values = histogram2::apply_normalization_2d(
4537                &next.raw_counts,
4538                &next.x_bin_edges,
4539                &next.y_bin_edges,
4540                &next.normalization,
4541            )?;
4542        }
4543        "displaystyle" => {
4544            next.display_style = histogram2::parse_display_style(value)?;
4545        }
4546        "showemptybins" => {
4547            next.show_empty_bins = histogram2::option_bool(value, "ShowEmptyBins")?;
4548        }
4549        "facealpha" => {
4550            let alpha = histogram2::option_scalar(value, "FaceAlpha")?;
4551            histogram2::validate_face_alpha(alpha)?;
4552            next.face_alpha = alpha;
4553        }
4554        "displayname" => {
4555            next.display_name = value_as_string(value).map(|s| s.to_string());
4556        }
4557        other => Err(plotting_error(
4558            builtin,
4559            format!("{builtin}: unsupported histogram2 property `{other}`"),
4560        ))?,
4561    }
4562    Ok(())
4563}
4564
4565fn refresh_histogram2_chart(
4566    next: super::state::Histogram2HandleState,
4567    builtin: &'static str,
4568) -> BuiltinResult<()> {
4569    let color_limits = current_histogram2_color_limits(&next, builtin)?;
4570    let surface = histogram2::build_surface_from_values(
4571        &next.values,
4572        &next.x_bin_edges,
4573        &next.y_bin_edges,
4574        next.display_style,
4575        next.show_empty_bins,
4576        next.face_alpha,
4577        next.display_name.as_deref(),
4578        color_limits,
4579    )?;
4580    super::state::update_plot_element(next.figure, next.plot_index, |plot| {
4581        if let runmat_plot::plots::figure::PlotElement::Surface(existing) = plot {
4582            *existing = surface;
4583        }
4584    })
4585    .map_err(|err| map_figure_error(builtin, err))?;
4586    super::state::update_histogram2_handle_for_plot(
4587        next.figure,
4588        next.axes_index,
4589        next.plot_index,
4590        |state| {
4591            state.values = next.values;
4592            state.raw_counts = next.raw_counts;
4593            state.x_bin_edges = next.x_bin_edges;
4594            state.y_bin_edges = next.y_bin_edges;
4595            state.normalization = next.normalization;
4596            state.display_style = next.display_style;
4597            state.show_empty_bins = next.show_empty_bins;
4598            state.face_alpha = next.face_alpha;
4599            state.display_name = next.display_name;
4600        },
4601    )
4602    .map_err(|err| map_figure_error(builtin, err))?;
4603    Ok(())
4604}
4605
4606fn current_histogram2_color_limits(
4607    hist: &super::state::Histogram2HandleState,
4608    builtin: &'static str,
4609) -> BuiltinResult<Option<(f64, f64)>> {
4610    let figure = super::state::clone_figure(hist.figure)
4611        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid histogram2 figure")))?;
4612    let plot = figure
4613        .plots()
4614        .nth(hist.plot_index)
4615        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid histogram2 handle")))?;
4616    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4617        return Err(plotting_error(
4618            builtin,
4619            format!("{builtin}: invalid histogram2 handle"),
4620        ));
4621    };
4622    Ok(surface.color_limits)
4623}
4624
4625fn apply_stem_property(
4626    stem_handle: &super::state::StemHandleState,
4627    key: &str,
4628    value: &Value,
4629    builtin: &'static str,
4630) -> BuiltinResult<()> {
4631    super::state::update_stem_plot(
4632        stem_handle.figure,
4633        stem_handle.plot_index,
4634        |stem| match key {
4635            "basevalue" => {
4636                if let Some(v) = value_as_f64(value) {
4637                    stem.baseline = v;
4638                }
4639            }
4640            "baseline" => {
4641                if let Some(v) = value_as_bool(value) {
4642                    stem.baseline_visible = v;
4643                }
4644            }
4645            "linewidth" => {
4646                if let Some(v) = value_as_f64(value) {
4647                    stem.line_width = v as f32;
4648                }
4649            }
4650            "linestyle" => {
4651                if let Some(s) = value_as_string(value) {
4652                    stem.line_style = parse_line_style_name_for_props(&s);
4653                }
4654            }
4655            "color" => {
4656                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4657                    stem.color = c;
4658                }
4659            }
4660            "marker" => {
4661                if let Some(s) = value_as_string(value) {
4662                    stem.marker = marker_from_name(&s, stem.marker.clone());
4663                }
4664            }
4665            "markersize" => {
4666                if let Some(v) = value_as_f64(value) {
4667                    if let Some(marker) = &mut stem.marker {
4668                        marker.size = v as f32;
4669                    }
4670                }
4671            }
4672            "markerfacecolor" => {
4673                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4674                    if let Some(marker) = &mut stem.marker {
4675                        marker.face_color = c;
4676                    }
4677                }
4678            }
4679            "markeredgecolor" => {
4680                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4681                    if let Some(marker) = &mut stem.marker {
4682                        marker.edge_color = c;
4683                    }
4684                }
4685            }
4686            "filled" => {
4687                if let Some(v) = value_as_bool(value) {
4688                    if let Some(marker) = &mut stem.marker {
4689                        marker.filled = v;
4690                    }
4691                }
4692            }
4693            _ => {}
4694        },
4695    )
4696    .map_err(|err| map_figure_error(builtin, err))?;
4697    Ok(())
4698}
4699
4700fn apply_errorbar_property(
4701    error_handle: &super::state::ErrorBarHandleState,
4702    key: &str,
4703    value: &Value,
4704    builtin: &'static str,
4705) -> BuiltinResult<()> {
4706    super::state::update_errorbar_plot(error_handle.figure, error_handle.plot_index, |errorbar| {
4707        match key {
4708            "linewidth" => {
4709                if let Some(v) = value_as_f64(value) {
4710                    errorbar.line_width = v as f32;
4711                }
4712            }
4713            "linestyle" => {
4714                if let Some(s) = value_as_string(value) {
4715                    errorbar.line_style = parse_line_style_name_for_props(&s);
4716                }
4717            }
4718            "color" => {
4719                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4720                    errorbar.color = c;
4721                }
4722            }
4723            "capsize" => {
4724                if let Some(v) = value_as_f64(value) {
4725                    errorbar.cap_size = v as f32;
4726                }
4727            }
4728            "marker" => {
4729                if let Some(s) = value_as_string(value) {
4730                    errorbar.marker = marker_from_name(&s, errorbar.marker.clone());
4731                }
4732            }
4733            "markersize" => {
4734                if let Some(v) = value_as_f64(value) {
4735                    if let Some(marker) = &mut errorbar.marker {
4736                        marker.size = v as f32;
4737                    }
4738                }
4739            }
4740            _ => {}
4741        }
4742    })
4743    .map_err(|err| map_figure_error(builtin, err))?;
4744    Ok(())
4745}
4746
4747fn apply_quiver_property(
4748    quiver_handle: &super::state::QuiverHandleState,
4749    key: &str,
4750    value: &Value,
4751    builtin: &'static str,
4752) -> BuiltinResult<()> {
4753    if matches!(
4754        key,
4755        "xdata" | "ydata" | "zdata" | "udata" | "vdata" | "wdata"
4756    ) {
4757        return apply_quiver_data_property(quiver_handle, key, value, builtin);
4758    }
4759    super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
4760        match key {
4761            "color" => {
4762                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4763                    quiver.color = c;
4764                    quiver.mark_dirty();
4765                }
4766            }
4767            "linewidth" => {
4768                if let Some(v) = value_as_f64(value) {
4769                    quiver.line_width = v as f32;
4770                }
4771            }
4772            "autoscalefactor" => {
4773                if let Some(v) = value_as_f64(value) {
4774                    quiver.scale = v as f32;
4775                    quiver.mark_dirty();
4776                }
4777            }
4778            "maxheadsize" => {
4779                if let Some(v) = value_as_f64(value) {
4780                    quiver.head_size = v as f32;
4781                    quiver.mark_dirty();
4782                }
4783            }
4784            _ => {}
4785        }
4786    })
4787    .map_err(|err| map_figure_error(builtin, err))?;
4788    Ok(())
4789}
4790
4791fn apply_quiver_data_property(
4792    quiver_handle: &super::state::QuiverHandleState,
4793    key: &str,
4794    value: &Value,
4795    builtin: &'static str,
4796) -> BuiltinResult<()> {
4797    if matches!(key, "zdata" | "wdata") && !quiver_handle.is_3d {
4798        return Err(plotting_error(
4799            builtin,
4800            format!("{builtin}: unsupported quiver property `{key}`"),
4801        ));
4802    }
4803    let figure = super::state::clone_figure(quiver_handle.figure)
4804        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
4805    let plot = figure
4806        .plots()
4807        .nth(quiver_handle.plot_index)
4808        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
4809    let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
4810        return Err(plotting_error(
4811            builtin,
4812            format!("{builtin}: invalid quiver handle"),
4813        ));
4814    };
4815    let expected_len = quiver
4816        .cpu_vector_data_len()
4817        .ok_or_else(|| quiver_data_unavailable_error(builtin))?;
4818    let tensor = Tensor::try_from(value).map_err(|err| {
4819        plotting_error(builtin, format!("{builtin}: {key} must be numeric: {err}"))
4820    })?;
4821    if tensor.data.len() != expected_len {
4822        return Err(plotting_error(
4823            builtin,
4824            format!("{builtin}: {key} length must match existing quiver data length"),
4825        ));
4826    }
4827    let data = tensor.data;
4828    super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
4829        match key {
4830            "xdata" => quiver.x = data,
4831            "ydata" => quiver.y = data,
4832            "zdata" => quiver.z = Some(data),
4833            "udata" => quiver.u = data,
4834            "vdata" => quiver.v = data,
4835            "wdata" => quiver.w = Some(data),
4836            _ => {}
4837        }
4838        quiver.mark_dirty();
4839    })
4840    .map_err(|err| map_figure_error(builtin, err))?;
4841    Ok(())
4842}
4843
4844fn apply_image_property(
4845    image_handle: &super::state::ImageHandleState,
4846    key: &str,
4847    value: &Value,
4848    builtin: &'static str,
4849) -> BuiltinResult<()> {
4850    super::state::update_image_plot(image_handle.figure, image_handle.plot_index, |surface| {
4851        match key {
4852            "xdata" => {
4853                if let Ok(tensor) = Tensor::try_from(value) {
4854                    surface.x_data = tensor.data;
4855                }
4856            }
4857            "ydata" => {
4858                if let Ok(tensor) = Tensor::try_from(value) {
4859                    surface.y_data = tensor.data;
4860                }
4861            }
4862            "cdatamapping" => {
4863                if let Some(text) = value_as_string(value) {
4864                    if text.trim().eq_ignore_ascii_case("direct") {
4865                        surface.image_mode = true;
4866                    }
4867                }
4868            }
4869            _ => {}
4870        }
4871    })
4872    .map_err(|err| map_figure_error(builtin, err))?;
4873    Ok(())
4874}
4875
4876fn apply_heatmap_property(
4877    heatmap_handle: &super::state::HeatmapHandleState,
4878    key: &str,
4879    value: &Value,
4880    builtin: &'static str,
4881) -> BuiltinResult<()> {
4882    match key {
4883        "title" => apply_axes_property(
4884            heatmap_handle.figure,
4885            heatmap_handle.axes_index,
4886            "title",
4887            value,
4888            builtin,
4889        ),
4890        "xlabel" => apply_axes_property(
4891            heatmap_handle.figure,
4892            heatmap_handle.axes_index,
4893            "xlabel",
4894            value,
4895            builtin,
4896        ),
4897        "ylabel" => apply_axes_property(
4898            heatmap_handle.figure,
4899            heatmap_handle.axes_index,
4900            "ylabel",
4901            value,
4902            builtin,
4903        ),
4904        "colorbar" | "colorbarvisible" => apply_axes_property(
4905            heatmap_handle.figure,
4906            heatmap_handle.axes_index,
4907            "colorbar",
4908            value,
4909            builtin,
4910        ),
4911        "colormap" => apply_axes_property(
4912            heatmap_handle.figure,
4913            heatmap_handle.axes_index,
4914            "colormap",
4915            value,
4916            builtin,
4917        ),
4918        "xdisplaylabels" => {
4919            let labels = label_strings_from_value(value, builtin, "labels")?;
4920            if labels.len() != heatmap_handle.x_labels.len() {
4921                return Err(plotting_error(
4922                    builtin,
4923                    format!("{builtin}: XDisplayLabels length must match heatmap columns"),
4924                ));
4925            }
4926            super::state::set_heatmap_display_labels(
4927                heatmap_handle.figure,
4928                heatmap_handle.axes_index,
4929                heatmap_handle.plot_index,
4930                Some(labels),
4931                None,
4932            )
4933            .map_err(|err| map_figure_error(builtin, err))
4934        }
4935        "ydisplaylabels" => {
4936            let labels = label_strings_from_value(value, builtin, "labels")?;
4937            if labels.len() != heatmap_handle.y_labels.len() {
4938                return Err(plotting_error(
4939                    builtin,
4940                    format!("{builtin}: YDisplayLabels length must match heatmap rows"),
4941                ));
4942            }
4943            super::state::set_heatmap_display_labels(
4944                heatmap_handle.figure,
4945                heatmap_handle.axes_index,
4946                heatmap_handle.plot_index,
4947                None,
4948                Some(labels),
4949            )
4950            .map_err(|err| map_figure_error(builtin, err))
4951        }
4952        other => Err(plotting_error(
4953            builtin,
4954            format!("{builtin}: unsupported heatmap property `{other}`"),
4955        )),
4956    }
4957}
4958
4959fn apply_area_property(
4960    area_handle: &super::state::AreaHandleState,
4961    key: &str,
4962    value: &Value,
4963    builtin: &'static str,
4964) -> BuiltinResult<()> {
4965    super::state::update_area_plot(
4966        area_handle.figure,
4967        area_handle.plot_index,
4968        |area| match key {
4969            "color" => {
4970                if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4971                    area.color = c;
4972                }
4973            }
4974            "basevalue" => {
4975                if let Some(v) = value_as_f64(value) {
4976                    area.baseline = v;
4977                    area.lower_y = None;
4978                }
4979            }
4980            _ => {}
4981        },
4982    )
4983    .map_err(|err| map_figure_error(builtin, err))?;
4984    Ok(())
4985}
4986
4987fn apply_line_property(
4988    line_handle: &super::state::SimplePlotHandleState,
4989    key: &str,
4990    value: &Value,
4991    builtin: &'static str,
4992) -> BuiltinResult<()> {
4993    apply_line_properties(line_handle, &[(key.to_string(), value)], builtin)
4994}
4995
4996fn apply_line_properties(
4997    line_handle: &super::state::SimplePlotHandleState,
4998    pairs: &[(String, &Value)],
4999    builtin: &'static str,
5000) -> BuiltinResult<()> {
5001    let current = get_simple_plot(line_handle, builtin)?;
5002    let runmat_plot::plots::figure::PlotElement::Line(current_line) = current else {
5003        return Err(plotting_error(
5004            builtin,
5005            format!("{builtin}: invalid line handle"),
5006        ));
5007    };
5008
5009    let (current_x, current_y) = line_xy_data_for_properties(&current_line, builtin)?;
5010    let mut x_update = None;
5011    let mut y_update = None;
5012    let mut style_pairs = Vec::new();
5013    for (key, value) in pairs {
5014        match key.as_str() {
5015            "xdata" => x_update = Some(line_numeric_data(value, "XData", builtin)?),
5016            "ydata" => y_update = Some(line_numeric_data(value, "YData", builtin)?),
5017            _ => style_pairs.push((key.as_str(), *value)),
5018        }
5019    }
5020
5021    let next_x = x_update.unwrap_or_else(|| current_x.clone());
5022    let next_y = y_update.unwrap_or_else(|| current_y.clone());
5023    let update_data = next_x != current_x || next_y != current_y;
5024    if update_data && next_x.len() != next_y.len() {
5025        return Err(plotting_error(
5026            builtin,
5027            format!("{builtin}: XData and YData must contain the same number of elements"),
5028        ));
5029    }
5030    for (key, value) in &style_pairs {
5031        if *key == "handlevisibility" {
5032            parse_handle_visibility(&LineStyleParseOptions::generic(builtin), value)?;
5033        }
5034    }
5035
5036    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
5037        if let runmat_plot::plots::figure::PlotElement::Line(line) = plot {
5038            if update_data {
5039                let _ = line.update_data(next_x.clone(), next_y.clone());
5040            }
5041            for (key, value) in &style_pairs {
5042                apply_line_plot_properties(line, key, value, builtin);
5043            }
5044        }
5045    })
5046    .map_err(|err| map_figure_error(builtin, err))?;
5047    Ok(())
5048}
5049
5050fn apply_animated_line_property(
5051    line_handle: &super::state::AnimatedLineHandleState,
5052    key: &str,
5053    value: &Value,
5054    builtin: &'static str,
5055) -> BuiltinResult<()> {
5056    apply_animated_line_properties(line_handle, &[(key.to_string(), value)], builtin)
5057}
5058
5059fn apply_animated_line_properties(
5060    line_handle: &super::state::AnimatedLineHandleState,
5061    pairs: &[(String, &Value)],
5062    builtin: &'static str,
5063) -> BuiltinResult<()> {
5064    let mut maximum = None;
5065    let mut plot_pairs = Vec::new();
5066    for (key, value) in pairs {
5067        match key.as_str() {
5068            "maximumnumpoints" => {
5069                maximum = Some(animated_line_maximum_from_value(value, builtin)?);
5070            }
5071            _ => plot_pairs.push((key.clone(), *value)),
5072        }
5073    }
5074
5075    if !plot_pairs.is_empty() {
5076        let simple = animated_line_simple_state(line_handle);
5077        if line_handle.is_3d {
5078            apply_line3_properties(&simple, &plot_pairs, builtin)?;
5079        } else {
5080            if plot_pairs.iter().any(|(key, _)| key == "zdata") {
5081                return Err(plotting_error(
5082                    builtin,
5083                    format!("{builtin}: ZData requires a 3-D animated line"),
5084                ));
5085            }
5086            apply_line_properties(&simple, &plot_pairs, builtin)?;
5087        }
5088    }
5089
5090    if let Some(maximum) = maximum {
5091        super::state::set_animated_line_maximum_num_points(line_handle, maximum)
5092            .map_err(|err| plotting_error(builtin, format!("{builtin}: {err}")))?;
5093    }
5094    Ok(())
5095}
5096
5097fn animated_line_maximum_from_value(
5098    value: &Value,
5099    builtin: &'static str,
5100) -> BuiltinResult<Option<usize>> {
5101    let Some(maximum) = value_as_f64(value) else {
5102        return Err(plotting_error(
5103            builtin,
5104            format!("{builtin}: MaximumNumPoints must be numeric"),
5105        ));
5106    };
5107    if maximum.is_infinite() && maximum.is_sign_positive() {
5108        return Ok(None);
5109    }
5110    if !maximum.is_finite() || maximum <= 0.0 {
5111        return Err(plotting_error(
5112            builtin,
5113            format!("{builtin}: MaximumNumPoints must be positive or Inf"),
5114        ));
5115    }
5116    if maximum > usize::MAX as f64 {
5117        return Err(plotting_error(
5118            builtin,
5119            format!("{builtin}: MaximumNumPoints is too large"),
5120        ));
5121    }
5122    Ok(Some(maximum.floor() as usize))
5123}
5124
5125fn apply_reference_line_property(
5126    line_handle: &super::state::SimplePlotHandleState,
5127    key: &str,
5128    value: &Value,
5129    builtin: &'static str,
5130) -> BuiltinResult<()> {
5131    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
5132        if let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot {
5133            match key {
5134                "value" => {
5135                    if let Some(v) = value_as_f64(value) {
5136                        if v.is_finite() {
5137                            line.value = v;
5138                        }
5139                    }
5140                }
5141                "color" => {
5142                    if let Ok(c) =
5143                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5144                    {
5145                        line.color = c;
5146                    }
5147                }
5148                "linewidth" => {
5149                    if let Some(v) = value_as_f64(value) {
5150                        if v > 0.0 {
5151                            line.line_width = v as f32;
5152                        }
5153                    }
5154                }
5155                "linestyle" => {
5156                    if let Some(s) = value_as_string(value) {
5157                        line.line_style = parse_line_style_name_for_props(&s);
5158                    }
5159                }
5160                "label" => {
5161                    line.label = value_as_string(value).map(|s| s.to_string());
5162                }
5163                "labelorientation" => {
5164                    if let Some(s) = value_as_string(value) {
5165                        line.label_orientation = s.to_ascii_lowercase();
5166                    }
5167                }
5168                "displayname" => {
5169                    line.display_name = value_as_string(value).map(|s| s.to_string());
5170                }
5171                "visible" => {
5172                    if let Some(v) = value_as_bool(value) {
5173                        line.visible = v;
5174                    } else if let Some(s) = value_as_string(value) {
5175                        line.visible =
5176                            !matches!(s.trim().to_ascii_lowercase().as_str(), "off" | "false");
5177                    }
5178                }
5179                _ => {}
5180            }
5181        }
5182    })
5183    .map_err(|err| map_figure_error(builtin, err))?;
5184    Ok(())
5185}
5186
5187fn apply_stairs_property(
5188    stairs_handle: &super::state::SimplePlotHandleState,
5189    key: &str,
5190    value: &Value,
5191    builtin: &'static str,
5192) -> BuiltinResult<()> {
5193    super::state::update_plot_element(stairs_handle.figure, stairs_handle.plot_index, |plot| {
5194        if let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot {
5195            match key {
5196                "color" => {
5197                    if let Ok(c) =
5198                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5199                    {
5200                        stairs.color = c;
5201                    }
5202                }
5203                "linewidth" => {
5204                    if let Some(v) = value_as_f64(value) {
5205                        stairs.line_width = v as f32;
5206                    }
5207                }
5208                "displayname" => {
5209                    stairs.label = value_as_string(value).map(|s| s.to_string());
5210                }
5211                _ => {}
5212            }
5213        }
5214    })
5215    .map_err(|err| map_figure_error(builtin, err))?;
5216    Ok(())
5217}
5218
5219fn apply_scatter_property(
5220    scatter_handle: &super::state::SimplePlotHandleState,
5221    key: &str,
5222    value: &Value,
5223    builtin: &'static str,
5224) -> BuiltinResult<()> {
5225    apply_scatter_properties(scatter_handle, &[(key.to_string(), value)], builtin)
5226}
5227
5228fn apply_scatter_properties(
5229    scatter_handle: &super::state::SimplePlotHandleState,
5230    pairs: &[(String, &Value)],
5231    builtin: &'static str,
5232) -> BuiltinResult<()> {
5233    let current = get_simple_plot(scatter_handle, builtin)?;
5234    let runmat_plot::plots::figure::PlotElement::Scatter(current_scatter) = current else {
5235        return Err(plotting_error(
5236            builtin,
5237            format!("{builtin}: invalid scatter handle"),
5238        ));
5239    };
5240
5241    let current_theta = current_scatter
5242        .theta_data
5243        .clone()
5244        .unwrap_or_else(|| cartesian_theta(&current_scatter.x_data, &current_scatter.y_data));
5245    let current_r = current_scatter
5246        .r_data
5247        .clone()
5248        .unwrap_or_else(|| cartesian_radius(&current_scatter.x_data, &current_scatter.y_data));
5249    let mut theta_update = None;
5250    let mut r_update = None;
5251    let mut style_pairs = Vec::new();
5252    for (key, value) in pairs {
5253        match key.as_str() {
5254            "thetadata" => theta_update = Some(line_numeric_data(value, "ThetaData", builtin)?),
5255            "rdata" => r_update = Some(line_numeric_data(value, "RData", builtin)?),
5256            _ => style_pairs.push((key.as_str(), *value)),
5257        }
5258    }
5259
5260    let next_theta = theta_update.unwrap_or_else(|| current_theta.clone());
5261    let next_r = r_update.unwrap_or_else(|| current_r.clone());
5262    let polar_update =
5263        (next_theta != current_theta || next_r != current_r).then_some((next_theta, next_r));
5264    if let Some((theta, r)) = polar_update.as_ref() {
5265        validate_polar_scatter_data(theta, r, builtin)?;
5266    }
5267
5268    super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
5269        if let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot {
5270            if let Some((theta, r)) = polar_update.as_ref() {
5271                replace_polar_scatter_data_unchecked(scatter, theta.clone(), r.clone());
5272            }
5273            for (key, value) in &style_pairs {
5274                match *key {
5275                    "marker" => {
5276                        if let Some(s) = value_as_string(value) {
5277                            scatter.marker_style =
5278                                scatter_marker_from_name(&s, scatter.marker_style);
5279                        }
5280                    }
5281                    "sizedata" => {
5282                        if let Some(v) = value_as_f64(value) {
5283                            scatter.marker_size = marker_area_points2_to_diameter_px(v);
5284                        }
5285                    }
5286                    "markerfacecolor" => {
5287                        if let Ok(c) =
5288                            parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5289                        {
5290                            scatter.set_face_color(c);
5291                        }
5292                    }
5293                    "markeredgecolor" => {
5294                        if let Ok(c) =
5295                            parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5296                        {
5297                            scatter.set_edge_color(c);
5298                        }
5299                    }
5300                    "linewidth" => {
5301                        if let Some(v) = value_as_f64(value) {
5302                            scatter.set_edge_thickness(v as f32);
5303                        }
5304                    }
5305                    "displayname" => {
5306                        scatter.label = value_as_string(value).map(|s| s.to_string());
5307                    }
5308                    _ => {}
5309                }
5310            }
5311        }
5312    })
5313    .map_err(|err| map_figure_error(builtin, err))?;
5314    Ok(())
5315}
5316
5317fn validate_polar_scatter_data(
5318    theta: &[f64],
5319    r: &[f64],
5320    builtin: &'static str,
5321) -> BuiltinResult<()> {
5322    if theta.len() != r.len() {
5323        return Err(plotting_error(
5324            builtin,
5325            format!("{builtin}: ThetaData and RData must contain the same number of elements"),
5326        ));
5327    }
5328    if theta.is_empty() {
5329        return Err(plotting_error(
5330            builtin,
5331            format!("{builtin}: ThetaData and RData cannot be empty"),
5332        ));
5333    }
5334    Ok(())
5335}
5336
5337fn replace_polar_scatter_data_unchecked(
5338    scatter: &mut runmat_plot::plots::ScatterPlot,
5339    theta: Vec<f64>,
5340    r: Vec<f64>,
5341) {
5342    let x_data = theta
5343        .iter()
5344        .zip(r.iter())
5345        .map(|(theta, r)| r * theta.cos())
5346        .collect();
5347    let y_data = theta
5348        .iter()
5349        .zip(r.iter())
5350        .map(|(theta, r)| r * theta.sin())
5351        .collect();
5352    let point_count = theta.len();
5353    scatter
5354        .update_data(x_data, y_data)
5355        .expect("validated polar scatter data can update cartesian scatter data");
5356    if scatter
5357        .per_point_sizes
5358        .as_ref()
5359        .is_some_and(|sizes| sizes.len() != point_count)
5360    {
5361        scatter.per_point_sizes = None;
5362    }
5363    if scatter
5364        .per_point_colors
5365        .as_ref()
5366        .is_some_and(|colors| colors.len() != point_count)
5367    {
5368        scatter.per_point_colors = None;
5369    }
5370    if scatter
5371        .color_values
5372        .as_ref()
5373        .is_some_and(|values| values.len() != point_count)
5374    {
5375        scatter.color_values = None;
5376        scatter.color_limits = None;
5377    }
5378    scatter.theta_data = Some(theta);
5379    scatter.r_data = Some(r);
5380}
5381
5382fn scatter_size_data_value(scatter: &runmat_plot::plots::ScatterPlot) -> Value {
5383    match scatter.per_point_sizes.as_ref() {
5384        Some(sizes) => tensor_from_vec(
5385            sizes
5386                .iter()
5387                .map(|size| marker_diameter_px_to_area_points2(*size))
5388                .collect(),
5389        ),
5390        None => Value::Num(marker_diameter_px_to_area_points2(scatter.marker_size)),
5391    }
5392}
5393
5394fn cartesian_theta(x: &[f64], y: &[f64]) -> Vec<f64> {
5395    x.iter().zip(y.iter()).map(|(x, y)| y.atan2(*x)).collect()
5396}
5397
5398fn cartesian_radius(x: &[f64], y: &[f64]) -> Vec<f64> {
5399    x.iter().zip(y.iter()).map(|(x, y)| x.hypot(*y)).collect()
5400}
5401
5402fn apply_bar_property(
5403    bar_handle: &super::state::SimplePlotHandleState,
5404    key: &str,
5405    value: &Value,
5406    builtin: &'static str,
5407) -> BuiltinResult<()> {
5408    super::state::update_plot_element(bar_handle.figure, bar_handle.plot_index, |plot| {
5409        if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
5410            match key {
5411                "facecolor" | "color" => {
5412                    if let Ok(c) =
5413                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5414                    {
5415                        bar.color = c;
5416                    }
5417                }
5418                "barwidth" => {
5419                    if let Some(v) = value_as_f64(value) {
5420                        bar.bar_width = v as f32;
5421                    }
5422                }
5423                "displayname" => {
5424                    bar.label = value_as_string(value).map(|s| s.to_string());
5425                }
5426                _ => {}
5427            }
5428        }
5429    })
5430    .map_err(|err| map_figure_error(builtin, err))?;
5431    Ok(())
5432}
5433
5434#[derive(Clone, Debug)]
5435enum SurfacePropertyUpdate {
5436    FaceAlpha(f32),
5437    DisplayName(Option<String>),
5438    Visible(bool),
5439    Colormap(ColorMap),
5440    Shading(ShadingMode),
5441    Wireframe(bool),
5442    FlattenZ(bool),
5443    Lighting(bool),
5444    FaceColor(glam::Vec4),
5445    FaceNone,
5446}
5447
5448fn apply_surface_property(
5449    surface_handle: &super::state::SimplePlotHandleState,
5450    key: &str,
5451    value: &Value,
5452    builtin: &'static str,
5453) -> BuiltinResult<()> {
5454    if matches!(key, "xdata" | "ydata" | "zdata") {
5455        return apply_surface_data_property(surface_handle, key, value, builtin);
5456    }
5457
5458    let update = surface_property_update(key, value, builtin)?;
5459    super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
5460        if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
5461            match &update {
5462                SurfacePropertyUpdate::FaceAlpha(alpha) => surface.alpha = *alpha,
5463                SurfacePropertyUpdate::DisplayName(label) => surface.label = label.clone(),
5464                SurfacePropertyUpdate::Visible(visible) => surface.visible = *visible,
5465                SurfacePropertyUpdate::Colormap(colormap) => surface.colormap = colormap.clone(),
5466                SurfacePropertyUpdate::Shading(shading) => surface.shading_mode = *shading,
5467                SurfacePropertyUpdate::Wireframe(wireframe) => surface.wireframe = *wireframe,
5468                SurfacePropertyUpdate::FlattenZ(flatten_z) => surface.flatten_z = *flatten_z,
5469                SurfacePropertyUpdate::Lighting(enabled) => surface.lighting_enabled = *enabled,
5470                SurfacePropertyUpdate::FaceColor(color) => {
5471                    surface.colormap = ColorMap::Custom(*color, *color);
5472                    surface.shading_mode = ShadingMode::None;
5473                    surface.lighting_enabled = false;
5474                }
5475                SurfacePropertyUpdate::FaceNone => surface.alpha = 0.0,
5476            }
5477        }
5478    })
5479    .map_err(|err| map_figure_error(builtin, err))?;
5480    Ok(())
5481}
5482
5483fn apply_surface_data_property(
5484    surface_handle: &super::state::SimplePlotHandleState,
5485    key: &str,
5486    value: &Value,
5487    builtin: &'static str,
5488) -> BuiltinResult<()> {
5489    let current = get_simple_plot(surface_handle, builtin)?;
5490    let runmat_plot::plots::figure::PlotElement::Surface(current_surface) = current else {
5491        return Err(plotting_error(
5492            builtin,
5493            format!("{builtin}: invalid surface handle"),
5494        ));
5495    };
5496    let z_grid = current_surface.z_data.clone().ok_or_else(|| {
5497        plotting_error(
5498            builtin,
5499            format!("{builtin}: changing {key} for GPU-only surface data is not supported yet"),
5500        )
5501    })?;
5502    let (x_len, y_len) = surface_grid_size(&z_grid)?;
5503
5504    let next = match key {
5505        "xdata" => {
5506            let data = surface_coordinate_data_from_value(value, "XData", builtin)?;
5507            let (x_grid, y_grid, z_grid) = match data {
5508                SurfaceCoordinateData::Vector(axis) => {
5509                    if axis.len() != x_len {
5510                        return Err(plotting_error(
5511                            builtin,
5512                            format!("{builtin}: XData vector length must match surface column count {x_len}"),
5513                        ));
5514                    }
5515                    let y_grid = current_surface
5516                        .y_grid
5517                        .clone()
5518                        .unwrap_or_else(|| axis_to_y_grid(&current_surface.y_data, x_len));
5519                    (axis_to_x_grid(&axis, y_len), y_grid, z_grid)
5520                }
5521                SurfaceCoordinateData::Grid(grid) => {
5522                    validate_surface_grid_shape(&grid, x_len, y_len, "XData", builtin)?;
5523                    let y_grid = current_surface
5524                        .y_grid
5525                        .clone()
5526                        .unwrap_or_else(|| axis_to_y_grid(&current_surface.y_data, x_len));
5527                    (grid, y_grid, z_grid)
5528                }
5529            };
5530            SurfaceDataUpdate::CoordinateGrids {
5531                x_grid,
5532                y_grid,
5533                z_grid,
5534            }
5535        }
5536        "ydata" => {
5537            let data = surface_coordinate_data_from_value(value, "YData", builtin)?;
5538            let (x_grid, y_grid, z_grid) = match data {
5539                SurfaceCoordinateData::Vector(axis) => {
5540                    if axis.len() != y_len {
5541                        return Err(plotting_error(
5542                            builtin,
5543                            format!("{builtin}: YData vector length must match surface row count {y_len}"),
5544                        ));
5545                    }
5546                    let x_grid = current_surface
5547                        .x_grid
5548                        .clone()
5549                        .unwrap_or_else(|| axis_to_x_grid(&current_surface.x_data, y_len));
5550                    (x_grid, axis_to_y_grid(&axis, x_len), z_grid)
5551                }
5552                SurfaceCoordinateData::Grid(grid) => {
5553                    validate_surface_grid_shape(&grid, x_len, y_len, "YData", builtin)?;
5554                    let x_grid = current_surface
5555                        .x_grid
5556                        .clone()
5557                        .unwrap_or_else(|| axis_to_x_grid(&current_surface.x_data, y_len));
5558                    (x_grid, grid, z_grid)
5559                }
5560            };
5561            SurfaceDataUpdate::CoordinateGrids {
5562                x_grid,
5563                y_grid,
5564                z_grid,
5565            }
5566        }
5567        "zdata" => {
5568            let z_grid = surface_z_grid_from_value(value, y_len, x_len, builtin)?;
5569            if let (Some(x_grid), Some(y_grid)) = (
5570                current_surface.x_grid.clone(),
5571                current_surface.y_grid.clone(),
5572            ) {
5573                SurfaceDataUpdate::CoordinateGrids {
5574                    x_grid,
5575                    y_grid,
5576                    z_grid,
5577                }
5578            } else {
5579                SurfaceDataUpdate::AxisData {
5580                    x_data: current_surface.x_data.clone(),
5581                    y_data: current_surface.y_data.clone(),
5582                    z_grid,
5583                }
5584            }
5585        }
5586        other => {
5587            return Err(plotting_error(
5588                builtin,
5589                format!("{builtin}: unsupported surface property `{other}`"),
5590            ))
5591        }
5592    };
5593
5594    super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
5595        if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
5596            match &next {
5597                SurfaceDataUpdate::AxisData {
5598                    x_data,
5599                    y_data,
5600                    z_grid,
5601                } => {
5602                    let _ =
5603                        surface.update_axis_data(x_data.clone(), y_data.clone(), z_grid.clone());
5604                }
5605                SurfaceDataUpdate::CoordinateGrids {
5606                    x_grid,
5607                    y_grid,
5608                    z_grid,
5609                } => {
5610                    let _ = surface.update_coordinate_grids(
5611                        x_grid.clone(),
5612                        y_grid.clone(),
5613                        z_grid.clone(),
5614                    );
5615                }
5616            }
5617        }
5618    })
5619    .map_err(|err| map_figure_error(builtin, err))?;
5620    Ok(())
5621}
5622
5623enum SurfaceDataUpdate {
5624    AxisData {
5625        x_data: Vec<f64>,
5626        y_data: Vec<f64>,
5627        z_grid: Vec<Vec<f64>>,
5628    },
5629    CoordinateGrids {
5630        x_grid: Vec<Vec<f64>>,
5631        y_grid: Vec<Vec<f64>>,
5632        z_grid: Vec<Vec<f64>>,
5633    },
5634}
5635
5636enum SurfaceCoordinateData {
5637    Vector(Vec<f64>),
5638    Grid(Vec<Vec<f64>>),
5639}
5640
5641fn surface_property_update(
5642    key: &str,
5643    value: &Value,
5644    builtin: &'static str,
5645) -> BuiltinResult<SurfacePropertyUpdate> {
5646    match key {
5647        "facealpha" | "alpha" => {
5648            let alpha = value_as_f64(value).ok_or_else(|| {
5649                plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
5650            })?;
5651            Ok(SurfacePropertyUpdate::FaceAlpha(
5652                alpha.clamp(0.0, 1.0) as f32
5653            ))
5654        }
5655        "displayname" | "label" => Ok(SurfacePropertyUpdate::DisplayName(
5656            value_as_string(value).map(|s| s.to_string()),
5657        )),
5658        "visible" => {
5659            let visible = value_as_bool(value).ok_or_else(|| {
5660                plotting_error(builtin, format!("{builtin}: Visible must be logical"))
5661            })?;
5662            Ok(SurfacePropertyUpdate::Visible(visible))
5663        }
5664        "colormap" => {
5665            if let Some(name) = value_as_string(value) {
5666                return Ok(SurfacePropertyUpdate::Colormap(parse_colormap_name(
5667                    &name, builtin,
5668                )?));
5669            }
5670            let color = parse_color_value(&LineStyleParseOptions::generic(builtin), value)?;
5671            Ok(SurfacePropertyUpdate::Colormap(ColorMap::Custom(
5672                color, color,
5673            )))
5674        }
5675        "shading" => Ok(SurfacePropertyUpdate::Shading(surface_shading_from_value(
5676            value, builtin,
5677        )?)),
5678        "edgecolor" => {
5679            let text = value_as_string(value).ok_or_else(|| {
5680                plotting_error(
5681                    builtin,
5682                    format!("{builtin}: EdgeColor must be 'auto', 'flat', 'interp', or 'none'"),
5683                )
5684            })?;
5685            match text.trim().to_ascii_lowercase().as_str() {
5686                "none" => Ok(SurfacePropertyUpdate::Wireframe(false)),
5687                "auto" | "flat" | "interp" => Ok(SurfacePropertyUpdate::Wireframe(true)),
5688                other => Err(plotting_error(
5689                    builtin,
5690                    format!("{builtin}: unsupported EdgeColor `{other}`"),
5691                )),
5692            }
5693        }
5694        "facecolor" | "color" => surface_face_color_update(value, builtin),
5695        "flattenz" => {
5696            let flatten_z = value_as_bool(value).ok_or_else(|| {
5697                plotting_error(builtin, format!("{builtin}: FlattenZ must be logical"))
5698            })?;
5699            Ok(SurfacePropertyUpdate::FlattenZ(flatten_z))
5700        }
5701        "lighting" => Ok(SurfacePropertyUpdate::Lighting(
5702            surface_lighting_from_value(value, builtin)?,
5703        )),
5704        other => Err(plotting_error(
5705            builtin,
5706            format!("{builtin}: unsupported surface property `{other}`"),
5707        )),
5708    }
5709}
5710
5711fn apply_function_surface_property(
5712    function_surface: &super::state::FunctionSurfaceHandleState,
5713    key: &str,
5714    value: &Value,
5715    builtin: &'static str,
5716) -> BuiltinResult<()> {
5717    match key {
5718        "facealpha" | "displayname" => {
5719            let surface_handle = super::state::SimplePlotHandleState {
5720                figure: function_surface.figure,
5721                axes_index: function_surface.axes_index,
5722                plot_index: function_surface.plot_index,
5723            };
5724            apply_surface_property(&surface_handle, key, value, builtin)
5725        }
5726        "meshdensity" | "xrange" | "yrange" | "function" | "xfunction" | "yfunction"
5727        | "zfunction" => Err(plotting_error(
5728            builtin,
5729            format!("{builtin}: changing {key} after fsurf sampling is not supported yet"),
5730        )),
5731        _ => Ok(()),
5732    }
5733}
5734
5735fn apply_function_contour_property(
5736    function_contour: &super::state::FunctionContourHandleState,
5737    key: &str,
5738    value: &Value,
5739    builtin: &'static str,
5740) -> BuiltinResult<()> {
5741    match key {
5742        "linewidth" | "displayname" => {
5743            let contour_handle = super::state::SimplePlotHandleState {
5744                figure: function_contour.figure,
5745                axes_index: function_contour.axes_index,
5746                plot_index: function_contour.plot_index,
5747            };
5748            apply_contour_property(&contour_handle, key, value, builtin)
5749        }
5750        "meshdensity" | "xrange" | "yrange" | "function" => Err(plotting_error(
5751            builtin,
5752            format!("{builtin}: changing {key} after fcontour sampling is not supported yet"),
5753        )),
5754        _ => Ok(()),
5755    }
5756}
5757
5758fn apply_binscatter_property(
5759    binscatter: &super::state::BinscatterHandleState,
5760    key: &str,
5761    value: &Value,
5762    builtin: &'static str,
5763) -> BuiltinResult<()> {
5764    let mut next = binscatter.clone();
5765    match key {
5766        "numbins" => {
5767            next.num_bins = binscatter::parse_num_bins(value)?;
5768            next.auto_bins = false;
5769        }
5770        "showemptybins" => {
5771            next.show_empty_bins = binscatter::option_bool(value, "ShowEmptyBins")?;
5772        }
5773        "xlimits" => {
5774            next.x_limits_option = Some(binscatter::parse_limits(value, "XLimits")?);
5775        }
5776        "ylimits" => {
5777            next.y_limits_option = Some(binscatter::parse_limits(value, "YLimits")?);
5778        }
5779        "xdata" => {
5780            next.x_data = binscatter_numeric_data(value, "XData", builtin)?;
5781        }
5782        "ydata" => {
5783            next.y_data = binscatter_numeric_data(value, "YData", builtin)?;
5784        }
5785        "facealpha" => {
5786            let alpha = value_as_f64(value).ok_or_else(|| {
5787                plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
5788            })?;
5789            binscatter::validate_face_alpha(alpha)?;
5790            next.face_alpha = alpha;
5791        }
5792        "displayname" => {
5793            next.display_name = value_as_string(value).map(|s| s.to_string());
5794        }
5795        other => Err(plotting_error(
5796            builtin,
5797            format!("{builtin}: unsupported binscatter property `{other}`"),
5798        ))?,
5799    }
5800    recompute_binscatter_chart(next, builtin)
5801}
5802
5803fn binscatter_numeric_data(
5804    value: &Value,
5805    name: &str,
5806    builtin: &'static str,
5807) -> BuiltinResult<Vec<f64>> {
5808    let tensor = tensor::value_to_tensor(value)
5809        .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
5810    Ok(tensor.data)
5811}
5812
5813fn recompute_binscatter_chart(
5814    mut next: super::state::BinscatterHandleState,
5815    builtin: &'static str,
5816) -> BuiltinResult<()> {
5817    if next.x_data.len() != next.y_data.len() {
5818        return Err(plotting_error(
5819            builtin,
5820            format!("{builtin}: XData and YData must contain the same number of elements"),
5821        ));
5822    }
5823    if next.auto_bins {
5824        next.num_bins = [
5825            binscatter::auto_bin_count(&next.x_data, next.x_limits_option, 100)?,
5826            binscatter::auto_bin_count(&next.y_data, next.y_limits_option, 100)?,
5827        ];
5828    }
5829    let color_limits = current_binscatter_color_limits(&next, builtin)?;
5830    let chart = binscatter::build_binscatter_chart(
5831        &next.x_data,
5832        &next.y_data,
5833        next.num_bins,
5834        next.x_limits_option,
5835        next.y_limits_option,
5836        next.show_empty_bins,
5837        next.face_alpha,
5838        next.display_name.as_deref(),
5839        color_limits,
5840    )?;
5841    let x_limits = (
5842        *chart.x_bin_edges.first().unwrap_or(&0.0),
5843        *chart.x_bin_edges.last().unwrap_or(&1.0),
5844    );
5845    let y_limits = (
5846        *chart.y_bin_edges.first().unwrap_or(&0.0),
5847        *chart.y_bin_edges.last().unwrap_or(&1.0),
5848    );
5849    let surface = chart.surface.clone();
5850    super::state::update_image_plot(next.figure, next.plot_index, |existing| {
5851        *existing = surface;
5852    })
5853    .map_err(|err| map_figure_error(builtin, err))?;
5854    super::state::update_binscatter_handle_for_plot(next.figure, next.plot_index, |state| {
5855        state.values = chart.values;
5856        state.x_bin_edges = chart.x_bin_edges;
5857        state.y_bin_edges = chart.y_bin_edges;
5858        state.x_data = next.x_data;
5859        state.y_data = next.y_data;
5860        state.num_bins = next.num_bins;
5861        state.auto_bins = next.auto_bins;
5862        state.x_limits_option = next.x_limits_option;
5863        state.y_limits_option = next.y_limits_option;
5864        state.x_limits = x_limits;
5865        state.y_limits = y_limits;
5866        state.show_empty_bins = next.show_empty_bins;
5867        state.face_alpha = next.face_alpha;
5868        state.display_name = next.display_name;
5869    })
5870    .map_err(|err| map_figure_error(builtin, err))?;
5871    Ok(())
5872}
5873
5874fn current_binscatter_color_limits(
5875    binscatter: &super::state::BinscatterHandleState,
5876    builtin: &'static str,
5877) -> BuiltinResult<Option<(f64, f64)>> {
5878    let figure = super::state::clone_figure(binscatter.figure)
5879        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid binscatter figure")))?;
5880    let plot = figure
5881        .plots()
5882        .nth(binscatter.plot_index)
5883        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid binscatter handle")))?;
5884    let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
5885        return Err(plotting_error(
5886            builtin,
5887            format!("{builtin}: invalid binscatter handle"),
5888        ));
5889    };
5890    Ok(surface.color_limits)
5891}
5892
5893fn apply_patch_property(
5894    patch_handle: &super::state::SimplePlotHandleState,
5895    key: &str,
5896    value: &Value,
5897    builtin: &'static str,
5898) -> BuiltinResult<()> {
5899    super::state::update_plot_element(patch_handle.figure, patch_handle.plot_index, |plot| {
5900        if let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot {
5901            match key {
5902                "facecolor" | "color" => {
5903                    if let Some(text) = value_as_string(value) {
5904                        match text.trim().to_ascii_lowercase().as_str() {
5905                            "none" => patch
5906                                .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::None),
5907                            "flat" => patch
5908                                .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Flat),
5909                            _ => {
5910                                if let Ok(c) = parse_color_value(
5911                                    &LineStyleParseOptions::generic(builtin),
5912                                    value,
5913                                ) {
5914                                    patch.set_face_color(c);
5915                                    patch.set_face_color_mode(
5916                                        runmat_plot::plots::PatchFaceColorMode::Color,
5917                                    );
5918                                }
5919                            }
5920                        }
5921                    } else if let Ok(c) =
5922                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5923                    {
5924                        patch.set_face_color(c);
5925                        patch.set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Color);
5926                    }
5927                }
5928                "edgecolor" => {
5929                    if let Some(text) = value_as_string(value) {
5930                        if text.trim().eq_ignore_ascii_case("none") {
5931                            patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::None);
5932                        } else if let Ok(c) =
5933                            parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5934                        {
5935                            patch.set_edge_color(c);
5936                            patch
5937                                .set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
5938                        }
5939                    } else if let Ok(c) =
5940                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5941                    {
5942                        patch.set_edge_color(c);
5943                        patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
5944                    }
5945                }
5946                "facealpha" => {
5947                    if let Some(v) = value_as_f64(value) {
5948                        patch.set_face_alpha(v as f32);
5949                    }
5950                }
5951                "edgealpha" => {
5952                    if let Some(v) = value_as_f64(value) {
5953                        patch.set_edge_alpha(v as f32);
5954                    }
5955                }
5956                "linewidth" => {
5957                    if let Some(v) = value_as_f64(value) {
5958                        patch.set_line_width(v as f32);
5959                    }
5960                }
5961                "displayname" => {
5962                    patch.set_label(value_as_string(value).map(|s| s.to_string()));
5963                }
5964                "visible" => {
5965                    if let Some(v) = value_as_bool(value) {
5966                        patch.set_visible(v);
5967                    }
5968                }
5969                _ => {}
5970            }
5971        }
5972    })
5973    .map_err(|err| map_figure_error(builtin, err))?;
5974    Ok(())
5975}
5976
5977fn apply_line3_property(
5978    line_handle: &super::state::SimplePlotHandleState,
5979    key: &str,
5980    value: &Value,
5981    builtin: &'static str,
5982) -> BuiltinResult<()> {
5983    apply_line3_properties(line_handle, &[(key.to_string(), value)], builtin)
5984}
5985
5986fn apply_line3_properties(
5987    line_handle: &super::state::SimplePlotHandleState,
5988    pairs: &[(String, &Value)],
5989    builtin: &'static str,
5990) -> BuiltinResult<()> {
5991    let current = get_simple_plot(line_handle, builtin)?;
5992    let runmat_plot::plots::figure::PlotElement::Line3(current_line) = current else {
5993        return Err(plotting_error(
5994            builtin,
5995            format!("{builtin}: invalid line handle"),
5996        ));
5997    };
5998
5999    let mut x_update = None;
6000    let mut y_update = None;
6001    let mut z_update = None;
6002    let mut style_pairs = Vec::new();
6003    for (key, value) in pairs {
6004        match key.as_str() {
6005            "xdata" => x_update = Some(line_numeric_data(value, "XData", builtin)?),
6006            "ydata" => y_update = Some(line_numeric_data(value, "YData", builtin)?),
6007            "zdata" => z_update = Some(line_numeric_data(value, "ZData", builtin)?),
6008            _ => style_pairs.push((key.as_str(), *value)),
6009        }
6010    }
6011
6012    let next_x = x_update.unwrap_or_else(|| current_line.x_data.clone());
6013    let next_y = y_update.unwrap_or_else(|| current_line.y_data.clone());
6014    let next_z = z_update.unwrap_or_else(|| current_line.z_data.clone());
6015    let update_data = next_x != current_line.x_data
6016        || next_y != current_line.y_data
6017        || next_z != current_line.z_data;
6018    if update_data
6019        && (next_x.is_empty() || next_x.len() != next_y.len() || next_x.len() != next_z.len())
6020    {
6021        return Err(plotting_error(
6022            builtin,
6023            format!("{builtin}: XData, YData, and ZData must contain the same nonzero number of elements"),
6024        ));
6025    }
6026
6027    super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
6028        if let runmat_plot::plots::figure::PlotElement::Line3(line) = plot {
6029            if update_data {
6030                let _ = line.update_data(next_x.clone(), next_y.clone(), next_z.clone());
6031            }
6032            for (key, value) in &style_pairs {
6033                match *key {
6034                    "color" => {
6035                        if let Ok(c) =
6036                            parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6037                        {
6038                            line.color = c;
6039                        }
6040                    }
6041                    "linewidth" => {
6042                        if let Some(v) = value_as_f64(value) {
6043                            line.line_width = v as f32;
6044                        }
6045                    }
6046                    "linestyle" => {
6047                        if let Some(s) = value_as_string(value) {
6048                            line.line_style = parse_line_style_name_for_props(&s);
6049                        }
6050                    }
6051                    "displayname" => {
6052                        line.label = value_as_string(value).map(|s| s.to_string());
6053                    }
6054                    "visible" => {
6055                        if let Some(v) = value_as_bool(value) {
6056                            line.set_visible(v);
6057                        } else if let Some(s) = value_as_string(value) {
6058                            line.set_visible(!matches!(
6059                                s.trim().to_ascii_lowercase().as_str(),
6060                                "off" | "false"
6061                            ));
6062                        }
6063                    }
6064                    _ => {}
6065                }
6066            }
6067        }
6068    })
6069    .map_err(|err| map_figure_error(builtin, err))?;
6070    Ok(())
6071}
6072
6073fn apply_scatter3_property(
6074    scatter_handle: &super::state::SimplePlotHandleState,
6075    key: &str,
6076    value: &Value,
6077    builtin: &'static str,
6078) -> BuiltinResult<()> {
6079    super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
6080        if let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot {
6081            match key {
6082                "sizedata" => {
6083                    if let Some(v) = value_as_f64(value) {
6084                        scatter.point_size = marker_area_points2_to_diameter_px(v);
6085                    }
6086                }
6087                "marker" => {
6088                    if let Some(s) = value_as_string(value) {
6089                        scatter.marker_style = scatter_marker_from_name(&s, scatter.marker_style);
6090                    }
6091                }
6092                "markerfacecolor" => {
6093                    if let Ok(c) =
6094                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6095                    {
6096                        let count = scatter.points.len().max(1);
6097                        scatter.colors = vec![c; count];
6098                    }
6099                }
6100                "markeredgecolor" => {
6101                    if let Ok(c) =
6102                        parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6103                    {
6104                        scatter.edge_color = c;
6105                    }
6106                }
6107                "linewidth" => {
6108                    if let Some(v) = value_as_f64(value) {
6109                        scatter.edge_thickness = v as f32;
6110                    }
6111                }
6112                "displayname" => {
6113                    scatter.label = value_as_string(value).map(|s| s.to_string());
6114                }
6115                _ => {}
6116            }
6117        }
6118    })
6119    .map_err(|err| map_figure_error(builtin, err))?;
6120    Ok(())
6121}
6122
6123fn apply_pie_property(
6124    pie_handle: &super::state::SimplePlotHandleState,
6125    key: &str,
6126    value: &Value,
6127    builtin: &'static str,
6128) -> BuiltinResult<()> {
6129    super::state::update_plot_element(pie_handle.figure, pie_handle.plot_index, |plot| {
6130        if let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot {
6131            if key == "displayname" {
6132                pie.label = value_as_string(value).map(|s| s.to_string());
6133            }
6134        }
6135    })
6136    .map_err(|err| map_figure_error(builtin, err))?;
6137    Ok(())
6138}
6139
6140fn apply_contour_property(
6141    contour_handle: &super::state::SimplePlotHandleState,
6142    key: &str,
6143    value: &Value,
6144    builtin: &'static str,
6145) -> BuiltinResult<()> {
6146    super::state::update_plot_element(contour_handle.figure, contour_handle.plot_index, |plot| {
6147        if let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot {
6148            if key == "displayname" {
6149                contour.label = value_as_string(value).map(|s| s.to_string());
6150            } else if key == "linewidth" {
6151                if let Some(width) = value_as_f64(value) {
6152                    contour.line_width = (width as f32).max(0.5);
6153                }
6154            }
6155        }
6156    })
6157    .map_err(|err| map_figure_error(builtin, err))?;
6158    Ok(())
6159}
6160
6161fn apply_contour_fill_property(
6162    fill_handle: &super::state::SimplePlotHandleState,
6163    key: &str,
6164    value: &Value,
6165    builtin: &'static str,
6166) -> BuiltinResult<()> {
6167    super::state::update_plot_element(fill_handle.figure, fill_handle.plot_index, |plot| {
6168        if let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot {
6169            if key == "displayname" {
6170                fill.label = value_as_string(value).map(|s| s.to_string());
6171            }
6172        }
6173    })
6174    .map_err(|err| map_figure_error(builtin, err))?;
6175    Ok(())
6176}
6177
6178fn apply_line_plot_properties(
6179    line: &mut runmat_plot::plots::LinePlot,
6180    key: &str,
6181    value: &Value,
6182    builtin: &'static str,
6183) {
6184    match key {
6185        "color" => {
6186            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6187                line.color = c;
6188            }
6189        }
6190        "linewidth" => {
6191            if let Some(v) = value_as_f64(value) {
6192                line.line_width = v as f32;
6193            }
6194        }
6195        "linestyle" => {
6196            if let Some(s) = value_as_string(value) {
6197                line.line_style = parse_line_style_name_for_props(&s);
6198            }
6199        }
6200        "displayname" => {
6201            line.label = value_as_string(value).map(|s| s.to_string());
6202        }
6203        "handlevisibility" => {
6204            if let Ok(visibility) =
6205                parse_handle_visibility(&LineStyleParseOptions::generic(builtin), value)
6206            {
6207                line.handle_visibility = visibility;
6208            }
6209        }
6210        "marker" => {
6211            if let Some(s) = value_as_string(value) {
6212                line.marker = marker_from_name(&s, line.marker.clone());
6213            }
6214        }
6215        "markersize" => {
6216            if let Some(v) = value_as_f64(value) {
6217                if let Some(marker) = &mut line.marker {
6218                    marker.size = v as f32;
6219                }
6220            }
6221        }
6222        "markerfacecolor" => {
6223            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6224                if let Some(marker) = &mut line.marker {
6225                    marker.face_color = c;
6226                }
6227            }
6228        }
6229        "markeredgecolor" => {
6230            if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6231                if let Some(marker) = &mut line.marker {
6232                    marker.edge_color = c;
6233                }
6234            }
6235        }
6236        "filled" => {
6237            if let Some(v) = value_as_bool(value) {
6238                if let Some(marker) = &mut line.marker {
6239                    marker.filled = v;
6240                }
6241            }
6242        }
6243        "visible" => {
6244            if let Some(v) = value_as_bool(value) {
6245                line.set_visible(v);
6246            } else if let Some(s) = value_as_string(value) {
6247                line.set_visible(!matches!(
6248                    s.trim().to_ascii_lowercase().as_str(),
6249                    "off" | "false"
6250                ));
6251            }
6252        }
6253        _ => {}
6254    }
6255}
6256
6257fn line_numeric_data(value: &Value, name: &str, builtin: &'static str) -> BuiltinResult<Vec<f64>> {
6258    let tensor = tensor::value_to_tensor(value)
6259        .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
6260    Ok(tensor.data)
6261}
6262
6263fn line_xy_data_for_properties(
6264    line: &runmat_plot::plots::LinePlot,
6265    builtin: &'static str,
6266) -> BuiltinResult<(Vec<f64>, Vec<f64>)> {
6267    if !line.x_data.is_empty() && line.x_data.len() == line.y_data.len() {
6268        return Ok((line.x_data.clone(), line.y_data.clone()));
6269    }
6270    if !line.x_data.is_empty() || !line.y_data.is_empty() {
6271        return Err(plotting_error(
6272            builtin,
6273            format!(
6274                "{builtin}: line source data is inconsistent: XData has {} values, YData has {} values",
6275                line.x_data.len(),
6276                line.y_data.len()
6277            ),
6278        ));
6279    }
6280    if !line.has_gpu_source_data() {
6281        return Ok((Vec::new(), Vec::new()));
6282    }
6283    futures::executor::block_on(line.export_scene_xy_data()).map_err(|err| {
6284        plotting_error(
6285            builtin,
6286            format!("{builtin}: unable to read line source data: {err}"),
6287        )
6288    })
6289}
6290
6291fn limits_from_optional_value(
6292    value: &Value,
6293    builtin: &'static str,
6294) -> BuiltinResult<Option<(f64, f64)>> {
6295    if let Some(text) = value_as_string(value) {
6296        let norm = text.trim().to_ascii_lowercase();
6297        if matches!(norm.as_str(), "auto" | "tight") {
6298            return Ok(None);
6299        }
6300    }
6301    Ok(Some(
6302        crate::builtins::plotting::op_common::limits::limits_from_value(value, builtin)?,
6303    ))
6304}
6305
6306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6307enum TickMode {
6308    Auto,
6309    Manual,
6310}
6311
6312fn ticks_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<f64>> {
6313    let tensor = runmat_builtins::Tensor::try_from(value)
6314        .map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
6315    let ticks = tensor.data;
6316    if ticks.iter().any(|value| !value.is_finite()) {
6317        return Err(plotting_error(
6318            builtin,
6319            format!("{builtin}: tick values must be finite"),
6320        ));
6321    }
6322    if ticks.windows(2).any(|pair| pair[1] <= pair[0]) {
6323        return Err(plotting_error(
6324            builtin,
6325            format!("{builtin}: tick values must be strictly increasing"),
6326        ));
6327    }
6328    Ok(ticks)
6329}
6330
6331fn tick_mode_from_value(
6332    value: &Value,
6333    builtin: &'static str,
6334    property: &str,
6335) -> BuiltinResult<TickMode> {
6336    let mode = value_as_string(value).ok_or_else(|| {
6337        plotting_error(builtin, format!("{builtin}: {property} must be a string"))
6338    })?;
6339    match mode.trim().to_ascii_lowercase().as_str() {
6340        "auto" => Ok(TickMode::Auto),
6341        "manual" => Ok(TickMode::Manual),
6342        _ => Err(plotting_error(
6343            builtin,
6344            format!("{builtin}: {property} must be 'auto' or 'manual'"),
6345        )),
6346    }
6347}
6348
6349fn tick_format_from_value(
6350    value: &Value,
6351    builtin: &'static str,
6352    property: &'static str,
6353) -> BuiltinResult<String> {
6354    let format = value_as_string(value)
6355        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: {property} must be text")))?;
6356    Ok(runmat_plot::core::plot_renderer::plot_utils::canonical_tick_label_format(&format))
6357}
6358
6359fn tick_angle_from_value(
6360    value: &Value,
6361    builtin: &'static str,
6362    property: &'static str,
6363) -> BuiltinResult<f64> {
6364    let angle = scalar_numeric_value(value)
6365        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: {property} must be numeric")))?;
6366    if angle.is_finite() {
6367        Ok(angle)
6368    } else {
6369        Err(plotting_error(
6370            builtin,
6371            format!("{builtin}: {property} must be finite"),
6372        ))
6373    }
6374}
6375
6376fn scalar_numeric_value(value: &Value) -> Option<f64> {
6377    match value {
6378        Value::Num(value) => Some(*value),
6379        Value::Int(value) => Some(value.to_f64()),
6380        Value::Tensor(tensor) if tensor.data.len() == 1 => Some(tensor.data[0]),
6381        _ => None,
6382    }
6383}
6384
6385fn tick_format_or_default(format: Option<&str>) -> String {
6386    runmat_plot::core::plot_renderer::plot_utils::canonical_tick_label_format(
6387        format.unwrap_or("%g"),
6388    )
6389}
6390
6391fn x_bounds(bounds: Option<(f64, f64, f64, f64)>) -> Option<(f64, f64)> {
6392    bounds.map(|(x_min, x_max, _, _)| (x_min, x_max))
6393}
6394
6395fn y_bounds(bounds: Option<(f64, f64, f64, f64)>) -> Option<(f64, f64)> {
6396    bounds.map(|(_, _, y_min, y_max)| (y_min, y_max))
6397}
6398
6399fn ticks_or_auto(explicit: Option<&[f64]>, bounds: Option<(f64, f64)>) -> Vec<f64> {
6400    if let Some(ticks) = explicit {
6401        return ticks.to_vec();
6402    }
6403    let (lo, hi) = bounds.unwrap_or((-1.0, 1.0));
6404    runmat_plot::core::plot_utils::generate_major_ticks(lo, hi)
6405}
6406
6407fn tick_value(data: Vec<f64>) -> Value {
6408    tensor_from_vec(data)
6409}
6410
6411fn tick_mode_value<T>(ticks: Option<&Vec<T>>) -> Value {
6412    Value::String(if ticks.is_some() { "manual" } else { "auto" }.into())
6413}
6414
6415fn tick_labels_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<String>> {
6416    match value {
6417        Value::String(s) => Ok(vec![s.clone()]),
6418        Value::StringArray(strings) => Ok(strings.data.clone()),
6419        Value::CharArray(chars) => Ok(char_array_rows(chars)),
6420        Value::Cell(cell) => {
6421            let mut labels = Vec::with_capacity(cell.data.len());
6422            for entry in &cell.data {
6423                labels.extend(tick_labels_from_value(entry, builtin)?);
6424            }
6425            Ok(labels)
6426        }
6427        Value::Tensor(tensor) if tensor.data.is_empty() => Ok(Vec::new()),
6428        other => Err(plotting_error(
6429            builtin,
6430            format!("{builtin}: tick labels must be a string array or cell array of text, got {other:?}"),
6431        )),
6432    }
6433}
6434
6435fn tick_labels_or_auto(
6436    explicit: Option<&[String]>,
6437    ticks: Option<&[f64]>,
6438    bounds: Option<(f64, f64)>,
6439    format: Option<&str>,
6440) -> Vec<String> {
6441    if let Some(labels) = explicit {
6442        return labels.to_vec();
6443    }
6444    let formatter = runmat_plot::core::plot_utils::TickLabelFormatter::new(format);
6445    ticks_or_auto(ticks, bounds)
6446        .into_iter()
6447        .map(|tick| formatter.format(tick))
6448        .collect()
6449}
6450
6451fn labels_padded_to_ticks(mut labels: Vec<String>, tick_count: usize) -> Vec<String> {
6452    if labels.len() < tick_count {
6453        labels.resize(tick_count, String::new());
6454    }
6455    labels
6456}
6457
6458fn tick_label_value(labels: Vec<String>) -> Value {
6459    let values = labels
6460        .iter()
6461        .map(|label| Value::CharArray(CharArray::new_row(label)))
6462        .collect::<Vec<_>>();
6463    Value::Cell(CellArray::new(values, 1, labels.len()).expect("valid tick-label cell array"))
6464}
6465
6466fn char_array_rows(chars: &CharArray) -> Vec<String> {
6467    if chars.rows == 0 || chars.cols == 0 {
6468        return Vec::new();
6469    }
6470    (0..chars.rows)
6471        .map(|row| {
6472            let start = row * chars.cols;
6473            let end = start + chars.cols;
6474            chars.data[start..end]
6475                .iter()
6476                .collect::<String>()
6477                .trim_end()
6478                .to_string()
6479        })
6480        .collect()
6481}
6482
6483fn parse_colormap_name(
6484    name: &str,
6485    builtin: &'static str,
6486) -> BuiltinResult<runmat_plot::plots::surface::ColorMap> {
6487    runmat_plot::plots::surface::ColorMap::from_name(name).ok_or_else(|| {
6488        plotting_error(
6489            builtin,
6490            format!("{builtin}: unknown colormap '{}'", name.trim()),
6491        )
6492    })
6493}
6494
6495fn apply_axes_text_alias(
6496    handle: FigureHandle,
6497    axes_index: usize,
6498    kind: PlotObjectKind,
6499    value: &Value,
6500    builtin: &'static str,
6501) -> BuiltinResult<()> {
6502    if let Some(text) = value_as_string(value) {
6503        set_text_properties_for_axes(handle, axes_index, kind, Some(text), None)
6504            .map_err(|err| map_figure_error(builtin, err))?;
6505        return Ok(());
6506    }
6507
6508    let scalar = handle_scalar(value, builtin)?;
6509    let (src_handle, src_axes, src_kind) =
6510        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6511    if src_kind != kind {
6512        return Err(plotting_error(
6513            builtin,
6514            format!(
6515                "{builtin}: expected a matching text handle for `{}`",
6516                key_name(kind)
6517            ),
6518        ));
6519    }
6520    let meta = axes_metadata_snapshot(src_handle, src_axes)
6521        .map_err(|err| map_figure_error(builtin, err))?;
6522    let (text, style) = match kind {
6523        PlotObjectKind::Title => (meta.title, meta.title_style),
6524        PlotObjectKind::Subtitle => (meta.subtitle, meta.subtitle_style),
6525        PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
6526        PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
6527        PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
6528        PlotObjectKind::Legend
6529        | PlotObjectKind::SuperTitle
6530        | PlotObjectKind::XAxis
6531        | PlotObjectKind::YAxis => unreachable!(),
6532    };
6533    set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
6534        .map_err(|err| map_figure_error(builtin, err))?;
6535    Ok(())
6536}
6537
6538fn validate_axes_text_alias(
6539    kind: PlotObjectKind,
6540    value: &Value,
6541    builtin: &'static str,
6542) -> BuiltinResult<()> {
6543    if value_as_string(value).is_some() {
6544        return Ok(());
6545    }
6546
6547    let scalar = handle_scalar(value, builtin)?;
6548    let (src_handle, src_axes, src_kind) =
6549        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6550    if src_kind != kind {
6551        return Err(plotting_error(
6552            builtin,
6553            format!(
6554                "{builtin}: expected a matching text handle for `{}`",
6555                key_name(kind)
6556            ),
6557        ));
6558    }
6559    axes_metadata_snapshot(src_handle, src_axes).map_err(|err| map_figure_error(builtin, err))?;
6560    Ok(())
6561}
6562
6563fn apply_figure_text_alias(
6564    handle: FigureHandle,
6565    kind: PlotObjectKind,
6566    value: &Value,
6567    builtin: &'static str,
6568) -> BuiltinResult<()> {
6569    if let Some(text) = value_as_text_string(value) {
6570        match kind {
6571            PlotObjectKind::SuperTitle => {
6572                set_sg_title_properties_for_figure(handle, Some(text), None)
6573                    .map_err(|err| map_figure_error(builtin, err))?;
6574            }
6575            _ => unreachable!(),
6576        }
6577        return Ok(());
6578    }
6579
6580    let scalar = handle_scalar(value, builtin)?;
6581    let (src_handle, _src_axes, src_kind) =
6582        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6583    if src_kind != kind {
6584        return Err(plotting_error(
6585            builtin,
6586            format!(
6587                "{builtin}: expected a matching text handle for `{}`",
6588                key_name(kind)
6589            ),
6590        ));
6591    }
6592
6593    let figure = super::state::clone_figure(src_handle)
6594        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
6595    let (text, style) = match kind {
6596        PlotObjectKind::SuperTitle => (figure.sg_title, figure.sg_title_style),
6597        _ => unreachable!(),
6598    };
6599    set_sg_title_properties_for_figure(handle, text, Some(style))
6600        .map_err(|err| map_figure_error(builtin, err))?;
6601    Ok(())
6602}
6603
6604fn validate_figure_text_alias(
6605    kind: PlotObjectKind,
6606    value: &Value,
6607    builtin: &'static str,
6608) -> BuiltinResult<()> {
6609    if value_as_text_string(value).is_some() {
6610        return Ok(());
6611    }
6612
6613    let scalar = handle_scalar(value, builtin)?;
6614    let (src_handle, _src_axes, src_kind) =
6615        decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6616    if src_kind != kind {
6617        return Err(plotting_error(
6618            builtin,
6619            format!(
6620                "{builtin}: expected a matching text handle for `{}`",
6621                key_name(kind)
6622            ),
6623        ));
6624    }
6625
6626    super::state::clone_figure(src_handle)
6627        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
6628    Ok(())
6629}
6630
6631fn collect_label_strings(builtin: &'static str, args: &[Value]) -> BuiltinResult<Vec<String>> {
6632    let mut labels = Vec::new();
6633    for arg in args {
6634        match arg {
6635            Value::StringArray(arr) => labels.extend(arr.data.iter().cloned()),
6636            Value::Cell(cell) => {
6637                for row in 0..cell.rows {
6638                    for col in 0..cell.cols {
6639                        let value = cell.get(row, col).map_err(|err| {
6640                            plotting_error(builtin, format!("legend: invalid label cell: {err}"))
6641                        })?;
6642                        labels.push(value_as_string(&value).ok_or_else(|| {
6643                            plotting_error(builtin, "legend: labels must be strings or char arrays")
6644                        })?);
6645                    }
6646                }
6647            }
6648            _ => labels.push(value_as_string(arg).ok_or_else(|| {
6649                plotting_error(builtin, "legend: labels must be strings or char arrays")
6650            })?),
6651        }
6652    }
6653    Ok(labels)
6654}
6655
6656fn handle_scalar(value: &Value, builtin: &'static str) -> BuiltinResult<f64> {
6657    match value {
6658        Value::Num(v) => Ok(*v),
6659        Value::Int(i) => Ok(i.to_f64()),
6660        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
6661        _ => Err(plotting_error(
6662            builtin,
6663            format!("{builtin}: expected plotting handle"),
6664        )),
6665    }
6666}
6667
6668fn legend_labels_value(labels: Vec<String>) -> Value {
6669    Value::StringArray(StringArray {
6670        rows: 1,
6671        cols: labels.len().max(1),
6672        shape: vec![1, labels.len().max(1)],
6673        data: labels,
6674    })
6675}
6676
6677fn text_value(text: Option<String>) -> Value {
6678    match text {
6679        Some(text) if text.contains('\n') => {
6680            let lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
6681            Value::StringArray(StringArray {
6682                rows: 1,
6683                cols: lines.len().max(1),
6684                shape: vec![1, lines.len().max(1)],
6685                data: lines,
6686            })
6687        }
6688        Some(text) => Value::String(text),
6689        None => Value::String(String::new()),
6690    }
6691}
6692
6693fn handles_value(handles: Vec<f64>) -> Value {
6694    Value::Tensor(runmat_builtins::Tensor {
6695        rows: 1,
6696        cols: handles.len(),
6697        shape: vec![1, handles.len()],
6698        data: handles,
6699        integer_data: None,
6700        dtype: runmat_builtins::NumericDType::F64,
6701    })
6702}
6703
6704fn tensor_from_vec(data: Vec<f64>) -> Value {
6705    Value::Tensor(runmat_builtins::Tensor {
6706        rows: 1,
6707        cols: data.len(),
6708        shape: vec![1, data.len()],
6709        data,
6710        integer_data: None,
6711        dtype: runmat_builtins::NumericDType::F64,
6712    })
6713}
6714
6715fn string_array_from_vec(data: Vec<String>) -> BuiltinResult<Value> {
6716    let cols = data.len();
6717    let array = StringArray::new(data, vec![1, cols])
6718        .map_err(|e| plotting_error("get", format!("get: {e}")))?;
6719    Ok(Value::StringArray(array))
6720}
6721
6722pub(crate) fn label_strings_from_value(
6723    value: &Value,
6724    builtin: &'static str,
6725    label_context: &str,
6726) -> BuiltinResult<Vec<String>> {
6727    match value {
6728        Value::StringArray(array) => Ok(array.data.clone()),
6729        Value::Cell(cell) => cell
6730            .data
6731            .iter()
6732            .map(|item| {
6733                value_as_text_string(item).ok_or_else(|| {
6734                    plotting_error(
6735                        builtin,
6736                        format!("{builtin}: {label_context} must contain text values"),
6737                    )
6738                })
6739            })
6740            .collect(),
6741        Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
6742        Value::String(text) => Ok(vec![text.clone()]),
6743        Value::Tensor(tensor) => Ok(tensor.data.iter().map(|v| v.to_string()).collect()),
6744        Value::Int(i) => Ok(vec![i.to_i64().to_string()]),
6745        Value::Num(v) => Ok(vec![v.to_string()]),
6746        other => Err(plotting_error(
6747            builtin,
6748            format!("{builtin}: unsupported {label_context} value {other:?}"),
6749        )),
6750    }
6751}
6752
6753fn tensor_from_matrix(data: Vec<Vec<f64>>) -> Value {
6754    let rows = data.len();
6755    let cols = data.first().map(|row| row.len()).unwrap_or(0);
6756    let flat = data.into_iter().flat_map(|row| row.into_iter()).collect();
6757    Value::Tensor(runmat_builtins::Tensor {
6758        rows,
6759        cols,
6760        shape: vec![rows, cols],
6761        data: flat,
6762        integer_data: None,
6763        dtype: runmat_builtins::NumericDType::F64,
6764    })
6765}
6766
6767fn surface_x_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6768    surface
6769        .x_grid
6770        .as_ref()
6771        .map(|grid| surface_grid_to_tensor(grid))
6772        .unwrap_or_else(|| tensor_from_vec(surface.x_data.clone()))
6773}
6774
6775fn surface_y_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6776    surface
6777        .y_grid
6778        .as_ref()
6779        .map(|grid| surface_grid_to_tensor(grid))
6780        .unwrap_or_else(|| tensor_from_vec(surface.y_data.clone()))
6781}
6782
6783fn surface_z_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6784    surface
6785        .z_data
6786        .as_ref()
6787        .map(|grid| surface_grid_to_tensor(grid))
6788        .unwrap_or_else(|| tensor_from_vec(Vec::new()))
6789}
6790
6791fn surface_grid_to_tensor(grid: &[Vec<f64>]) -> Value {
6792    let cols = grid.len();
6793    let rows = grid.first().map_or(0, Vec::len);
6794    let mut data = Vec::with_capacity(rows * cols);
6795    for column in grid {
6796        data.extend(column.iter().copied());
6797    }
6798    Value::Tensor(runmat_builtins::Tensor {
6799        rows,
6800        cols,
6801        shape: vec![rows, cols],
6802        data,
6803        integer_data: None,
6804        dtype: runmat_builtins::NumericDType::F64,
6805    })
6806}
6807
6808fn surface_coordinate_data_from_value(
6809    value: &Value,
6810    name: &str,
6811    builtin: &'static str,
6812) -> BuiltinResult<SurfaceCoordinateData> {
6813    let tensor = Tensor::try_from(value)
6814        .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
6815    if tensor.data.is_empty() {
6816        return Err(plotting_error(
6817            builtin,
6818            format!("{builtin}: {name} must be non-empty"),
6819        ));
6820    }
6821    if tensor.shape.len() > 2 {
6822        return Err(plotting_error(
6823            builtin,
6824            format!("{builtin}: {name} must be a vector or 2-D matrix"),
6825        ));
6826    }
6827    if tensor.data.iter().any(|value| !value.is_finite()) {
6828        return Err(plotting_error(
6829            builtin,
6830            format!("{builtin}: {name} must contain finite coordinates"),
6831        ));
6832    }
6833    if tensor.rows == 1 || tensor.cols == 1 {
6834        return Ok(SurfaceCoordinateData::Vector(tensor.data));
6835    }
6836    let rows = tensor.rows;
6837    let cols = tensor.cols;
6838    let grid = super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin)?;
6839    Ok(SurfaceCoordinateData::Grid(grid))
6840}
6841
6842fn surface_z_grid_from_value(
6843    value: &Value,
6844    rows: usize,
6845    cols: usize,
6846    builtin: &'static str,
6847) -> BuiltinResult<Vec<Vec<f64>>> {
6848    let mut tensor = Tensor::try_from(value)
6849        .map_err(|_| plotting_error(builtin, format!("{builtin}: ZData must be numeric")))?;
6850    let expected = rows
6851        .checked_mul(cols)
6852        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: grid dimensions overflowed")))?;
6853    if tensor.data.len() != expected {
6854        return Err(plotting_error(
6855            builtin,
6856            format!("{builtin}: ZData must contain exactly {expected} values"),
6857        ));
6858    }
6859    if tensor.rows == rows && tensor.cols == cols {
6860        return super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin);
6861    }
6862    if tensor.rows == 1 || tensor.cols == 1 {
6863        tensor.rows = rows;
6864        tensor.cols = cols;
6865        tensor.shape = vec![rows, cols];
6866        return super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin);
6867    }
6868    Err(plotting_error(
6869        builtin,
6870        format!("{builtin}: ZData must have shape {rows}x{cols}"),
6871    ))
6872}
6873
6874fn surface_grid_size(grid: &[Vec<f64>]) -> BuiltinResult<(usize, usize)> {
6875    let x_len = grid.len();
6876    let y_len = grid.first().map_or(0, Vec::len);
6877    if x_len == 0 || y_len == 0 || grid.iter().any(|row| row.len() != y_len) {
6878        return Err(plotting_error(
6879            "set",
6880            "set: surface source data is internally inconsistent",
6881        ));
6882    }
6883    Ok((x_len, y_len))
6884}
6885
6886fn validate_surface_grid_shape(
6887    grid: &[Vec<f64>],
6888    x_len: usize,
6889    y_len: usize,
6890    name: &str,
6891    builtin: &'static str,
6892) -> BuiltinResult<()> {
6893    if grid.len() != x_len || grid.iter().any(|row| row.len() != y_len) {
6894        return Err(plotting_error(
6895            builtin,
6896            format!("{builtin}: {name} matrix must have shape {y_len}x{x_len}"),
6897        ));
6898    }
6899    Ok(())
6900}
6901
6902fn axis_to_x_grid(axis: &[f64], y_len: usize) -> Vec<Vec<f64>> {
6903    axis.iter().map(|&x| vec![x; y_len]).collect()
6904}
6905
6906fn axis_to_y_grid(axis: &[f64], x_len: usize) -> Vec<Vec<f64>> {
6907    vec![axis.to_vec(); x_len]
6908}
6909
6910fn surface_face_color_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6911    match &surface.colormap {
6912        ColorMap::Custom(min, max) if (*min - *max).abs().max_element() < 1e-6 => {
6913            Value::String(color_to_short_name(*min))
6914        }
6915        _ => Value::String("flat".into()),
6916    }
6917}
6918
6919fn surface_edge_color_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6920    Value::String(if surface.wireframe { "flat" } else { "none" }.into())
6921}
6922
6923fn surface_shading_name(shading: ShadingMode) -> &'static str {
6924    match shading {
6925        ShadingMode::Flat => "flat",
6926        ShadingMode::Smooth => "interp",
6927        ShadingMode::Faceted => "faceted",
6928        ShadingMode::None => "none",
6929    }
6930}
6931
6932fn surface_shading_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<ShadingMode> {
6933    let text = value_as_string(value)
6934        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Shading must be text")))?;
6935    match text.trim().to_ascii_lowercase().as_str() {
6936        "flat" => Ok(ShadingMode::Flat),
6937        "interp" | "gouraud" => Ok(ShadingMode::Smooth),
6938        "faceted" => Ok(ShadingMode::Faceted),
6939        "none" => Ok(ShadingMode::None),
6940        other => Err(plotting_error(
6941            builtin,
6942            format!("{builtin}: unsupported Shading `{other}`"),
6943        )),
6944    }
6945}
6946
6947fn surface_lighting_name(enabled: bool) -> &'static str {
6948    if enabled {
6949        "gouraud"
6950    } else {
6951        "none"
6952    }
6953}
6954
6955fn surface_lighting_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<bool> {
6956    if let Some(text) = value_as_string(value) {
6957        return match text.trim().to_ascii_lowercase().as_str() {
6958            "none" | "off" => Ok(false),
6959            "flat" | "gouraud" | "phong" | "auto" | "on" => Ok(true),
6960            other => Err(plotting_error(
6961                builtin,
6962                format!("{builtin}: unsupported Lighting `{other}`"),
6963            )),
6964        };
6965    }
6966    value_as_bool(value)
6967        .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Lighting must be logical")))
6968}
6969
6970fn surface_face_color_update(
6971    value: &Value,
6972    builtin: &'static str,
6973) -> BuiltinResult<SurfacePropertyUpdate> {
6974    if let Some(text) = value_as_string(value) {
6975        return match text.trim().to_ascii_lowercase().as_str() {
6976            "none" => Ok(SurfacePropertyUpdate::FaceNone),
6977            "flat" => Ok(SurfacePropertyUpdate::Shading(ShadingMode::Flat)),
6978            "interp" => Ok(SurfacePropertyUpdate::Shading(ShadingMode::Smooth)),
6979            "texturemap" => Ok(SurfacePropertyUpdate::FlattenZ(true)),
6980            _ => parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6981                .map(SurfacePropertyUpdate::FaceColor),
6982        };
6983    }
6984    parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6985        .map(SurfacePropertyUpdate::FaceColor)
6986}
6987
6988fn vertices_tensor(vertices: &[glam::Vec3]) -> Value {
6989    let rows = vertices.len();
6990    let cols = 3;
6991    let mut data = Vec::with_capacity(rows * cols);
6992    for col in 0..cols {
6993        for vertex in vertices {
6994            data.push(match col {
6995                0 => vertex.x as f64,
6996                1 => vertex.y as f64,
6997                _ => vertex.z as f64,
6998            });
6999        }
7000    }
7001    Value::Tensor(runmat_builtins::Tensor {
7002        rows,
7003        cols,
7004        shape: vec![rows, cols],
7005        data,
7006        integer_data: None,
7007        dtype: runmat_builtins::NumericDType::F64,
7008    })
7009}
7010
7011fn faces_tensor(faces: &[Vec<usize>]) -> Value {
7012    let rows = faces.len();
7013    let cols = faces.iter().map(|face| face.len()).max().unwrap_or(0);
7014    let mut data = Vec::with_capacity(rows * cols);
7015    for col in 0..cols {
7016        for face in faces {
7017            data.push(
7018                face.get(col)
7019                    .map(|idx| *idx as f64 + 1.0)
7020                    .unwrap_or(f64::NAN),
7021            );
7022        }
7023    }
7024    Value::Tensor(runmat_builtins::Tensor {
7025        rows,
7026        cols,
7027        shape: vec![rows, cols],
7028        data,
7029        integer_data: None,
7030        dtype: runmat_builtins::NumericDType::F64,
7031    })
7032}
7033
7034fn patch_color_property(mode: runmat_plot::plots::PatchFaceColorMode, color: glam::Vec4) -> Value {
7035    match mode {
7036        runmat_plot::plots::PatchFaceColorMode::None => Value::String("none".into()),
7037        runmat_plot::plots::PatchFaceColorMode::Flat => Value::String("flat".into()),
7038        runmat_plot::plots::PatchFaceColorMode::Color => Value::String(color_to_short_name(color)),
7039    }
7040}
7041
7042fn patch_edge_color_property(
7043    mode: runmat_plot::plots::PatchEdgeColorMode,
7044    color: glam::Vec4,
7045) -> Value {
7046    match mode {
7047        runmat_plot::plots::PatchEdgeColorMode::None => Value::String("none".into()),
7048        runmat_plot::plots::PatchEdgeColorMode::Color => Value::String(color_to_short_name(color)),
7049    }
7050}
7051
7052fn insert_line_marker_struct_props(
7053    st: &mut StructValue,
7054    marker: Option<&runmat_plot::plots::line::LineMarkerAppearance>,
7055) {
7056    if let Some(marker) = marker {
7057        st.insert(
7058            "Marker",
7059            Value::String(marker_style_name(marker.kind).into()),
7060        );
7061        st.insert("MarkerSize", Value::Num(marker.size as f64));
7062        st.insert(
7063            "MarkerFaceColor",
7064            Value::String(color_to_short_name(marker.face_color)),
7065        );
7066        st.insert(
7067            "MarkerEdgeColor",
7068            Value::String(color_to_short_name(marker.edge_color)),
7069        );
7070        st.insert("Filled", Value::Bool(marker.filled));
7071    }
7072}
7073
7074fn line_marker_property_value(
7075    marker: &Option<runmat_plot::plots::line::LineMarkerAppearance>,
7076    name: &str,
7077    builtin: &'static str,
7078) -> BuiltinResult<Value> {
7079    match name {
7080        "marker" => Ok(Value::String(
7081            marker
7082                .as_ref()
7083                .map(|m| marker_style_name(m.kind).to_string())
7084                .unwrap_or_else(|| "none".into()),
7085        )),
7086        "markersize" => Ok(Value::Num(
7087            marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
7088        )),
7089        "markerfacecolor" => Ok(Value::String(
7090            marker
7091                .as_ref()
7092                .map(|m| color_to_short_name(m.face_color))
7093                .unwrap_or_else(|| "none".into()),
7094        )),
7095        "markeredgecolor" => Ok(Value::String(
7096            marker
7097                .as_ref()
7098                .map(|m| color_to_short_name(m.edge_color))
7099                .unwrap_or_else(|| "none".into()),
7100        )),
7101        "filled" => Ok(Value::Bool(
7102            marker.as_ref().map(|m| m.filled).unwrap_or(false),
7103        )),
7104        other => Err(plotting_error(
7105            builtin,
7106            format!("{builtin}: unsupported line property `{other}`"),
7107        )),
7108    }
7109}
7110
7111fn histogram_labels_from_edges(edges: &[f64]) -> Vec<String> {
7112    edges
7113        .windows(2)
7114        .map(|pair| format!("[{:.3}, {:.3})", pair[0], pair[1]))
7115        .collect()
7116}
7117
7118pub(crate) fn validate_histogram_normalization(
7119    norm: &str,
7120    builtin: &'static str,
7121) -> BuiltinResult<()> {
7122    match norm {
7123        "count" | "probability" | "countdensity" | "pdf" | "cumcount" | "cdf" => Ok(()),
7124        other => Err(plotting_error(
7125            builtin,
7126            format!("{builtin}: unsupported histogram normalization `{other}`"),
7127        )),
7128    }
7129}
7130
7131pub(crate) fn apply_histogram_normalization(
7132    raw_counts: &[f64],
7133    edges: &[f64],
7134    norm: &str,
7135) -> Vec<f64> {
7136    let widths: Vec<f64> = edges.windows(2).map(|pair| pair[1] - pair[0]).collect();
7137    let total: f64 = raw_counts.iter().sum();
7138    match norm {
7139        "count" => raw_counts.to_vec(),
7140        "probability" => {
7141            if total > 0.0 {
7142                raw_counts.iter().map(|&c| c / total).collect()
7143            } else {
7144                vec![0.0; raw_counts.len()]
7145            }
7146        }
7147        "countdensity" => raw_counts
7148            .iter()
7149            .zip(widths.iter())
7150            .map(|(&c, &w)| if w > 0.0 { c / w } else { 0.0 })
7151            .collect(),
7152        "pdf" => {
7153            if total > 0.0 {
7154                raw_counts
7155                    .iter()
7156                    .zip(widths.iter())
7157                    .map(|(&c, &w)| if w > 0.0 { c / (total * w) } else { 0.0 })
7158                    .collect()
7159            } else {
7160                vec![0.0; raw_counts.len()]
7161            }
7162        }
7163        "cumcount" => {
7164            let mut acc = 0.0;
7165            raw_counts
7166                .iter()
7167                .map(|&c| {
7168                    acc += c;
7169                    acc
7170                })
7171                .collect()
7172        }
7173        "cdf" => {
7174            if total > 0.0 {
7175                let mut acc = 0.0;
7176                raw_counts
7177                    .iter()
7178                    .map(|&c| {
7179                        acc += c;
7180                        acc / total
7181                    })
7182                    .collect()
7183            } else {
7184                vec![0.0; raw_counts.len()]
7185            }
7186        }
7187        _ => raw_counts.to_vec(),
7188    }
7189}
7190
7191fn line_style_name(style: runmat_plot::plots::line::LineStyle) -> &'static str {
7192    match style {
7193        runmat_plot::plots::line::LineStyle::None => "none",
7194        runmat_plot::plots::line::LineStyle::Solid => "-",
7195        runmat_plot::plots::line::LineStyle::Dashed => "--",
7196        runmat_plot::plots::line::LineStyle::Dotted => ":",
7197        runmat_plot::plots::line::LineStyle::DashDot => "-.",
7198    }
7199}
7200
7201fn parse_line_style_name_for_props(name: &str) -> runmat_plot::plots::line::LineStyle {
7202    match name.trim() {
7203        "none" => runmat_plot::plots::line::LineStyle::None,
7204        "--" | "dashed" => runmat_plot::plots::line::LineStyle::Dashed,
7205        ":" | "dotted" => runmat_plot::plots::line::LineStyle::Dotted,
7206        "-." | "dashdot" => runmat_plot::plots::line::LineStyle::DashDot,
7207        _ => runmat_plot::plots::line::LineStyle::Solid,
7208    }
7209}
7210
7211fn marker_style_name(style: runmat_plot::plots::scatter::MarkerStyle) -> &'static str {
7212    match style {
7213        runmat_plot::plots::scatter::MarkerStyle::Circle => "o",
7214        runmat_plot::plots::scatter::MarkerStyle::Square => "s",
7215        runmat_plot::plots::scatter::MarkerStyle::Triangle => "^",
7216        runmat_plot::plots::scatter::MarkerStyle::Diamond => "d",
7217        runmat_plot::plots::scatter::MarkerStyle::Plus => "+",
7218        runmat_plot::plots::scatter::MarkerStyle::Cross => "x",
7219        runmat_plot::plots::scatter::MarkerStyle::Star => "*",
7220        runmat_plot::plots::scatter::MarkerStyle::Hexagon => "h",
7221    }
7222}
7223
7224fn marker_from_name(
7225    name: &str,
7226    current: Option<runmat_plot::plots::line::LineMarkerAppearance>,
7227) -> Option<runmat_plot::plots::line::LineMarkerAppearance> {
7228    let mut marker = current.unwrap_or(runmat_plot::plots::line::LineMarkerAppearance {
7229        kind: runmat_plot::plots::scatter::MarkerStyle::Circle,
7230        size: 6.0,
7231        edge_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
7232        face_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
7233        filled: false,
7234    });
7235    marker.kind = match name.trim() {
7236        "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
7237        "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
7238        "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
7239        "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
7240        "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
7241        "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
7242        "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
7243        "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
7244        "none" => return None,
7245        _ => marker.kind,
7246    };
7247    Some(marker)
7248}
7249
7250fn scatter_marker_from_name(
7251    name: &str,
7252    current: runmat_plot::plots::scatter::MarkerStyle,
7253) -> runmat_plot::plots::scatter::MarkerStyle {
7254    match name.trim() {
7255        "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
7256        "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
7257        "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
7258        "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
7259        "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
7260        "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
7261        "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
7262        "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
7263        _ => current,
7264    }
7265}
7266
7267trait Unzip3Vec<A, B, C> {
7268    fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>);
7269}
7270
7271impl<I, A, B, C> Unzip3Vec<A, B, C> for I
7272where
7273    I: Iterator<Item = (A, B, C)>,
7274{
7275    fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>) {
7276        let mut a = Vec::new();
7277        let mut b = Vec::new();
7278        let mut c = Vec::new();
7279        for (va, vb, vc) in self {
7280            a.push(va);
7281            b.push(vb);
7282            c.push(vc);
7283        }
7284        (a, b, c)
7285    }
7286}
7287
7288fn color_to_short_name(color: glam::Vec4) -> String {
7289    let candidates = [
7290        (glam::Vec4::new(1.0, 0.0, 0.0, 1.0), "r"),
7291        (glam::Vec4::new(0.0, 1.0, 0.0, 1.0), "g"),
7292        (glam::Vec4::new(0.0, 0.0, 1.0, 1.0), "b"),
7293        (glam::Vec4::new(0.0, 0.0, 0.0, 1.0), "k"),
7294        (glam::Vec4::new(1.0, 1.0, 1.0, 1.0), "w"),
7295        (glam::Vec4::new(1.0, 1.0, 0.0, 1.0), "y"),
7296        (glam::Vec4::new(1.0, 0.0, 1.0, 1.0), "m"),
7297        (glam::Vec4::new(0.0, 1.0, 1.0, 1.0), "c"),
7298    ];
7299    for (candidate, name) in candidates {
7300        if (candidate - color).abs().max_element() < 1e-6 {
7301            return name.to_string();
7302        }
7303    }
7304    format!("[{:.3},{:.3},{:.3}]", color.x, color.y, color.z)
7305}
7306
7307fn key_name(kind: PlotObjectKind) -> &'static str {
7308    match kind {
7309        PlotObjectKind::Title => "Title",
7310        PlotObjectKind::Subtitle => "Subtitle",
7311        PlotObjectKind::XLabel => "XLabel",
7312        PlotObjectKind::YLabel => "YLabel",
7313        PlotObjectKind::ZLabel => "ZLabel",
7314        PlotObjectKind::Legend => "Legend",
7315        PlotObjectKind::SuperTitle => "SGTitle",
7316        PlotObjectKind::XAxis => "XAxis",
7317        PlotObjectKind::YAxis => "YAxis",
7318    }
7319}
7320
7321trait AxesMetadataExt {
7322    fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle;
7323}
7324
7325impl AxesMetadataExt for runmat_plot::plots::AxesMetadata {
7326    fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle {
7327        match kind {
7328            PlotObjectKind::Title => self.title_style.clone(),
7329            PlotObjectKind::Subtitle => self.subtitle_style.clone(),
7330            PlotObjectKind::XLabel => self.x_label_style.clone(),
7331            PlotObjectKind::YLabel => self.y_label_style.clone(),
7332            PlotObjectKind::ZLabel => self.z_label_style.clone(),
7333            PlotObjectKind::Legend
7334            | PlotObjectKind::SuperTitle
7335            | PlotObjectKind::XAxis
7336            | PlotObjectKind::YAxis => TextStyle::default(),
7337        }
7338    }
7339}