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