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