Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

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