Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

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