Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

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