Skip to main content

runmat_runtime/builtins/plotting/core/
properties.rs

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