1use runmat_builtins::{CellArray, CharArray, StringArray, StructValue, Tensor, Value};
2use runmat_plot::plots::{
3 ColorMap, LegendStyle, PolarHistogramDisplayStyle, ShadingMode, TextStyle,
4};
5use std::borrow::Cow;
6
7use super::point::{marker_area_points2_to_diameter_px, marker_diameter_px_to_area_points2};
8use super::state::{
9 axes_handle_exists, axes_handles_for_figure, axes_metadata_snapshot, axes_state_snapshot,
10 axis_display_bounds_snapshot_for_axes, current_axes_handle_for_figure,
11 current_figure_handle_if_exists, decode_axes_handle, decode_plot_object_handle,
12 figure_handle_exists, figure_has_sg_title, figure_tag, legend_entries_snapshot,
13 present_figure_update, root_default_properties, root_default_property, root_figure_handles,
14 root_show_hidden_handles, root_units, select_axes_for_figure, select_current_figure_if_exists,
15 set_axes_position_for_axes, set_axes_style_for_axes, set_axes_units_for_axes,
16 set_axis_tick_angles_for_axes, set_axis_tick_formats_for_axes, set_axis_tick_labels_for_axes,
17 set_axis_ticks_for_axes, set_figure_background_color, set_figure_name, set_figure_number_title,
18 set_figure_position, set_figure_tag, set_figure_visible, set_legend_for_axes,
19 set_root_default_property, set_root_show_hidden_handles, set_root_units,
20 set_sg_title_properties_for_figure, set_text_annotation_properties_for_axes,
21 set_text_properties_for_axes, FigureHandle, PlotObjectKind, RootPropertyValue,
22};
23use super::style::{
24 color_from_name_or_token, parse_color_value, parse_handle_visibility, value_as_bool,
25 value_as_f64, value_as_string, LineStyleParseOptions,
26};
27use super::{plotting_error, plotting_error_with_source};
28use crate::builtins::common::tensor;
29use crate::builtins::plotting::axis_scale::scale_mode_from_value;
30use crate::builtins::plotting::histogram2;
31use crate::builtins::plotting::op_common::limits::limit_value;
32use crate::builtins::plotting::op_common::value_as_text_string;
33use crate::builtins::stats::summary::binscatter;
34use crate::BuiltinResult;
35
36const MAX_AXES_FONT_SIZE_POINTS: f64 = 512.0;
37
38#[derive(Clone, Debug)]
39pub enum PlotHandle {
40 Root,
41 Figure(FigureHandle),
42 Axes(FigureHandle, usize),
43 Ruler(FigureHandle, usize, PlotObjectKind),
44 Text(FigureHandle, usize, PlotObjectKind),
45 Legend(FigureHandle, usize),
46 PlotChild(f64, Box<super::state::PlotChildHandleState>),
47}
48
49pub fn resolve_plot_handle(value: &Value, builtin: &'static str) -> BuiltinResult<PlotHandle> {
50 let scalar = handle_scalar(value, builtin)?;
51 if scalar == 0.0 {
52 return Ok(PlotHandle::Root);
53 }
54 if !scalar.is_finite() || scalar < 0.0 {
55 return Err(plotting_error(
56 builtin,
57 format!("{builtin}: unsupported or invalid plotting handle"),
58 ));
59 }
60 if let Ok(state) = super::state::plot_child_handle_snapshot(scalar) {
61 return Ok(PlotHandle::PlotChild(scalar.round(), Box::new(state)));
62 }
63 if let Ok((handle, axes_index, kind)) = decode_plot_object_handle(scalar) {
64 if axes_handle_exists(handle, axes_index) {
65 return Ok(match kind {
66 PlotObjectKind::Legend => PlotHandle::Legend(handle, axes_index),
67 PlotObjectKind::XAxis | PlotObjectKind::YAxis => {
68 PlotHandle::Ruler(handle, axes_index, kind)
69 }
70 _ => PlotHandle::Text(handle, axes_index, kind),
71 });
72 }
73 }
74 if let Ok((handle, axes_index)) = decode_axes_handle(scalar) {
75 if axes_handle_exists(handle, axes_index) {
76 return Ok(PlotHandle::Axes(handle, axes_index));
77 }
78 return Err(plotting_error(
79 builtin,
80 format!("{builtin}: invalid axes handle"),
81 ));
82 }
83 let figure = FigureHandle::from(scalar.round() as u32);
84 if figure_handle_exists(figure) {
85 return Ok(PlotHandle::Figure(figure));
86 }
87 Err(plotting_error(
88 builtin,
89 format!("{builtin}: unsupported or invalid plotting handle"),
90 ))
91}
92
93pub fn get_properties(
94 handle: PlotHandle,
95 property: Option<&str>,
96 builtin: &'static str,
97) -> BuiltinResult<Value> {
98 match handle {
99 PlotHandle::Root => get_root_property(property, builtin),
100 PlotHandle::Axes(handle, axes_index) => {
101 get_axes_property(handle, axes_index, property, builtin)
102 }
103 PlotHandle::Ruler(handle, axes_index, kind) => {
104 get_ruler_property(handle, axes_index, kind, property, builtin)
105 }
106 PlotHandle::Text(handle, axes_index, kind) => {
107 get_text_property(handle, axes_index, kind, property, builtin)
108 }
109 PlotHandle::Legend(handle, axes_index) => {
110 get_legend_property(handle, axes_index, property, builtin)
111 }
112 PlotHandle::Figure(handle) => get_figure_property(handle, property, builtin),
113 PlotHandle::PlotChild(_, state) => get_plot_child_property(&state, property, builtin),
114 }
115}
116
117pub fn set_properties(
118 handle: PlotHandle,
119 args: &[Value],
120 builtin: &'static str,
121) -> BuiltinResult<()> {
122 if args.is_empty() || !args.len().is_multiple_of(2) {
123 return Err(plotting_error(
124 builtin,
125 format!("{builtin}: property/value arguments must come in pairs"),
126 ));
127 }
128 match handle {
129 PlotHandle::Root => {
130 for pair in args.chunks_exact(2) {
131 let raw_key = property_name_text(&pair[0], builtin)?;
132 let key = canonical_property_name(raw_key.trim()).into_owned();
133 apply_root_property(&key, raw_key.trim(), &pair[1], builtin)?;
134 }
135 Ok(())
136 }
137 PlotHandle::Figure(handle) => {
138 let mut needs_present = false;
139 for pair in args.chunks_exact(2) {
140 validate_figure_property_value(&pair[0], &pair[1], Some(handle), builtin)?;
141 }
142 for pair in args.chunks_exact(2) {
143 let key = property_name(&pair[0], builtin)?;
144 needs_present |= apply_figure_property(handle, &key, &pair[1], builtin)?;
145 }
146 if needs_present {
147 let figure = super::state::clone_figure(handle)
148 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
149 let _ = present_figure_update(builtin, handle, figure)?;
150 }
151 Ok(())
152 }
153 PlotHandle::Axes(handle, axes_index) => {
154 for pair in args.chunks_exact(2) {
155 let key = property_name(&pair[0], builtin)?;
156 apply_axes_property(handle, axes_index, &key, &pair[1], builtin)?;
157 }
158 Ok(())
159 }
160 PlotHandle::Ruler(handle, axes_index, kind) => {
161 for pair in args.chunks_exact(2) {
162 let key = property_name(&pair[0], builtin)?;
163 apply_ruler_property(handle, axes_index, kind, &key, &pair[1], builtin)?;
164 }
165 Ok(())
166 }
167 PlotHandle::Text(handle, axes_index, kind) => {
168 let mut text: Option<String> = None;
169 let mut style = if matches!(kind, PlotObjectKind::SuperTitle) {
170 super::state::clone_figure(handle)
171 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?
172 .sg_title_style
173 } else {
174 axes_metadata_snapshot(handle, axes_index)
175 .map_err(|err| map_figure_error(builtin, err))?
176 .text_style_for(kind)
177 };
178 for pair in args.chunks_exact(2) {
179 let key = property_name(&pair[0], builtin)?;
180 apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
181 }
182 if matches!(kind, PlotObjectKind::SuperTitle) {
183 set_sg_title_properties_for_figure(handle, text, Some(style))
184 .map_err(|err| map_figure_error(builtin, err))?;
185 } else {
186 set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
187 .map_err(|err| map_figure_error(builtin, err))?;
188 }
189 Ok(())
190 }
191 PlotHandle::Legend(handle, axes_index) => {
192 let snapshot = axes_metadata_snapshot(handle, axes_index)
193 .map_err(|err| map_figure_error(builtin, err))?;
194 let mut style = snapshot.legend_style;
195 let mut enabled = snapshot.legend_enabled;
196 let mut labels: Option<Vec<String>> = None;
197 for pair in args.chunks_exact(2) {
198 let key = property_name(&pair[0], builtin)?;
199 apply_legend_property(
200 &mut style,
201 &mut enabled,
202 &mut labels,
203 &key,
204 &pair[1],
205 builtin,
206 )?;
207 }
208 set_legend_for_axes(handle, axes_index, enabled, labels.as_deref(), Some(style))
209 .map_err(|err| map_figure_error(builtin, err))?;
210 Ok(())
211 }
212 PlotHandle::PlotChild(handle, state) => {
213 apply_plot_child_properties(handle, &state, args, builtin)
214 }
215 }
216}
217
218pub fn parse_text_style_pairs(builtin: &'static str, args: &[Value]) -> BuiltinResult<TextStyle> {
219 if args.is_empty() {
220 return Ok(TextStyle::default());
221 }
222 if !args.len().is_multiple_of(2) {
223 return Err(plotting_error(
224 builtin,
225 format!("{builtin}: property/value arguments must come in pairs"),
226 ));
227 }
228 let mut style = TextStyle::default();
229 let mut text = None;
230 for pair in args.chunks_exact(2) {
231 let key = property_name(&pair[0], builtin)?;
232 apply_text_property(&mut text, &mut style, &key, &pair[1], builtin)?;
233 }
234 Ok(style)
235}
236
237pub fn validate_heatmap_property_pairs(
238 args: &[Value],
239 x_label_len: usize,
240 y_label_len: usize,
241 builtin: &'static str,
242) -> BuiltinResult<()> {
243 if args.is_empty() {
244 return Ok(());
245 }
246 if !args.len().is_multiple_of(2) {
247 return Err(plotting_error(
248 builtin,
249 format!("{builtin}: property/value arguments must come in pairs"),
250 ));
251 }
252 for pair in args.chunks_exact(2) {
253 let key = property_name(&pair[0], builtin)?;
254 match key.as_str() {
255 "title" => validate_axes_text_alias(PlotObjectKind::Title, &pair[1], builtin)?,
256 "subtitle" => validate_axes_text_alias(PlotObjectKind::Subtitle, &pair[1], builtin)?,
257 "xlabel" => validate_axes_text_alias(PlotObjectKind::XLabel, &pair[1], builtin)?,
258 "ylabel" => validate_axes_text_alias(PlotObjectKind::YLabel, &pair[1], builtin)?,
259 "colorbar" | "colorbarvisible" => {
260 value_as_bool(&pair[1]).ok_or_else(|| {
261 plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
262 })?;
263 }
264 "colormap" => {
265 let name = value_as_string(&pair[1]).ok_or_else(|| {
266 plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
267 })?;
268 parse_colormap_name(&name, builtin)?;
269 }
270 "xdisplaylabels" => {
271 let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
272 if labels.len() != x_label_len {
273 return Err(plotting_error(
274 builtin,
275 format!("{builtin}: XDisplayLabels length must match heatmap columns"),
276 ));
277 }
278 }
279 "ydisplaylabels" => {
280 let labels = label_strings_from_value(&pair[1], builtin, "labels")?;
281 if labels.len() != y_label_len {
282 return Err(plotting_error(
283 builtin,
284 format!("{builtin}: YDisplayLabels length must match heatmap rows"),
285 ));
286 }
287 }
288 other => {
289 return Err(plotting_error(
290 builtin,
291 format!("{builtin}: unsupported heatmap property `{other}`"),
292 ));
293 }
294 }
295 }
296 Ok(())
297}
298
299pub fn split_legend_style_pairs<'a>(
300 builtin: &'static str,
301 args: &'a [Value],
302) -> BuiltinResult<(&'a [Value], LegendStyle)> {
303 let mut style = LegendStyle::default();
304 let mut enabled = true;
305 let mut labels = None;
306 let mut split = args.len();
307 while split >= 2 {
308 let key_idx = split - 2;
309 let Ok(key) = property_name(&args[key_idx], builtin) else {
310 break;
311 };
312 if !matches!(
313 key.as_str(),
314 "location"
315 | "fontsize"
316 | "fontweight"
317 | "fontangle"
318 | "interpreter"
319 | "textcolor"
320 | "color"
321 | "visible"
322 | "string"
323 | "box"
324 | "orientation"
325 ) {
326 break;
327 }
328 apply_legend_property(
329 &mut style,
330 &mut enabled,
331 &mut labels,
332 &key,
333 &args[key_idx + 1],
334 builtin,
335 )?;
336 split -= 2;
337 }
338 Ok((&args[..split], style))
339}
340
341pub fn map_figure_error(
342 builtin: &'static str,
343 err: impl std::error::Error + Send + Sync + 'static,
344) -> crate::RuntimeError {
345 let message = format!("{builtin}: {err}");
346 plotting_error_with_source(builtin, message, err)
347}
348
349fn get_figure_property(
350 handle: FigureHandle,
351 property: Option<&str>,
352 builtin: &'static str,
353) -> BuiltinResult<Value> {
354 let figure = super::state::clone_figure(handle)
355 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
356 let axes = axes_handles_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
357 let current_axes =
358 current_axes_handle_for_figure(handle).map_err(|err| map_figure_error(builtin, err))?;
359 let sg_title_handle =
360 super::state::encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle);
361 let has_sg_title = figure_has_sg_title(handle);
362 match property.map(canonical_property_name).as_deref() {
363 None => {
364 let mut st = StructValue::new();
365 st.insert("Handle", Value::Num(handle.as_u32() as f64));
366 st.insert("Number", Value::Num(handle.as_u32() as f64));
367 st.insert("Type", Value::String("figure".into()));
368 st.insert("CurrentAxes", Value::Num(current_axes));
369 let mut children = axes;
370 if has_sg_title {
371 children.push(sg_title_handle);
372 }
373 st.insert("Children", handles_value(children));
374 st.insert("Parent", Value::Num(f64::NAN));
375 st.insert(
376 "Name",
377 Value::String(figure.name.clone().unwrap_or_default()),
378 );
379 st.insert("NumberTitle", Value::Bool(figure.number_title));
380 st.insert("Visible", Value::Bool(figure.visible));
381 st.insert("Position", figure_position_value(figure.position));
382 st.insert(
383 "Color",
384 Value::String(color_to_short_name(figure.background_color)),
385 );
386 st.insert("Tag", Value::String(figure_tag(handle).unwrap_or_default()));
387 if let Some(waitbar) = super::state::waitbar_state_snapshot(handle)
388 .map_err(|err| map_figure_error(builtin, err))?
389 {
390 st.insert("WaitbarProgress", Value::Num(waitbar.progress));
391 st.insert("WaitbarMessage", Value::String(waitbar.message));
392 }
393 st.insert("SGTitle", Value::Num(sg_title_handle));
394 Ok(Value::Struct(st))
395 }
396 Some("number") => Ok(Value::Num(handle.as_u32() as f64)),
397 Some("type") => Ok(Value::String("figure".into())),
398 Some("currentaxes") => Ok(Value::Num(current_axes)),
399 Some("children") => Ok(handles_value({
400 let mut children = axes;
401 if has_sg_title {
402 children.push(sg_title_handle);
403 }
404 children
405 })),
406 Some("parent") => Ok(Value::Num(f64::NAN)),
407 Some("name") => Ok(Value::String(figure.name.unwrap_or_default())),
408 Some("numbertitle") => Ok(Value::Bool(figure.number_title)),
409 Some("visible") => Ok(Value::Bool(figure.visible)),
410 Some("position") => Ok(figure_position_value(figure.position)),
411 Some("color") => Ok(Value::String(color_to_short_name(figure.background_color))),
412 Some("waitbarprogress") => Ok(Value::Num(
413 super::state::waitbar_state_snapshot(handle)
414 .map_err(|err| map_figure_error(builtin, err))?
415 .map(|state| state.progress)
416 .unwrap_or(f64::NAN),
417 )),
418 Some("waitbarmessage") => Ok(Value::String(
419 super::state::waitbar_state_snapshot(handle)
420 .map_err(|err| map_figure_error(builtin, err))?
421 .map(|state| state.message)
422 .unwrap_or_default(),
423 )),
424 Some("tag") => Ok(Value::String(figure_tag(handle).unwrap_or_default())),
425 Some("sgtitle") => Ok(Value::Num(sg_title_handle)),
426 Some(other) => Err(plotting_error(
427 builtin,
428 format!("{builtin}: unsupported figure property `{other}`"),
429 )),
430 }
431}
432
433fn get_root_property(property: Option<&str>, builtin: &'static str) -> BuiltinResult<Value> {
434 match property.map(canonical_property_name).as_deref() {
435 None => {
436 let mut st = StructValue::new();
437 st.insert("Handle", Value::Num(0.0));
438 st.insert("Type", Value::String("root".into()));
439 st.insert("CurrentFigure", current_root_figure_value());
440 st.insert("Children", root_children_value());
441 st.insert("Parent", empty_handle_value());
442 st.insert("ScreenSize", root_screen_size_value());
443 st.insert("MonitorPositions", root_screen_size_value());
444 st.insert("Units", Value::String(root_units()));
445 st.insert(
446 "ShowHiddenHandles",
447 Value::String(on_off(root_show_hidden_handles()).into()),
448 );
449 for (name, value) in root_default_properties() {
450 st.insert(name, root_property_value_to_value(value));
451 }
452 Ok(Value::Struct(st))
453 }
454 Some("handle") => Ok(Value::Num(0.0)),
455 Some("type") => Ok(Value::String("root".into())),
456 Some("currentfigure") => Ok(current_root_figure_value()),
457 Some("children") => Ok(root_children_value()),
458 Some("parent") => Ok(empty_handle_value()),
459 Some("screensize") => Ok(root_screen_size_value()),
460 Some("monitorpositions") => Ok(root_screen_size_value()),
461 Some("units") => Ok(Value::String(root_units())),
462 Some("showhiddenhandles") => Ok(Value::String(on_off(root_show_hidden_handles()).into())),
463 Some(default) if is_root_default_property(default) => Ok(root_default_property(default)
464 .map(root_property_value_to_value)
465 .unwrap_or_else(|| Value::String(String::new()))),
466 Some(other) => Err(plotting_error(
467 builtin,
468 format!("{builtin}: unsupported root property `{other}`"),
469 )),
470 }
471}
472
473fn apply_root_property(
474 key: &str,
475 display_key: &str,
476 value: &Value,
477 builtin: &'static str,
478) -> BuiltinResult<()> {
479 match key {
480 "currentfigure" => {
481 let resolved = resolve_plot_handle(value, builtin)?;
482 let PlotHandle::Figure(handle) = resolved else {
483 return Err(plotting_error(
484 builtin,
485 format!("{builtin}: CurrentFigure must be a figure handle"),
486 ));
487 };
488 select_current_figure_if_exists(handle)
489 .map_err(|err| map_figure_error(builtin, err))?;
490 Ok(())
491 }
492 "units" => {
493 let units = value_as_text_string(value)
494 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Units must be text")))?
495 .trim()
496 .to_ascii_lowercase();
497 if !matches!(
498 units.as_str(),
499 "pixels" | "normalized" | "inches" | "centimeters" | "points" | "characters"
500 ) {
501 return Err(plotting_error(
502 builtin,
503 format!("{builtin}: unsupported root Units `{units}`"),
504 ));
505 }
506 set_root_units(units);
507 Ok(())
508 }
509 "showhiddenhandles" => {
510 let enabled = value_as_bool(value).ok_or_else(|| {
511 plotting_error(
512 builtin,
513 format!("{builtin}: ShowHiddenHandles must be 'on' or 'off'"),
514 )
515 })?;
516 set_root_show_hidden_handles(enabled);
517 Ok(())
518 }
519 "handle" | "type" | "children" | "parent" | "screensize" | "monitorpositions" => {
520 Err(plotting_error(
521 builtin,
522 format!("{builtin}: root property `{key}` is read-only"),
523 ))
524 }
525 default if is_root_default_property(default) => {
526 let stored = root_property_value_from_value(value, builtin)?;
527 set_root_default_property(default.to_string(), display_key.to_string(), stored);
528 Ok(())
529 }
530 other => Err(plotting_error(
531 builtin,
532 format!("{builtin}: unsupported root property `{other}`"),
533 )),
534 }
535}
536
537fn current_root_figure_value() -> Value {
538 current_figure_handle_if_exists()
539 .map(|handle| Value::Num(handle.as_u32() as f64))
540 .unwrap_or_else(empty_handle_value)
541}
542
543fn root_children_value() -> Value {
544 handles_value(
545 root_figure_handles()
546 .into_iter()
547 .map(|handle| handle.as_u32() as f64)
548 .collect(),
549 )
550}
551
552fn root_screen_size_value() -> Value {
553 tensor_from_vec(vec![1.0, 1.0, 1920.0, 1080.0])
554}
555
556fn empty_handle_value() -> Value {
557 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty tensor shape is valid"))
558}
559
560fn on_off(value: bool) -> &'static str {
561 if value {
562 "on"
563 } else {
564 "off"
565 }
566}
567
568fn is_root_default_property(name: &str) -> bool {
569 name.starts_with("default") && name.len() > "default".len()
570}
571
572fn root_property_value_from_value(
573 value: &Value,
574 builtin: &'static str,
575) -> BuiltinResult<RootPropertyValue> {
576 match value {
577 Value::Bool(value) => Ok(RootPropertyValue::Bool(*value)),
578 Value::Num(value) => Ok(RootPropertyValue::Num(*value)),
579 Value::Int(value) => Ok(RootPropertyValue::Num(value.to_f64())),
580 Value::String(value) => Ok(RootPropertyValue::String(value.clone())),
581 Value::CharArray(value) => Ok(RootPropertyValue::String(value.data.iter().collect())),
582 Value::Tensor(value) => Ok(RootPropertyValue::Tensor(value.clone())),
583 Value::StringArray(value) => Ok(RootPropertyValue::StringArray {
584 rows: value.rows,
585 cols: value.cols,
586 shape: value.shape.clone(),
587 data: value.data.clone(),
588 }),
589 _ => Err(plotting_error(
590 builtin,
591 format!("{builtin}: unsupported root default property value"),
592 )),
593 }
594}
595
596fn root_property_value_to_value(value: RootPropertyValue) -> Value {
597 match value {
598 RootPropertyValue::Bool(value) => Value::Bool(value),
599 RootPropertyValue::Num(value) => Value::Num(value),
600 RootPropertyValue::String(value) => Value::String(value),
601 RootPropertyValue::Tensor(value) => Value::Tensor(value),
602 RootPropertyValue::StringArray {
603 rows,
604 cols,
605 shape,
606 data,
607 } => Value::StringArray(StringArray {
608 rows,
609 cols,
610 shape,
611 data,
612 }),
613 }
614}
615
616fn get_axes_property(
617 handle: FigureHandle,
618 axes_index: usize,
619 property: Option<&str>,
620 builtin: &'static str,
621) -> BuiltinResult<Value> {
622 let meta =
623 axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
624 let axes =
625 axes_state_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
626 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
627 .map_err(|err| map_figure_error(builtin, err))?;
628 match property.map(canonical_property_name).as_deref() {
629 None => {
630 let mut st = StructValue::new();
631 st.insert(
632 "Handle",
633 Value::Num(super::state::encode_axes_handle(handle, axes_index)),
634 );
635 st.insert("Figure", Value::Num(handle.as_u32() as f64));
636 st.insert("Rows", Value::Num(axes.rows as f64));
637 st.insert("Cols", Value::Num(axes.cols as f64));
638 st.insert("Index", Value::Num((axes_index + 1) as f64));
639 st.insert(
640 "Title",
641 Value::Num(super::state::encode_plot_object_handle(
642 handle,
643 axes_index,
644 PlotObjectKind::Title,
645 )),
646 );
647 st.insert(
648 "Subtitle",
649 Value::Num(super::state::encode_plot_object_handle(
650 handle,
651 axes_index,
652 PlotObjectKind::Subtitle,
653 )),
654 );
655 st.insert(
656 "XLabel",
657 Value::Num(super::state::encode_plot_object_handle(
658 handle,
659 axes_index,
660 PlotObjectKind::XLabel,
661 )),
662 );
663 st.insert(
664 "YLabel",
665 Value::Num(super::state::encode_plot_object_handle(
666 handle,
667 axes_index,
668 PlotObjectKind::YLabel,
669 )),
670 );
671 st.insert(
672 "ZLabel",
673 Value::Num(super::state::encode_plot_object_handle(
674 handle,
675 axes_index,
676 PlotObjectKind::ZLabel,
677 )),
678 );
679 st.insert(
680 "Legend",
681 Value::Num(super::state::encode_plot_object_handle(
682 handle,
683 axes_index,
684 PlotObjectKind::Legend,
685 )),
686 );
687 st.insert(
688 "XAxis",
689 Value::Num(super::state::encode_plot_object_handle(
690 handle,
691 axes_index,
692 PlotObjectKind::XAxis,
693 )),
694 );
695 st.insert(
696 "YAxis",
697 Value::Num(super::state::encode_plot_object_handle(
698 handle,
699 axes_index,
700 PlotObjectKind::YAxis,
701 )),
702 );
703 st.insert("LegendVisible", Value::Bool(meta.legend_enabled));
704 st.insert("Type", Value::String("axes".into()));
705 st.insert("Parent", Value::Num(handle.as_u32() as f64));
706 st.insert("Position", figure_position_value(meta.position));
707 st.insert("Units", Value::String(meta.units.clone()));
708 st.insert(
709 "Children",
710 handles_value(vec![
711 super::state::encode_plot_object_handle(
712 handle,
713 axes_index,
714 PlotObjectKind::Title,
715 ),
716 super::state::encode_plot_object_handle(
717 handle,
718 axes_index,
719 PlotObjectKind::Subtitle,
720 ),
721 super::state::encode_plot_object_handle(
722 handle,
723 axes_index,
724 PlotObjectKind::XLabel,
725 ),
726 super::state::encode_plot_object_handle(
727 handle,
728 axes_index,
729 PlotObjectKind::YLabel,
730 ),
731 super::state::encode_plot_object_handle(
732 handle,
733 axes_index,
734 PlotObjectKind::ZLabel,
735 ),
736 super::state::encode_plot_object_handle(
737 handle,
738 axes_index,
739 PlotObjectKind::Legend,
740 ),
741 ]),
742 );
743 st.insert("Grid", Value::Bool(meta.grid_enabled));
744 st.insert("MinorGrid", Value::Bool(meta.minor_grid_enabled));
745 st.insert(
746 "HiddenLineRemoval",
747 Value::String(on_off(meta.hidden_line_removal).into()),
748 );
749 st.insert("Box", Value::Bool(meta.box_enabled));
750 st.insert("AxisEqual", Value::Bool(meta.axis_equal));
751 st.insert(
752 "DataAspectRatio",
753 tensor_from_vec(meta.data_aspect_ratio.to_vec()),
754 );
755 st.insert(
756 "DataAspectRatioMode",
757 Value::String(meta.data_aspect_ratio_mode.clone()),
758 );
759 st.insert("Colorbar", Value::Bool(meta.colorbar_enabled));
760 st.insert(
761 "Colormap",
762 Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
763 );
764 st.insert("XLim", limit_value(meta.x_limits));
765 st.insert("YLim", limit_value(meta.y_limits));
766 st.insert("ZLim", limit_value(meta.z_limits));
767 st.insert("CLim", limit_value(meta.color_limits));
768 st.insert(
769 "XTick",
770 tick_value(ticks_or_auto(
771 meta.x_ticks.as_deref(),
772 x_bounds(display_bounds),
773 )),
774 );
775 st.insert(
776 "YTick",
777 tick_value(ticks_or_auto(
778 meta.y_ticks.as_deref(),
779 y_bounds(display_bounds),
780 )),
781 );
782 st.insert("XTickMode", tick_mode_value(meta.x_ticks.as_ref()));
783 st.insert("YTickMode", tick_mode_value(meta.y_ticks.as_ref()));
784 st.insert(
785 "XTickLabel",
786 tick_label_value(tick_labels_or_auto(
787 meta.x_tick_labels.as_deref(),
788 meta.x_ticks.as_deref(),
789 x_bounds(display_bounds),
790 meta.x_tick_format.as_deref(),
791 )),
792 );
793 st.insert(
794 "YTickLabel",
795 tick_label_value(tick_labels_or_auto(
796 meta.y_tick_labels.as_deref(),
797 meta.y_ticks.as_deref(),
798 y_bounds(display_bounds),
799 meta.y_tick_format.as_deref(),
800 )),
801 );
802 st.insert(
803 "XTickLabelMode",
804 tick_mode_value(meta.x_tick_labels.as_ref()),
805 );
806 st.insert(
807 "YTickLabelMode",
808 tick_mode_value(meta.y_tick_labels.as_ref()),
809 );
810 st.insert(
811 "XTickLabelFormat",
812 Value::String(tick_format_or_default(meta.x_tick_format.as_deref())),
813 );
814 st.insert(
815 "YTickLabelFormat",
816 Value::String(tick_format_or_default(meta.y_tick_format.as_deref())),
817 );
818 st.insert(
819 "XTickLabelRotation",
820 Value::Num(meta.x_tick_label_rotation.unwrap_or(0.0)),
821 );
822 st.insert(
823 "YTickLabelRotation",
824 Value::Num(meta.y_tick_label_rotation.unwrap_or(0.0)),
825 );
826 st.insert(
827 "FontSize",
828 Value::Num(meta.axes_style.font_size.unwrap_or(10.0) as f64),
829 );
830 st.insert(
831 "XScale",
832 Value::String(if meta.x_log { "log" } else { "linear" }.into()),
833 );
834 st.insert(
835 "YScale",
836 Value::String(if meta.y_log { "log" } else { "linear" }.into()),
837 );
838 st.insert("YAxisLocation", Value::String(meta.y_axis_location.clone()));
839 Ok(Value::Struct(st))
840 }
841 Some("title") => Ok(Value::Num(super::state::encode_plot_object_handle(
842 handle,
843 axes_index,
844 PlotObjectKind::Title,
845 ))),
846 Some("subtitle") => Ok(Value::Num(super::state::encode_plot_object_handle(
847 handle,
848 axes_index,
849 PlotObjectKind::Subtitle,
850 ))),
851 Some("xlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
852 handle,
853 axes_index,
854 PlotObjectKind::XLabel,
855 ))),
856 Some("ylabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
857 handle,
858 axes_index,
859 PlotObjectKind::YLabel,
860 ))),
861 Some("zlabel") => Ok(Value::Num(super::state::encode_plot_object_handle(
862 handle,
863 axes_index,
864 PlotObjectKind::ZLabel,
865 ))),
866 Some("legend") => Ok(Value::Num(super::state::encode_plot_object_handle(
867 handle,
868 axes_index,
869 PlotObjectKind::Legend,
870 ))),
871 Some("xaxis") => Ok(Value::Num(super::state::encode_plot_object_handle(
872 handle,
873 axes_index,
874 PlotObjectKind::XAxis,
875 ))),
876 Some("yaxis") => Ok(Value::Num(super::state::encode_plot_object_handle(
877 handle,
878 axes_index,
879 PlotObjectKind::YAxis,
880 ))),
881 Some("view") => {
882 let az = meta.view_azimuth_deg.unwrap_or(-37.5) as f64;
883 let el = meta.view_elevation_deg.unwrap_or(30.0) as f64;
884 Ok(Value::Tensor(runmat_builtins::Tensor {
885 rows: 1,
886 cols: 2,
887 shape: vec![1, 2],
888 data: vec![az, el],
889 integer_data: None,
890 dtype: runmat_builtins::NumericDType::F64,
891 }))
892 }
893 Some("grid") => Ok(Value::Bool(meta.grid_enabled)),
894 Some("minorgrid") => Ok(Value::Bool(meta.minor_grid_enabled)),
895 Some("hiddenlineremoval") => Ok(Value::String(on_off(meta.hidden_line_removal).into())),
896 Some("box") => Ok(Value::Bool(meta.box_enabled)),
897 Some("axisequal") => Ok(Value::Bool(meta.axis_equal)),
898 Some("dataaspectratio") => Ok(tensor_from_vec(meta.data_aspect_ratio.to_vec())),
899 Some("dataaspectratiomode") => Ok(Value::String(meta.data_aspect_ratio_mode.clone())),
900 Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
901 Some("colormap") => Ok(Value::String(
902 format!("{:?}", meta.colormap).to_ascii_lowercase(),
903 )),
904 Some("xlim") => Ok(limit_value(meta.x_limits)),
905 Some("ylim") => Ok(limit_value(meta.y_limits)),
906 Some("zlim") => Ok(limit_value(meta.z_limits)),
907 Some("clim") => Ok(limit_value(meta.color_limits)),
908 Some("xtick") => Ok(tick_value(ticks_or_auto(
909 meta.x_ticks.as_deref(),
910 x_bounds(display_bounds),
911 ))),
912 Some("ytick") => Ok(tick_value(ticks_or_auto(
913 meta.y_ticks.as_deref(),
914 y_bounds(display_bounds),
915 ))),
916 Some("xtickmode") => Ok(tick_mode_value(meta.x_ticks.as_ref())),
917 Some("ytickmode") => Ok(tick_mode_value(meta.y_ticks.as_ref())),
918 Some("xticklabel") => Ok(tick_label_value(tick_labels_or_auto(
919 meta.x_tick_labels.as_deref(),
920 meta.x_ticks.as_deref(),
921 x_bounds(display_bounds),
922 meta.x_tick_format.as_deref(),
923 ))),
924 Some("yticklabel") => Ok(tick_label_value(tick_labels_or_auto(
925 meta.y_tick_labels.as_deref(),
926 meta.y_ticks.as_deref(),
927 y_bounds(display_bounds),
928 meta.y_tick_format.as_deref(),
929 ))),
930 Some("xticklabelmode") => Ok(tick_mode_value(meta.x_tick_labels.as_ref())),
931 Some("yticklabelmode") => Ok(tick_mode_value(meta.y_tick_labels.as_ref())),
932 Some("xticklabelformat") => Ok(Value::String(tick_format_or_default(
933 meta.x_tick_format.as_deref(),
934 ))),
935 Some("yticklabelformat") => Ok(Value::String(tick_format_or_default(
936 meta.y_tick_format.as_deref(),
937 ))),
938 Some("xticklabelrotation") => Ok(Value::Num(meta.x_tick_label_rotation.unwrap_or(0.0))),
939 Some("yticklabelrotation") => Ok(Value::Num(meta.y_tick_label_rotation.unwrap_or(0.0))),
940 Some("fontsize") => Ok(Value::Num(meta.axes_style.font_size.unwrap_or(10.0) as f64)),
941 Some("xscale") => Ok(Value::String(
942 if meta.x_log { "log" } else { "linear" }.into(),
943 )),
944 Some("yscale") => Ok(Value::String(
945 if meta.y_log { "log" } else { "linear" }.into(),
946 )),
947 Some("yaxislocation") => Ok(Value::String(meta.y_axis_location)),
948 Some("type") => Ok(Value::String("axes".into())),
949 Some("parent") => Ok(Value::Num(handle.as_u32() as f64)),
950 Some("position") => Ok(figure_position_value(meta.position)),
951 Some("units") => Ok(Value::String(meta.units)),
952 Some("legendvisible") => Ok(Value::Bool(meta.legend_enabled)),
953 Some("children") => Ok(handles_value(vec![
954 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title),
955 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Subtitle),
956 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel),
957 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel),
958 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel),
959 super::state::encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend),
960 ])),
961 Some(other) => Err(plotting_error(
962 builtin,
963 format!("{builtin}: unsupported axes property `{other}`"),
964 )),
965 }
966}
967
968fn get_text_property(
969 handle: FigureHandle,
970 axes_index: usize,
971 kind: PlotObjectKind,
972 property: Option<&str>,
973 builtin: &'static str,
974) -> BuiltinResult<Value> {
975 let (text, style) = match kind {
976 PlotObjectKind::SuperTitle => {
977 let figure = super::state::clone_figure(handle)
978 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure")))?;
979 (figure.sg_title, figure.sg_title_style)
980 }
981 PlotObjectKind::Title
982 | PlotObjectKind::Subtitle
983 | PlotObjectKind::XLabel
984 | PlotObjectKind::YLabel
985 | PlotObjectKind::ZLabel => {
986 let meta = axes_metadata_snapshot(handle, axes_index)
987 .map_err(|err| map_figure_error(builtin, err))?;
988 match kind {
989 PlotObjectKind::Title => (meta.title, meta.title_style),
990 PlotObjectKind::Subtitle => (meta.subtitle, meta.subtitle_style),
991 PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
992 PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
993 PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
994 PlotObjectKind::Legend
995 | PlotObjectKind::SuperTitle
996 | PlotObjectKind::XAxis
997 | PlotObjectKind::YAxis => unreachable!(),
998 }
999 }
1000 PlotObjectKind::Legend | PlotObjectKind::XAxis | PlotObjectKind::YAxis => unreachable!(),
1001 };
1002 let parent = if matches!(kind, PlotObjectKind::SuperTitle) {
1003 Value::Num(handle.as_u32() as f64)
1004 } else {
1005 Value::Num(super::state::encode_axes_handle(handle, axes_index))
1006 };
1007 match property.map(canonical_property_name).as_deref() {
1008 None => {
1009 let mut st = StructValue::new();
1010 st.insert("Type", Value::String("text".into()));
1011 st.insert("Parent", parent.clone());
1012 st.insert("Children", handles_value(Vec::new()));
1013 st.insert("String", text_value(text));
1014 st.insert("Visible", Value::Bool(style.visible));
1015 if let Some(size) = style.font_size {
1016 st.insert("FontSize", Value::Num(size as f64));
1017 }
1018 if let Some(weight) = style.font_weight {
1019 st.insert("FontWeight", Value::String(weight));
1020 }
1021 if let Some(angle) = style.font_angle {
1022 st.insert("FontAngle", Value::String(angle));
1023 }
1024 if let Some(interpreter) = style.interpreter {
1025 st.insert("Interpreter", Value::String(interpreter));
1026 }
1027 if let Some(color) = style.color {
1028 st.insert("Color", Value::String(color_to_short_name(color)));
1029 }
1030 Ok(Value::Struct(st))
1031 }
1032 Some("type") => Ok(Value::String("text".into())),
1033 Some("parent") => Ok(parent),
1034 Some("children") => Ok(handles_value(Vec::new())),
1035 Some("string") => Ok(text_value(text)),
1036 Some("visible") => Ok(Value::Bool(style.visible)),
1037 Some("fontsize") => Ok(style
1038 .font_size
1039 .map(|v| Value::Num(v as f64))
1040 .unwrap_or(Value::Num(f64::NAN))),
1041 Some("fontweight") => Ok(style
1042 .font_weight
1043 .map(Value::String)
1044 .unwrap_or_else(|| Value::String(String::new()))),
1045 Some("fontangle") => Ok(style
1046 .font_angle
1047 .map(Value::String)
1048 .unwrap_or_else(|| Value::String(String::new()))),
1049 Some("interpreter") => Ok(style
1050 .interpreter
1051 .map(Value::String)
1052 .unwrap_or_else(|| Value::String(String::new()))),
1053 Some("color") => Ok(style
1054 .color
1055 .map(|c| Value::String(color_to_short_name(c)))
1056 .unwrap_or_else(|| Value::String(String::new()))),
1057 Some(other) => Err(plotting_error(
1058 builtin,
1059 format!("{builtin}: unsupported text property `{other}`"),
1060 )),
1061 }
1062}
1063
1064fn get_ruler_property(
1065 handle: FigureHandle,
1066 axes_index: usize,
1067 kind: PlotObjectKind,
1068 property: Option<&str>,
1069 builtin: &'static str,
1070) -> BuiltinResult<Value> {
1071 let meta =
1072 axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
1073 let (axis_name, format, rotation) = match kind {
1074 PlotObjectKind::XAxis => (
1075 "x",
1076 meta.x_tick_format.as_deref(),
1077 meta.x_tick_label_rotation,
1078 ),
1079 PlotObjectKind::YAxis => (
1080 "y",
1081 meta.y_tick_format.as_deref(),
1082 meta.y_tick_label_rotation,
1083 ),
1084 _ => {
1085 return Err(plotting_error(
1086 builtin,
1087 format!("{builtin}: invalid ruler handle"),
1088 ))
1089 }
1090 };
1091 let parent = Value::Num(super::state::encode_axes_handle(handle, axes_index));
1092 match property.map(canonical_property_name).as_deref() {
1093 None => {
1094 let mut st = StructValue::new();
1095 st.insert("Type", Value::String("numericruler".into()));
1096 st.insert("Parent", parent);
1097 st.insert("Children", handles_value(Vec::new()));
1098 st.insert("Axis", Value::String(axis_name.into()));
1099 st.insert(
1100 "TickLabelFormat",
1101 Value::String(tick_format_or_default(format)),
1102 );
1103 st.insert("TickLabelRotation", Value::Num(rotation.unwrap_or(0.0)));
1104 Ok(Value::Struct(st))
1105 }
1106 Some("type") => Ok(Value::String("numericruler".into())),
1107 Some("parent") => Ok(parent),
1108 Some("children") => Ok(handles_value(Vec::new())),
1109 Some("axis") => Ok(Value::String(axis_name.into())),
1110 Some("ticklabelformat") => Ok(Value::String(tick_format_or_default(format))),
1111 Some("ticklabelrotation") => Ok(Value::Num(rotation.unwrap_or(0.0))),
1112 Some(other) => Err(plotting_error(
1113 builtin,
1114 format!("{builtin}: unsupported ruler property `{other}`"),
1115 )),
1116 }
1117}
1118
1119fn get_legend_property(
1120 handle: FigureHandle,
1121 axes_index: usize,
1122 property: Option<&str>,
1123 builtin: &'static str,
1124) -> BuiltinResult<Value> {
1125 let meta =
1126 axes_metadata_snapshot(handle, axes_index).map_err(|err| map_figure_error(builtin, err))?;
1127 let entries = legend_entries_snapshot(handle, axes_index)
1128 .map_err(|err| map_figure_error(builtin, err))?;
1129 match property.map(canonical_property_name).as_deref() {
1130 None => {
1131 let mut st = StructValue::new();
1132 st.insert("Type", Value::String("legend".into()));
1133 st.insert(
1134 "Parent",
1135 Value::Num(super::state::encode_axes_handle(handle, axes_index)),
1136 );
1137 st.insert("Children", handles_value(Vec::new()));
1138 st.insert(
1139 "Visible",
1140 Value::Bool(meta.legend_enabled && meta.legend_style.visible),
1141 );
1142 st.insert(
1143 "String",
1144 legend_labels_value(entries.iter().map(|e| e.label.clone()).collect()),
1145 );
1146 if let Some(location) = meta.legend_style.location {
1147 st.insert("Location", Value::String(location));
1148 }
1149 if let Some(size) = meta.legend_style.font_size {
1150 st.insert("FontSize", Value::Num(size as f64));
1151 }
1152 if let Some(weight) = meta.legend_style.font_weight {
1153 st.insert("FontWeight", Value::String(weight));
1154 }
1155 if let Some(angle) = meta.legend_style.font_angle {
1156 st.insert("FontAngle", Value::String(angle));
1157 }
1158 if let Some(interpreter) = meta.legend_style.interpreter {
1159 st.insert("Interpreter", Value::String(interpreter));
1160 }
1161 if let Some(box_visible) = meta.legend_style.box_visible {
1162 st.insert("Box", Value::Bool(box_visible));
1163 }
1164 if let Some(orientation) = meta.legend_style.orientation {
1165 st.insert("Orientation", Value::String(orientation));
1166 }
1167 if let Some(color) = meta.legend_style.text_color {
1168 st.insert("TextColor", Value::String(color_to_short_name(color)));
1169 }
1170 Ok(Value::Struct(st))
1171 }
1172 Some("visible") => Ok(Value::Bool(
1173 meta.legend_enabled && meta.legend_style.visible,
1174 )),
1175 Some("type") => Ok(Value::String("legend".into())),
1176 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
1177 handle, axes_index,
1178 ))),
1179 Some("children") => Ok(handles_value(Vec::new())),
1180 Some("string") => Ok(legend_labels_value(
1181 entries.into_iter().map(|e| e.label).collect(),
1182 )),
1183 Some("location") => Ok(meta
1184 .legend_style
1185 .location
1186 .map(Value::String)
1187 .unwrap_or_else(|| Value::String(String::new()))),
1188 Some("fontsize") => Ok(meta
1189 .legend_style
1190 .font_size
1191 .map(|v| Value::Num(v as f64))
1192 .unwrap_or(Value::Num(f64::NAN))),
1193 Some("fontweight") => Ok(meta
1194 .legend_style
1195 .font_weight
1196 .map(Value::String)
1197 .unwrap_or_else(|| Value::String(String::new()))),
1198 Some("fontangle") => Ok(meta
1199 .legend_style
1200 .font_angle
1201 .map(Value::String)
1202 .unwrap_or_else(|| Value::String(String::new()))),
1203 Some("interpreter") => Ok(meta
1204 .legend_style
1205 .interpreter
1206 .map(Value::String)
1207 .unwrap_or_else(|| Value::String(String::new()))),
1208 Some("box") => Ok(meta
1209 .legend_style
1210 .box_visible
1211 .map(Value::Bool)
1212 .unwrap_or(Value::Bool(true))),
1213 Some("orientation") => Ok(meta
1214 .legend_style
1215 .orientation
1216 .map(Value::String)
1217 .unwrap_or_else(|| Value::String(String::new()))),
1218 Some("textcolor") | Some("color") => Ok(meta
1219 .legend_style
1220 .text_color
1221 .map(|c| Value::String(color_to_short_name(c)))
1222 .unwrap_or_else(|| Value::String(String::new()))),
1223 Some(other) => Err(plotting_error(
1224 builtin,
1225 format!("{builtin}: unsupported legend property `{other}`"),
1226 )),
1227 }
1228}
1229
1230fn property_name(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
1231 property_name_text(value, builtin).map(|s| canonical_property_name(s.trim()).into_owned())
1232}
1233
1234fn property_name_text(value: &Value, builtin: &'static str) -> BuiltinResult<String> {
1235 value_as_string(value)
1236 .map(|s| s.trim().to_string())
1237 .ok_or_else(|| {
1238 plotting_error(
1239 builtin,
1240 format!("{builtin}: property names must be strings"),
1241 )
1242 })
1243}
1244
1245pub(crate) fn data_aspect_ratio_from_value(
1246 value: &Value,
1247 builtin: &'static str,
1248) -> BuiltinResult<[f64; 3]> {
1249 let tensor =
1250 Tensor::try_from(value).map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
1251 if tensor.data.len() != 3
1252 || tensor
1253 .data
1254 .iter()
1255 .any(|value| !value.is_finite() || *value <= 0.0)
1256 {
1257 return Err(plotting_error(
1258 builtin,
1259 format!(
1260 "{builtin}: DataAspectRatio must be a 3-element positive finite numeric vector"
1261 ),
1262 ));
1263 }
1264 Ok([tensor.data[0], tensor.data[1], tensor.data[2]])
1265}
1266
1267pub(crate) fn data_aspect_ratio_mode_from_value(
1268 value: &Value,
1269 builtin: &'static str,
1270) -> BuiltinResult<&'static str> {
1271 let mode = value_as_string(value).ok_or_else(|| {
1272 plotting_error(
1273 builtin,
1274 format!("{builtin}: DataAspectRatioMode must be text"),
1275 )
1276 })?;
1277 match mode.trim().to_ascii_lowercase().as_str() {
1278 "auto" => Ok("auto"),
1279 "manual" => Ok("manual"),
1280 other => Err(plotting_error(
1281 builtin,
1282 format!("{builtin}: unsupported DataAspectRatioMode `{other}`"),
1283 )),
1284 }
1285}
1286
1287fn canonical_property_name(name: &str) -> Cow<'_, str> {
1288 match name.to_ascii_lowercase().as_str() {
1289 "textcolor" => Cow::Borrowed("textcolor"),
1290 "color" | "backgroundcolor" => Cow::Borrowed("color"),
1291 "fontsize" => Cow::Borrowed("fontsize"),
1292 "fontweight" => Cow::Borrowed("fontweight"),
1293 "fontangle" => Cow::Borrowed("fontangle"),
1294 "interpreter" => Cow::Borrowed("interpreter"),
1295 "visible" => Cow::Borrowed("visible"),
1296 "location" => Cow::Borrowed("location"),
1297 "box" => Cow::Borrowed("box"),
1298 "orientation" => Cow::Borrowed("orientation"),
1299 "string" => Cow::Borrowed("string"),
1300 "title" => Cow::Borrowed("title"),
1301 "xlabel" => Cow::Borrowed("xlabel"),
1302 "ylabel" => Cow::Borrowed("ylabel"),
1303 "zlabel" => Cow::Borrowed("zlabel"),
1304 "xaxis" => Cow::Borrowed("xaxis"),
1305 "yaxis" => Cow::Borrowed("yaxis"),
1306 "axis" => Cow::Borrowed("axis"),
1307 "view" => Cow::Borrowed("view"),
1308 "grid" | "xgrid" | "ygrid" | "zgrid" => Cow::Borrowed("grid"),
1309 "minorgrid" | "xminorgrid" | "yminorgrid" | "zminorgrid" => Cow::Borrowed("minorgrid"),
1310 "axisequal" => Cow::Borrowed("axisequal"),
1311 "colorbar" => Cow::Borrowed("colorbar"),
1312 "colorbarvisible" => Cow::Borrowed("colorbarvisible"),
1313 "colormap" => Cow::Borrowed("colormap"),
1314 "xdisplaylabels" => Cow::Borrowed("xdisplaylabels"),
1315 "ydisplaylabels" => Cow::Borrowed("ydisplaylabels"),
1316 "colordata" | "cdata" => Cow::Borrowed("colordata"),
1317 "xlim" => Cow::Borrowed("xlim"),
1318 "ylim" => Cow::Borrowed("ylim"),
1319 "zlim" => Cow::Borrowed("zlim"),
1320 "clim" | "caxis" => Cow::Borrowed("clim"),
1321 "xtick" => Cow::Borrowed("xtick"),
1322 "ytick" => Cow::Borrowed("ytick"),
1323 "xtickmode" => Cow::Borrowed("xtickmode"),
1324 "ytickmode" => Cow::Borrowed("ytickmode"),
1325 "xticklabel" => Cow::Borrowed("xticklabel"),
1326 "yticklabel" => Cow::Borrowed("yticklabel"),
1327 "xticklabelmode" => Cow::Borrowed("xticklabelmode"),
1328 "yticklabelmode" => Cow::Borrowed("yticklabelmode"),
1329 "xticklabelformat" => Cow::Borrowed("xticklabelformat"),
1330 "yticklabelformat" => Cow::Borrowed("yticklabelformat"),
1331 "ticklabelformat" => Cow::Borrowed("ticklabelformat"),
1332 "xticklabelrotation" => Cow::Borrowed("xticklabelrotation"),
1333 "yticklabelrotation" => Cow::Borrowed("yticklabelrotation"),
1334 "ticklabelrotation" => Cow::Borrowed("ticklabelrotation"),
1335 "hiddenlineremoval" => Cow::Borrowed("hiddenlineremoval"),
1336 "xscale" => Cow::Borrowed("xscale"),
1337 "yscale" => Cow::Borrowed("yscale"),
1338 "yaxislocation" => Cow::Borrowed("yaxislocation"),
1339 "currentaxes" => Cow::Borrowed("currentaxes"),
1340 "currentfigure" => Cow::Borrowed("currentfigure"),
1341 "sgtitle" | "supertitle" => Cow::Borrowed("sgtitle"),
1342 "children" => Cow::Borrowed("children"),
1343 "handle" => Cow::Borrowed("handle"),
1344 "parent" => Cow::Borrowed("parent"),
1345 "type" => Cow::Borrowed("type"),
1346 "screensize" => Cow::Borrowed("screensize"),
1347 "monitorpositions" => Cow::Borrowed("monitorpositions"),
1348 "units" => Cow::Borrowed("units"),
1349 "showhiddenhandles" => Cow::Borrowed("showhiddenhandles"),
1350 "number" => Cow::Borrowed("number"),
1351 "name" => Cow::Borrowed("name"),
1352 "numbertitle" => Cow::Borrowed("numbertitle"),
1353 "legend" => Cow::Borrowed("legend"),
1354 "legendvisible" => Cow::Borrowed("legendvisible"),
1355 "autoscalefactor" => Cow::Borrowed("autoscalefactor"),
1356 "barwidth" => Cow::Borrowed("barwidth"),
1357 "baseline" => Cow::Borrowed("baseline"),
1358 "basevalue" => Cow::Borrowed("basevalue"),
1359 "bincounts" => Cow::Borrowed("bincounts"),
1360 "binedges" => Cow::Borrowed("binedges"),
1361 "capsize" => Cow::Borrowed("capsize"),
1362 "cdatamapping" => Cow::Borrowed("cdatamapping"),
1363 "displayname" => Cow::Borrowed("displayname"),
1364 "edgealpha" => Cow::Borrowed("edgealpha"),
1365 "edgecolor" => Cow::Borrowed("edgecolor"),
1366 "facealpha" => Cow::Borrowed("facealpha"),
1367 "facecolor" => Cow::Borrowed("facecolor"),
1368 "faces" => Cow::Borrowed("faces"),
1369 "filled" => Cow::Borrowed("filled"),
1370 "label" => Cow::Borrowed("label"),
1371 "labelorientation" => Cow::Borrowed("labelorientation"),
1372 "linestyle" => Cow::Borrowed("linestyle"),
1373 "linewidth" => Cow::Borrowed("linewidth"),
1374 "marker" => Cow::Borrowed("marker"),
1375 "markeredgecolor" => Cow::Borrowed("markeredgecolor"),
1376 "markerfacecolor" => Cow::Borrowed("markerfacecolor"),
1377 "markersize" => Cow::Borrowed("markersize"),
1378 "maxheadsize" => Cow::Borrowed("maxheadsize"),
1379 "normalization" => Cow::Borrowed("normalization"),
1380 "numbins" => Cow::Borrowed("numbins"),
1381 "position" => Cow::Borrowed("position"),
1382 "sizedata" => Cow::Borrowed("sizedata"),
1383 "value" => Cow::Borrowed("value"),
1384 "vertices" => Cow::Borrowed("vertices"),
1385 "xdata" => Cow::Borrowed("xdata"),
1386 "ydata" => Cow::Borrowed("ydata"),
1387 "zdata" => Cow::Borrowed("zdata"),
1388 other => Cow::Owned(other.to_string()),
1389 }
1390}
1391
1392fn apply_text_property(
1393 text: &mut Option<String>,
1394 style: &mut TextStyle,
1395 key: &str,
1396 value: &Value,
1397 builtin: &'static str,
1398) -> BuiltinResult<()> {
1399 let opts = LineStyleParseOptions::generic(builtin);
1400 match key {
1401 "string" => {
1402 *text = Some(value_as_text_string(value).ok_or_else(|| {
1403 plotting_error(builtin, format!("{builtin}: String must be text"))
1404 })?);
1405 }
1406 "color" => style.color = Some(parse_color_value(&opts, value)?),
1407 "fontsize" => {
1408 style.font_size = Some(value_as_f64(value).ok_or_else(|| {
1409 plotting_error(builtin, format!("{builtin}: FontSize must be numeric"))
1410 })? as f32)
1411 }
1412 "fontweight" => {
1413 style.font_weight = Some(value_as_string(value).ok_or_else(|| {
1414 plotting_error(builtin, format!("{builtin}: FontWeight must be a string"))
1415 })?)
1416 }
1417 "fontangle" => {
1418 style.font_angle = Some(value_as_string(value).ok_or_else(|| {
1419 plotting_error(builtin, format!("{builtin}: FontAngle must be a string"))
1420 })?)
1421 }
1422 "interpreter" => {
1423 style.interpreter = Some(value_as_string(value).ok_or_else(|| {
1424 plotting_error(builtin, format!("{builtin}: Interpreter must be a string"))
1425 })?)
1426 }
1427 "visible" => {
1428 style.visible = value_as_bool(value).ok_or_else(|| {
1429 plotting_error(builtin, format!("{builtin}: Visible must be logical"))
1430 })?
1431 }
1432 other => {
1433 return Err(plotting_error(
1434 builtin,
1435 format!("{builtin}: unsupported property `{other}`"),
1436 ))
1437 }
1438 }
1439 Ok(())
1440}
1441
1442fn apply_legend_property(
1443 style: &mut LegendStyle,
1444 enabled: &mut bool,
1445 labels: &mut Option<Vec<String>>,
1446 key: &str,
1447 value: &Value,
1448 builtin: &'static str,
1449) -> BuiltinResult<()> {
1450 let opts = LineStyleParseOptions::generic(builtin);
1451 match key {
1452 "string" => *labels = Some(collect_label_strings(builtin, std::slice::from_ref(value))?),
1453 "location" => {
1454 style.location = Some(
1455 value_as_string(value)
1456 .ok_or_else(|| plotting_error(builtin, "legend: Location must be a string"))?,
1457 )
1458 }
1459 "fontsize" => {
1460 style.font_size = Some(
1461 value_as_f64(value)
1462 .ok_or_else(|| plotting_error(builtin, "legend: FontSize must be numeric"))?
1463 as f32,
1464 )
1465 }
1466 "fontweight" => {
1467 style.font_weight =
1468 Some(value_as_string(value).ok_or_else(|| {
1469 plotting_error(builtin, "legend: FontWeight must be a string")
1470 })?)
1471 }
1472 "fontangle" => {
1473 style.font_angle = Some(
1474 value_as_string(value)
1475 .ok_or_else(|| plotting_error(builtin, "legend: FontAngle must be a string"))?,
1476 )
1477 }
1478 "interpreter" => {
1479 style.interpreter =
1480 Some(value_as_string(value).ok_or_else(|| {
1481 plotting_error(builtin, "legend: Interpreter must be a string")
1482 })?)
1483 }
1484 "textcolor" | "color" => style.text_color = Some(parse_color_value(&opts, value)?),
1485 "visible" => {
1486 let visible = value_as_bool(value)
1487 .ok_or_else(|| plotting_error(builtin, "legend: Visible must be logical"))?;
1488 style.visible = visible;
1489 *enabled = visible;
1490 }
1491 "box" => {
1492 style.box_visible = Some(
1493 value_as_bool(value)
1494 .ok_or_else(|| plotting_error(builtin, "legend: Box must be logical"))?,
1495 )
1496 }
1497 "orientation" => {
1498 style.orientation =
1499 Some(value_as_string(value).ok_or_else(|| {
1500 plotting_error(builtin, "legend: Orientation must be a string")
1501 })?)
1502 }
1503 other => {
1504 return Err(plotting_error(
1505 builtin,
1506 format!("{builtin}: unsupported property `{other}`"),
1507 ))
1508 }
1509 }
1510 Ok(())
1511}
1512
1513fn apply_axes_property(
1514 handle: FigureHandle,
1515 axes_index: usize,
1516 key: &str,
1517 value: &Value,
1518 builtin: &'static str,
1519) -> BuiltinResult<()> {
1520 match key {
1521 "legendvisible" => {
1522 let visible = value_as_bool(value).ok_or_else(|| {
1523 plotting_error(builtin, format!("{builtin}: LegendVisible must be logical"))
1524 })?;
1525 set_legend_for_axes(handle, axes_index, visible, None, None)
1526 .map_err(|err| map_figure_error(builtin, err))?;
1527 Ok(())
1528 }
1529 "title" => apply_axes_text_alias(handle, axes_index, PlotObjectKind::Title, value, builtin),
1530 "subtitle" => {
1531 apply_axes_text_alias(handle, axes_index, PlotObjectKind::Subtitle, value, builtin)
1532 }
1533 "xlabel" => {
1534 apply_axes_text_alias(handle, axes_index, PlotObjectKind::XLabel, value, builtin)
1535 }
1536 "ylabel" => {
1537 apply_axes_text_alias(handle, axes_index, PlotObjectKind::YLabel, value, builtin)
1538 }
1539 "zlabel" => {
1540 apply_axes_text_alias(handle, axes_index, PlotObjectKind::ZLabel, value, builtin)
1541 }
1542 "view" => {
1543 let tensor = runmat_builtins::Tensor::try_from(value)
1544 .map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
1545 if tensor.data.len() != 2 || !tensor.data[0].is_finite() || !tensor.data[1].is_finite()
1546 {
1547 return Err(plotting_error(
1548 builtin,
1549 format!("{builtin}: View must be a 2-element finite numeric vector"),
1550 ));
1551 }
1552 crate::builtins::plotting::state::set_view_for_axes(
1553 handle,
1554 axes_index,
1555 tensor.data[0] as f32,
1556 tensor.data[1] as f32,
1557 )
1558 .map_err(|err| map_figure_error(builtin, err))?;
1559 Ok(())
1560 }
1561 "grid" => {
1562 let enabled = value_as_bool(value).ok_or_else(|| {
1563 plotting_error(builtin, format!("{builtin}: Grid must be logical"))
1564 })?;
1565 crate::builtins::plotting::state::set_grid_enabled_for_axes(
1566 handle, axes_index, enabled,
1567 )
1568 .map_err(|err| map_figure_error(builtin, err))?;
1569 Ok(())
1570 }
1571 "minorgrid" => {
1572 let enabled = value_as_bool(value).ok_or_else(|| {
1573 plotting_error(builtin, format!("{builtin}: MinorGrid must be logical"))
1574 })?;
1575 crate::builtins::plotting::state::set_minor_grid_enabled_for_axes(
1576 handle, axes_index, enabled,
1577 )
1578 .map_err(|err| map_figure_error(builtin, err))?;
1579 Ok(())
1580 }
1581 "box" => {
1582 let enabled = value_as_bool(value).ok_or_else(|| {
1583 plotting_error(builtin, format!("{builtin}: Box must be logical"))
1584 })?;
1585 crate::builtins::plotting::state::set_box_enabled_for_axes(handle, axes_index, enabled)
1586 .map_err(|err| map_figure_error(builtin, err))?;
1587 Ok(())
1588 }
1589 "hiddenlineremoval" => {
1590 let enabled = value_as_bool(value).ok_or_else(|| {
1591 plotting_error(
1592 builtin,
1593 format!("{builtin}: HiddenLineRemoval must be 'on' or 'off'"),
1594 )
1595 })?;
1596 crate::builtins::plotting::state::set_hidden_line_removal_for_axes(
1597 handle, axes_index, enabled,
1598 )
1599 .map_err(|err| map_figure_error(builtin, err))?;
1600 Ok(())
1601 }
1602 "axisequal" => {
1603 let enabled = value_as_bool(value).ok_or_else(|| {
1604 plotting_error(builtin, format!("{builtin}: AxisEqual must be logical"))
1605 })?;
1606 crate::builtins::plotting::state::set_axis_equal_for_axes(handle, axes_index, enabled)
1607 .map_err(|err| map_figure_error(builtin, err))?;
1608 Ok(())
1609 }
1610 "dataaspectratio" => {
1611 let ratio = data_aspect_ratio_from_value(value, builtin)?;
1612 crate::builtins::plotting::state::set_data_aspect_ratio_for_axes(
1613 handle, axes_index, ratio, "manual",
1614 )
1615 .map_err(|err| map_figure_error(builtin, err))?;
1616 Ok(())
1617 }
1618 "dataaspectratiomode" => {
1619 let mode = data_aspect_ratio_mode_from_value(value, builtin)?;
1620 let (ratio, _) = crate::builtins::plotting::state::data_aspect_ratio_snapshot_for_axes(
1621 handle, axes_index,
1622 )
1623 .map_err(|err| map_figure_error(builtin, err))?;
1624 crate::builtins::plotting::state::set_data_aspect_ratio_for_axes(
1625 handle, axes_index, ratio, mode,
1626 )
1627 .map_err(|err| map_figure_error(builtin, err))?;
1628 Ok(())
1629 }
1630 "colorbar" => {
1631 let enabled = value_as_bool(value).ok_or_else(|| {
1632 plotting_error(builtin, format!("{builtin}: Colorbar must be logical"))
1633 })?;
1634 crate::builtins::plotting::state::set_colorbar_enabled_for_axes(
1635 handle, axes_index, enabled,
1636 )
1637 .map_err(|err| map_figure_error(builtin, err))?;
1638 Ok(())
1639 }
1640 "colormap" => {
1641 let name = value_as_string(value).ok_or_else(|| {
1642 plotting_error(builtin, format!("{builtin}: Colormap must be a string"))
1643 })?;
1644 let cmap = parse_colormap_name(&name, builtin)?;
1645 crate::builtins::plotting::state::set_colormap_for_axes(handle, axes_index, cmap)
1646 .map_err(|err| map_figure_error(builtin, err))?;
1647 Ok(())
1648 }
1649 "fontsize" => {
1650 let font_size = value_as_f64(value).ok_or_else(|| {
1651 plotting_error(builtin, format!("{builtin}: FontSize must be numeric"))
1652 })?;
1653 if !font_size.is_finite() || font_size <= 0.0 || font_size > MAX_AXES_FONT_SIZE_POINTS {
1654 return Err(plotting_error(
1655 builtin,
1656 format!(
1657 "{builtin}: FontSize must be a positive finite value no larger than {MAX_AXES_FONT_SIZE_POINTS}"
1658 ),
1659 ));
1660 }
1661 let meta = axes_metadata_snapshot(handle, axes_index)
1662 .map_err(|err| map_figure_error(builtin, err))?;
1663 let mut style = meta.axes_style;
1664 style.font_size = Some(font_size as f32);
1665 set_axes_style_for_axes(handle, axes_index, style)
1666 .map_err(|err| map_figure_error(builtin, err))?;
1667 Ok(())
1668 }
1669 "xlim" => {
1670 let limits = limits_from_optional_value(value, builtin)?;
1671 let meta = axes_metadata_snapshot(handle, axes_index)
1672 .map_err(|err| map_figure_error(builtin, err))?;
1673 crate::builtins::plotting::state::set_axis_limits_for_axes(
1674 handle,
1675 axes_index,
1676 limits,
1677 meta.y_limits,
1678 )
1679 .map_err(|err| map_figure_error(builtin, err))?;
1680 Ok(())
1681 }
1682 "ylim" => {
1683 let limits = limits_from_optional_value(value, builtin)?;
1684 let meta = axes_metadata_snapshot(handle, axes_index)
1685 .map_err(|err| map_figure_error(builtin, err))?;
1686 crate::builtins::plotting::state::set_axis_limits_for_axes(
1687 handle,
1688 axes_index,
1689 meta.x_limits,
1690 limits,
1691 )
1692 .map_err(|err| map_figure_error(builtin, err))?;
1693 Ok(())
1694 }
1695 "zlim" => {
1696 let limits = limits_from_optional_value(value, builtin)?;
1697 crate::builtins::plotting::state::set_z_limits_for_axes(handle, axes_index, limits)
1698 .map_err(|err| map_figure_error(builtin, err))?;
1699 Ok(())
1700 }
1701 "clim" => {
1702 let limits = limits_from_optional_value(value, builtin)?;
1703 crate::builtins::plotting::state::set_color_limits_for_axes(handle, axes_index, limits)
1704 .map_err(|err| map_figure_error(builtin, err))?;
1705 Ok(())
1706 }
1707 "xtick" => {
1708 let ticks = ticks_from_value(value, builtin)?;
1709 let meta = axes_metadata_snapshot(handle, axes_index)
1710 .map_err(|err| map_figure_error(builtin, err))?;
1711 set_axis_ticks_for_axes(handle, axes_index, Some(ticks), meta.y_ticks)
1712 .map_err(|err| map_figure_error(builtin, err))?;
1713 Ok(())
1714 }
1715 "ytick" => {
1716 let ticks = ticks_from_value(value, builtin)?;
1717 let meta = axes_metadata_snapshot(handle, axes_index)
1718 .map_err(|err| map_figure_error(builtin, err))?;
1719 set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks))
1720 .map_err(|err| map_figure_error(builtin, err))?;
1721 Ok(())
1722 }
1723 "xtickmode" => {
1724 let mode = tick_mode_from_value(value, builtin, "XTickMode")?;
1725 let meta = axes_metadata_snapshot(handle, axes_index)
1726 .map_err(|err| map_figure_error(builtin, err))?;
1727 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1728 .map_err(|err| map_figure_error(builtin, err))?;
1729 let x = match mode {
1730 TickMode::Auto => None,
1731 TickMode::Manual => Some(ticks_or_auto(
1732 meta.x_ticks.as_deref(),
1733 x_bounds(display_bounds),
1734 )),
1735 };
1736 set_axis_ticks_for_axes(handle, axes_index, x, meta.y_ticks)
1737 .map_err(|err| map_figure_error(builtin, err))?;
1738 Ok(())
1739 }
1740 "ytickmode" => {
1741 let mode = tick_mode_from_value(value, builtin, "YTickMode")?;
1742 let meta = axes_metadata_snapshot(handle, axes_index)
1743 .map_err(|err| map_figure_error(builtin, err))?;
1744 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1745 .map_err(|err| map_figure_error(builtin, err))?;
1746 let y = match mode {
1747 TickMode::Auto => None,
1748 TickMode::Manual => Some(ticks_or_auto(
1749 meta.y_ticks.as_deref(),
1750 y_bounds(display_bounds),
1751 )),
1752 };
1753 set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, y)
1754 .map_err(|err| map_figure_error(builtin, err))?;
1755 Ok(())
1756 }
1757 "xticklabel" => {
1758 let labels = tick_labels_from_value(value, builtin)?;
1759 let meta = axes_metadata_snapshot(handle, axes_index)
1760 .map_err(|err| map_figure_error(builtin, err))?;
1761 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1762 .map_err(|err| map_figure_error(builtin, err))?;
1763 let ticks = ticks_or_auto(meta.x_ticks.as_deref(), x_bounds(display_bounds));
1764 set_axis_ticks_for_axes(handle, axes_index, Some(ticks.clone()), meta.y_ticks)
1765 .map_err(|err| map_figure_error(builtin, err))?;
1766 set_axis_tick_labels_for_axes(
1767 handle,
1768 axes_index,
1769 Some(labels_padded_to_ticks(labels, ticks.len())),
1770 meta.y_tick_labels,
1771 )
1772 .map_err(|err| map_figure_error(builtin, err))?;
1773 Ok(())
1774 }
1775 "yticklabel" => {
1776 let labels = tick_labels_from_value(value, builtin)?;
1777 let meta = axes_metadata_snapshot(handle, axes_index)
1778 .map_err(|err| map_figure_error(builtin, err))?;
1779 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1780 .map_err(|err| map_figure_error(builtin, err))?;
1781 let ticks = ticks_or_auto(meta.y_ticks.as_deref(), y_bounds(display_bounds));
1782 set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks.clone()))
1783 .map_err(|err| map_figure_error(builtin, err))?;
1784 set_axis_tick_labels_for_axes(
1785 handle,
1786 axes_index,
1787 meta.x_tick_labels,
1788 Some(labels_padded_to_ticks(labels, ticks.len())),
1789 )
1790 .map_err(|err| map_figure_error(builtin, err))?;
1791 Ok(())
1792 }
1793 "xticklabelmode" => {
1794 let mode = tick_mode_from_value(value, builtin, "XTickLabelMode")?;
1795 let meta = axes_metadata_snapshot(handle, axes_index)
1796 .map_err(|err| map_figure_error(builtin, err))?;
1797 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1798 .map_err(|err| map_figure_error(builtin, err))?;
1799 let x_labels = match mode {
1800 TickMode::Auto => None,
1801 TickMode::Manual => Some(tick_labels_or_auto(
1802 meta.x_tick_labels.as_deref(),
1803 meta.x_ticks.as_deref(),
1804 x_bounds(display_bounds),
1805 meta.x_tick_format.as_deref(),
1806 )),
1807 };
1808 if matches!(mode, TickMode::Manual) {
1809 let ticks = ticks_or_auto(meta.x_ticks.as_deref(), x_bounds(display_bounds));
1810 set_axis_ticks_for_axes(handle, axes_index, Some(ticks), meta.y_ticks)
1811 .map_err(|err| map_figure_error(builtin, err))?;
1812 }
1813 set_axis_tick_labels_for_axes(handle, axes_index, x_labels, meta.y_tick_labels)
1814 .map_err(|err| map_figure_error(builtin, err))?;
1815 Ok(())
1816 }
1817 "yticklabelmode" => {
1818 let mode = tick_mode_from_value(value, builtin, "YTickLabelMode")?;
1819 let meta = axes_metadata_snapshot(handle, axes_index)
1820 .map_err(|err| map_figure_error(builtin, err))?;
1821 let display_bounds = axis_display_bounds_snapshot_for_axes(handle, axes_index)
1822 .map_err(|err| map_figure_error(builtin, err))?;
1823 let y_labels = match mode {
1824 TickMode::Auto => None,
1825 TickMode::Manual => Some(tick_labels_or_auto(
1826 meta.y_tick_labels.as_deref(),
1827 meta.y_ticks.as_deref(),
1828 y_bounds(display_bounds),
1829 meta.y_tick_format.as_deref(),
1830 )),
1831 };
1832 if matches!(mode, TickMode::Manual) {
1833 let ticks = ticks_or_auto(meta.y_ticks.as_deref(), y_bounds(display_bounds));
1834 set_axis_ticks_for_axes(handle, axes_index, meta.x_ticks, Some(ticks))
1835 .map_err(|err| map_figure_error(builtin, err))?;
1836 }
1837 set_axis_tick_labels_for_axes(handle, axes_index, meta.x_tick_labels, y_labels)
1838 .map_err(|err| map_figure_error(builtin, err))?;
1839 Ok(())
1840 }
1841 "xticklabelformat" => {
1842 let format = tick_format_from_value(value, builtin, "XTickLabelFormat")?;
1843 let meta = axes_metadata_snapshot(handle, axes_index)
1844 .map_err(|err| map_figure_error(builtin, err))?;
1845 set_axis_tick_formats_for_axes(handle, axes_index, Some(format), meta.y_tick_format)
1846 .map_err(|err| map_figure_error(builtin, err))?;
1847 Ok(())
1848 }
1849 "yticklabelformat" => {
1850 let format = tick_format_from_value(value, builtin, "YTickLabelFormat")?;
1851 let meta = axes_metadata_snapshot(handle, axes_index)
1852 .map_err(|err| map_figure_error(builtin, err))?;
1853 set_axis_tick_formats_for_axes(handle, axes_index, meta.x_tick_format, Some(format))
1854 .map_err(|err| map_figure_error(builtin, err))?;
1855 Ok(())
1856 }
1857 "xticklabelrotation" => {
1858 let angle = tick_angle_from_value(value, builtin, "XTickLabelRotation")?;
1859 let meta = axes_metadata_snapshot(handle, axes_index)
1860 .map_err(|err| map_figure_error(builtin, err))?;
1861 set_axis_tick_angles_for_axes(
1862 handle,
1863 axes_index,
1864 Some(angle),
1865 meta.y_tick_label_rotation,
1866 )
1867 .map_err(|err| map_figure_error(builtin, err))?;
1868 Ok(())
1869 }
1870 "yticklabelrotation" => {
1871 let angle = tick_angle_from_value(value, builtin, "YTickLabelRotation")?;
1872 let meta = axes_metadata_snapshot(handle, axes_index)
1873 .map_err(|err| map_figure_error(builtin, err))?;
1874 set_axis_tick_angles_for_axes(
1875 handle,
1876 axes_index,
1877 meta.x_tick_label_rotation,
1878 Some(angle),
1879 )
1880 .map_err(|err| map_figure_error(builtin, err))?;
1881 Ok(())
1882 }
1883 "xscale" => {
1884 let mode = scale_mode_from_value(value, builtin)?;
1885 let meta = axes_metadata_snapshot(handle, axes_index)
1886 .map_err(|err| map_figure_error(builtin, err))?;
1887 crate::builtins::plotting::state::set_log_modes_for_axes(
1888 handle,
1889 axes_index,
1890 mode.is_log(),
1891 meta.y_log,
1892 )
1893 .map_err(|err| map_figure_error(builtin, err))?;
1894 Ok(())
1895 }
1896 "yscale" => {
1897 let mode = scale_mode_from_value(value, builtin)?;
1898 let meta = axes_metadata_snapshot(handle, axes_index)
1899 .map_err(|err| map_figure_error(builtin, err))?;
1900 crate::builtins::plotting::state::set_log_modes_for_axes(
1901 handle,
1902 axes_index,
1903 meta.x_log,
1904 mode.is_log(),
1905 )
1906 .map_err(|err| map_figure_error(builtin, err))?;
1907 Ok(())
1908 }
1909 "yaxislocation" => {
1910 let location = value_as_string(value)
1911 .ok_or_else(|| {
1912 plotting_error(builtin, format!("{builtin}: YAxisLocation must be text"))
1913 })?
1914 .trim()
1915 .to_ascii_lowercase();
1916 if !matches!(location.as_str(), "left" | "right") {
1917 return Err(plotting_error(
1918 builtin,
1919 format!("{builtin}: YAxisLocation must be 'left' or 'right'"),
1920 ));
1921 }
1922 crate::builtins::plotting::state::set_y_axis_location_for_axes(
1923 handle, axes_index, location,
1924 )
1925 .map_err(|err| map_figure_error(builtin, err))?;
1926 Ok(())
1927 }
1928 "position" => {
1929 let position = parse_figure_position(value, builtin)?;
1930 set_axes_position_for_axes(handle, axes_index, position)
1931 .map_err(|err| map_figure_error(builtin, err))?;
1932 Ok(())
1933 }
1934 "units" => {
1935 let units = value_as_string(value)
1936 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Units must be text")))?
1937 .trim()
1938 .to_ascii_lowercase();
1939 if !matches!(
1940 units.as_str(),
1941 "normalized" | "pixels" | "inches" | "centimeters" | "points" | "characters"
1942 ) {
1943 return Err(plotting_error(
1944 builtin,
1945 format!("{builtin}: unsupported axes Units `{units}`"),
1946 ));
1947 }
1948 set_axes_units_for_axes(handle, axes_index, units)
1949 .map_err(|err| map_figure_error(builtin, err))?;
1950 Ok(())
1951 }
1952 other => Err(plotting_error(
1953 builtin,
1954 format!("{builtin}: unsupported axes property `{other}`"),
1955 )),
1956 }
1957}
1958
1959fn apply_ruler_property(
1960 handle: FigureHandle,
1961 axes_index: usize,
1962 kind: PlotObjectKind,
1963 key: &str,
1964 value: &Value,
1965 builtin: &'static str,
1966) -> BuiltinResult<()> {
1967 match key {
1968 "ticklabelformat" => {
1969 let format = tick_format_from_value(value, builtin, "TickLabelFormat")?;
1970 let meta = axes_metadata_snapshot(handle, axes_index)
1971 .map_err(|err| map_figure_error(builtin, err))?;
1972 let (x_format, y_format) = match kind {
1973 PlotObjectKind::XAxis => (Some(format), meta.y_tick_format),
1974 PlotObjectKind::YAxis => (meta.x_tick_format, Some(format)),
1975 _ => {
1976 return Err(plotting_error(
1977 builtin,
1978 format!("{builtin}: invalid ruler handle"),
1979 ))
1980 }
1981 };
1982 set_axis_tick_formats_for_axes(handle, axes_index, x_format, y_format)
1983 .map_err(|err| map_figure_error(builtin, err))?;
1984 Ok(())
1985 }
1986 "ticklabelrotation" => {
1987 let angle = tick_angle_from_value(value, builtin, "TickLabelRotation")?;
1988 let meta = axes_metadata_snapshot(handle, axes_index)
1989 .map_err(|err| map_figure_error(builtin, err))?;
1990 let (x_angle, y_angle) = match kind {
1991 PlotObjectKind::XAxis => (Some(angle), meta.y_tick_label_rotation),
1992 PlotObjectKind::YAxis => (meta.x_tick_label_rotation, Some(angle)),
1993 _ => {
1994 return Err(plotting_error(
1995 builtin,
1996 format!("{builtin}: invalid ruler handle"),
1997 ))
1998 }
1999 };
2000 set_axis_tick_angles_for_axes(handle, axes_index, x_angle, y_angle)
2001 .map_err(|err| map_figure_error(builtin, err))?;
2002 Ok(())
2003 }
2004 other => Err(plotting_error(
2005 builtin,
2006 format!("{builtin}: unsupported ruler property `{other}`"),
2007 )),
2008 }
2009}
2010
2011fn apply_figure_property(
2012 figure_handle: FigureHandle,
2013 key: &str,
2014 value: &Value,
2015 builtin: &'static str,
2016) -> BuiltinResult<bool> {
2017 let opts = LineStyleParseOptions::generic(builtin);
2018 match key {
2019 "name" => {
2020 let name = value_as_text_string(value)
2021 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Name must be text")))?;
2022 set_figure_name(figure_handle, name).map_err(|err| map_figure_error(builtin, err))?;
2023 Ok(true)
2024 }
2025 "numbertitle" => {
2026 let enabled = value_as_bool(value).ok_or_else(|| {
2027 plotting_error(builtin, format!("{builtin}: NumberTitle must be logical"))
2028 })?;
2029 set_figure_number_title(figure_handle, enabled)
2030 .map_err(|err| map_figure_error(builtin, err))?;
2031 Ok(true)
2032 }
2033 "visible" => {
2034 let visible = value_as_bool(value).ok_or_else(|| {
2035 plotting_error(builtin, format!("{builtin}: Visible must be logical"))
2036 })?;
2037 let _ = set_figure_visible(figure_handle, visible)
2038 .map_err(|err| map_figure_error(builtin, err))?;
2039 Ok(true)
2040 }
2041 "position" => {
2042 let position = parse_figure_position(value, builtin)?;
2043 set_figure_position(figure_handle, position)
2044 .map_err(|err| map_figure_error(builtin, err))?;
2045 Ok(true)
2046 }
2047 "color" => {
2048 let color = parse_color_value(&opts, value)?;
2049 set_figure_background_color(figure_handle, color)
2050 .map_err(|err| map_figure_error(builtin, err))?;
2051 Ok(true)
2052 }
2053 "tag" => {
2054 let tag = value_as_text_string(value)
2055 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Tag must be text")))?;
2056 set_figure_tag(figure_handle, tag).map_err(|err| map_figure_error(builtin, err))?;
2057 Ok(true)
2058 }
2059 "currentaxes" => {
2060 let resolved = resolve_plot_handle(value, builtin)?;
2061 let PlotHandle::Axes(fig, axes_index) = resolved else {
2062 return Err(plotting_error(
2063 builtin,
2064 format!("{builtin}: CurrentAxes must be an axes handle"),
2065 ));
2066 };
2067 if fig != figure_handle {
2068 return Err(plotting_error(
2069 builtin,
2070 format!("{builtin}: CurrentAxes must belong to the target figure"),
2071 ));
2072 }
2073 select_axes_for_figure(figure_handle, axes_index)
2074 .map_err(|err| map_figure_error(builtin, err))?;
2075 Ok(true)
2076 }
2077 "sgtitle" => {
2078 apply_figure_text_alias(figure_handle, PlotObjectKind::SuperTitle, value, builtin)?;
2079 Ok(true)
2080 }
2081 other => Err(plotting_error(
2082 builtin,
2083 format!("{builtin}: unsupported figure property `{other}`"),
2084 )),
2085 }
2086}
2087
2088pub(crate) fn validate_figure_property_value(
2089 key_value: &Value,
2090 property_value: &Value,
2091 target_figure: Option<FigureHandle>,
2092 builtin: &'static str,
2093) -> BuiltinResult<()> {
2094 let key = property_name(key_value, builtin)?;
2095 let opts = LineStyleParseOptions::generic(builtin);
2096 match key.as_str() {
2097 "name" => {
2098 value_as_text_string(property_value)
2099 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Name must be text")))?;
2100 }
2101 "numbertitle" => {
2102 value_as_bool(property_value).ok_or_else(|| {
2103 plotting_error(builtin, format!("{builtin}: NumberTitle must be logical"))
2104 })?;
2105 }
2106 "visible" => {
2107 value_as_bool(property_value).ok_or_else(|| {
2108 plotting_error(builtin, format!("{builtin}: Visible must be logical"))
2109 })?;
2110 }
2111 "position" => {
2112 let _ = parse_figure_position(property_value, builtin)?;
2113 }
2114 "color" => {
2115 let _ = parse_color_value(&opts, property_value)?;
2116 }
2117 "tag" => {
2118 value_as_text_string(property_value)
2119 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Tag must be text")))?;
2120 }
2121 "currentaxes" => {
2122 let resolved = resolve_plot_handle(property_value, builtin)?;
2123 let PlotHandle::Axes(fig, _) = resolved else {
2124 return Err(plotting_error(
2125 builtin,
2126 format!("{builtin}: CurrentAxes must be an axes handle"),
2127 ));
2128 };
2129 let Some(target) = target_figure else {
2130 return Err(plotting_error(
2131 builtin,
2132 format!("{builtin}: CurrentAxes requires an existing target figure"),
2133 ));
2134 };
2135 if fig != target {
2136 return Err(plotting_error(
2137 builtin,
2138 format!("{builtin}: CurrentAxes must belong to the target figure"),
2139 ));
2140 }
2141 }
2142 "sgtitle" => {
2143 validate_figure_text_alias(PlotObjectKind::SuperTitle, property_value, builtin)?
2144 }
2145 other => {
2146 return Err(plotting_error(
2147 builtin,
2148 format!("{builtin}: unsupported figure property `{other}`"),
2149 ));
2150 }
2151 }
2152 Ok(())
2153}
2154
2155fn get_histogram_property(
2156 hist: &super::state::HistogramHandleState,
2157 property: Option<&str>,
2158 builtin: &'static str,
2159) -> BuiltinResult<Value> {
2160 let normalized =
2161 apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &hist.normalization);
2162 match property.map(canonical_property_name).as_deref() {
2163 None => {
2164 let mut st = StructValue::new();
2165 st.insert("Type", Value::String("histogram".into()));
2166 st.insert(
2167 "Parent",
2168 Value::Num(super::state::encode_axes_handle(
2169 hist.figure,
2170 hist.axes_index,
2171 )),
2172 );
2173 st.insert("Children", handles_value(Vec::new()));
2174 st.insert("BinEdges", tensor_from_vec(hist.bin_edges.clone()));
2175 st.insert("BinCounts", tensor_from_vec(normalized));
2176 st.insert(
2177 "Values",
2178 tensor_from_vec(apply_histogram_normalization(
2179 &hist.raw_counts,
2180 &hist.bin_edges,
2181 &hist.normalization,
2182 )),
2183 );
2184 if let Some(data) = &hist.metadata.data {
2185 st.insert("Data", tensor_from_vec(data.clone()));
2186 }
2187 st.insert("Normalization", Value::String(hist.normalization.clone()));
2188 st.insert("NumBins", Value::Num(hist.raw_counts.len() as f64));
2189 st.insert("BinWidth", Value::Num(hist.metadata.bin_width));
2190 st.insert(
2191 "BinLimits",
2192 tensor_from_vec(vec![hist.metadata.bin_limits.0, hist.metadata.bin_limits.1]),
2193 );
2194 st.insert("FaceColor", Value::String(hist.metadata.face_color.clone()));
2195 st.insert("FaceAlpha", Value::Num(hist.metadata.face_alpha));
2196 st.insert("EdgeColor", Value::String(hist.metadata.edge_color.clone()));
2197 st.insert(
2198 "DisplayStyle",
2199 Value::String(hist.metadata.display_style.clone()),
2200 );
2201 st.insert(
2202 "DisplayName",
2203 Value::String(hist.display_name.clone().unwrap_or_default()),
2204 );
2205 Ok(Value::Struct(st))
2206 }
2207 Some("type") => Ok(Value::String("histogram".into())),
2208 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2209 hist.figure,
2210 hist.axes_index,
2211 ))),
2212 Some("children") => Ok(handles_value(Vec::new())),
2213 Some("binedges") => Ok(tensor_from_vec(hist.bin_edges.clone())),
2214 Some("bincounts") => Ok(tensor_from_vec(normalized)),
2215 Some("values") => Ok(tensor_from_vec(apply_histogram_normalization(
2216 &hist.raw_counts,
2217 &hist.bin_edges,
2218 &hist.normalization,
2219 ))),
2220 Some("data") => Ok(tensor_from_vec(
2221 hist.metadata.data.clone().unwrap_or_default(),
2222 )),
2223 Some("normalization") => Ok(Value::String(hist.normalization.clone())),
2224 Some("numbins") => Ok(Value::Num(hist.raw_counts.len() as f64)),
2225 Some("binwidth") => Ok(Value::Num(hist.metadata.bin_width)),
2226 Some("binlimits") => Ok(tensor_from_vec(vec![
2227 hist.metadata.bin_limits.0,
2228 hist.metadata.bin_limits.1,
2229 ])),
2230 Some("facecolor") => Ok(Value::String(hist.metadata.face_color.clone())),
2231 Some("facealpha") => Ok(Value::Num(hist.metadata.face_alpha)),
2232 Some("edgecolor") => Ok(Value::String(hist.metadata.edge_color.clone())),
2233 Some("displaystyle") => Ok(Value::String(hist.metadata.display_style.clone())),
2234 Some("displayname") => Ok(Value::String(hist.display_name.clone().unwrap_or_default())),
2235 Some(other) => Err(plotting_error(
2236 builtin,
2237 format!("{builtin}: unsupported histogram property `{other}`"),
2238 )),
2239 }
2240}
2241
2242fn get_histogram2_property(
2243 hist: &super::state::Histogram2HandleState,
2244 property: Option<&str>,
2245 builtin: &'static str,
2246) -> BuiltinResult<Value> {
2247 match property.map(canonical_property_name).as_deref() {
2248 None => {
2249 let mut st = child_base_struct("histogram2", hist.figure, hist.axes_index);
2250 st.insert("Values", Value::Tensor(hist.values.clone()));
2251 st.insert("BinCounts", Value::Tensor(hist.values.clone()));
2252 st.insert("XBinEdges", tensor_from_vec(hist.x_bin_edges.clone()));
2253 st.insert("YBinEdges", tensor_from_vec(hist.y_bin_edges.clone()));
2254 st.insert("NumBins", tensor_from_vec(histogram2_num_bins(hist)));
2255 st.insert("Normalization", Value::String(hist.normalization.clone()));
2256 st.insert(
2257 "DisplayStyle",
2258 Value::String(hist.display_style.as_str().into()),
2259 );
2260 st.insert("ShowEmptyBins", Value::Bool(hist.show_empty_bins));
2261 st.insert("FaceAlpha", Value::Num(hist.face_alpha));
2262 st.insert(
2263 "DisplayName",
2264 Value::String(hist.display_name.clone().unwrap_or_default()),
2265 );
2266 Ok(Value::Struct(st))
2267 }
2268 Some("type") => Ok(Value::String("histogram2".into())),
2269 Some("parent") => Ok(child_parent_handle(hist.figure, hist.axes_index)),
2270 Some("children") => Ok(handles_value(Vec::new())),
2271 Some("values") | Some("bincounts") | Some("bindata") => {
2272 Ok(Value::Tensor(hist.values.clone()))
2273 }
2274 Some("xbinedges") => Ok(tensor_from_vec(hist.x_bin_edges.clone())),
2275 Some("ybinedges") => Ok(tensor_from_vec(hist.y_bin_edges.clone())),
2276 Some("numbins") => Ok(tensor_from_vec(histogram2_num_bins(hist))),
2277 Some("normalization") => Ok(Value::String(hist.normalization.clone())),
2278 Some("displaystyle") => Ok(Value::String(hist.display_style.as_str().into())),
2279 Some("showemptybins") => Ok(Value::Bool(hist.show_empty_bins)),
2280 Some("facealpha") => Ok(Value::Num(hist.face_alpha)),
2281 Some("displayname") => Ok(Value::String(hist.display_name.clone().unwrap_or_default())),
2282 Some(other) => Err(plotting_error(
2283 builtin,
2284 format!("{builtin}: unsupported histogram2 property `{other}`"),
2285 )),
2286 }
2287}
2288
2289fn histogram2_num_bins(hist: &super::state::Histogram2HandleState) -> Vec<f64> {
2290 vec![
2291 hist.x_bin_edges.len().saturating_sub(1) as f64,
2292 hist.y_bin_edges.len().saturating_sub(1) as f64,
2293 ]
2294}
2295
2296fn get_binscatter_property(
2297 binscatter: &super::state::BinscatterHandleState,
2298 property: Option<&str>,
2299 builtin: &'static str,
2300) -> BuiltinResult<Value> {
2301 match property.map(canonical_property_name).as_deref() {
2302 None => {
2303 let mut st = child_base_struct("binscatter", binscatter.figure, binscatter.axes_index);
2304 st.insert("Values", Value::Tensor(binscatter.values.clone()));
2305 st.insert("XData", tensor_from_vec(binscatter.x_data.clone()));
2306 st.insert("YData", tensor_from_vec(binscatter.y_data.clone()));
2307 st.insert("XBinEdges", tensor_from_vec(binscatter.x_bin_edges.clone()));
2308 st.insert("YBinEdges", tensor_from_vec(binscatter.y_bin_edges.clone()));
2309 st.insert("NumBins", tensor_from_vec(num_bins_value(binscatter)));
2310 st.insert("XLimits", tensor_from_vec(binscatter_x_limits(binscatter)));
2311 st.insert("YLimits", tensor_from_vec(binscatter_y_limits(binscatter)));
2312 st.insert("ShowEmptyBins", Value::Bool(binscatter.show_empty_bins));
2313 st.insert("FaceAlpha", Value::Num(binscatter.face_alpha));
2314 st.insert(
2315 "DisplayName",
2316 Value::String(binscatter.display_name.clone().unwrap_or_default()),
2317 );
2318 Ok(Value::Struct(st))
2319 }
2320 Some("type") => Ok(Value::String("binscatter".into())),
2321 Some("parent") => Ok(child_parent_handle(
2322 binscatter.figure,
2323 binscatter.axes_index,
2324 )),
2325 Some("children") => Ok(handles_value(Vec::new())),
2326 Some("values") | Some("bindata") => Ok(Value::Tensor(binscatter.values.clone())),
2327 Some("xdata") => Ok(tensor_from_vec(binscatter.x_data.clone())),
2328 Some("ydata") => Ok(tensor_from_vec(binscatter.y_data.clone())),
2329 Some("xbinedges") => Ok(tensor_from_vec(binscatter.x_bin_edges.clone())),
2330 Some("ybinedges") => Ok(tensor_from_vec(binscatter.y_bin_edges.clone())),
2331 Some("numbins") => Ok(tensor_from_vec(num_bins_value(binscatter))),
2332 Some("xlimits") => Ok(tensor_from_vec(binscatter_x_limits(binscatter))),
2333 Some("ylimits") => Ok(tensor_from_vec(binscatter_y_limits(binscatter))),
2334 Some("showemptybins") => Ok(Value::Bool(binscatter.show_empty_bins)),
2335 Some("facealpha") => Ok(Value::Num(binscatter.face_alpha)),
2336 Some("displayname") => Ok(Value::String(
2337 binscatter.display_name.clone().unwrap_or_default(),
2338 )),
2339 Some(other) => Err(plotting_error(
2340 builtin,
2341 format!("{builtin}: unsupported binscatter property `{other}`"),
2342 )),
2343 }
2344}
2345
2346fn num_bins_value(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2347 vec![binscatter.num_bins[0] as f64, binscatter.num_bins[1] as f64]
2348}
2349
2350fn binscatter_x_limits(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2351 vec![binscatter.x_limits.0, binscatter.x_limits.1]
2352}
2353
2354fn binscatter_y_limits(binscatter: &super::state::BinscatterHandleState) -> Vec<f64> {
2355 vec![binscatter.y_limits.0, binscatter.y_limits.1]
2356}
2357
2358fn get_plot_child_property(
2359 state: &super::state::PlotChildHandleState,
2360 property: Option<&str>,
2361 builtin: &'static str,
2362) -> BuiltinResult<Value> {
2363 match state {
2364 super::state::PlotChildHandleState::Histogram(hist) => {
2365 get_histogram_property(hist, property, builtin)
2366 }
2367 super::state::PlotChildHandleState::Histogram2(hist) => {
2368 get_histogram2_property(hist, property, builtin)
2369 }
2370 super::state::PlotChildHandleState::Line(plot) => {
2371 get_line_property(plot, property, builtin)
2372 }
2373 super::state::PlotChildHandleState::AnimatedLine(plot) => {
2374 get_animated_line_property(plot, property, builtin)
2375 }
2376 super::state::PlotChildHandleState::Scatter(plot) => {
2377 get_scatter_property(plot, property, builtin)
2378 }
2379 super::state::PlotChildHandleState::Bar(plot) => get_bar_property(plot, property, builtin),
2380 super::state::PlotChildHandleState::Stem(stem) => {
2381 get_stem_property(stem, property, builtin)
2382 }
2383 super::state::PlotChildHandleState::ErrorBar(errorbar) => {
2384 get_errorbar_property(errorbar, property, builtin)
2385 }
2386 super::state::PlotChildHandleState::Stairs(plot) => {
2387 get_stairs_property(plot, property, builtin)
2388 }
2389 super::state::PlotChildHandleState::Quiver(quiver) => {
2390 get_quiver_property(quiver, property, builtin)
2391 }
2392 super::state::PlotChildHandleState::Image(image) => {
2393 get_image_property(image, property, builtin)
2394 }
2395 super::state::PlotChildHandleState::Heatmap(heatmap) => {
2396 get_heatmap_property(heatmap, property, builtin)
2397 }
2398 super::state::PlotChildHandleState::Binscatter(binscatter) => {
2399 get_binscatter_property(binscatter, property, builtin)
2400 }
2401 super::state::PlotChildHandleState::FunctionSurface(function_surface) => {
2402 get_function_surface_property(function_surface, property, builtin)
2403 }
2404 super::state::PlotChildHandleState::FunctionContour(function_contour) => {
2405 get_function_contour_property(function_contour, property, builtin)
2406 }
2407 super::state::PlotChildHandleState::Area(area) => {
2408 get_area_property(area, property, builtin)
2409 }
2410 super::state::PlotChildHandleState::Surface(plot) => {
2411 get_surface_property(plot, property, builtin)
2412 }
2413 super::state::PlotChildHandleState::Patch(plot) => {
2414 get_patch_property(plot, property, builtin)
2415 }
2416 super::state::PlotChildHandleState::Line3(plot) => {
2417 get_line3_property(plot, property, builtin)
2418 }
2419 super::state::PlotChildHandleState::Scatter3(plot) => {
2420 get_scatter3_property(plot, property, builtin)
2421 }
2422 super::state::PlotChildHandleState::Contour(plot) => {
2423 get_contour_property(plot, property, builtin)
2424 }
2425 super::state::PlotChildHandleState::ContourFill(plot) => {
2426 get_contour_fill_property(plot, property, builtin)
2427 }
2428 super::state::PlotChildHandleState::ReferenceLine(plot) => {
2429 get_reference_line_property(plot, property, builtin)
2430 }
2431 super::state::PlotChildHandleState::Pie(plot) => get_pie_property(plot, property, builtin),
2432 super::state::PlotChildHandleState::Text(text) => {
2433 get_world_text_property(text, property, builtin)
2434 }
2435 super::state::PlotChildHandleState::TextScatter(textscatter) => {
2436 crate::builtins::plotting::textscatter::get_textscatter_property(
2437 textscatter,
2438 property,
2439 builtin,
2440 )
2441 }
2442 super::state::PlotChildHandleState::WordCloud(wordcloud) => {
2443 crate::builtins::plotting::wordcloud::get_wordcloud_property(
2444 wordcloud, property, builtin,
2445 )
2446 }
2447 super::state::PlotChildHandleState::StackedPlot(stacked) => {
2448 crate::builtins::plotting::stackedplot::get_stackedplot_property(
2449 stacked, property, builtin,
2450 )
2451 }
2452 }
2453}
2454
2455fn apply_plot_child_property(
2456 handle: f64,
2457 state: &super::state::PlotChildHandleState,
2458 key: &str,
2459 value: &Value,
2460 builtin: &'static str,
2461) -> BuiltinResult<()> {
2462 match state {
2463 super::state::PlotChildHandleState::Histogram(hist) => {
2464 apply_histogram_property(hist, key, value, builtin)
2465 }
2466 super::state::PlotChildHandleState::Histogram2(hist) => {
2467 apply_histogram2_property(hist, key, value, builtin)
2468 }
2469 super::state::PlotChildHandleState::Line(plot) => {
2470 apply_line_property(plot, key, value, builtin)
2471 }
2472 super::state::PlotChildHandleState::AnimatedLine(plot) => {
2473 apply_animated_line_property(plot, key, value, builtin)
2474 }
2475 super::state::PlotChildHandleState::Scatter(plot) => {
2476 apply_scatter_property(plot, key, value, builtin)
2477 }
2478 super::state::PlotChildHandleState::Bar(plot) => {
2479 apply_bar_property(plot, key, value, builtin)
2480 }
2481 super::state::PlotChildHandleState::Stem(stem) => {
2482 apply_stem_property(stem, key, value, builtin)
2483 }
2484 super::state::PlotChildHandleState::ErrorBar(errorbar) => {
2485 apply_errorbar_property(errorbar, key, value, builtin)
2486 }
2487 super::state::PlotChildHandleState::Stairs(plot) => {
2488 apply_stairs_property(plot, key, value, builtin)
2489 }
2490 super::state::PlotChildHandleState::Quiver(quiver) => {
2491 apply_quiver_property(quiver, key, value, builtin)
2492 }
2493 super::state::PlotChildHandleState::Image(image) => {
2494 apply_image_property(image, key, value, builtin)
2495 }
2496 super::state::PlotChildHandleState::Heatmap(heatmap) => {
2497 apply_heatmap_property(heatmap, key, value, builtin)
2498 }
2499 super::state::PlotChildHandleState::Binscatter(binscatter) => {
2500 apply_binscatter_property(binscatter, key, value, builtin)
2501 }
2502 super::state::PlotChildHandleState::FunctionSurface(function_surface) => {
2503 apply_function_surface_property(function_surface, key, value, builtin)
2504 }
2505 super::state::PlotChildHandleState::FunctionContour(function_contour) => {
2506 apply_function_contour_property(function_contour, key, value, builtin)
2507 }
2508 super::state::PlotChildHandleState::Area(area) => {
2509 apply_area_property(area, key, value, builtin)
2510 }
2511 super::state::PlotChildHandleState::Surface(plot) => {
2512 apply_surface_property(plot, key, value, builtin)
2513 }
2514 super::state::PlotChildHandleState::Patch(plot) => {
2515 apply_patch_property(plot, key, value, builtin)
2516 }
2517 super::state::PlotChildHandleState::Line3(plot) => {
2518 apply_line3_property(plot, key, value, builtin)
2519 }
2520 super::state::PlotChildHandleState::Scatter3(plot) => {
2521 apply_scatter3_property(plot, key, value, builtin)
2522 }
2523 super::state::PlotChildHandleState::Contour(plot) => {
2524 apply_contour_property(plot, key, value, builtin)
2525 }
2526 super::state::PlotChildHandleState::ContourFill(plot) => {
2527 apply_contour_fill_property(plot, key, value, builtin)
2528 }
2529 super::state::PlotChildHandleState::ReferenceLine(plot) => {
2530 apply_reference_line_property(plot, key, value, builtin)
2531 }
2532 super::state::PlotChildHandleState::Pie(plot) => {
2533 apply_pie_property(plot, key, value, builtin)
2534 }
2535 super::state::PlotChildHandleState::Text(text) => {
2536 apply_world_text_property(text, key, value, builtin)
2537 }
2538 super::state::PlotChildHandleState::TextScatter(textscatter) => {
2539 crate::builtins::plotting::textscatter::apply_textscatter_property(
2540 handle,
2541 textscatter,
2542 key,
2543 value,
2544 builtin,
2545 )
2546 }
2547 super::state::PlotChildHandleState::WordCloud(wordcloud) => {
2548 crate::builtins::plotting::wordcloud::apply_wordcloud_property(
2549 handle, wordcloud, key, value, builtin,
2550 )
2551 }
2552 super::state::PlotChildHandleState::StackedPlot(stacked) => {
2553 crate::builtins::plotting::stackedplot::apply_stackedplot_property(
2554 handle, stacked, key, value, builtin,
2555 )
2556 }
2557 }
2558}
2559
2560fn apply_plot_child_properties(
2561 handle: f64,
2562 state: &super::state::PlotChildHandleState,
2563 args: &[Value],
2564 builtin: &'static str,
2565) -> BuiltinResult<()> {
2566 let mut pairs = Vec::with_capacity(args.len() / 2);
2567 for pair in args.chunks_exact(2) {
2568 pairs.push((property_name(&pair[0], builtin)?, &pair[1]));
2569 }
2570 match state {
2571 super::state::PlotChildHandleState::Line(plot) => {
2572 apply_line_properties(plot, &pairs, builtin)
2573 }
2574 super::state::PlotChildHandleState::AnimatedLine(plot) => {
2575 apply_animated_line_properties(plot, &pairs, builtin)
2576 }
2577 super::state::PlotChildHandleState::Line3(plot) => {
2578 apply_line3_properties(plot, &pairs, builtin)
2579 }
2580 super::state::PlotChildHandleState::Histogram2(hist) => {
2581 apply_histogram2_properties(hist, &pairs, builtin)
2582 }
2583 super::state::PlotChildHandleState::Scatter(plot) => {
2584 apply_scatter_properties(plot, &pairs, builtin)
2585 }
2586 _ => {
2587 for (key, value) in pairs {
2588 apply_plot_child_property(handle, state, &key, value, builtin)?;
2589 }
2590 Ok(())
2591 }
2592 }
2593}
2594
2595fn child_parent_handle(figure: FigureHandle, axes_index: usize) -> Value {
2596 Value::Num(super::state::encode_axes_handle(figure, axes_index))
2597}
2598
2599fn child_base_struct(kind: &str, figure: FigureHandle, axes_index: usize) -> StructValue {
2600 let mut st = StructValue::new();
2601 st.insert("Type", Value::String(kind.into()));
2602 st.insert("Parent", child_parent_handle(figure, axes_index));
2603 st.insert("Children", handles_value(Vec::new()));
2604 st
2605}
2606
2607fn figure_position_value(position: [f64; 4]) -> Value {
2608 Value::Tensor(Tensor {
2609 rows: 1,
2610 cols: 4,
2611 shape: vec![1, 4],
2612 data: position.to_vec(),
2613 integer_data: None,
2614 dtype: runmat_builtins::NumericDType::F64,
2615 })
2616}
2617
2618fn parse_figure_position(value: &Value, builtin: &'static str) -> BuiltinResult<[f64; 4]> {
2619 let values = match value {
2620 Value::Tensor(t) if t.data.len() == 4 && is_figure_position_vector_shape(t) => &t.data,
2621 _ => {
2622 return Err(plotting_error(
2623 builtin,
2624 format!("{builtin}: Position must be a 4-element numeric vector"),
2625 ))
2626 }
2627 };
2628 let mut position = [0.0; 4];
2629 position.copy_from_slice(values);
2630 if !position.iter().all(|value| value.is_finite()) {
2631 return Err(plotting_error(
2632 builtin,
2633 format!("{builtin}: Position values must be finite"),
2634 ));
2635 }
2636 if position[2] <= 0.0 || position[3] <= 0.0 {
2637 return Err(plotting_error(
2638 builtin,
2639 format!("{builtin}: Position width and height must be positive"),
2640 ));
2641 }
2642 Ok(position)
2643}
2644
2645fn is_figure_position_vector_shape(tensor: &Tensor) -> bool {
2646 tensor.shape.len() <= 1 || (tensor.shape.len() == 2 && (tensor.rows == 1 || tensor.cols == 1))
2647}
2648
2649fn text_position_value(position: glam::Vec3) -> Value {
2650 Value::Tensor(Tensor {
2651 rows: 1,
2652 cols: 3,
2653 shape: vec![1, 3],
2654 data: vec![position.x as f64, position.y as f64, position.z as f64],
2655 integer_data: None,
2656 dtype: runmat_builtins::NumericDType::F64,
2657 })
2658}
2659
2660fn parse_text_position(value: &Value, builtin: &'static str) -> BuiltinResult<glam::Vec3> {
2661 match value {
2662 Value::Tensor(t) if t.data.len() == 2 || t.data.len() == 3 => Ok(glam::Vec3::new(
2663 t.data[0] as f32,
2664 t.data[1] as f32,
2665 t.data.get(2).copied().unwrap_or(0.0) as f32,
2666 )),
2667 _ => Err(plotting_error(
2668 builtin,
2669 format!("{builtin}: Position must be a 2-element or 3-element vector"),
2670 )),
2671 }
2672}
2673
2674fn get_world_text_property(
2675 handle: &super::state::TextAnnotationHandleState,
2676 property: Option<&str>,
2677 builtin: &'static str,
2678) -> BuiltinResult<Value> {
2679 let figure = super::state::clone_figure(handle.figure)
2680 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
2681 let annotation = figure
2682 .axes_text_annotation(handle.axes_index, handle.annotation_index)
2683 .cloned()
2684 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
2685 match property.map(canonical_property_name).as_deref() {
2686 None => {
2687 let mut st = child_base_struct("text", handle.figure, handle.axes_index);
2688 st.insert("String", Value::String(annotation.text.clone()));
2689 st.insert("Position", text_position_value(annotation.position));
2690 if let Some(weight) = annotation.style.font_weight.clone() {
2691 st.insert("FontWeight", Value::String(weight));
2692 }
2693 if let Some(angle) = annotation.style.font_angle.clone() {
2694 st.insert("FontAngle", Value::String(angle));
2695 }
2696 if let Some(interpreter) = annotation.style.interpreter.clone() {
2697 st.insert("Interpreter", Value::String(interpreter));
2698 }
2699 if let Some(color) = annotation.style.color {
2700 st.insert("Color", Value::String(color_to_short_name(color)));
2701 }
2702 if let Some(font_size) = annotation.style.font_size {
2703 st.insert("FontSize", Value::Num(font_size as f64));
2704 }
2705 st.insert("Visible", Value::Bool(annotation.style.visible));
2706 Ok(Value::Struct(st))
2707 }
2708 Some("type") => Ok(Value::String("text".into())),
2709 Some("parent") => Ok(child_parent_handle(handle.figure, handle.axes_index)),
2710 Some("children") => Ok(handles_value(Vec::new())),
2711 Some("string") => Ok(Value::String(annotation.text)),
2712 Some("position") => Ok(text_position_value(annotation.position)),
2713 Some("fontweight") => Ok(annotation
2714 .style
2715 .font_weight
2716 .map(Value::String)
2717 .unwrap_or_else(|| Value::String(String::new()))),
2718 Some("fontangle") => Ok(annotation
2719 .style
2720 .font_angle
2721 .map(Value::String)
2722 .unwrap_or_else(|| Value::String(String::new()))),
2723 Some("interpreter") => Ok(annotation
2724 .style
2725 .interpreter
2726 .map(Value::String)
2727 .unwrap_or_else(|| Value::String(String::new()))),
2728 Some("color") => Ok(annotation
2729 .style
2730 .color
2731 .map(|c| Value::String(color_to_short_name(c)))
2732 .unwrap_or_else(|| Value::String(String::new()))),
2733 Some("fontsize") => Ok(Value::Num(
2734 annotation.style.font_size.unwrap_or_default() as f64
2735 )),
2736 Some("visible") => Ok(Value::Bool(annotation.style.visible)),
2737 Some(other) => Err(plotting_error(
2738 builtin,
2739 format!("{builtin}: unsupported text property `{other}`"),
2740 )),
2741 }
2742}
2743
2744fn apply_world_text_property(
2745 handle: &super::state::TextAnnotationHandleState,
2746 key: &str,
2747 value: &Value,
2748 builtin: &'static str,
2749) -> BuiltinResult<()> {
2750 let figure = super::state::clone_figure(handle.figure)
2751 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
2752 let annotation = figure
2753 .axes_text_annotation(handle.axes_index, handle.annotation_index)
2754 .cloned()
2755 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
2756 let mut text = None;
2757 let mut position = None;
2758 let mut style = annotation.style;
2759 match canonical_property_name(key).as_ref() {
2760 "string" => {
2761 text = Some(value_as_text_string(value).ok_or_else(|| {
2762 plotting_error(builtin, format!("{builtin}: String must be text"))
2763 })?);
2764 }
2765 "position" => position = Some(parse_text_position(value, builtin)?),
2766 other => apply_text_property(&mut text, &mut style, other, value, builtin)?,
2767 }
2768 set_text_annotation_properties_for_axes(
2769 handle.figure,
2770 handle.axes_index,
2771 handle.annotation_index,
2772 text,
2773 position,
2774 Some(style),
2775 )
2776 .map_err(|err| map_figure_error(builtin, err))?;
2777 Ok(())
2778}
2779
2780fn get_simple_plot(
2781 plot: &super::state::SimplePlotHandleState,
2782 builtin: &'static str,
2783) -> BuiltinResult<runmat_plot::plots::figure::PlotElement> {
2784 let figure = super::state::clone_figure(plot.figure)
2785 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot figure")))?;
2786 let resolved = figure
2787 .plots()
2788 .nth(plot.plot_index)
2789 .cloned()
2790 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot handle")))?;
2791 Ok(resolved)
2792}
2793
2794fn get_line_property(
2795 line_handle: &super::state::SimplePlotHandleState,
2796 property: Option<&str>,
2797 builtin: &'static str,
2798) -> BuiltinResult<Value> {
2799 let plot = get_simple_plot(line_handle, builtin)?;
2800 let runmat_plot::plots::figure::PlotElement::Line(line) = plot else {
2801 return Err(plotting_error(
2802 builtin,
2803 format!("{builtin}: invalid line handle"),
2804 ));
2805 };
2806 let (x_data, y_data) = line_xy_data_for_properties(&line, builtin)?;
2807 match property.map(canonical_property_name).as_deref() {
2808 None => {
2809 let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
2810 st.insert("XData", tensor_from_vec(x_data.clone()));
2811 st.insert("YData", tensor_from_vec(y_data.clone()));
2812 st.insert("Color", Value::String(color_to_short_name(line.color)));
2813 st.insert("LineWidth", Value::Num(line.line_width as f64));
2814 st.insert(
2815 "LineStyle",
2816 Value::String(line_style_name(line.line_style).into()),
2817 );
2818 st.insert(
2819 "DisplayName",
2820 Value::String(line.label.clone().unwrap_or_default()),
2821 );
2822 st.insert(
2823 "HandleVisibility",
2824 Value::String(line.handle_visibility.clone()),
2825 );
2826 st.insert(
2827 "Visible",
2828 Value::String(if line.visible { "on" } else { "off" }.into()),
2829 );
2830 insert_line_marker_struct_props(&mut st, line.marker.as_ref());
2831 Ok(Value::Struct(st))
2832 }
2833 Some("type") => Ok(Value::String("line".into())),
2834 Some("parent") => Ok(child_parent_handle(
2835 line_handle.figure,
2836 line_handle.axes_index,
2837 )),
2838 Some("children") => Ok(handles_value(Vec::new())),
2839 Some("xdata") => Ok(tensor_from_vec(x_data)),
2840 Some("ydata") => Ok(tensor_from_vec(y_data)),
2841 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2842 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2843 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2844 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2845 Some("handlevisibility") => Ok(Value::String(line.handle_visibility)),
2846 Some("visible") => Ok(Value::String(
2847 if line.visible { "on" } else { "off" }.into(),
2848 )),
2849 Some(name) => line_marker_property_value(&line.marker, name, builtin),
2850 }
2851}
2852
2853fn get_animated_line_property(
2854 line_handle: &super::state::AnimatedLineHandleState,
2855 property: Option<&str>,
2856 builtin: &'static str,
2857) -> BuiltinResult<Value> {
2858 let simple = animated_line_simple_state(line_handle);
2859 let plot = get_simple_plot(&simple, builtin)?;
2860 let property = property.map(canonical_property_name);
2861 match plot {
2862 runmat_plot::plots::figure::PlotElement::Line(line) => {
2863 let (x_data, y_data) = line_xy_data_for_properties(&line, builtin)?;
2864 match property.as_deref() {
2865 None => {
2866 let mut st = child_base_struct(
2867 "animatedline",
2868 line_handle.figure,
2869 line_handle.axes_index,
2870 );
2871 st.insert("XData", tensor_from_vec(x_data));
2872 st.insert("YData", tensor_from_vec(y_data));
2873 st.insert("Color", Value::String(color_to_short_name(line.color)));
2874 st.insert("LineWidth", Value::Num(line.line_width as f64));
2875 st.insert(
2876 "LineStyle",
2877 Value::String(line_style_name(line.line_style).into()),
2878 );
2879 st.insert(
2880 "DisplayName",
2881 Value::String(line.label.clone().unwrap_or_default()),
2882 );
2883 st.insert(
2884 "Visible",
2885 Value::String(if line.visible { "on" } else { "off" }.into()),
2886 );
2887 st.insert(
2888 "MaximumNumPoints",
2889 animated_line_maximum_value(line_handle.maximum_num_points),
2890 );
2891 insert_line_marker_struct_props(&mut st, line.marker.as_ref());
2892 Ok(Value::Struct(st))
2893 }
2894 Some("type") => Ok(Value::String("animatedline".into())),
2895 Some("parent") => Ok(child_parent_handle(
2896 line_handle.figure,
2897 line_handle.axes_index,
2898 )),
2899 Some("children") => Ok(handles_value(Vec::new())),
2900 Some("xdata") => Ok(tensor_from_vec(x_data)),
2901 Some("ydata") => Ok(tensor_from_vec(y_data)),
2902 Some("zdata") => Ok(tensor_from_vec(Vec::new())),
2903 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2904 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2905 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2906 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2907 Some("visible") => Ok(Value::String(
2908 if line.visible { "on" } else { "off" }.into(),
2909 )),
2910 Some("maximumnumpoints") => {
2911 Ok(animated_line_maximum_value(line_handle.maximum_num_points))
2912 }
2913 Some(name) => line_marker_property_value(&line.marker, name, builtin),
2914 }
2915 }
2916 runmat_plot::plots::figure::PlotElement::Line3(line) => match property.as_deref() {
2917 None => {
2918 let mut st =
2919 child_base_struct("animatedline", line_handle.figure, line_handle.axes_index);
2920 st.insert("XData", tensor_from_vec(line.x_data.clone()));
2921 st.insert("YData", tensor_from_vec(line.y_data.clone()));
2922 st.insert("ZData", tensor_from_vec(line.z_data.clone()));
2923 st.insert("Color", Value::String(color_to_short_name(line.color)));
2924 st.insert("LineWidth", Value::Num(line.line_width as f64));
2925 st.insert(
2926 "LineStyle",
2927 Value::String(line_style_name(line.line_style).into()),
2928 );
2929 st.insert(
2930 "DisplayName",
2931 Value::String(line.label.clone().unwrap_or_default()),
2932 );
2933 st.insert(
2934 "Visible",
2935 Value::String(if line.visible { "on" } else { "off" }.into()),
2936 );
2937 st.insert(
2938 "MaximumNumPoints",
2939 animated_line_maximum_value(line_handle.maximum_num_points),
2940 );
2941 Ok(Value::Struct(st))
2942 }
2943 Some("type") => Ok(Value::String("animatedline".into())),
2944 Some("parent") => Ok(child_parent_handle(
2945 line_handle.figure,
2946 line_handle.axes_index,
2947 )),
2948 Some("children") => Ok(handles_value(Vec::new())),
2949 Some("xdata") => Ok(tensor_from_vec(line.x_data)),
2950 Some("ydata") => Ok(tensor_from_vec(line.y_data)),
2951 Some("zdata") => Ok(tensor_from_vec(line.z_data)),
2952 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
2953 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
2954 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
2955 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
2956 Some("visible") => Ok(Value::String(
2957 if line.visible { "on" } else { "off" }.into(),
2958 )),
2959 Some("maximumnumpoints") => {
2960 Ok(animated_line_maximum_value(line_handle.maximum_num_points))
2961 }
2962 Some(other) => Err(plotting_error(
2963 builtin,
2964 format!("{builtin}: unsupported animatedline property `{other}`"),
2965 )),
2966 },
2967 _ => Err(plotting_error(
2968 builtin,
2969 format!("{builtin}: invalid animatedline handle"),
2970 )),
2971 }
2972}
2973
2974fn animated_line_maximum_value(maximum: Option<usize>) -> Value {
2975 match maximum {
2976 Some(value) => Value::Num(value as f64),
2977 None => Value::Num(f64::INFINITY),
2978 }
2979}
2980
2981fn animated_line_simple_state(
2982 line_handle: &super::state::AnimatedLineHandleState,
2983) -> super::state::SimplePlotHandleState {
2984 super::state::SimplePlotHandleState {
2985 figure: line_handle.figure,
2986 axes_index: line_handle.axes_index,
2987 plot_index: line_handle.plot_index,
2988 }
2989}
2990
2991fn get_reference_line_property(
2992 line_handle: &super::state::SimplePlotHandleState,
2993 property: Option<&str>,
2994 builtin: &'static str,
2995) -> BuiltinResult<Value> {
2996 let plot = get_simple_plot(line_handle, builtin)?;
2997 let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot else {
2998 return Err(plotting_error(
2999 builtin,
3000 format!("{builtin}: invalid reference line handle"),
3001 ));
3002 };
3003 let orientation = match line.orientation {
3004 runmat_plot::plots::ReferenceLineOrientation::Vertical => "vertical",
3005 runmat_plot::plots::ReferenceLineOrientation::Horizontal => "horizontal",
3006 };
3007 match property.map(canonical_property_name).as_deref() {
3008 None => {
3009 let mut st =
3010 child_base_struct("constantline", line_handle.figure, line_handle.axes_index);
3011 st.insert("Value", Value::Num(line.value));
3012 st.insert("Orientation", Value::String(orientation.into()));
3013 st.insert("Color", Value::String(color_to_short_name(line.color)));
3014 st.insert("LineWidth", Value::Num(line.line_width as f64));
3015 st.insert(
3016 "LineStyle",
3017 Value::String(line_style_name(line.line_style).into()),
3018 );
3019 st.insert(
3020 "Label",
3021 Value::String(line.label.clone().unwrap_or_default()),
3022 );
3023 st.insert(
3024 "LabelOrientation",
3025 Value::String(line.label_orientation.clone()),
3026 );
3027 st.insert(
3028 "DisplayName",
3029 Value::String(line.display_name.clone().unwrap_or_default()),
3030 );
3031 st.insert(
3032 "Visible",
3033 Value::String(if line.visible { "on" } else { "off" }.into()),
3034 );
3035 Ok(Value::Struct(st))
3036 }
3037 Some("type") => Ok(Value::String("constantline".into())),
3038 Some("parent") => Ok(child_parent_handle(
3039 line_handle.figure,
3040 line_handle.axes_index,
3041 )),
3042 Some("children") => Ok(handles_value(Vec::new())),
3043 Some("value") => Ok(Value::Num(line.value)),
3044 Some("orientation") => Ok(Value::String(orientation.into())),
3045 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
3046 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
3047 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
3048 Some("label") => Ok(Value::String(line.label.unwrap_or_default())),
3049 Some("labelorientation") => Ok(Value::String(line.label_orientation)),
3050 Some("displayname") => Ok(Value::String(line.display_name.unwrap_or_default())),
3051 Some("visible") => Ok(Value::String(
3052 if line.visible { "on" } else { "off" }.into(),
3053 )),
3054 Some(other) => Err(plotting_error(
3055 builtin,
3056 format!("{builtin}: unsupported reference line property `{other}`"),
3057 )),
3058 }
3059}
3060
3061fn get_stairs_property(
3062 stairs_handle: &super::state::SimplePlotHandleState,
3063 property: Option<&str>,
3064 builtin: &'static str,
3065) -> BuiltinResult<Value> {
3066 let plot = get_simple_plot(stairs_handle, builtin)?;
3067 let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot else {
3068 return Err(plotting_error(
3069 builtin,
3070 format!("{builtin}: invalid stairs handle"),
3071 ));
3072 };
3073 match property.map(canonical_property_name).as_deref() {
3074 None => {
3075 let mut st =
3076 child_base_struct("stairs", stairs_handle.figure, stairs_handle.axes_index);
3077 st.insert("XData", tensor_from_vec(stairs.x.clone()));
3078 st.insert("YData", tensor_from_vec(stairs.y.clone()));
3079 st.insert("Color", Value::String(color_to_short_name(stairs.color)));
3080 st.insert("LineWidth", Value::Num(stairs.line_width as f64));
3081 if let Some(label) = stairs.label.clone() {
3082 st.insert("DisplayName", Value::String(label));
3083 }
3084 Ok(Value::Struct(st))
3085 }
3086 Some("type") => Ok(Value::String("stairs".into())),
3087 Some("parent") => Ok(child_parent_handle(
3088 stairs_handle.figure,
3089 stairs_handle.axes_index,
3090 )),
3091 Some("children") => Ok(handles_value(Vec::new())),
3092 Some("xdata") => Ok(tensor_from_vec(stairs.x.clone())),
3093 Some("ydata") => Ok(tensor_from_vec(stairs.y.clone())),
3094 Some("color") => Ok(Value::String(color_to_short_name(stairs.color))),
3095 Some("linewidth") => Ok(Value::Num(stairs.line_width as f64)),
3096 Some("displayname") => Ok(Value::String(stairs.label.unwrap_or_default())),
3097 Some(other) => Err(plotting_error(
3098 builtin,
3099 format!("{builtin}: unsupported stairs property `{other}`"),
3100 )),
3101 }
3102}
3103
3104fn get_scatter_property(
3105 scatter_handle: &super::state::SimplePlotHandleState,
3106 property: Option<&str>,
3107 builtin: &'static str,
3108) -> BuiltinResult<Value> {
3109 let plot = get_simple_plot(scatter_handle, builtin)?;
3110 let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot else {
3111 return Err(plotting_error(
3112 builtin,
3113 format!("{builtin}: invalid scatter handle"),
3114 ));
3115 };
3116 match property.map(canonical_property_name).as_deref() {
3117 None => {
3118 let mut st =
3119 child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
3120 st.insert("XData", tensor_from_vec(scatter.x_data.clone()));
3121 st.insert("YData", tensor_from_vec(scatter.y_data.clone()));
3122 if let Some(theta) = scatter.theta_data.clone() {
3123 st.insert("ThetaData", tensor_from_vec(theta));
3124 }
3125 if let Some(r) = scatter.r_data.clone() {
3126 st.insert("RData", tensor_from_vec(r));
3127 }
3128 st.insert(
3129 "Marker",
3130 Value::String(marker_style_name(scatter.marker_style).into()),
3131 );
3132 st.insert("SizeData", scatter_size_data_value(&scatter));
3133 st.insert(
3134 "MarkerFaceColor",
3135 Value::String(color_to_short_name(scatter.color)),
3136 );
3137 st.insert(
3138 "MarkerEdgeColor",
3139 Value::String(color_to_short_name(scatter.edge_color)),
3140 );
3141 st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
3142 if let Some(label) = scatter.label.clone() {
3143 st.insert("DisplayName", Value::String(label));
3144 }
3145 Ok(Value::Struct(st))
3146 }
3147 Some("type") => Ok(Value::String("scatter".into())),
3148 Some("parent") => Ok(child_parent_handle(
3149 scatter_handle.figure,
3150 scatter_handle.axes_index,
3151 )),
3152 Some("children") => Ok(handles_value(Vec::new())),
3153 Some("xdata") => Ok(tensor_from_vec(scatter.x_data.clone())),
3154 Some("ydata") => Ok(tensor_from_vec(scatter.y_data.clone())),
3155 Some("thetadata") => {
3156 Ok(tensor_from_vec(scatter.theta_data.clone().unwrap_or_else(
3157 || cartesian_theta(&scatter.x_data, &scatter.y_data),
3158 )))
3159 }
3160 Some("rdata") => {
3161 Ok(tensor_from_vec(scatter.r_data.clone().unwrap_or_else(
3162 || cartesian_radius(&scatter.x_data, &scatter.y_data),
3163 )))
3164 }
3165 Some("marker") => Ok(Value::String(
3166 marker_style_name(scatter.marker_style).into(),
3167 )),
3168 Some("sizedata") => Ok(scatter_size_data_value(&scatter)),
3169 Some("markerfacecolor") => Ok(Value::String(color_to_short_name(scatter.color))),
3170 Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
3171 Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
3172 Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
3173 Some(other) => Err(plotting_error(
3174 builtin,
3175 format!("{builtin}: unsupported scatter property `{other}`"),
3176 )),
3177 }
3178}
3179
3180fn get_bar_property(
3181 bar_handle: &super::state::SimplePlotHandleState,
3182 property: Option<&str>,
3183 builtin: &'static str,
3184) -> BuiltinResult<Value> {
3185 let plot = get_simple_plot(bar_handle, builtin)?;
3186 let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot else {
3187 return Err(plotting_error(
3188 builtin,
3189 format!("{builtin}: invalid bar handle"),
3190 ));
3191 };
3192 match property.map(canonical_property_name).as_deref() {
3193 None => {
3194 let mut st = child_base_struct("bar", bar_handle.figure, bar_handle.axes_index);
3195 st.insert("FaceColor", Value::String(color_to_short_name(bar.color)));
3196 st.insert("BarWidth", Value::Num(bar.bar_width as f64));
3197 if let Some(label) = bar.label.clone() {
3198 st.insert("DisplayName", Value::String(label));
3199 }
3200 Ok(Value::Struct(st))
3201 }
3202 Some("type") => Ok(Value::String("bar".into())),
3203 Some("parent") => Ok(child_parent_handle(
3204 bar_handle.figure,
3205 bar_handle.axes_index,
3206 )),
3207 Some("children") => Ok(handles_value(Vec::new())),
3208 Some("facecolor") | Some("color") => Ok(Value::String(color_to_short_name(bar.color))),
3209 Some("barwidth") => Ok(Value::Num(bar.bar_width as f64)),
3210 Some("displayname") => Ok(Value::String(bar.label.unwrap_or_default())),
3211 Some(other) => Err(plotting_error(
3212 builtin,
3213 format!("{builtin}: unsupported bar property `{other}`"),
3214 )),
3215 }
3216}
3217
3218fn get_surface_property(
3219 surface_handle: &super::state::SimplePlotHandleState,
3220 property: Option<&str>,
3221 builtin: &'static str,
3222) -> BuiltinResult<Value> {
3223 let plot = get_simple_plot(surface_handle, builtin)?;
3224 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
3225 return Err(plotting_error(
3226 builtin,
3227 format!("{builtin}: invalid surface handle"),
3228 ));
3229 };
3230 match property.map(canonical_property_name).as_deref() {
3231 None => {
3232 let mut st =
3233 child_base_struct("surface", surface_handle.figure, surface_handle.axes_index);
3234 st.insert("XData", surface_x_data_value(&surface));
3235 st.insert("YData", surface_y_data_value(&surface));
3236 st.insert("ZData", surface_z_data_value(&surface));
3237 st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
3238 st.insert("FaceColor", surface_face_color_value(&surface));
3239 st.insert("EdgeColor", surface_edge_color_value(&surface));
3240 st.insert(
3241 "Shading",
3242 Value::String(surface_shading_name(surface.shading_mode).into()),
3243 );
3244 st.insert(
3245 "Lighting",
3246 Value::String(surface_lighting_name(surface.lighting_enabled).into()),
3247 );
3248 st.insert("Visible", Value::Bool(surface.visible));
3249 st.insert(
3250 "DisplayName",
3251 Value::String(surface.label.clone().unwrap_or_default()),
3252 );
3253 Ok(Value::Struct(st))
3254 }
3255 Some("type") => Ok(Value::String("surface".into())),
3256 Some("parent") => Ok(child_parent_handle(
3257 surface_handle.figure,
3258 surface_handle.axes_index,
3259 )),
3260 Some("children") => Ok(handles_value(Vec::new())),
3261 Some("xdata") => Ok(surface_x_data_value(&surface)),
3262 Some("ydata") => Ok(surface_y_data_value(&surface)),
3263 Some("zdata") => Ok(surface_z_data_value(&surface)),
3264 Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
3265 Some("facecolor") | Some("color") => Ok(surface_face_color_value(&surface)),
3266 Some("edgecolor") => Ok(surface_edge_color_value(&surface)),
3267 Some("shading") => Ok(Value::String(
3268 surface_shading_name(surface.shading_mode).into(),
3269 )),
3270 Some("lighting") => Ok(Value::String(
3271 surface_lighting_name(surface.lighting_enabled).into(),
3272 )),
3273 Some("visible") => Ok(Value::Bool(surface.visible)),
3274 Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
3275 Some(other) => Err(plotting_error(
3276 builtin,
3277 format!("{builtin}: unsupported surface property `{other}`"),
3278 )),
3279 }
3280}
3281
3282fn get_function_surface_property(
3283 function_surface: &super::state::FunctionSurfaceHandleState,
3284 property: Option<&str>,
3285 builtin: &'static str,
3286) -> BuiltinResult<Value> {
3287 let surface_handle = super::state::SimplePlotHandleState {
3288 figure: function_surface.figure,
3289 axes_index: function_surface.axes_index,
3290 plot_index: function_surface.plot_index,
3291 };
3292 let plot = get_simple_plot(&surface_handle, builtin)?;
3293 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
3294 return Err(plotting_error(
3295 builtin,
3296 format!("{builtin}: invalid function surface handle"),
3297 ));
3298 };
3299 match property.map(canonical_property_name).as_deref() {
3300 None => {
3301 let mut st = child_base_struct(
3302 "functionsurface",
3303 function_surface.figure,
3304 function_surface.axes_index,
3305 );
3306 insert_function_surface_metadata(&mut st, function_surface);
3307 st.insert("XData", surface_x_data_value(&surface));
3308 st.insert("YData", surface_y_data_value(&surface));
3309 st.insert("ZData", surface_z_data_value(&surface));
3310 st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
3311 st.insert("FaceColor", surface_face_color_value(&surface));
3312 st.insert("EdgeColor", surface_edge_color_value(&surface));
3313 st.insert(
3314 "Shading",
3315 Value::String(surface_shading_name(surface.shading_mode).into()),
3316 );
3317 st.insert(
3318 "Lighting",
3319 Value::String(surface_lighting_name(surface.lighting_enabled).into()),
3320 );
3321 st.insert("Visible", Value::Bool(surface.visible));
3322 st.insert(
3323 "DisplayName",
3324 Value::String(surface.label.clone().unwrap_or_default()),
3325 );
3326 Ok(Value::Struct(st))
3327 }
3328 Some("type") => Ok(Value::String("functionsurface".into())),
3329 Some("parent") => Ok(child_parent_handle(
3330 function_surface.figure,
3331 function_surface.axes_index,
3332 )),
3333 Some("children") => Ok(handles_value(Vec::new())),
3334 Some("function") => match &function_surface.function {
3335 super::state::FunctionSurfaceFunctionState::Explicit(function) => {
3336 Ok(function_surface_function_value(function))
3337 }
3338 super::state::FunctionSurfaceFunctionState::Parametric { .. } => Err(plotting_error(
3339 builtin,
3340 format!(
3341 "{builtin}: parametric function surfaces use XFunction/YFunction/ZFunction"
3342 ),
3343 )),
3344 },
3345 Some("xfunction") => match &function_surface.function {
3346 super::state::FunctionSurfaceFunctionState::Parametric { x, .. } => {
3347 Ok(function_surface_function_value(x))
3348 }
3349 super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3350 builtin,
3351 format!("{builtin}: non-parametric function surfaces use Function"),
3352 )),
3353 },
3354 Some("yfunction") => match &function_surface.function {
3355 super::state::FunctionSurfaceFunctionState::Parametric { y, .. } => {
3356 Ok(function_surface_function_value(y))
3357 }
3358 super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3359 builtin,
3360 format!("{builtin}: non-parametric function surfaces use Function"),
3361 )),
3362 },
3363 Some("zfunction") => match &function_surface.function {
3364 super::state::FunctionSurfaceFunctionState::Parametric { z, .. } => {
3365 Ok(function_surface_function_value(z))
3366 }
3367 super::state::FunctionSurfaceFunctionState::Explicit(_) => Err(plotting_error(
3368 builtin,
3369 format!("{builtin}: non-parametric function surfaces use Function"),
3370 )),
3371 },
3372 Some("meshdensity") => Ok(Value::Num(function_surface.mesh_density as f64)),
3373 Some("xrange") => Ok(tensor_from_vec(vec![
3374 function_surface.x_range.0,
3375 function_surface.x_range.1,
3376 ])),
3377 Some("yrange") => Ok(tensor_from_vec(vec![
3378 function_surface.y_range.0,
3379 function_surface.y_range.1,
3380 ])),
3381 Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
3382 Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
3383 Some("zdata") => Ok(surface
3384 .z_data
3385 .clone()
3386 .map(tensor_from_matrix)
3387 .unwrap_or_else(|| tensor_from_vec(Vec::new()))),
3388 Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
3389 Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
3390 Some(other) => Err(plotting_error(
3391 builtin,
3392 format!("{builtin}: unsupported functionsurface property `{other}`"),
3393 )),
3394 }
3395}
3396
3397fn insert_function_surface_metadata(
3398 st: &mut StructValue,
3399 function_surface: &super::state::FunctionSurfaceHandleState,
3400) {
3401 st.insert(
3402 "MeshDensity",
3403 Value::Num(function_surface.mesh_density as f64),
3404 );
3405 st.insert(
3406 "XRange",
3407 tensor_from_vec(vec![function_surface.x_range.0, function_surface.x_range.1]),
3408 );
3409 st.insert(
3410 "YRange",
3411 tensor_from_vec(vec![function_surface.y_range.0, function_surface.y_range.1]),
3412 );
3413 match &function_surface.function {
3414 super::state::FunctionSurfaceFunctionState::Explicit(function) => {
3415 st.insert("Function", function_surface_function_value(function));
3416 }
3417 super::state::FunctionSurfaceFunctionState::Parametric { x, y, z } => {
3418 st.insert("XFunction", function_surface_function_value(x));
3419 st.insert("YFunction", function_surface_function_value(y));
3420 st.insert("ZFunction", function_surface_function_value(z));
3421 }
3422 }
3423}
3424
3425fn function_surface_function_value(function: &super::state::FunctionSurfaceFunctionRef) -> Value {
3426 match function {
3427 super::state::FunctionSurfaceFunctionRef::FunctionHandle(name) => {
3428 Value::FunctionHandle(name.clone())
3429 }
3430 super::state::FunctionSurfaceFunctionRef::ExternalFunctionHandle(name) => {
3431 Value::ExternalFunctionHandle(name.clone())
3432 }
3433 super::state::FunctionSurfaceFunctionRef::MethodFunctionHandle(name) => {
3434 Value::MethodFunctionHandle(name.clone())
3435 }
3436 super::state::FunctionSurfaceFunctionRef::BoundFunctionHandle { name, function } => {
3437 Value::BoundFunctionHandle {
3438 name: name.clone(),
3439 function: *function,
3440 }
3441 }
3442 super::state::FunctionSurfaceFunctionRef::ClosureSummary {
3443 function_name,
3444 bound_function,
3445 } => match bound_function {
3446 Some(function) => Value::BoundFunctionHandle {
3447 name: function_name.clone(),
3448 function: *function,
3449 },
3450 None => Value::String(function_name.clone()),
3451 },
3452 }
3453}
3454
3455fn get_function_contour_property(
3456 function_contour: &super::state::FunctionContourHandleState,
3457 property: Option<&str>,
3458 builtin: &'static str,
3459) -> BuiltinResult<Value> {
3460 let contour_handle = super::state::SimplePlotHandleState {
3461 figure: function_contour.figure,
3462 axes_index: function_contour.axes_index,
3463 plot_index: function_contour.plot_index,
3464 };
3465 let plot = get_simple_plot(&contour_handle, builtin)?;
3466 let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
3467 return Err(plotting_error(
3468 builtin,
3469 format!("{builtin}: invalid function contour handle"),
3470 ));
3471 };
3472 match property.map(canonical_property_name).as_deref() {
3473 None => {
3474 let mut st = child_base_struct(
3475 "functioncontour",
3476 function_contour.figure,
3477 function_contour.axes_index,
3478 );
3479 st.insert(
3480 "Function",
3481 function_surface_function_value(&function_contour.function),
3482 );
3483 st.insert(
3484 "MeshDensity",
3485 Value::Num(function_contour.mesh_density as f64),
3486 );
3487 st.insert(
3488 "XRange",
3489 tensor_from_vec(vec![function_contour.x_range.0, function_contour.x_range.1]),
3490 );
3491 st.insert(
3492 "YRange",
3493 tensor_from_vec(vec![function_contour.y_range.0, function_contour.y_range.1]),
3494 );
3495 st.insert("LineWidth", Value::Num(contour.line_width as f64));
3496 st.insert(
3497 "DisplayName",
3498 Value::String(contour.label.clone().unwrap_or_default()),
3499 );
3500 Ok(Value::Struct(st))
3501 }
3502 Some("type") => Ok(Value::String("functioncontour".into())),
3503 Some("parent") => Ok(child_parent_handle(
3504 function_contour.figure,
3505 function_contour.axes_index,
3506 )),
3507 Some("children") => Ok(handles_value(Vec::new())),
3508 Some("function") => Ok(function_surface_function_value(&function_contour.function)),
3509 Some("meshdensity") => Ok(Value::Num(function_contour.mesh_density as f64)),
3510 Some("xrange") => Ok(tensor_from_vec(vec![
3511 function_contour.x_range.0,
3512 function_contour.x_range.1,
3513 ])),
3514 Some("yrange") => Ok(tensor_from_vec(vec![
3515 function_contour.y_range.0,
3516 function_contour.y_range.1,
3517 ])),
3518 Some("linewidth") => Ok(Value::Num(contour.line_width as f64)),
3519 Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
3520 Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
3521 Some(other) => Err(plotting_error(
3522 builtin,
3523 format!("{builtin}: unsupported functioncontour property `{other}`"),
3524 )),
3525 }
3526}
3527
3528fn get_patch_property(
3529 patch_handle: &super::state::SimplePlotHandleState,
3530 property: Option<&str>,
3531 builtin: &'static str,
3532) -> BuiltinResult<Value> {
3533 let plot = get_simple_plot(patch_handle, builtin)?;
3534 let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot else {
3535 return Err(plotting_error(
3536 builtin,
3537 format!("{builtin}: invalid patch handle"),
3538 ));
3539 };
3540 match property.map(canonical_property_name).as_deref() {
3541 None => {
3542 let mut st = child_base_struct("patch", patch_handle.figure, patch_handle.axes_index);
3543 st.insert("Faces", faces_tensor(patch.faces()));
3544 st.insert("Vertices", vertices_tensor(patch.vertices()));
3545 st.insert(
3546 "XData",
3547 tensor_from_vec(patch.vertices().iter().map(|p| p.x as f64).collect()),
3548 );
3549 st.insert(
3550 "YData",
3551 tensor_from_vec(patch.vertices().iter().map(|p| p.y as f64).collect()),
3552 );
3553 st.insert(
3554 "ZData",
3555 tensor_from_vec(patch.vertices().iter().map(|p| p.z as f64).collect()),
3556 );
3557 st.insert(
3558 "FaceColor",
3559 patch_color_property(patch.face_color_mode(), patch.face_color()),
3560 );
3561 st.insert(
3562 "EdgeColor",
3563 patch_edge_color_property(patch.edge_color_mode(), patch.edge_color()),
3564 );
3565 st.insert("FaceAlpha", Value::Num(patch.face_alpha() as f64));
3566 st.insert("EdgeAlpha", Value::Num(patch.edge_alpha() as f64));
3567 st.insert("LineWidth", Value::Num(patch.line_width() as f64));
3568 st.insert("Visible", Value::Bool(patch.is_visible()));
3569 if let Some(label) = patch.label() {
3570 st.insert("DisplayName", Value::String(label.to_string()));
3571 }
3572 Ok(Value::Struct(st))
3573 }
3574 Some("type") => Ok(Value::String("patch".into())),
3575 Some("parent") => Ok(child_parent_handle(
3576 patch_handle.figure,
3577 patch_handle.axes_index,
3578 )),
3579 Some("children") => Ok(handles_value(Vec::new())),
3580 Some("faces") => Ok(faces_tensor(patch.faces())),
3581 Some("vertices") => Ok(vertices_tensor(patch.vertices())),
3582 Some("xdata") => Ok(tensor_from_vec(
3583 patch.vertices().iter().map(|p| p.x as f64).collect(),
3584 )),
3585 Some("ydata") => Ok(tensor_from_vec(
3586 patch.vertices().iter().map(|p| p.y as f64).collect(),
3587 )),
3588 Some("zdata") => Ok(tensor_from_vec(
3589 patch.vertices().iter().map(|p| p.z as f64).collect(),
3590 )),
3591 Some("facecolor") | Some("color") => Ok(patch_color_property(
3592 patch.face_color_mode(),
3593 patch.face_color(),
3594 )),
3595 Some("edgecolor") => Ok(patch_edge_color_property(
3596 patch.edge_color_mode(),
3597 patch.edge_color(),
3598 )),
3599 Some("facealpha") => Ok(Value::Num(patch.face_alpha() as f64)),
3600 Some("edgealpha") => Ok(Value::Num(patch.edge_alpha() as f64)),
3601 Some("linewidth") => Ok(Value::Num(patch.line_width() as f64)),
3602 Some("displayname") => Ok(Value::String(patch.label().unwrap_or_default().to_string())),
3603 Some("visible") => Ok(Value::Bool(patch.is_visible())),
3604 Some(other) => Err(plotting_error(
3605 builtin,
3606 format!("{builtin}: unsupported patch property `{other}`"),
3607 )),
3608 }
3609}
3610
3611fn get_line3_property(
3612 line_handle: &super::state::SimplePlotHandleState,
3613 property: Option<&str>,
3614 builtin: &'static str,
3615) -> BuiltinResult<Value> {
3616 let plot = get_simple_plot(line_handle, builtin)?;
3617 let runmat_plot::plots::figure::PlotElement::Line3(line) = plot else {
3618 return Err(plotting_error(
3619 builtin,
3620 format!("{builtin}: invalid plot3 handle"),
3621 ));
3622 };
3623 match property.map(canonical_property_name).as_deref() {
3624 None => {
3625 let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
3626 st.insert("XData", tensor_from_vec(line.x_data.clone()));
3627 st.insert("YData", tensor_from_vec(line.y_data.clone()));
3628 st.insert("ZData", tensor_from_vec(line.z_data.clone()));
3629 st.insert("Color", Value::String(color_to_short_name(line.color)));
3630 st.insert("LineWidth", Value::Num(line.line_width as f64));
3631 st.insert(
3632 "LineStyle",
3633 Value::String(line_style_name(line.line_style).into()),
3634 );
3635 st.insert(
3636 "DisplayName",
3637 Value::String(line.label.clone().unwrap_or_default()),
3638 );
3639 st.insert(
3640 "Visible",
3641 Value::String(if line.visible { "on" } else { "off" }.into()),
3642 );
3643 Ok(Value::Struct(st))
3644 }
3645 Some("type") => Ok(Value::String("line".into())),
3646 Some("parent") => Ok(child_parent_handle(
3647 line_handle.figure,
3648 line_handle.axes_index,
3649 )),
3650 Some("children") => Ok(handles_value(Vec::new())),
3651 Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
3652 Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
3653 Some("zdata") => Ok(tensor_from_vec(line.z_data.clone())),
3654 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
3655 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
3656 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
3657 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
3658 Some("visible") => Ok(Value::String(
3659 if line.visible { "on" } else { "off" }.into(),
3660 )),
3661 Some(other) => Err(plotting_error(
3662 builtin,
3663 format!("{builtin}: unsupported plot3 property `{other}`"),
3664 )),
3665 }
3666}
3667
3668fn get_scatter3_property(
3669 scatter_handle: &super::state::SimplePlotHandleState,
3670 property: Option<&str>,
3671 builtin: &'static str,
3672) -> BuiltinResult<Value> {
3673 let plot = get_simple_plot(scatter_handle, builtin)?;
3674 let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot else {
3675 return Err(plotting_error(
3676 builtin,
3677 format!("{builtin}: invalid scatter3 handle"),
3678 ));
3679 };
3680 let (x, y, z): (Vec<f64>, Vec<f64>, Vec<f64>) = scatter
3681 .points
3682 .iter()
3683 .map(|p| (p.x as f64, p.y as f64, p.z as f64))
3684 .unzip_n_vec();
3685 match property.map(canonical_property_name).as_deref() {
3686 None => {
3687 let mut st =
3688 child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
3689 st.insert("XData", tensor_from_vec(x));
3690 st.insert("YData", tensor_from_vec(y));
3691 st.insert("ZData", tensor_from_vec(z));
3692 st.insert(
3693 "Marker",
3694 Value::String(marker_style_name(scatter.marker_style).into()),
3695 );
3696 st.insert(
3697 "SizeData",
3698 Value::Num(marker_diameter_px_to_area_points2(scatter.point_size)),
3699 );
3700 st.insert(
3701 "MarkerFaceColor",
3702 Value::String(color_to_short_name(
3703 scatter.colors.first().copied().unwrap_or_default(),
3704 )),
3705 );
3706 st.insert(
3707 "MarkerEdgeColor",
3708 Value::String(color_to_short_name(scatter.edge_color)),
3709 );
3710 st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
3711 if let Some(label) = scatter.label.clone() {
3712 st.insert("DisplayName", Value::String(label));
3713 }
3714 Ok(Value::Struct(st))
3715 }
3716 Some("type") => Ok(Value::String("scatter".into())),
3717 Some("parent") => Ok(child_parent_handle(
3718 scatter_handle.figure,
3719 scatter_handle.axes_index,
3720 )),
3721 Some("children") => Ok(handles_value(Vec::new())),
3722 Some("marker") => Ok(Value::String(
3723 marker_style_name(scatter.marker_style).into(),
3724 )),
3725 Some("sizedata") => Ok(Value::Num(marker_diameter_px_to_area_points2(
3726 scatter.point_size,
3727 ))),
3728 Some("markerfacecolor") => Ok(Value::String(color_to_short_name(
3729 scatter.colors.first().copied().unwrap_or_default(),
3730 ))),
3731 Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
3732 Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
3733 Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
3734 Some(other) => Err(plotting_error(
3735 builtin,
3736 format!("{builtin}: unsupported scatter3 property `{other}`"),
3737 )),
3738 }
3739}
3740
3741fn get_pie_property(
3742 pie_handle: &super::state::SimplePlotHandleState,
3743 property: Option<&str>,
3744 builtin: &'static str,
3745) -> BuiltinResult<Value> {
3746 let plot = get_simple_plot(pie_handle, builtin)?;
3747 let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot else {
3748 return Err(plotting_error(
3749 builtin,
3750 format!("{builtin}: invalid pie handle"),
3751 ));
3752 };
3753 match property.map(canonical_property_name).as_deref() {
3754 None => {
3755 let mut st = child_base_struct("pie", pie_handle.figure, pie_handle.axes_index);
3756 if let Some(label) = pie.label.clone() {
3757 st.insert("DisplayName", Value::String(label));
3758 }
3759 Ok(Value::Struct(st))
3760 }
3761 Some("type") => Ok(Value::String("pie".into())),
3762 Some("parent") => Ok(child_parent_handle(
3763 pie_handle.figure,
3764 pie_handle.axes_index,
3765 )),
3766 Some("children") => Ok(handles_value(Vec::new())),
3767 Some("displayname") => Ok(Value::String(pie.label.unwrap_or_default())),
3768 Some(other) => Err(plotting_error(
3769 builtin,
3770 format!("{builtin}: unsupported pie property `{other}`"),
3771 )),
3772 }
3773}
3774
3775fn get_contour_property(
3776 contour_handle: &super::state::SimplePlotHandleState,
3777 property: Option<&str>,
3778 builtin: &'static str,
3779) -> BuiltinResult<Value> {
3780 let plot = get_simple_plot(contour_handle, builtin)?;
3781 let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
3782 return Err(plotting_error(
3783 builtin,
3784 format!("{builtin}: invalid contour handle"),
3785 ));
3786 };
3787 match property.map(canonical_property_name).as_deref() {
3788 None => {
3789 let mut st =
3790 child_base_struct("contour", contour_handle.figure, contour_handle.axes_index);
3791 st.insert("ZData", Value::Num(contour.base_z as f64));
3792 if let Some(label) = contour.label.clone() {
3793 st.insert("DisplayName", Value::String(label));
3794 }
3795 Ok(Value::Struct(st))
3796 }
3797 Some("type") => Ok(Value::String("contour".into())),
3798 Some("parent") => Ok(child_parent_handle(
3799 contour_handle.figure,
3800 contour_handle.axes_index,
3801 )),
3802 Some("children") => Ok(handles_value(Vec::new())),
3803 Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
3804 Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
3805 Some(other) => Err(plotting_error(
3806 builtin,
3807 format!("{builtin}: unsupported contour property `{other}`"),
3808 )),
3809 }
3810}
3811
3812fn get_contour_fill_property(
3813 fill_handle: &super::state::SimplePlotHandleState,
3814 property: Option<&str>,
3815 builtin: &'static str,
3816) -> BuiltinResult<Value> {
3817 let plot = get_simple_plot(fill_handle, builtin)?;
3818 let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot else {
3819 return Err(plotting_error(
3820 builtin,
3821 format!("{builtin}: invalid contourf handle"),
3822 ));
3823 };
3824 match property.map(canonical_property_name).as_deref() {
3825 None => {
3826 let mut st = child_base_struct("contour", fill_handle.figure, fill_handle.axes_index);
3827 if let Some(label) = fill.label.clone() {
3828 st.insert("DisplayName", Value::String(label));
3829 }
3830 Ok(Value::Struct(st))
3831 }
3832 Some("type") => Ok(Value::String("contour".into())),
3833 Some("parent") => Ok(child_parent_handle(
3834 fill_handle.figure,
3835 fill_handle.axes_index,
3836 )),
3837 Some("children") => Ok(handles_value(Vec::new())),
3838 Some("displayname") => Ok(Value::String(fill.label.unwrap_or_default())),
3839 Some(other) => Err(plotting_error(
3840 builtin,
3841 format!("{builtin}: unsupported contourf property `{other}`"),
3842 )),
3843 }
3844}
3845
3846fn get_stem_property(
3847 stem_handle: &super::state::StemHandleState,
3848 property: Option<&str>,
3849 builtin: &'static str,
3850) -> BuiltinResult<Value> {
3851 let figure = super::state::clone_figure(stem_handle.figure)
3852 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem figure")))?;
3853 let plot = figure
3854 .plots()
3855 .nth(stem_handle.plot_index)
3856 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem handle")))?;
3857 let runmat_plot::plots::figure::PlotElement::Stem(stem) = plot else {
3858 return Err(plotting_error(
3859 builtin,
3860 format!("{builtin}: invalid stem handle"),
3861 ));
3862 };
3863 match property.map(canonical_property_name).as_deref() {
3864 None => {
3865 let mut st = StructValue::new();
3866 st.insert("Type", Value::String("stem".into()));
3867 st.insert(
3868 "Parent",
3869 Value::Num(super::state::encode_axes_handle(
3870 stem_handle.figure,
3871 stem_handle.axes_index,
3872 )),
3873 );
3874 st.insert("Children", handles_value(Vec::new()));
3875 st.insert("BaseValue", Value::Num(stem.baseline));
3876 st.insert("BaseLine", Value::Bool(stem.baseline_visible));
3877 st.insert("LineWidth", Value::Num(stem.line_width as f64));
3878 st.insert(
3879 "LineStyle",
3880 Value::String(line_style_name(stem.line_style).into()),
3881 );
3882 st.insert("Color", Value::String(color_to_short_name(stem.color)));
3883 if let Some(marker) = &stem.marker {
3884 st.insert(
3885 "Marker",
3886 Value::String(marker_style_name(marker.kind).into()),
3887 );
3888 st.insert("MarkerSize", Value::Num(marker.size as f64));
3889 st.insert(
3890 "MarkerFaceColor",
3891 Value::String(color_to_short_name(marker.face_color)),
3892 );
3893 st.insert(
3894 "MarkerEdgeColor",
3895 Value::String(color_to_short_name(marker.edge_color)),
3896 );
3897 st.insert("Filled", Value::Bool(marker.filled));
3898 }
3899 Ok(Value::Struct(st))
3900 }
3901 Some("type") => Ok(Value::String("stem".into())),
3902 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
3903 stem_handle.figure,
3904 stem_handle.axes_index,
3905 ))),
3906 Some("children") => Ok(handles_value(Vec::new())),
3907 Some("basevalue") => Ok(Value::Num(stem.baseline)),
3908 Some("baseline") => Ok(Value::Bool(stem.baseline_visible)),
3909 Some("linewidth") => Ok(Value::Num(stem.line_width as f64)),
3910 Some("linestyle") => Ok(Value::String(line_style_name(stem.line_style).into())),
3911 Some("color") => Ok(Value::String(color_to_short_name(stem.color))),
3912 Some("marker") => Ok(Value::String(
3913 stem.marker
3914 .as_ref()
3915 .map(|m| marker_style_name(m.kind).to_string())
3916 .unwrap_or("none".into()),
3917 )),
3918 Some("markersize") => Ok(Value::Num(
3919 stem.marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
3920 )),
3921 Some("markerfacecolor") => Ok(Value::String(
3922 stem.marker
3923 .as_ref()
3924 .map(|m| color_to_short_name(m.face_color))
3925 .unwrap_or("none".into()),
3926 )),
3927 Some("markeredgecolor") => Ok(Value::String(
3928 stem.marker
3929 .as_ref()
3930 .map(|m| color_to_short_name(m.edge_color))
3931 .unwrap_or("none".into()),
3932 )),
3933 Some("filled") => Ok(Value::Bool(
3934 stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
3935 )),
3936 Some(other) => Err(plotting_error(
3937 builtin,
3938 format!("{builtin}: unsupported stem property `{other}`"),
3939 )),
3940 }
3941}
3942
3943fn get_errorbar_property(
3944 error_handle: &super::state::ErrorBarHandleState,
3945 property: Option<&str>,
3946 builtin: &'static str,
3947) -> BuiltinResult<Value> {
3948 let figure = super::state::clone_figure(error_handle.figure)
3949 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar figure")))?;
3950 let plot = figure
3951 .plots()
3952 .nth(error_handle.plot_index)
3953 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar handle")))?;
3954 let runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) = plot else {
3955 return Err(plotting_error(
3956 builtin,
3957 format!("{builtin}: invalid errorbar handle"),
3958 ));
3959 };
3960 match property.map(canonical_property_name).as_deref() {
3961 None => {
3962 let mut st = StructValue::new();
3963 st.insert("Type", Value::String("errorbar".into()));
3964 st.insert(
3965 "Parent",
3966 Value::Num(super::state::encode_axes_handle(
3967 error_handle.figure,
3968 error_handle.axes_index,
3969 )),
3970 );
3971 st.insert("Children", handles_value(Vec::new()));
3972 st.insert("LineWidth", Value::Num(errorbar.line_width as f64));
3973 st.insert(
3974 "LineStyle",
3975 Value::String(line_style_name(errorbar.line_style).into()),
3976 );
3977 st.insert("Color", Value::String(color_to_short_name(errorbar.color)));
3978 st.insert("CapSize", Value::Num(errorbar.cap_size as f64));
3979 if let Some(marker) = &errorbar.marker {
3980 st.insert(
3981 "Marker",
3982 Value::String(marker_style_name(marker.kind).into()),
3983 );
3984 st.insert("MarkerSize", Value::Num(marker.size as f64));
3985 }
3986 Ok(Value::Struct(st))
3987 }
3988 Some("type") => Ok(Value::String("errorbar".into())),
3989 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
3990 error_handle.figure,
3991 error_handle.axes_index,
3992 ))),
3993 Some("children") => Ok(handles_value(Vec::new())),
3994 Some("linewidth") => Ok(Value::Num(errorbar.line_width as f64)),
3995 Some("linestyle") => Ok(Value::String(line_style_name(errorbar.line_style).into())),
3996 Some("color") => Ok(Value::String(color_to_short_name(errorbar.color))),
3997 Some("capsize") => Ok(Value::Num(errorbar.cap_size as f64)),
3998 Some("marker") => Ok(Value::String(
3999 errorbar
4000 .marker
4001 .as_ref()
4002 .map(|m| marker_style_name(m.kind).to_string())
4003 .unwrap_or("none".into()),
4004 )),
4005 Some("markersize") => Ok(Value::Num(
4006 errorbar
4007 .marker
4008 .as_ref()
4009 .map(|m| m.size as f64)
4010 .unwrap_or(0.0),
4011 )),
4012 Some(other) => Err(plotting_error(
4013 builtin,
4014 format!("{builtin}: unsupported errorbar property `{other}`"),
4015 )),
4016 }
4017}
4018
4019fn get_quiver_property(
4020 quiver_handle: &super::state::QuiverHandleState,
4021 property: Option<&str>,
4022 builtin: &'static str,
4023) -> BuiltinResult<Value> {
4024 let figure = super::state::clone_figure(quiver_handle.figure)
4025 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
4026 let plot = figure
4027 .plots()
4028 .nth(quiver_handle.plot_index)
4029 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
4030 let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
4031 return Err(plotting_error(
4032 builtin,
4033 format!("{builtin}: invalid quiver handle"),
4034 ));
4035 };
4036 let has_cpu_data = quiver.has_cpu_vector_data();
4037 match property.map(canonical_property_name).as_deref() {
4038 None => {
4039 let mut st = StructValue::new();
4040 st.insert("Type", Value::String("quiver".into()));
4041 st.insert(
4042 "Parent",
4043 Value::Num(super::state::encode_axes_handle(
4044 quiver_handle.figure,
4045 quiver_handle.axes_index,
4046 )),
4047 );
4048 st.insert("Children", handles_value(Vec::new()));
4049 st.insert("Color", Value::String(color_to_short_name(quiver.color)));
4050 st.insert("LineWidth", Value::Num(quiver.line_width as f64));
4051 st.insert("AutoScaleFactor", Value::Num(quiver.scale as f64));
4052 st.insert("MaxHeadSize", Value::Num(quiver.head_size as f64));
4053 if has_cpu_data {
4054 st.insert("XData", tensor_from_vec(quiver.x.clone()));
4055 st.insert("YData", tensor_from_vec(quiver.y.clone()));
4056 if quiver_handle.is_3d {
4057 st.insert(
4058 "ZData",
4059 tensor_from_vec(quiver.z.clone().unwrap_or_default()),
4060 );
4061 st.insert(
4062 "WData",
4063 tensor_from_vec(quiver.w.clone().unwrap_or_default()),
4064 );
4065 }
4066 st.insert("UData", tensor_from_vec(quiver.u.clone()));
4067 st.insert("VData", tensor_from_vec(quiver.v.clone()));
4068 }
4069 Ok(Value::Struct(st))
4070 }
4071 Some("type") => Ok(Value::String("quiver".into())),
4072 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4073 quiver_handle.figure,
4074 quiver_handle.axes_index,
4075 ))),
4076 Some("children") => Ok(handles_value(Vec::new())),
4077 Some("color") => Ok(Value::String(color_to_short_name(quiver.color))),
4078 Some("linewidth") => Ok(Value::Num(quiver.line_width as f64)),
4079 Some("autoscalefactor") => Ok(Value::Num(quiver.scale as f64)),
4080 Some("maxheadsize") => Ok(Value::Num(quiver.head_size as f64)),
4081 Some("xdata") if has_cpu_data => Ok(tensor_from_vec(quiver.x.clone())),
4082 Some("ydata") if has_cpu_data => Ok(tensor_from_vec(quiver.y.clone())),
4083 Some("zdata") if quiver_handle.is_3d => {
4084 if has_cpu_data {
4085 Ok(tensor_from_vec(quiver.z.clone().unwrap_or_default()))
4086 } else {
4087 Err(quiver_data_unavailable_error(builtin))
4088 }
4089 }
4090 Some("udata") if has_cpu_data => Ok(tensor_from_vec(quiver.u.clone())),
4091 Some("vdata") if has_cpu_data => Ok(tensor_from_vec(quiver.v.clone())),
4092 Some("wdata") if quiver_handle.is_3d => {
4093 if has_cpu_data {
4094 Ok(tensor_from_vec(quiver.w.clone().unwrap_or_default()))
4095 } else {
4096 Err(quiver_data_unavailable_error(builtin))
4097 }
4098 }
4099 Some("xdata") | Some("ydata") | Some("udata") | Some("vdata") => {
4100 Err(quiver_data_unavailable_error(builtin))
4101 }
4102 Some(other) => Err(plotting_error(
4103 builtin,
4104 format!("{builtin}: unsupported quiver property `{other}`"),
4105 )),
4106 }
4107}
4108
4109fn quiver_data_unavailable_error(builtin: &'static str) -> crate::RuntimeError {
4110 plotting_error(
4111 builtin,
4112 format!("{builtin}: quiver data properties are unavailable for GPU-backed quiver handles"),
4113 )
4114}
4115
4116fn get_image_property(
4117 image_handle: &super::state::ImageHandleState,
4118 property: Option<&str>,
4119 builtin: &'static str,
4120) -> BuiltinResult<Value> {
4121 let figure = super::state::clone_figure(image_handle.figure)
4122 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image figure")))?;
4123 let plot = figure
4124 .plots()
4125 .nth(image_handle.plot_index)
4126 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image handle")))?;
4127 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4128 return Err(plotting_error(
4129 builtin,
4130 format!("{builtin}: invalid image handle"),
4131 ));
4132 };
4133 if !surface.image_mode {
4134 return Err(plotting_error(
4135 builtin,
4136 format!("{builtin}: handle does not reference an image plot"),
4137 ));
4138 }
4139 match property.map(canonical_property_name).as_deref() {
4140 None => {
4141 let mut st = StructValue::new();
4142 st.insert("Type", Value::String("image".into()));
4143 st.insert(
4144 "Parent",
4145 Value::Num(super::state::encode_axes_handle(
4146 image_handle.figure,
4147 image_handle.axes_index,
4148 )),
4149 );
4150 st.insert("Children", handles_value(Vec::new()));
4151 st.insert("XData", tensor_from_vec(surface.x_data.clone()));
4152 st.insert("YData", tensor_from_vec(surface.y_data.clone()));
4153 st.insert(
4154 "CDataMapping",
4155 Value::String(if surface.color_grid.is_some() {
4156 "direct".into()
4157 } else {
4158 "scaled".into()
4159 }),
4160 );
4161 Ok(Value::Struct(st))
4162 }
4163 Some("type") => Ok(Value::String("image".into())),
4164 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4165 image_handle.figure,
4166 image_handle.axes_index,
4167 ))),
4168 Some("children") => Ok(handles_value(Vec::new())),
4169 Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
4170 Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
4171 Some("cdatamapping") => Ok(Value::String(if surface.color_grid.is_some() {
4172 "direct".into()
4173 } else {
4174 "scaled".into()
4175 })),
4176 Some(other) => Err(plotting_error(
4177 builtin,
4178 format!("{builtin}: unsupported image property `{other}`"),
4179 )),
4180 }
4181}
4182
4183fn get_heatmap_property(
4184 heatmap_handle: &super::state::HeatmapHandleState,
4185 property: Option<&str>,
4186 builtin: &'static str,
4187) -> BuiltinResult<Value> {
4188 let figure = super::state::clone_figure(heatmap_handle.figure)
4189 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap figure")))?;
4190 let plot = figure
4191 .plots()
4192 .nth(heatmap_handle.plot_index)
4193 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap handle")))?;
4194 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4195 return Err(plotting_error(
4196 builtin,
4197 format!("{builtin}: invalid heatmap handle"),
4198 ));
4199 };
4200 if !surface.image_mode {
4201 return Err(plotting_error(
4202 builtin,
4203 format!("{builtin}: handle does not reference a heatmap plot"),
4204 ));
4205 }
4206 let meta = figure
4207 .axes_metadata(heatmap_handle.axes_index)
4208 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap axes")))?;
4209 match property.map(canonical_property_name).as_deref() {
4210 None => {
4211 let mut st = StructValue::new();
4212 st.insert("Type", Value::String("heatmap".into()));
4213 st.insert(
4214 "Parent",
4215 Value::Num(super::state::encode_axes_handle(
4216 heatmap_handle.figure,
4217 heatmap_handle.axes_index,
4218 )),
4219 );
4220 st.insert("Children", handles_value(Vec::new()));
4221 st.insert(
4222 "Title",
4223 Value::String(meta.title.clone().unwrap_or_default()),
4224 );
4225 st.insert(
4226 "XLabel",
4227 Value::String(meta.x_label.clone().unwrap_or_default()),
4228 );
4229 st.insert(
4230 "YLabel",
4231 Value::String(meta.y_label.clone().unwrap_or_default()),
4232 );
4233 st.insert(
4234 "XDisplayLabels",
4235 string_array_from_vec(heatmap_handle.x_labels.clone())?,
4236 );
4237 st.insert(
4238 "YDisplayLabels",
4239 string_array_from_vec(heatmap_handle.y_labels.clone())?,
4240 );
4241 st.insert(
4242 "ColorData",
4243 Value::Tensor(heatmap_handle.color_data.clone()),
4244 );
4245 st.insert("ColorbarVisible", Value::Bool(meta.colorbar_enabled));
4246 st.insert(
4247 "Colormap",
4248 Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
4249 );
4250 Ok(Value::Struct(st))
4251 }
4252 Some("type") => Ok(Value::String("heatmap".into())),
4253 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4254 heatmap_handle.figure,
4255 heatmap_handle.axes_index,
4256 ))),
4257 Some("children") => Ok(handles_value(Vec::new())),
4258 Some("title") => Ok(Value::String(meta.title.clone().unwrap_or_default())),
4259 Some("xlabel") => Ok(Value::String(meta.x_label.clone().unwrap_or_default())),
4260 Some("ylabel") => Ok(Value::String(meta.y_label.clone().unwrap_or_default())),
4261 Some("xdisplaylabels") => string_array_from_vec(heatmap_handle.x_labels.clone()),
4262 Some("ydisplaylabels") => string_array_from_vec(heatmap_handle.y_labels.clone()),
4263 Some("colordata") => Ok(Value::Tensor(heatmap_handle.color_data.clone())),
4264 Some("colorbarvisible") | Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
4265 Some("colormap") => Ok(Value::String(
4266 format!("{:?}", meta.colormap).to_ascii_lowercase(),
4267 )),
4268 Some(other) => Err(plotting_error(
4269 builtin,
4270 format!("{builtin}: unsupported heatmap property `{other}`"),
4271 )),
4272 }
4273}
4274
4275fn get_area_property(
4276 area_handle: &super::state::AreaHandleState,
4277 property: Option<&str>,
4278 builtin: &'static str,
4279) -> BuiltinResult<Value> {
4280 let figure = super::state::clone_figure(area_handle.figure)
4281 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area figure")))?;
4282 let plot = figure
4283 .plots()
4284 .nth(area_handle.plot_index)
4285 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area handle")))?;
4286 let runmat_plot::plots::figure::PlotElement::Area(area) = plot else {
4287 return Err(plotting_error(
4288 builtin,
4289 format!("{builtin}: invalid area handle"),
4290 ));
4291 };
4292 match property.map(canonical_property_name).as_deref() {
4293 None => {
4294 let mut st = StructValue::new();
4295 st.insert("Type", Value::String("area".into()));
4296 st.insert(
4297 "Parent",
4298 Value::Num(super::state::encode_axes_handle(
4299 area_handle.figure,
4300 area_handle.axes_index,
4301 )),
4302 );
4303 st.insert("Children", handles_value(Vec::new()));
4304 st.insert("XData", tensor_from_vec(area.x.clone()));
4305 st.insert("YData", tensor_from_vec(area.y.clone()));
4306 st.insert("BaseValue", Value::Num(area.baseline));
4307 st.insert("Color", Value::String(color_to_short_name(area.color)));
4308 Ok(Value::Struct(st))
4309 }
4310 Some("type") => Ok(Value::String("area".into())),
4311 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
4312 area_handle.figure,
4313 area_handle.axes_index,
4314 ))),
4315 Some("children") => Ok(handles_value(Vec::new())),
4316 Some("xdata") => Ok(tensor_from_vec(area.x.clone())),
4317 Some("ydata") => Ok(tensor_from_vec(area.y.clone())),
4318 Some("basevalue") => Ok(Value::Num(area.baseline)),
4319 Some("color") => Ok(Value::String(color_to_short_name(area.color))),
4320 Some(other) => Err(plotting_error(
4321 builtin,
4322 format!("{builtin}: unsupported area property `{other}`"),
4323 )),
4324 }
4325}
4326
4327fn apply_histogram_property(
4328 hist: &super::state::HistogramHandleState,
4329 key: &str,
4330 value: &Value,
4331 builtin: &'static str,
4332) -> BuiltinResult<()> {
4333 match key {
4334 "normalization" => {
4335 let norm = value_as_string(value)
4336 .ok_or_else(|| {
4337 plotting_error(
4338 builtin,
4339 format!("{builtin}: Normalization must be a string"),
4340 )
4341 })?
4342 .trim()
4343 .to_ascii_lowercase();
4344 validate_histogram_normalization(&norm, builtin)?;
4345 let normalized =
4346 apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &norm);
4347 let labels = histogram_labels_from_edges(&hist.bin_edges);
4348 super::state::update_histogram_plot_data(
4349 hist.figure,
4350 hist.plot_index,
4351 labels,
4352 normalized,
4353 )
4354 .map_err(|err| map_figure_error(builtin, err))?;
4355 super::state::update_histogram_handle_for_plot(
4356 hist.figure,
4357 hist.axes_index,
4358 hist.plot_index,
4359 norm,
4360 hist.raw_counts.clone(),
4361 )
4362 .map_err(|err| map_figure_error(builtin, err))?;
4363 Ok(())
4364 }
4365 "displayname" => {
4366 let display_name = value_as_string(value).map(|s| s.to_string());
4367 super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4368 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4369 bar.label = display_name.clone();
4370 }
4371 })
4372 .map_err(|err| map_figure_error(builtin, err))?;
4373 super::state::set_histogram_handle_display_name(
4374 hist.figure,
4375 hist.axes_index,
4376 hist.plot_index,
4377 display_name,
4378 )
4379 .map_err(|err| map_figure_error(builtin, err))?;
4380 Ok(())
4381 }
4382 "displaystyle" => {
4383 let display_style = value_as_string(value)
4384 .ok_or_else(|| {
4385 plotting_error(builtin, format!("{builtin}: DisplayStyle must be a string"))
4386 })?
4387 .trim()
4388 .to_ascii_lowercase();
4389 let style = match display_style.as_str() {
4390 "bar" => PolarHistogramDisplayStyle::Bar,
4391 "stairs" => PolarHistogramDisplayStyle::Stairs,
4392 other => {
4393 return Err(plotting_error(
4394 builtin,
4395 format!("{builtin}: unsupported histogram DisplayStyle `{other}`"),
4396 ));
4397 }
4398 };
4399 if !hist.metadata.is_polar && display_style == "stairs" {
4400 return Err(plotting_error(
4401 builtin,
4402 format!(
4403 "{builtin}: DisplayStyle 'stairs' is only supported for polar histograms"
4404 ),
4405 ));
4406 }
4407 super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4408 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4409 if bar.is_polar_histogram() {
4410 bar.set_polar_histogram_display_style(style);
4411 }
4412 }
4413 })
4414 .map_err(|err| map_figure_error(builtin, err))?;
4415 super::state::update_histogram_handle_metadata_for_plot(
4416 hist.figure,
4417 hist.axes_index,
4418 hist.plot_index,
4419 |metadata| metadata.display_style = display_style.clone(),
4420 )
4421 .map_err(|err| map_figure_error(builtin, err))?;
4422 Ok(())
4423 }
4424 "facealpha" => {
4425 let alpha = value_as_f64(value).ok_or_else(|| {
4426 plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
4427 })?;
4428 let alpha = alpha.clamp(0.0, 1.0);
4429 super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4430 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4431 let mut color = bar.color;
4432 color.w = alpha as f32;
4433 bar.apply_face_style(color, bar.bar_width);
4434 }
4435 })
4436 .map_err(|err| map_figure_error(builtin, err))?;
4437 super::state::update_histogram_handle_metadata_for_plot(
4438 hist.figure,
4439 hist.axes_index,
4440 hist.plot_index,
4441 |metadata| metadata.face_alpha = alpha,
4442 )
4443 .map_err(|err| map_figure_error(builtin, err))?;
4444 Ok(())
4445 }
4446 "facecolor" | "edgecolor" => {
4447 let Some(color_text) = value_as_string(value) else {
4448 return Err(plotting_error(
4449 builtin,
4450 format!("{builtin}: color property must be a string"),
4451 ));
4452 };
4453 let lower = color_text.trim().to_ascii_lowercase();
4454 let color = match lower.as_str() {
4455 "auto" => None,
4456 "none" if key == "edgecolor" => None,
4457 _ => Some(color_from_name_or_token(&lower).ok_or_else(|| {
4458 plotting_error(builtin, format!("{builtin}: unsupported color `{lower}`"))
4459 })?),
4460 };
4461 super::state::update_plot_element(hist.figure, hist.plot_index, |plot| {
4462 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
4463 if key == "facecolor" {
4464 if let Some(mut color) = color {
4465 color.w = hist.metadata.face_alpha as f32;
4466 bar.apply_face_style(color, bar.bar_width);
4467 }
4468 } else {
4469 bar.apply_outline_style(color, bar.outline_width);
4470 }
4471 }
4472 })
4473 .map_err(|err| map_figure_error(builtin, err))?;
4474 super::state::update_histogram_handle_metadata_for_plot(
4475 hist.figure,
4476 hist.axes_index,
4477 hist.plot_index,
4478 |metadata| {
4479 if key == "facecolor" {
4480 metadata.face_color = lower.clone();
4481 } else {
4482 metadata.edge_color = lower.clone();
4483 }
4484 },
4485 )
4486 .map_err(|err| map_figure_error(builtin, err))?;
4487 Ok(())
4488 }
4489 other => Err(plotting_error(
4490 builtin,
4491 format!("{builtin}: unsupported histogram property `{other}`"),
4492 )),
4493 }
4494}
4495
4496fn apply_histogram2_property(
4497 hist: &super::state::Histogram2HandleState,
4498 key: &str,
4499 value: &Value,
4500 builtin: &'static str,
4501) -> BuiltinResult<()> {
4502 let mut next = hist.clone();
4503 apply_histogram2_property_to_state(&mut next, key, value, builtin)?;
4504 refresh_histogram2_chart(next, builtin)
4505}
4506
4507fn apply_histogram2_properties(
4508 hist: &super::state::Histogram2HandleState,
4509 pairs: &[(String, &Value)],
4510 builtin: &'static str,
4511) -> BuiltinResult<()> {
4512 let mut next = hist.clone();
4513 for (key, value) in pairs {
4514 apply_histogram2_property_to_state(&mut next, key, value, builtin)?;
4515 }
4516 refresh_histogram2_chart(next, builtin)
4517}
4518
4519fn apply_histogram2_property_to_state(
4520 next: &mut super::state::Histogram2HandleState,
4521 key: &str,
4522 value: &Value,
4523 builtin: &'static str,
4524) -> BuiltinResult<()> {
4525 match key {
4526 "normalization" => {
4527 next.normalization = value_as_string(value)
4528 .ok_or_else(|| {
4529 plotting_error(
4530 builtin,
4531 format!("{builtin}: Normalization must be a string"),
4532 )
4533 })?
4534 .trim()
4535 .to_ascii_lowercase();
4536 next.values = histogram2::apply_normalization_2d(
4537 &next.raw_counts,
4538 &next.x_bin_edges,
4539 &next.y_bin_edges,
4540 &next.normalization,
4541 )?;
4542 }
4543 "displaystyle" => {
4544 next.display_style = histogram2::parse_display_style(value)?;
4545 }
4546 "showemptybins" => {
4547 next.show_empty_bins = histogram2::option_bool(value, "ShowEmptyBins")?;
4548 }
4549 "facealpha" => {
4550 let alpha = histogram2::option_scalar(value, "FaceAlpha")?;
4551 histogram2::validate_face_alpha(alpha)?;
4552 next.face_alpha = alpha;
4553 }
4554 "displayname" => {
4555 next.display_name = value_as_string(value).map(|s| s.to_string());
4556 }
4557 other => Err(plotting_error(
4558 builtin,
4559 format!("{builtin}: unsupported histogram2 property `{other}`"),
4560 ))?,
4561 }
4562 Ok(())
4563}
4564
4565fn refresh_histogram2_chart(
4566 next: super::state::Histogram2HandleState,
4567 builtin: &'static str,
4568) -> BuiltinResult<()> {
4569 let color_limits = current_histogram2_color_limits(&next, builtin)?;
4570 let surface = histogram2::build_surface_from_values(
4571 &next.values,
4572 &next.x_bin_edges,
4573 &next.y_bin_edges,
4574 next.display_style,
4575 next.show_empty_bins,
4576 next.face_alpha,
4577 next.display_name.as_deref(),
4578 color_limits,
4579 )?;
4580 super::state::update_plot_element(next.figure, next.plot_index, |plot| {
4581 if let runmat_plot::plots::figure::PlotElement::Surface(existing) = plot {
4582 *existing = surface;
4583 }
4584 })
4585 .map_err(|err| map_figure_error(builtin, err))?;
4586 super::state::update_histogram2_handle_for_plot(
4587 next.figure,
4588 next.axes_index,
4589 next.plot_index,
4590 |state| {
4591 state.values = next.values;
4592 state.raw_counts = next.raw_counts;
4593 state.x_bin_edges = next.x_bin_edges;
4594 state.y_bin_edges = next.y_bin_edges;
4595 state.normalization = next.normalization;
4596 state.display_style = next.display_style;
4597 state.show_empty_bins = next.show_empty_bins;
4598 state.face_alpha = next.face_alpha;
4599 state.display_name = next.display_name;
4600 },
4601 )
4602 .map_err(|err| map_figure_error(builtin, err))?;
4603 Ok(())
4604}
4605
4606fn current_histogram2_color_limits(
4607 hist: &super::state::Histogram2HandleState,
4608 builtin: &'static str,
4609) -> BuiltinResult<Option<(f64, f64)>> {
4610 let figure = super::state::clone_figure(hist.figure)
4611 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid histogram2 figure")))?;
4612 let plot = figure
4613 .plots()
4614 .nth(hist.plot_index)
4615 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid histogram2 handle")))?;
4616 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
4617 return Err(plotting_error(
4618 builtin,
4619 format!("{builtin}: invalid histogram2 handle"),
4620 ));
4621 };
4622 Ok(surface.color_limits)
4623}
4624
4625fn apply_stem_property(
4626 stem_handle: &super::state::StemHandleState,
4627 key: &str,
4628 value: &Value,
4629 builtin: &'static str,
4630) -> BuiltinResult<()> {
4631 super::state::update_stem_plot(
4632 stem_handle.figure,
4633 stem_handle.plot_index,
4634 |stem| match key {
4635 "basevalue" => {
4636 if let Some(v) = value_as_f64(value) {
4637 stem.baseline = v;
4638 }
4639 }
4640 "baseline" => {
4641 if let Some(v) = value_as_bool(value) {
4642 stem.baseline_visible = v;
4643 }
4644 }
4645 "linewidth" => {
4646 if let Some(v) = value_as_f64(value) {
4647 stem.line_width = v as f32;
4648 }
4649 }
4650 "linestyle" => {
4651 if let Some(s) = value_as_string(value) {
4652 stem.line_style = parse_line_style_name_for_props(&s);
4653 }
4654 }
4655 "color" => {
4656 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4657 stem.color = c;
4658 }
4659 }
4660 "marker" => {
4661 if let Some(s) = value_as_string(value) {
4662 stem.marker = marker_from_name(&s, stem.marker.clone());
4663 }
4664 }
4665 "markersize" => {
4666 if let Some(v) = value_as_f64(value) {
4667 if let Some(marker) = &mut stem.marker {
4668 marker.size = v as f32;
4669 }
4670 }
4671 }
4672 "markerfacecolor" => {
4673 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4674 if let Some(marker) = &mut stem.marker {
4675 marker.face_color = c;
4676 }
4677 }
4678 }
4679 "markeredgecolor" => {
4680 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4681 if let Some(marker) = &mut stem.marker {
4682 marker.edge_color = c;
4683 }
4684 }
4685 }
4686 "filled" => {
4687 if let Some(v) = value_as_bool(value) {
4688 if let Some(marker) = &mut stem.marker {
4689 marker.filled = v;
4690 }
4691 }
4692 }
4693 _ => {}
4694 },
4695 )
4696 .map_err(|err| map_figure_error(builtin, err))?;
4697 Ok(())
4698}
4699
4700fn apply_errorbar_property(
4701 error_handle: &super::state::ErrorBarHandleState,
4702 key: &str,
4703 value: &Value,
4704 builtin: &'static str,
4705) -> BuiltinResult<()> {
4706 super::state::update_errorbar_plot(error_handle.figure, error_handle.plot_index, |errorbar| {
4707 match key {
4708 "linewidth" => {
4709 if let Some(v) = value_as_f64(value) {
4710 errorbar.line_width = v as f32;
4711 }
4712 }
4713 "linestyle" => {
4714 if let Some(s) = value_as_string(value) {
4715 errorbar.line_style = parse_line_style_name_for_props(&s);
4716 }
4717 }
4718 "color" => {
4719 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4720 errorbar.color = c;
4721 }
4722 }
4723 "capsize" => {
4724 if let Some(v) = value_as_f64(value) {
4725 errorbar.cap_size = v as f32;
4726 }
4727 }
4728 "marker" => {
4729 if let Some(s) = value_as_string(value) {
4730 errorbar.marker = marker_from_name(&s, errorbar.marker.clone());
4731 }
4732 }
4733 "markersize" => {
4734 if let Some(v) = value_as_f64(value) {
4735 if let Some(marker) = &mut errorbar.marker {
4736 marker.size = v as f32;
4737 }
4738 }
4739 }
4740 _ => {}
4741 }
4742 })
4743 .map_err(|err| map_figure_error(builtin, err))?;
4744 Ok(())
4745}
4746
4747fn apply_quiver_property(
4748 quiver_handle: &super::state::QuiverHandleState,
4749 key: &str,
4750 value: &Value,
4751 builtin: &'static str,
4752) -> BuiltinResult<()> {
4753 if matches!(
4754 key,
4755 "xdata" | "ydata" | "zdata" | "udata" | "vdata" | "wdata"
4756 ) {
4757 return apply_quiver_data_property(quiver_handle, key, value, builtin);
4758 }
4759 super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
4760 match key {
4761 "color" => {
4762 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4763 quiver.color = c;
4764 quiver.mark_dirty();
4765 }
4766 }
4767 "linewidth" => {
4768 if let Some(v) = value_as_f64(value) {
4769 quiver.line_width = v as f32;
4770 }
4771 }
4772 "autoscalefactor" => {
4773 if let Some(v) = value_as_f64(value) {
4774 quiver.scale = v as f32;
4775 quiver.mark_dirty();
4776 }
4777 }
4778 "maxheadsize" => {
4779 if let Some(v) = value_as_f64(value) {
4780 quiver.head_size = v as f32;
4781 quiver.mark_dirty();
4782 }
4783 }
4784 _ => {}
4785 }
4786 })
4787 .map_err(|err| map_figure_error(builtin, err))?;
4788 Ok(())
4789}
4790
4791fn apply_quiver_data_property(
4792 quiver_handle: &super::state::QuiverHandleState,
4793 key: &str,
4794 value: &Value,
4795 builtin: &'static str,
4796) -> BuiltinResult<()> {
4797 if matches!(key, "zdata" | "wdata") && !quiver_handle.is_3d {
4798 return Err(plotting_error(
4799 builtin,
4800 format!("{builtin}: unsupported quiver property `{key}`"),
4801 ));
4802 }
4803 let figure = super::state::clone_figure(quiver_handle.figure)
4804 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
4805 let plot = figure
4806 .plots()
4807 .nth(quiver_handle.plot_index)
4808 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
4809 let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
4810 return Err(plotting_error(
4811 builtin,
4812 format!("{builtin}: invalid quiver handle"),
4813 ));
4814 };
4815 let expected_len = quiver
4816 .cpu_vector_data_len()
4817 .ok_or_else(|| quiver_data_unavailable_error(builtin))?;
4818 let tensor = Tensor::try_from(value).map_err(|err| {
4819 plotting_error(builtin, format!("{builtin}: {key} must be numeric: {err}"))
4820 })?;
4821 if tensor.data.len() != expected_len {
4822 return Err(plotting_error(
4823 builtin,
4824 format!("{builtin}: {key} length must match existing quiver data length"),
4825 ));
4826 }
4827 let data = tensor.data;
4828 super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
4829 match key {
4830 "xdata" => quiver.x = data,
4831 "ydata" => quiver.y = data,
4832 "zdata" => quiver.z = Some(data),
4833 "udata" => quiver.u = data,
4834 "vdata" => quiver.v = data,
4835 "wdata" => quiver.w = Some(data),
4836 _ => {}
4837 }
4838 quiver.mark_dirty();
4839 })
4840 .map_err(|err| map_figure_error(builtin, err))?;
4841 Ok(())
4842}
4843
4844fn apply_image_property(
4845 image_handle: &super::state::ImageHandleState,
4846 key: &str,
4847 value: &Value,
4848 builtin: &'static str,
4849) -> BuiltinResult<()> {
4850 super::state::update_image_plot(image_handle.figure, image_handle.plot_index, |surface| {
4851 match key {
4852 "xdata" => {
4853 if let Ok(tensor) = Tensor::try_from(value) {
4854 surface.x_data = tensor.data;
4855 }
4856 }
4857 "ydata" => {
4858 if let Ok(tensor) = Tensor::try_from(value) {
4859 surface.y_data = tensor.data;
4860 }
4861 }
4862 "cdatamapping" => {
4863 if let Some(text) = value_as_string(value) {
4864 if text.trim().eq_ignore_ascii_case("direct") {
4865 surface.image_mode = true;
4866 }
4867 }
4868 }
4869 _ => {}
4870 }
4871 })
4872 .map_err(|err| map_figure_error(builtin, err))?;
4873 Ok(())
4874}
4875
4876fn apply_heatmap_property(
4877 heatmap_handle: &super::state::HeatmapHandleState,
4878 key: &str,
4879 value: &Value,
4880 builtin: &'static str,
4881) -> BuiltinResult<()> {
4882 match key {
4883 "title" => apply_axes_property(
4884 heatmap_handle.figure,
4885 heatmap_handle.axes_index,
4886 "title",
4887 value,
4888 builtin,
4889 ),
4890 "xlabel" => apply_axes_property(
4891 heatmap_handle.figure,
4892 heatmap_handle.axes_index,
4893 "xlabel",
4894 value,
4895 builtin,
4896 ),
4897 "ylabel" => apply_axes_property(
4898 heatmap_handle.figure,
4899 heatmap_handle.axes_index,
4900 "ylabel",
4901 value,
4902 builtin,
4903 ),
4904 "colorbar" | "colorbarvisible" => apply_axes_property(
4905 heatmap_handle.figure,
4906 heatmap_handle.axes_index,
4907 "colorbar",
4908 value,
4909 builtin,
4910 ),
4911 "colormap" => apply_axes_property(
4912 heatmap_handle.figure,
4913 heatmap_handle.axes_index,
4914 "colormap",
4915 value,
4916 builtin,
4917 ),
4918 "xdisplaylabels" => {
4919 let labels = label_strings_from_value(value, builtin, "labels")?;
4920 if labels.len() != heatmap_handle.x_labels.len() {
4921 return Err(plotting_error(
4922 builtin,
4923 format!("{builtin}: XDisplayLabels length must match heatmap columns"),
4924 ));
4925 }
4926 super::state::set_heatmap_display_labels(
4927 heatmap_handle.figure,
4928 heatmap_handle.axes_index,
4929 heatmap_handle.plot_index,
4930 Some(labels),
4931 None,
4932 )
4933 .map_err(|err| map_figure_error(builtin, err))
4934 }
4935 "ydisplaylabels" => {
4936 let labels = label_strings_from_value(value, builtin, "labels")?;
4937 if labels.len() != heatmap_handle.y_labels.len() {
4938 return Err(plotting_error(
4939 builtin,
4940 format!("{builtin}: YDisplayLabels length must match heatmap rows"),
4941 ));
4942 }
4943 super::state::set_heatmap_display_labels(
4944 heatmap_handle.figure,
4945 heatmap_handle.axes_index,
4946 heatmap_handle.plot_index,
4947 None,
4948 Some(labels),
4949 )
4950 .map_err(|err| map_figure_error(builtin, err))
4951 }
4952 other => Err(plotting_error(
4953 builtin,
4954 format!("{builtin}: unsupported heatmap property `{other}`"),
4955 )),
4956 }
4957}
4958
4959fn apply_area_property(
4960 area_handle: &super::state::AreaHandleState,
4961 key: &str,
4962 value: &Value,
4963 builtin: &'static str,
4964) -> BuiltinResult<()> {
4965 super::state::update_area_plot(
4966 area_handle.figure,
4967 area_handle.plot_index,
4968 |area| match key {
4969 "color" => {
4970 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
4971 area.color = c;
4972 }
4973 }
4974 "basevalue" => {
4975 if let Some(v) = value_as_f64(value) {
4976 area.baseline = v;
4977 area.lower_y = None;
4978 }
4979 }
4980 _ => {}
4981 },
4982 )
4983 .map_err(|err| map_figure_error(builtin, err))?;
4984 Ok(())
4985}
4986
4987fn apply_line_property(
4988 line_handle: &super::state::SimplePlotHandleState,
4989 key: &str,
4990 value: &Value,
4991 builtin: &'static str,
4992) -> BuiltinResult<()> {
4993 apply_line_properties(line_handle, &[(key.to_string(), value)], builtin)
4994}
4995
4996fn apply_line_properties(
4997 line_handle: &super::state::SimplePlotHandleState,
4998 pairs: &[(String, &Value)],
4999 builtin: &'static str,
5000) -> BuiltinResult<()> {
5001 let current = get_simple_plot(line_handle, builtin)?;
5002 let runmat_plot::plots::figure::PlotElement::Line(current_line) = current else {
5003 return Err(plotting_error(
5004 builtin,
5005 format!("{builtin}: invalid line handle"),
5006 ));
5007 };
5008
5009 let (current_x, current_y) = line_xy_data_for_properties(¤t_line, builtin)?;
5010 let mut x_update = None;
5011 let mut y_update = None;
5012 let mut style_pairs = Vec::new();
5013 for (key, value) in pairs {
5014 match key.as_str() {
5015 "xdata" => x_update = Some(line_numeric_data(value, "XData", builtin)?),
5016 "ydata" => y_update = Some(line_numeric_data(value, "YData", builtin)?),
5017 _ => style_pairs.push((key.as_str(), *value)),
5018 }
5019 }
5020
5021 let next_x = x_update.unwrap_or_else(|| current_x.clone());
5022 let next_y = y_update.unwrap_or_else(|| current_y.clone());
5023 let update_data = next_x != current_x || next_y != current_y;
5024 if update_data && next_x.len() != next_y.len() {
5025 return Err(plotting_error(
5026 builtin,
5027 format!("{builtin}: XData and YData must contain the same number of elements"),
5028 ));
5029 }
5030 for (key, value) in &style_pairs {
5031 if *key == "handlevisibility" {
5032 parse_handle_visibility(&LineStyleParseOptions::generic(builtin), value)?;
5033 }
5034 }
5035
5036 super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
5037 if let runmat_plot::plots::figure::PlotElement::Line(line) = plot {
5038 if update_data {
5039 let _ = line.update_data(next_x.clone(), next_y.clone());
5040 }
5041 for (key, value) in &style_pairs {
5042 apply_line_plot_properties(line, key, value, builtin);
5043 }
5044 }
5045 })
5046 .map_err(|err| map_figure_error(builtin, err))?;
5047 Ok(())
5048}
5049
5050fn apply_animated_line_property(
5051 line_handle: &super::state::AnimatedLineHandleState,
5052 key: &str,
5053 value: &Value,
5054 builtin: &'static str,
5055) -> BuiltinResult<()> {
5056 apply_animated_line_properties(line_handle, &[(key.to_string(), value)], builtin)
5057}
5058
5059fn apply_animated_line_properties(
5060 line_handle: &super::state::AnimatedLineHandleState,
5061 pairs: &[(String, &Value)],
5062 builtin: &'static str,
5063) -> BuiltinResult<()> {
5064 let mut maximum = None;
5065 let mut plot_pairs = Vec::new();
5066 for (key, value) in pairs {
5067 match key.as_str() {
5068 "maximumnumpoints" => {
5069 maximum = Some(animated_line_maximum_from_value(value, builtin)?);
5070 }
5071 _ => plot_pairs.push((key.clone(), *value)),
5072 }
5073 }
5074
5075 if !plot_pairs.is_empty() {
5076 let simple = animated_line_simple_state(line_handle);
5077 if line_handle.is_3d {
5078 apply_line3_properties(&simple, &plot_pairs, builtin)?;
5079 } else {
5080 if plot_pairs.iter().any(|(key, _)| key == "zdata") {
5081 return Err(plotting_error(
5082 builtin,
5083 format!("{builtin}: ZData requires a 3-D animated line"),
5084 ));
5085 }
5086 apply_line_properties(&simple, &plot_pairs, builtin)?;
5087 }
5088 }
5089
5090 if let Some(maximum) = maximum {
5091 super::state::set_animated_line_maximum_num_points(line_handle, maximum)
5092 .map_err(|err| plotting_error(builtin, format!("{builtin}: {err}")))?;
5093 }
5094 Ok(())
5095}
5096
5097fn animated_line_maximum_from_value(
5098 value: &Value,
5099 builtin: &'static str,
5100) -> BuiltinResult<Option<usize>> {
5101 let Some(maximum) = value_as_f64(value) else {
5102 return Err(plotting_error(
5103 builtin,
5104 format!("{builtin}: MaximumNumPoints must be numeric"),
5105 ));
5106 };
5107 if maximum.is_infinite() && maximum.is_sign_positive() {
5108 return Ok(None);
5109 }
5110 if !maximum.is_finite() || maximum <= 0.0 {
5111 return Err(plotting_error(
5112 builtin,
5113 format!("{builtin}: MaximumNumPoints must be positive or Inf"),
5114 ));
5115 }
5116 if maximum > usize::MAX as f64 {
5117 return Err(plotting_error(
5118 builtin,
5119 format!("{builtin}: MaximumNumPoints is too large"),
5120 ));
5121 }
5122 Ok(Some(maximum.floor() as usize))
5123}
5124
5125fn apply_reference_line_property(
5126 line_handle: &super::state::SimplePlotHandleState,
5127 key: &str,
5128 value: &Value,
5129 builtin: &'static str,
5130) -> BuiltinResult<()> {
5131 super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
5132 if let runmat_plot::plots::figure::PlotElement::ReferenceLine(line) = plot {
5133 match key {
5134 "value" => {
5135 if let Some(v) = value_as_f64(value) {
5136 if v.is_finite() {
5137 line.value = v;
5138 }
5139 }
5140 }
5141 "color" => {
5142 if let Ok(c) =
5143 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5144 {
5145 line.color = c;
5146 }
5147 }
5148 "linewidth" => {
5149 if let Some(v) = value_as_f64(value) {
5150 if v > 0.0 {
5151 line.line_width = v as f32;
5152 }
5153 }
5154 }
5155 "linestyle" => {
5156 if let Some(s) = value_as_string(value) {
5157 line.line_style = parse_line_style_name_for_props(&s);
5158 }
5159 }
5160 "label" => {
5161 line.label = value_as_string(value).map(|s| s.to_string());
5162 }
5163 "labelorientation" => {
5164 if let Some(s) = value_as_string(value) {
5165 line.label_orientation = s.to_ascii_lowercase();
5166 }
5167 }
5168 "displayname" => {
5169 line.display_name = value_as_string(value).map(|s| s.to_string());
5170 }
5171 "visible" => {
5172 if let Some(v) = value_as_bool(value) {
5173 line.visible = v;
5174 } else if let Some(s) = value_as_string(value) {
5175 line.visible =
5176 !matches!(s.trim().to_ascii_lowercase().as_str(), "off" | "false");
5177 }
5178 }
5179 _ => {}
5180 }
5181 }
5182 })
5183 .map_err(|err| map_figure_error(builtin, err))?;
5184 Ok(())
5185}
5186
5187fn apply_stairs_property(
5188 stairs_handle: &super::state::SimplePlotHandleState,
5189 key: &str,
5190 value: &Value,
5191 builtin: &'static str,
5192) -> BuiltinResult<()> {
5193 super::state::update_plot_element(stairs_handle.figure, stairs_handle.plot_index, |plot| {
5194 if let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot {
5195 match key {
5196 "color" => {
5197 if let Ok(c) =
5198 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5199 {
5200 stairs.color = c;
5201 }
5202 }
5203 "linewidth" => {
5204 if let Some(v) = value_as_f64(value) {
5205 stairs.line_width = v as f32;
5206 }
5207 }
5208 "displayname" => {
5209 stairs.label = value_as_string(value).map(|s| s.to_string());
5210 }
5211 _ => {}
5212 }
5213 }
5214 })
5215 .map_err(|err| map_figure_error(builtin, err))?;
5216 Ok(())
5217}
5218
5219fn apply_scatter_property(
5220 scatter_handle: &super::state::SimplePlotHandleState,
5221 key: &str,
5222 value: &Value,
5223 builtin: &'static str,
5224) -> BuiltinResult<()> {
5225 apply_scatter_properties(scatter_handle, &[(key.to_string(), value)], builtin)
5226}
5227
5228fn apply_scatter_properties(
5229 scatter_handle: &super::state::SimplePlotHandleState,
5230 pairs: &[(String, &Value)],
5231 builtin: &'static str,
5232) -> BuiltinResult<()> {
5233 let current = get_simple_plot(scatter_handle, builtin)?;
5234 let runmat_plot::plots::figure::PlotElement::Scatter(current_scatter) = current else {
5235 return Err(plotting_error(
5236 builtin,
5237 format!("{builtin}: invalid scatter handle"),
5238 ));
5239 };
5240
5241 let current_theta = current_scatter
5242 .theta_data
5243 .clone()
5244 .unwrap_or_else(|| cartesian_theta(¤t_scatter.x_data, ¤t_scatter.y_data));
5245 let current_r = current_scatter
5246 .r_data
5247 .clone()
5248 .unwrap_or_else(|| cartesian_radius(¤t_scatter.x_data, ¤t_scatter.y_data));
5249 let mut theta_update = None;
5250 let mut r_update = None;
5251 let mut style_pairs = Vec::new();
5252 for (key, value) in pairs {
5253 match key.as_str() {
5254 "thetadata" => theta_update = Some(line_numeric_data(value, "ThetaData", builtin)?),
5255 "rdata" => r_update = Some(line_numeric_data(value, "RData", builtin)?),
5256 _ => style_pairs.push((key.as_str(), *value)),
5257 }
5258 }
5259
5260 let next_theta = theta_update.unwrap_or_else(|| current_theta.clone());
5261 let next_r = r_update.unwrap_or_else(|| current_r.clone());
5262 let polar_update =
5263 (next_theta != current_theta || next_r != current_r).then_some((next_theta, next_r));
5264 if let Some((theta, r)) = polar_update.as_ref() {
5265 validate_polar_scatter_data(theta, r, builtin)?;
5266 }
5267
5268 super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
5269 if let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot {
5270 if let Some((theta, r)) = polar_update.as_ref() {
5271 replace_polar_scatter_data_unchecked(scatter, theta.clone(), r.clone());
5272 }
5273 for (key, value) in &style_pairs {
5274 match *key {
5275 "marker" => {
5276 if let Some(s) = value_as_string(value) {
5277 scatter.marker_style =
5278 scatter_marker_from_name(&s, scatter.marker_style);
5279 }
5280 }
5281 "sizedata" => {
5282 if let Some(v) = value_as_f64(value) {
5283 scatter.marker_size = marker_area_points2_to_diameter_px(v);
5284 }
5285 }
5286 "markerfacecolor" => {
5287 if let Ok(c) =
5288 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5289 {
5290 scatter.set_face_color(c);
5291 }
5292 }
5293 "markeredgecolor" => {
5294 if let Ok(c) =
5295 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5296 {
5297 scatter.set_edge_color(c);
5298 }
5299 }
5300 "linewidth" => {
5301 if let Some(v) = value_as_f64(value) {
5302 scatter.set_edge_thickness(v as f32);
5303 }
5304 }
5305 "displayname" => {
5306 scatter.label = value_as_string(value).map(|s| s.to_string());
5307 }
5308 _ => {}
5309 }
5310 }
5311 }
5312 })
5313 .map_err(|err| map_figure_error(builtin, err))?;
5314 Ok(())
5315}
5316
5317fn validate_polar_scatter_data(
5318 theta: &[f64],
5319 r: &[f64],
5320 builtin: &'static str,
5321) -> BuiltinResult<()> {
5322 if theta.len() != r.len() {
5323 return Err(plotting_error(
5324 builtin,
5325 format!("{builtin}: ThetaData and RData must contain the same number of elements"),
5326 ));
5327 }
5328 if theta.is_empty() {
5329 return Err(plotting_error(
5330 builtin,
5331 format!("{builtin}: ThetaData and RData cannot be empty"),
5332 ));
5333 }
5334 Ok(())
5335}
5336
5337fn replace_polar_scatter_data_unchecked(
5338 scatter: &mut runmat_plot::plots::ScatterPlot,
5339 theta: Vec<f64>,
5340 r: Vec<f64>,
5341) {
5342 let x_data = theta
5343 .iter()
5344 .zip(r.iter())
5345 .map(|(theta, r)| r * theta.cos())
5346 .collect();
5347 let y_data = theta
5348 .iter()
5349 .zip(r.iter())
5350 .map(|(theta, r)| r * theta.sin())
5351 .collect();
5352 let point_count = theta.len();
5353 scatter
5354 .update_data(x_data, y_data)
5355 .expect("validated polar scatter data can update cartesian scatter data");
5356 if scatter
5357 .per_point_sizes
5358 .as_ref()
5359 .is_some_and(|sizes| sizes.len() != point_count)
5360 {
5361 scatter.per_point_sizes = None;
5362 }
5363 if scatter
5364 .per_point_colors
5365 .as_ref()
5366 .is_some_and(|colors| colors.len() != point_count)
5367 {
5368 scatter.per_point_colors = None;
5369 }
5370 if scatter
5371 .color_values
5372 .as_ref()
5373 .is_some_and(|values| values.len() != point_count)
5374 {
5375 scatter.color_values = None;
5376 scatter.color_limits = None;
5377 }
5378 scatter.theta_data = Some(theta);
5379 scatter.r_data = Some(r);
5380}
5381
5382fn scatter_size_data_value(scatter: &runmat_plot::plots::ScatterPlot) -> Value {
5383 match scatter.per_point_sizes.as_ref() {
5384 Some(sizes) => tensor_from_vec(
5385 sizes
5386 .iter()
5387 .map(|size| marker_diameter_px_to_area_points2(*size))
5388 .collect(),
5389 ),
5390 None => Value::Num(marker_diameter_px_to_area_points2(scatter.marker_size)),
5391 }
5392}
5393
5394fn cartesian_theta(x: &[f64], y: &[f64]) -> Vec<f64> {
5395 x.iter().zip(y.iter()).map(|(x, y)| y.atan2(*x)).collect()
5396}
5397
5398fn cartesian_radius(x: &[f64], y: &[f64]) -> Vec<f64> {
5399 x.iter().zip(y.iter()).map(|(x, y)| x.hypot(*y)).collect()
5400}
5401
5402fn apply_bar_property(
5403 bar_handle: &super::state::SimplePlotHandleState,
5404 key: &str,
5405 value: &Value,
5406 builtin: &'static str,
5407) -> BuiltinResult<()> {
5408 super::state::update_plot_element(bar_handle.figure, bar_handle.plot_index, |plot| {
5409 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
5410 match key {
5411 "facecolor" | "color" => {
5412 if let Ok(c) =
5413 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5414 {
5415 bar.color = c;
5416 }
5417 }
5418 "barwidth" => {
5419 if let Some(v) = value_as_f64(value) {
5420 bar.bar_width = v as f32;
5421 }
5422 }
5423 "displayname" => {
5424 bar.label = value_as_string(value).map(|s| s.to_string());
5425 }
5426 _ => {}
5427 }
5428 }
5429 })
5430 .map_err(|err| map_figure_error(builtin, err))?;
5431 Ok(())
5432}
5433
5434#[derive(Clone, Debug)]
5435enum SurfacePropertyUpdate {
5436 FaceAlpha(f32),
5437 DisplayName(Option<String>),
5438 Visible(bool),
5439 Colormap(ColorMap),
5440 Shading(ShadingMode),
5441 Wireframe(bool),
5442 FlattenZ(bool),
5443 Lighting(bool),
5444 FaceColor(glam::Vec4),
5445 FaceNone,
5446}
5447
5448fn apply_surface_property(
5449 surface_handle: &super::state::SimplePlotHandleState,
5450 key: &str,
5451 value: &Value,
5452 builtin: &'static str,
5453) -> BuiltinResult<()> {
5454 if matches!(key, "xdata" | "ydata" | "zdata") {
5455 return apply_surface_data_property(surface_handle, key, value, builtin);
5456 }
5457
5458 let update = surface_property_update(key, value, builtin)?;
5459 super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
5460 if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
5461 match &update {
5462 SurfacePropertyUpdate::FaceAlpha(alpha) => surface.alpha = *alpha,
5463 SurfacePropertyUpdate::DisplayName(label) => surface.label = label.clone(),
5464 SurfacePropertyUpdate::Visible(visible) => surface.visible = *visible,
5465 SurfacePropertyUpdate::Colormap(colormap) => surface.colormap = colormap.clone(),
5466 SurfacePropertyUpdate::Shading(shading) => surface.shading_mode = *shading,
5467 SurfacePropertyUpdate::Wireframe(wireframe) => surface.wireframe = *wireframe,
5468 SurfacePropertyUpdate::FlattenZ(flatten_z) => surface.flatten_z = *flatten_z,
5469 SurfacePropertyUpdate::Lighting(enabled) => surface.lighting_enabled = *enabled,
5470 SurfacePropertyUpdate::FaceColor(color) => {
5471 surface.colormap = ColorMap::Custom(*color, *color);
5472 surface.shading_mode = ShadingMode::None;
5473 surface.lighting_enabled = false;
5474 }
5475 SurfacePropertyUpdate::FaceNone => surface.alpha = 0.0,
5476 }
5477 }
5478 })
5479 .map_err(|err| map_figure_error(builtin, err))?;
5480 Ok(())
5481}
5482
5483fn apply_surface_data_property(
5484 surface_handle: &super::state::SimplePlotHandleState,
5485 key: &str,
5486 value: &Value,
5487 builtin: &'static str,
5488) -> BuiltinResult<()> {
5489 let current = get_simple_plot(surface_handle, builtin)?;
5490 let runmat_plot::plots::figure::PlotElement::Surface(current_surface) = current else {
5491 return Err(plotting_error(
5492 builtin,
5493 format!("{builtin}: invalid surface handle"),
5494 ));
5495 };
5496 let z_grid = current_surface.z_data.clone().ok_or_else(|| {
5497 plotting_error(
5498 builtin,
5499 format!("{builtin}: changing {key} for GPU-only surface data is not supported yet"),
5500 )
5501 })?;
5502 let (x_len, y_len) = surface_grid_size(&z_grid)?;
5503
5504 let next = match key {
5505 "xdata" => {
5506 let data = surface_coordinate_data_from_value(value, "XData", builtin)?;
5507 let (x_grid, y_grid, z_grid) = match data {
5508 SurfaceCoordinateData::Vector(axis) => {
5509 if axis.len() != x_len {
5510 return Err(plotting_error(
5511 builtin,
5512 format!("{builtin}: XData vector length must match surface column count {x_len}"),
5513 ));
5514 }
5515 let y_grid = current_surface
5516 .y_grid
5517 .clone()
5518 .unwrap_or_else(|| axis_to_y_grid(¤t_surface.y_data, x_len));
5519 (axis_to_x_grid(&axis, y_len), y_grid, z_grid)
5520 }
5521 SurfaceCoordinateData::Grid(grid) => {
5522 validate_surface_grid_shape(&grid, x_len, y_len, "XData", builtin)?;
5523 let y_grid = current_surface
5524 .y_grid
5525 .clone()
5526 .unwrap_or_else(|| axis_to_y_grid(¤t_surface.y_data, x_len));
5527 (grid, y_grid, z_grid)
5528 }
5529 };
5530 SurfaceDataUpdate::CoordinateGrids {
5531 x_grid,
5532 y_grid,
5533 z_grid,
5534 }
5535 }
5536 "ydata" => {
5537 let data = surface_coordinate_data_from_value(value, "YData", builtin)?;
5538 let (x_grid, y_grid, z_grid) = match data {
5539 SurfaceCoordinateData::Vector(axis) => {
5540 if axis.len() != y_len {
5541 return Err(plotting_error(
5542 builtin,
5543 format!("{builtin}: YData vector length must match surface row count {y_len}"),
5544 ));
5545 }
5546 let x_grid = current_surface
5547 .x_grid
5548 .clone()
5549 .unwrap_or_else(|| axis_to_x_grid(¤t_surface.x_data, y_len));
5550 (x_grid, axis_to_y_grid(&axis, x_len), z_grid)
5551 }
5552 SurfaceCoordinateData::Grid(grid) => {
5553 validate_surface_grid_shape(&grid, x_len, y_len, "YData", builtin)?;
5554 let x_grid = current_surface
5555 .x_grid
5556 .clone()
5557 .unwrap_or_else(|| axis_to_x_grid(¤t_surface.x_data, y_len));
5558 (x_grid, grid, z_grid)
5559 }
5560 };
5561 SurfaceDataUpdate::CoordinateGrids {
5562 x_grid,
5563 y_grid,
5564 z_grid,
5565 }
5566 }
5567 "zdata" => {
5568 let z_grid = surface_z_grid_from_value(value, y_len, x_len, builtin)?;
5569 if let (Some(x_grid), Some(y_grid)) = (
5570 current_surface.x_grid.clone(),
5571 current_surface.y_grid.clone(),
5572 ) {
5573 SurfaceDataUpdate::CoordinateGrids {
5574 x_grid,
5575 y_grid,
5576 z_grid,
5577 }
5578 } else {
5579 SurfaceDataUpdate::AxisData {
5580 x_data: current_surface.x_data.clone(),
5581 y_data: current_surface.y_data.clone(),
5582 z_grid,
5583 }
5584 }
5585 }
5586 other => {
5587 return Err(plotting_error(
5588 builtin,
5589 format!("{builtin}: unsupported surface property `{other}`"),
5590 ))
5591 }
5592 };
5593
5594 super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
5595 if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
5596 match &next {
5597 SurfaceDataUpdate::AxisData {
5598 x_data,
5599 y_data,
5600 z_grid,
5601 } => {
5602 let _ =
5603 surface.update_axis_data(x_data.clone(), y_data.clone(), z_grid.clone());
5604 }
5605 SurfaceDataUpdate::CoordinateGrids {
5606 x_grid,
5607 y_grid,
5608 z_grid,
5609 } => {
5610 let _ = surface.update_coordinate_grids(
5611 x_grid.clone(),
5612 y_grid.clone(),
5613 z_grid.clone(),
5614 );
5615 }
5616 }
5617 }
5618 })
5619 .map_err(|err| map_figure_error(builtin, err))?;
5620 Ok(())
5621}
5622
5623enum SurfaceDataUpdate {
5624 AxisData {
5625 x_data: Vec<f64>,
5626 y_data: Vec<f64>,
5627 z_grid: Vec<Vec<f64>>,
5628 },
5629 CoordinateGrids {
5630 x_grid: Vec<Vec<f64>>,
5631 y_grid: Vec<Vec<f64>>,
5632 z_grid: Vec<Vec<f64>>,
5633 },
5634}
5635
5636enum SurfaceCoordinateData {
5637 Vector(Vec<f64>),
5638 Grid(Vec<Vec<f64>>),
5639}
5640
5641fn surface_property_update(
5642 key: &str,
5643 value: &Value,
5644 builtin: &'static str,
5645) -> BuiltinResult<SurfacePropertyUpdate> {
5646 match key {
5647 "facealpha" | "alpha" => {
5648 let alpha = value_as_f64(value).ok_or_else(|| {
5649 plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
5650 })?;
5651 Ok(SurfacePropertyUpdate::FaceAlpha(
5652 alpha.clamp(0.0, 1.0) as f32
5653 ))
5654 }
5655 "displayname" | "label" => Ok(SurfacePropertyUpdate::DisplayName(
5656 value_as_string(value).map(|s| s.to_string()),
5657 )),
5658 "visible" => {
5659 let visible = value_as_bool(value).ok_or_else(|| {
5660 plotting_error(builtin, format!("{builtin}: Visible must be logical"))
5661 })?;
5662 Ok(SurfacePropertyUpdate::Visible(visible))
5663 }
5664 "colormap" => {
5665 if let Some(name) = value_as_string(value) {
5666 return Ok(SurfacePropertyUpdate::Colormap(parse_colormap_name(
5667 &name, builtin,
5668 )?));
5669 }
5670 let color = parse_color_value(&LineStyleParseOptions::generic(builtin), value)?;
5671 Ok(SurfacePropertyUpdate::Colormap(ColorMap::Custom(
5672 color, color,
5673 )))
5674 }
5675 "shading" => Ok(SurfacePropertyUpdate::Shading(surface_shading_from_value(
5676 value, builtin,
5677 )?)),
5678 "edgecolor" => {
5679 let text = value_as_string(value).ok_or_else(|| {
5680 plotting_error(
5681 builtin,
5682 format!("{builtin}: EdgeColor must be 'auto', 'flat', 'interp', or 'none'"),
5683 )
5684 })?;
5685 match text.trim().to_ascii_lowercase().as_str() {
5686 "none" => Ok(SurfacePropertyUpdate::Wireframe(false)),
5687 "auto" | "flat" | "interp" => Ok(SurfacePropertyUpdate::Wireframe(true)),
5688 other => Err(plotting_error(
5689 builtin,
5690 format!("{builtin}: unsupported EdgeColor `{other}`"),
5691 )),
5692 }
5693 }
5694 "facecolor" | "color" => surface_face_color_update(value, builtin),
5695 "flattenz" => {
5696 let flatten_z = value_as_bool(value).ok_or_else(|| {
5697 plotting_error(builtin, format!("{builtin}: FlattenZ must be logical"))
5698 })?;
5699 Ok(SurfacePropertyUpdate::FlattenZ(flatten_z))
5700 }
5701 "lighting" => Ok(SurfacePropertyUpdate::Lighting(
5702 surface_lighting_from_value(value, builtin)?,
5703 )),
5704 other => Err(plotting_error(
5705 builtin,
5706 format!("{builtin}: unsupported surface property `{other}`"),
5707 )),
5708 }
5709}
5710
5711fn apply_function_surface_property(
5712 function_surface: &super::state::FunctionSurfaceHandleState,
5713 key: &str,
5714 value: &Value,
5715 builtin: &'static str,
5716) -> BuiltinResult<()> {
5717 match key {
5718 "facealpha" | "displayname" => {
5719 let surface_handle = super::state::SimplePlotHandleState {
5720 figure: function_surface.figure,
5721 axes_index: function_surface.axes_index,
5722 plot_index: function_surface.plot_index,
5723 };
5724 apply_surface_property(&surface_handle, key, value, builtin)
5725 }
5726 "meshdensity" | "xrange" | "yrange" | "function" | "xfunction" | "yfunction"
5727 | "zfunction" => Err(plotting_error(
5728 builtin,
5729 format!("{builtin}: changing {key} after fsurf sampling is not supported yet"),
5730 )),
5731 _ => Ok(()),
5732 }
5733}
5734
5735fn apply_function_contour_property(
5736 function_contour: &super::state::FunctionContourHandleState,
5737 key: &str,
5738 value: &Value,
5739 builtin: &'static str,
5740) -> BuiltinResult<()> {
5741 match key {
5742 "linewidth" | "displayname" => {
5743 let contour_handle = super::state::SimplePlotHandleState {
5744 figure: function_contour.figure,
5745 axes_index: function_contour.axes_index,
5746 plot_index: function_contour.plot_index,
5747 };
5748 apply_contour_property(&contour_handle, key, value, builtin)
5749 }
5750 "meshdensity" | "xrange" | "yrange" | "function" => Err(plotting_error(
5751 builtin,
5752 format!("{builtin}: changing {key} after fcontour sampling is not supported yet"),
5753 )),
5754 _ => Ok(()),
5755 }
5756}
5757
5758fn apply_binscatter_property(
5759 binscatter: &super::state::BinscatterHandleState,
5760 key: &str,
5761 value: &Value,
5762 builtin: &'static str,
5763) -> BuiltinResult<()> {
5764 let mut next = binscatter.clone();
5765 match key {
5766 "numbins" => {
5767 next.num_bins = binscatter::parse_num_bins(value)?;
5768 next.auto_bins = false;
5769 }
5770 "showemptybins" => {
5771 next.show_empty_bins = binscatter::option_bool(value, "ShowEmptyBins")?;
5772 }
5773 "xlimits" => {
5774 next.x_limits_option = Some(binscatter::parse_limits(value, "XLimits")?);
5775 }
5776 "ylimits" => {
5777 next.y_limits_option = Some(binscatter::parse_limits(value, "YLimits")?);
5778 }
5779 "xdata" => {
5780 next.x_data = binscatter_numeric_data(value, "XData", builtin)?;
5781 }
5782 "ydata" => {
5783 next.y_data = binscatter_numeric_data(value, "YData", builtin)?;
5784 }
5785 "facealpha" => {
5786 let alpha = value_as_f64(value).ok_or_else(|| {
5787 plotting_error(builtin, format!("{builtin}: FaceAlpha must be numeric"))
5788 })?;
5789 binscatter::validate_face_alpha(alpha)?;
5790 next.face_alpha = alpha;
5791 }
5792 "displayname" => {
5793 next.display_name = value_as_string(value).map(|s| s.to_string());
5794 }
5795 other => Err(plotting_error(
5796 builtin,
5797 format!("{builtin}: unsupported binscatter property `{other}`"),
5798 ))?,
5799 }
5800 recompute_binscatter_chart(next, builtin)
5801}
5802
5803fn binscatter_numeric_data(
5804 value: &Value,
5805 name: &str,
5806 builtin: &'static str,
5807) -> BuiltinResult<Vec<f64>> {
5808 let tensor = tensor::value_to_tensor(value)
5809 .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
5810 Ok(tensor.data)
5811}
5812
5813fn recompute_binscatter_chart(
5814 mut next: super::state::BinscatterHandleState,
5815 builtin: &'static str,
5816) -> BuiltinResult<()> {
5817 if next.x_data.len() != next.y_data.len() {
5818 return Err(plotting_error(
5819 builtin,
5820 format!("{builtin}: XData and YData must contain the same number of elements"),
5821 ));
5822 }
5823 if next.auto_bins {
5824 next.num_bins = [
5825 binscatter::auto_bin_count(&next.x_data, next.x_limits_option, 100)?,
5826 binscatter::auto_bin_count(&next.y_data, next.y_limits_option, 100)?,
5827 ];
5828 }
5829 let color_limits = current_binscatter_color_limits(&next, builtin)?;
5830 let chart = binscatter::build_binscatter_chart(
5831 &next.x_data,
5832 &next.y_data,
5833 next.num_bins,
5834 next.x_limits_option,
5835 next.y_limits_option,
5836 next.show_empty_bins,
5837 next.face_alpha,
5838 next.display_name.as_deref(),
5839 color_limits,
5840 )?;
5841 let x_limits = (
5842 *chart.x_bin_edges.first().unwrap_or(&0.0),
5843 *chart.x_bin_edges.last().unwrap_or(&1.0),
5844 );
5845 let y_limits = (
5846 *chart.y_bin_edges.first().unwrap_or(&0.0),
5847 *chart.y_bin_edges.last().unwrap_or(&1.0),
5848 );
5849 let surface = chart.surface.clone();
5850 super::state::update_image_plot(next.figure, next.plot_index, |existing| {
5851 *existing = surface;
5852 })
5853 .map_err(|err| map_figure_error(builtin, err))?;
5854 super::state::update_binscatter_handle_for_plot(next.figure, next.plot_index, |state| {
5855 state.values = chart.values;
5856 state.x_bin_edges = chart.x_bin_edges;
5857 state.y_bin_edges = chart.y_bin_edges;
5858 state.x_data = next.x_data;
5859 state.y_data = next.y_data;
5860 state.num_bins = next.num_bins;
5861 state.auto_bins = next.auto_bins;
5862 state.x_limits_option = next.x_limits_option;
5863 state.y_limits_option = next.y_limits_option;
5864 state.x_limits = x_limits;
5865 state.y_limits = y_limits;
5866 state.show_empty_bins = next.show_empty_bins;
5867 state.face_alpha = next.face_alpha;
5868 state.display_name = next.display_name;
5869 })
5870 .map_err(|err| map_figure_error(builtin, err))?;
5871 Ok(())
5872}
5873
5874fn current_binscatter_color_limits(
5875 binscatter: &super::state::BinscatterHandleState,
5876 builtin: &'static str,
5877) -> BuiltinResult<Option<(f64, f64)>> {
5878 let figure = super::state::clone_figure(binscatter.figure)
5879 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid binscatter figure")))?;
5880 let plot = figure
5881 .plots()
5882 .nth(binscatter.plot_index)
5883 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid binscatter handle")))?;
5884 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
5885 return Err(plotting_error(
5886 builtin,
5887 format!("{builtin}: invalid binscatter handle"),
5888 ));
5889 };
5890 Ok(surface.color_limits)
5891}
5892
5893fn apply_patch_property(
5894 patch_handle: &super::state::SimplePlotHandleState,
5895 key: &str,
5896 value: &Value,
5897 builtin: &'static str,
5898) -> BuiltinResult<()> {
5899 super::state::update_plot_element(patch_handle.figure, patch_handle.plot_index, |plot| {
5900 if let runmat_plot::plots::figure::PlotElement::Patch(patch) = plot {
5901 match key {
5902 "facecolor" | "color" => {
5903 if let Some(text) = value_as_string(value) {
5904 match text.trim().to_ascii_lowercase().as_str() {
5905 "none" => patch
5906 .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::None),
5907 "flat" => patch
5908 .set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Flat),
5909 _ => {
5910 if let Ok(c) = parse_color_value(
5911 &LineStyleParseOptions::generic(builtin),
5912 value,
5913 ) {
5914 patch.set_face_color(c);
5915 patch.set_face_color_mode(
5916 runmat_plot::plots::PatchFaceColorMode::Color,
5917 );
5918 }
5919 }
5920 }
5921 } else if let Ok(c) =
5922 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5923 {
5924 patch.set_face_color(c);
5925 patch.set_face_color_mode(runmat_plot::plots::PatchFaceColorMode::Color);
5926 }
5927 }
5928 "edgecolor" => {
5929 if let Some(text) = value_as_string(value) {
5930 if text.trim().eq_ignore_ascii_case("none") {
5931 patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::None);
5932 } else if let Ok(c) =
5933 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5934 {
5935 patch.set_edge_color(c);
5936 patch
5937 .set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
5938 }
5939 } else if let Ok(c) =
5940 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
5941 {
5942 patch.set_edge_color(c);
5943 patch.set_edge_color_mode(runmat_plot::plots::PatchEdgeColorMode::Color);
5944 }
5945 }
5946 "facealpha" => {
5947 if let Some(v) = value_as_f64(value) {
5948 patch.set_face_alpha(v as f32);
5949 }
5950 }
5951 "edgealpha" => {
5952 if let Some(v) = value_as_f64(value) {
5953 patch.set_edge_alpha(v as f32);
5954 }
5955 }
5956 "linewidth" => {
5957 if let Some(v) = value_as_f64(value) {
5958 patch.set_line_width(v as f32);
5959 }
5960 }
5961 "displayname" => {
5962 patch.set_label(value_as_string(value).map(|s| s.to_string()));
5963 }
5964 "visible" => {
5965 if let Some(v) = value_as_bool(value) {
5966 patch.set_visible(v);
5967 }
5968 }
5969 _ => {}
5970 }
5971 }
5972 })
5973 .map_err(|err| map_figure_error(builtin, err))?;
5974 Ok(())
5975}
5976
5977fn apply_line3_property(
5978 line_handle: &super::state::SimplePlotHandleState,
5979 key: &str,
5980 value: &Value,
5981 builtin: &'static str,
5982) -> BuiltinResult<()> {
5983 apply_line3_properties(line_handle, &[(key.to_string(), value)], builtin)
5984}
5985
5986fn apply_line3_properties(
5987 line_handle: &super::state::SimplePlotHandleState,
5988 pairs: &[(String, &Value)],
5989 builtin: &'static str,
5990) -> BuiltinResult<()> {
5991 let current = get_simple_plot(line_handle, builtin)?;
5992 let runmat_plot::plots::figure::PlotElement::Line3(current_line) = current else {
5993 return Err(plotting_error(
5994 builtin,
5995 format!("{builtin}: invalid line handle"),
5996 ));
5997 };
5998
5999 let mut x_update = None;
6000 let mut y_update = None;
6001 let mut z_update = None;
6002 let mut style_pairs = Vec::new();
6003 for (key, value) in pairs {
6004 match key.as_str() {
6005 "xdata" => x_update = Some(line_numeric_data(value, "XData", builtin)?),
6006 "ydata" => y_update = Some(line_numeric_data(value, "YData", builtin)?),
6007 "zdata" => z_update = Some(line_numeric_data(value, "ZData", builtin)?),
6008 _ => style_pairs.push((key.as_str(), *value)),
6009 }
6010 }
6011
6012 let next_x = x_update.unwrap_or_else(|| current_line.x_data.clone());
6013 let next_y = y_update.unwrap_or_else(|| current_line.y_data.clone());
6014 let next_z = z_update.unwrap_or_else(|| current_line.z_data.clone());
6015 let update_data = next_x != current_line.x_data
6016 || next_y != current_line.y_data
6017 || next_z != current_line.z_data;
6018 if update_data
6019 && (next_x.is_empty() || next_x.len() != next_y.len() || next_x.len() != next_z.len())
6020 {
6021 return Err(plotting_error(
6022 builtin,
6023 format!("{builtin}: XData, YData, and ZData must contain the same nonzero number of elements"),
6024 ));
6025 }
6026
6027 super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
6028 if let runmat_plot::plots::figure::PlotElement::Line3(line) = plot {
6029 if update_data {
6030 let _ = line.update_data(next_x.clone(), next_y.clone(), next_z.clone());
6031 }
6032 for (key, value) in &style_pairs {
6033 match *key {
6034 "color" => {
6035 if let Ok(c) =
6036 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6037 {
6038 line.color = c;
6039 }
6040 }
6041 "linewidth" => {
6042 if let Some(v) = value_as_f64(value) {
6043 line.line_width = v as f32;
6044 }
6045 }
6046 "linestyle" => {
6047 if let Some(s) = value_as_string(value) {
6048 line.line_style = parse_line_style_name_for_props(&s);
6049 }
6050 }
6051 "displayname" => {
6052 line.label = value_as_string(value).map(|s| s.to_string());
6053 }
6054 "visible" => {
6055 if let Some(v) = value_as_bool(value) {
6056 line.set_visible(v);
6057 } else if let Some(s) = value_as_string(value) {
6058 line.set_visible(!matches!(
6059 s.trim().to_ascii_lowercase().as_str(),
6060 "off" | "false"
6061 ));
6062 }
6063 }
6064 _ => {}
6065 }
6066 }
6067 }
6068 })
6069 .map_err(|err| map_figure_error(builtin, err))?;
6070 Ok(())
6071}
6072
6073fn apply_scatter3_property(
6074 scatter_handle: &super::state::SimplePlotHandleState,
6075 key: &str,
6076 value: &Value,
6077 builtin: &'static str,
6078) -> BuiltinResult<()> {
6079 super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
6080 if let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot {
6081 match key {
6082 "sizedata" => {
6083 if let Some(v) = value_as_f64(value) {
6084 scatter.point_size = marker_area_points2_to_diameter_px(v);
6085 }
6086 }
6087 "marker" => {
6088 if let Some(s) = value_as_string(value) {
6089 scatter.marker_style = scatter_marker_from_name(&s, scatter.marker_style);
6090 }
6091 }
6092 "markerfacecolor" => {
6093 if let Ok(c) =
6094 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6095 {
6096 let count = scatter.points.len().max(1);
6097 scatter.colors = vec![c; count];
6098 }
6099 }
6100 "markeredgecolor" => {
6101 if let Ok(c) =
6102 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6103 {
6104 scatter.edge_color = c;
6105 }
6106 }
6107 "linewidth" => {
6108 if let Some(v) = value_as_f64(value) {
6109 scatter.edge_thickness = v as f32;
6110 }
6111 }
6112 "displayname" => {
6113 scatter.label = value_as_string(value).map(|s| s.to_string());
6114 }
6115 _ => {}
6116 }
6117 }
6118 })
6119 .map_err(|err| map_figure_error(builtin, err))?;
6120 Ok(())
6121}
6122
6123fn apply_pie_property(
6124 pie_handle: &super::state::SimplePlotHandleState,
6125 key: &str,
6126 value: &Value,
6127 builtin: &'static str,
6128) -> BuiltinResult<()> {
6129 super::state::update_plot_element(pie_handle.figure, pie_handle.plot_index, |plot| {
6130 if let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot {
6131 if key == "displayname" {
6132 pie.label = value_as_string(value).map(|s| s.to_string());
6133 }
6134 }
6135 })
6136 .map_err(|err| map_figure_error(builtin, err))?;
6137 Ok(())
6138}
6139
6140fn apply_contour_property(
6141 contour_handle: &super::state::SimplePlotHandleState,
6142 key: &str,
6143 value: &Value,
6144 builtin: &'static str,
6145) -> BuiltinResult<()> {
6146 super::state::update_plot_element(contour_handle.figure, contour_handle.plot_index, |plot| {
6147 if let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot {
6148 if key == "displayname" {
6149 contour.label = value_as_string(value).map(|s| s.to_string());
6150 } else if key == "linewidth" {
6151 if let Some(width) = value_as_f64(value) {
6152 contour.line_width = (width as f32).max(0.5);
6153 }
6154 }
6155 }
6156 })
6157 .map_err(|err| map_figure_error(builtin, err))?;
6158 Ok(())
6159}
6160
6161fn apply_contour_fill_property(
6162 fill_handle: &super::state::SimplePlotHandleState,
6163 key: &str,
6164 value: &Value,
6165 builtin: &'static str,
6166) -> BuiltinResult<()> {
6167 super::state::update_plot_element(fill_handle.figure, fill_handle.plot_index, |plot| {
6168 if let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot {
6169 if key == "displayname" {
6170 fill.label = value_as_string(value).map(|s| s.to_string());
6171 }
6172 }
6173 })
6174 .map_err(|err| map_figure_error(builtin, err))?;
6175 Ok(())
6176}
6177
6178fn apply_line_plot_properties(
6179 line: &mut runmat_plot::plots::LinePlot,
6180 key: &str,
6181 value: &Value,
6182 builtin: &'static str,
6183) {
6184 match key {
6185 "color" => {
6186 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6187 line.color = c;
6188 }
6189 }
6190 "linewidth" => {
6191 if let Some(v) = value_as_f64(value) {
6192 line.line_width = v as f32;
6193 }
6194 }
6195 "linestyle" => {
6196 if let Some(s) = value_as_string(value) {
6197 line.line_style = parse_line_style_name_for_props(&s);
6198 }
6199 }
6200 "displayname" => {
6201 line.label = value_as_string(value).map(|s| s.to_string());
6202 }
6203 "handlevisibility" => {
6204 if let Ok(visibility) =
6205 parse_handle_visibility(&LineStyleParseOptions::generic(builtin), value)
6206 {
6207 line.handle_visibility = visibility;
6208 }
6209 }
6210 "marker" => {
6211 if let Some(s) = value_as_string(value) {
6212 line.marker = marker_from_name(&s, line.marker.clone());
6213 }
6214 }
6215 "markersize" => {
6216 if let Some(v) = value_as_f64(value) {
6217 if let Some(marker) = &mut line.marker {
6218 marker.size = v as f32;
6219 }
6220 }
6221 }
6222 "markerfacecolor" => {
6223 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6224 if let Some(marker) = &mut line.marker {
6225 marker.face_color = c;
6226 }
6227 }
6228 }
6229 "markeredgecolor" => {
6230 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
6231 if let Some(marker) = &mut line.marker {
6232 marker.edge_color = c;
6233 }
6234 }
6235 }
6236 "filled" => {
6237 if let Some(v) = value_as_bool(value) {
6238 if let Some(marker) = &mut line.marker {
6239 marker.filled = v;
6240 }
6241 }
6242 }
6243 "visible" => {
6244 if let Some(v) = value_as_bool(value) {
6245 line.set_visible(v);
6246 } else if let Some(s) = value_as_string(value) {
6247 line.set_visible(!matches!(
6248 s.trim().to_ascii_lowercase().as_str(),
6249 "off" | "false"
6250 ));
6251 }
6252 }
6253 _ => {}
6254 }
6255}
6256
6257fn line_numeric_data(value: &Value, name: &str, builtin: &'static str) -> BuiltinResult<Vec<f64>> {
6258 let tensor = tensor::value_to_tensor(value)
6259 .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
6260 Ok(tensor.data)
6261}
6262
6263fn line_xy_data_for_properties(
6264 line: &runmat_plot::plots::LinePlot,
6265 builtin: &'static str,
6266) -> BuiltinResult<(Vec<f64>, Vec<f64>)> {
6267 if !line.x_data.is_empty() && line.x_data.len() == line.y_data.len() {
6268 return Ok((line.x_data.clone(), line.y_data.clone()));
6269 }
6270 if !line.x_data.is_empty() || !line.y_data.is_empty() {
6271 return Err(plotting_error(
6272 builtin,
6273 format!(
6274 "{builtin}: line source data is inconsistent: XData has {} values, YData has {} values",
6275 line.x_data.len(),
6276 line.y_data.len()
6277 ),
6278 ));
6279 }
6280 if !line.has_gpu_source_data() {
6281 return Ok((Vec::new(), Vec::new()));
6282 }
6283 futures::executor::block_on(line.export_scene_xy_data()).map_err(|err| {
6284 plotting_error(
6285 builtin,
6286 format!("{builtin}: unable to read line source data: {err}"),
6287 )
6288 })
6289}
6290
6291fn limits_from_optional_value(
6292 value: &Value,
6293 builtin: &'static str,
6294) -> BuiltinResult<Option<(f64, f64)>> {
6295 if let Some(text) = value_as_string(value) {
6296 let norm = text.trim().to_ascii_lowercase();
6297 if matches!(norm.as_str(), "auto" | "tight") {
6298 return Ok(None);
6299 }
6300 }
6301 Ok(Some(
6302 crate::builtins::plotting::op_common::limits::limits_from_value(value, builtin)?,
6303 ))
6304}
6305
6306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6307enum TickMode {
6308 Auto,
6309 Manual,
6310}
6311
6312fn ticks_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<f64>> {
6313 let tensor = runmat_builtins::Tensor::try_from(value)
6314 .map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
6315 let ticks = tensor.data;
6316 if ticks.iter().any(|value| !value.is_finite()) {
6317 return Err(plotting_error(
6318 builtin,
6319 format!("{builtin}: tick values must be finite"),
6320 ));
6321 }
6322 if ticks.windows(2).any(|pair| pair[1] <= pair[0]) {
6323 return Err(plotting_error(
6324 builtin,
6325 format!("{builtin}: tick values must be strictly increasing"),
6326 ));
6327 }
6328 Ok(ticks)
6329}
6330
6331fn tick_mode_from_value(
6332 value: &Value,
6333 builtin: &'static str,
6334 property: &str,
6335) -> BuiltinResult<TickMode> {
6336 let mode = value_as_string(value).ok_or_else(|| {
6337 plotting_error(builtin, format!("{builtin}: {property} must be a string"))
6338 })?;
6339 match mode.trim().to_ascii_lowercase().as_str() {
6340 "auto" => Ok(TickMode::Auto),
6341 "manual" => Ok(TickMode::Manual),
6342 _ => Err(plotting_error(
6343 builtin,
6344 format!("{builtin}: {property} must be 'auto' or 'manual'"),
6345 )),
6346 }
6347}
6348
6349fn tick_format_from_value(
6350 value: &Value,
6351 builtin: &'static str,
6352 property: &'static str,
6353) -> BuiltinResult<String> {
6354 let format = value_as_string(value)
6355 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: {property} must be text")))?;
6356 Ok(runmat_plot::core::plot_renderer::plot_utils::canonical_tick_label_format(&format))
6357}
6358
6359fn tick_angle_from_value(
6360 value: &Value,
6361 builtin: &'static str,
6362 property: &'static str,
6363) -> BuiltinResult<f64> {
6364 let angle = scalar_numeric_value(value)
6365 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: {property} must be numeric")))?;
6366 if angle.is_finite() {
6367 Ok(angle)
6368 } else {
6369 Err(plotting_error(
6370 builtin,
6371 format!("{builtin}: {property} must be finite"),
6372 ))
6373 }
6374}
6375
6376fn scalar_numeric_value(value: &Value) -> Option<f64> {
6377 match value {
6378 Value::Num(value) => Some(*value),
6379 Value::Int(value) => Some(value.to_f64()),
6380 Value::Tensor(tensor) if tensor.data.len() == 1 => Some(tensor.data[0]),
6381 _ => None,
6382 }
6383}
6384
6385fn tick_format_or_default(format: Option<&str>) -> String {
6386 runmat_plot::core::plot_renderer::plot_utils::canonical_tick_label_format(
6387 format.unwrap_or("%g"),
6388 )
6389}
6390
6391fn x_bounds(bounds: Option<(f64, f64, f64, f64)>) -> Option<(f64, f64)> {
6392 bounds.map(|(x_min, x_max, _, _)| (x_min, x_max))
6393}
6394
6395fn y_bounds(bounds: Option<(f64, f64, f64, f64)>) -> Option<(f64, f64)> {
6396 bounds.map(|(_, _, y_min, y_max)| (y_min, y_max))
6397}
6398
6399fn ticks_or_auto(explicit: Option<&[f64]>, bounds: Option<(f64, f64)>) -> Vec<f64> {
6400 if let Some(ticks) = explicit {
6401 return ticks.to_vec();
6402 }
6403 let (lo, hi) = bounds.unwrap_or((-1.0, 1.0));
6404 runmat_plot::core::plot_utils::generate_major_ticks(lo, hi)
6405}
6406
6407fn tick_value(data: Vec<f64>) -> Value {
6408 tensor_from_vec(data)
6409}
6410
6411fn tick_mode_value<T>(ticks: Option<&Vec<T>>) -> Value {
6412 Value::String(if ticks.is_some() { "manual" } else { "auto" }.into())
6413}
6414
6415fn tick_labels_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<String>> {
6416 match value {
6417 Value::String(s) => Ok(vec![s.clone()]),
6418 Value::StringArray(strings) => Ok(strings.data.clone()),
6419 Value::CharArray(chars) => Ok(char_array_rows(chars)),
6420 Value::Cell(cell) => {
6421 let mut labels = Vec::with_capacity(cell.data.len());
6422 for entry in &cell.data {
6423 labels.extend(tick_labels_from_value(entry, builtin)?);
6424 }
6425 Ok(labels)
6426 }
6427 Value::Tensor(tensor) if tensor.data.is_empty() => Ok(Vec::new()),
6428 other => Err(plotting_error(
6429 builtin,
6430 format!("{builtin}: tick labels must be a string array or cell array of text, got {other:?}"),
6431 )),
6432 }
6433}
6434
6435fn tick_labels_or_auto(
6436 explicit: Option<&[String]>,
6437 ticks: Option<&[f64]>,
6438 bounds: Option<(f64, f64)>,
6439 format: Option<&str>,
6440) -> Vec<String> {
6441 if let Some(labels) = explicit {
6442 return labels.to_vec();
6443 }
6444 let formatter = runmat_plot::core::plot_utils::TickLabelFormatter::new(format);
6445 ticks_or_auto(ticks, bounds)
6446 .into_iter()
6447 .map(|tick| formatter.format(tick))
6448 .collect()
6449}
6450
6451fn labels_padded_to_ticks(mut labels: Vec<String>, tick_count: usize) -> Vec<String> {
6452 if labels.len() < tick_count {
6453 labels.resize(tick_count, String::new());
6454 }
6455 labels
6456}
6457
6458fn tick_label_value(labels: Vec<String>) -> Value {
6459 let values = labels
6460 .iter()
6461 .map(|label| Value::CharArray(CharArray::new_row(label)))
6462 .collect::<Vec<_>>();
6463 Value::Cell(CellArray::new(values, 1, labels.len()).expect("valid tick-label cell array"))
6464}
6465
6466fn char_array_rows(chars: &CharArray) -> Vec<String> {
6467 if chars.rows == 0 || chars.cols == 0 {
6468 return Vec::new();
6469 }
6470 (0..chars.rows)
6471 .map(|row| {
6472 let start = row * chars.cols;
6473 let end = start + chars.cols;
6474 chars.data[start..end]
6475 .iter()
6476 .collect::<String>()
6477 .trim_end()
6478 .to_string()
6479 })
6480 .collect()
6481}
6482
6483fn parse_colormap_name(
6484 name: &str,
6485 builtin: &'static str,
6486) -> BuiltinResult<runmat_plot::plots::surface::ColorMap> {
6487 runmat_plot::plots::surface::ColorMap::from_name(name).ok_or_else(|| {
6488 plotting_error(
6489 builtin,
6490 format!("{builtin}: unknown colormap '{}'", name.trim()),
6491 )
6492 })
6493}
6494
6495fn apply_axes_text_alias(
6496 handle: FigureHandle,
6497 axes_index: usize,
6498 kind: PlotObjectKind,
6499 value: &Value,
6500 builtin: &'static str,
6501) -> BuiltinResult<()> {
6502 if let Some(text) = value_as_string(value) {
6503 set_text_properties_for_axes(handle, axes_index, kind, Some(text), None)
6504 .map_err(|err| map_figure_error(builtin, err))?;
6505 return Ok(());
6506 }
6507
6508 let scalar = handle_scalar(value, builtin)?;
6509 let (src_handle, src_axes, src_kind) =
6510 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6511 if src_kind != kind {
6512 return Err(plotting_error(
6513 builtin,
6514 format!(
6515 "{builtin}: expected a matching text handle for `{}`",
6516 key_name(kind)
6517 ),
6518 ));
6519 }
6520 let meta = axes_metadata_snapshot(src_handle, src_axes)
6521 .map_err(|err| map_figure_error(builtin, err))?;
6522 let (text, style) = match kind {
6523 PlotObjectKind::Title => (meta.title, meta.title_style),
6524 PlotObjectKind::Subtitle => (meta.subtitle, meta.subtitle_style),
6525 PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
6526 PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
6527 PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
6528 PlotObjectKind::Legend
6529 | PlotObjectKind::SuperTitle
6530 | PlotObjectKind::XAxis
6531 | PlotObjectKind::YAxis => unreachable!(),
6532 };
6533 set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
6534 .map_err(|err| map_figure_error(builtin, err))?;
6535 Ok(())
6536}
6537
6538fn validate_axes_text_alias(
6539 kind: PlotObjectKind,
6540 value: &Value,
6541 builtin: &'static str,
6542) -> BuiltinResult<()> {
6543 if value_as_string(value).is_some() {
6544 return Ok(());
6545 }
6546
6547 let scalar = handle_scalar(value, builtin)?;
6548 let (src_handle, src_axes, src_kind) =
6549 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6550 if src_kind != kind {
6551 return Err(plotting_error(
6552 builtin,
6553 format!(
6554 "{builtin}: expected a matching text handle for `{}`",
6555 key_name(kind)
6556 ),
6557 ));
6558 }
6559 axes_metadata_snapshot(src_handle, src_axes).map_err(|err| map_figure_error(builtin, err))?;
6560 Ok(())
6561}
6562
6563fn apply_figure_text_alias(
6564 handle: FigureHandle,
6565 kind: PlotObjectKind,
6566 value: &Value,
6567 builtin: &'static str,
6568) -> BuiltinResult<()> {
6569 if let Some(text) = value_as_text_string(value) {
6570 match kind {
6571 PlotObjectKind::SuperTitle => {
6572 set_sg_title_properties_for_figure(handle, Some(text), None)
6573 .map_err(|err| map_figure_error(builtin, err))?;
6574 }
6575 _ => unreachable!(),
6576 }
6577 return Ok(());
6578 }
6579
6580 let scalar = handle_scalar(value, builtin)?;
6581 let (src_handle, _src_axes, src_kind) =
6582 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6583 if src_kind != kind {
6584 return Err(plotting_error(
6585 builtin,
6586 format!(
6587 "{builtin}: expected a matching text handle for `{}`",
6588 key_name(kind)
6589 ),
6590 ));
6591 }
6592
6593 let figure = super::state::clone_figure(src_handle)
6594 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
6595 let (text, style) = match kind {
6596 PlotObjectKind::SuperTitle => (figure.sg_title, figure.sg_title_style),
6597 _ => unreachable!(),
6598 };
6599 set_sg_title_properties_for_figure(handle, text, Some(style))
6600 .map_err(|err| map_figure_error(builtin, err))?;
6601 Ok(())
6602}
6603
6604fn validate_figure_text_alias(
6605 kind: PlotObjectKind,
6606 value: &Value,
6607 builtin: &'static str,
6608) -> BuiltinResult<()> {
6609 if value_as_text_string(value).is_some() {
6610 return Ok(());
6611 }
6612
6613 let scalar = handle_scalar(value, builtin)?;
6614 let (src_handle, _src_axes, src_kind) =
6615 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
6616 if src_kind != kind {
6617 return Err(plotting_error(
6618 builtin,
6619 format!(
6620 "{builtin}: expected a matching text handle for `{}`",
6621 key_name(kind)
6622 ),
6623 ));
6624 }
6625
6626 super::state::clone_figure(src_handle)
6627 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
6628 Ok(())
6629}
6630
6631fn collect_label_strings(builtin: &'static str, args: &[Value]) -> BuiltinResult<Vec<String>> {
6632 let mut labels = Vec::new();
6633 for arg in args {
6634 match arg {
6635 Value::StringArray(arr) => labels.extend(arr.data.iter().cloned()),
6636 Value::Cell(cell) => {
6637 for row in 0..cell.rows {
6638 for col in 0..cell.cols {
6639 let value = cell.get(row, col).map_err(|err| {
6640 plotting_error(builtin, format!("legend: invalid label cell: {err}"))
6641 })?;
6642 labels.push(value_as_string(&value).ok_or_else(|| {
6643 plotting_error(builtin, "legend: labels must be strings or char arrays")
6644 })?);
6645 }
6646 }
6647 }
6648 _ => labels.push(value_as_string(arg).ok_or_else(|| {
6649 plotting_error(builtin, "legend: labels must be strings or char arrays")
6650 })?),
6651 }
6652 }
6653 Ok(labels)
6654}
6655
6656fn handle_scalar(value: &Value, builtin: &'static str) -> BuiltinResult<f64> {
6657 match value {
6658 Value::Num(v) => Ok(*v),
6659 Value::Int(i) => Ok(i.to_f64()),
6660 Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
6661 _ => Err(plotting_error(
6662 builtin,
6663 format!("{builtin}: expected plotting handle"),
6664 )),
6665 }
6666}
6667
6668fn legend_labels_value(labels: Vec<String>) -> Value {
6669 Value::StringArray(StringArray {
6670 rows: 1,
6671 cols: labels.len().max(1),
6672 shape: vec![1, labels.len().max(1)],
6673 data: labels,
6674 })
6675}
6676
6677fn text_value(text: Option<String>) -> Value {
6678 match text {
6679 Some(text) if text.contains('\n') => {
6680 let lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
6681 Value::StringArray(StringArray {
6682 rows: 1,
6683 cols: lines.len().max(1),
6684 shape: vec![1, lines.len().max(1)],
6685 data: lines,
6686 })
6687 }
6688 Some(text) => Value::String(text),
6689 None => Value::String(String::new()),
6690 }
6691}
6692
6693fn handles_value(handles: Vec<f64>) -> Value {
6694 Value::Tensor(runmat_builtins::Tensor {
6695 rows: 1,
6696 cols: handles.len(),
6697 shape: vec![1, handles.len()],
6698 data: handles,
6699 integer_data: None,
6700 dtype: runmat_builtins::NumericDType::F64,
6701 })
6702}
6703
6704fn tensor_from_vec(data: Vec<f64>) -> Value {
6705 Value::Tensor(runmat_builtins::Tensor {
6706 rows: 1,
6707 cols: data.len(),
6708 shape: vec![1, data.len()],
6709 data,
6710 integer_data: None,
6711 dtype: runmat_builtins::NumericDType::F64,
6712 })
6713}
6714
6715fn string_array_from_vec(data: Vec<String>) -> BuiltinResult<Value> {
6716 let cols = data.len();
6717 let array = StringArray::new(data, vec![1, cols])
6718 .map_err(|e| plotting_error("get", format!("get: {e}")))?;
6719 Ok(Value::StringArray(array))
6720}
6721
6722pub(crate) fn label_strings_from_value(
6723 value: &Value,
6724 builtin: &'static str,
6725 label_context: &str,
6726) -> BuiltinResult<Vec<String>> {
6727 match value {
6728 Value::StringArray(array) => Ok(array.data.clone()),
6729 Value::Cell(cell) => cell
6730 .data
6731 .iter()
6732 .map(|item| {
6733 value_as_text_string(item).ok_or_else(|| {
6734 plotting_error(
6735 builtin,
6736 format!("{builtin}: {label_context} must contain text values"),
6737 )
6738 })
6739 })
6740 .collect(),
6741 Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
6742 Value::String(text) => Ok(vec![text.clone()]),
6743 Value::Tensor(tensor) => Ok(tensor.data.iter().map(|v| v.to_string()).collect()),
6744 Value::Int(i) => Ok(vec![i.to_i64().to_string()]),
6745 Value::Num(v) => Ok(vec![v.to_string()]),
6746 other => Err(plotting_error(
6747 builtin,
6748 format!("{builtin}: unsupported {label_context} value {other:?}"),
6749 )),
6750 }
6751}
6752
6753fn tensor_from_matrix(data: Vec<Vec<f64>>) -> Value {
6754 let rows = data.len();
6755 let cols = data.first().map(|row| row.len()).unwrap_or(0);
6756 let flat = data.into_iter().flat_map(|row| row.into_iter()).collect();
6757 Value::Tensor(runmat_builtins::Tensor {
6758 rows,
6759 cols,
6760 shape: vec![rows, cols],
6761 data: flat,
6762 integer_data: None,
6763 dtype: runmat_builtins::NumericDType::F64,
6764 })
6765}
6766
6767fn surface_x_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6768 surface
6769 .x_grid
6770 .as_ref()
6771 .map(|grid| surface_grid_to_tensor(grid))
6772 .unwrap_or_else(|| tensor_from_vec(surface.x_data.clone()))
6773}
6774
6775fn surface_y_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6776 surface
6777 .y_grid
6778 .as_ref()
6779 .map(|grid| surface_grid_to_tensor(grid))
6780 .unwrap_or_else(|| tensor_from_vec(surface.y_data.clone()))
6781}
6782
6783fn surface_z_data_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6784 surface
6785 .z_data
6786 .as_ref()
6787 .map(|grid| surface_grid_to_tensor(grid))
6788 .unwrap_or_else(|| tensor_from_vec(Vec::new()))
6789}
6790
6791fn surface_grid_to_tensor(grid: &[Vec<f64>]) -> Value {
6792 let cols = grid.len();
6793 let rows = grid.first().map_or(0, Vec::len);
6794 let mut data = Vec::with_capacity(rows * cols);
6795 for column in grid {
6796 data.extend(column.iter().copied());
6797 }
6798 Value::Tensor(runmat_builtins::Tensor {
6799 rows,
6800 cols,
6801 shape: vec![rows, cols],
6802 data,
6803 integer_data: None,
6804 dtype: runmat_builtins::NumericDType::F64,
6805 })
6806}
6807
6808fn surface_coordinate_data_from_value(
6809 value: &Value,
6810 name: &str,
6811 builtin: &'static str,
6812) -> BuiltinResult<SurfaceCoordinateData> {
6813 let tensor = Tensor::try_from(value)
6814 .map_err(|_| plotting_error(builtin, format!("{builtin}: {name} must be numeric")))?;
6815 if tensor.data.is_empty() {
6816 return Err(plotting_error(
6817 builtin,
6818 format!("{builtin}: {name} must be non-empty"),
6819 ));
6820 }
6821 if tensor.shape.len() > 2 {
6822 return Err(plotting_error(
6823 builtin,
6824 format!("{builtin}: {name} must be a vector or 2-D matrix"),
6825 ));
6826 }
6827 if tensor.data.iter().any(|value| !value.is_finite()) {
6828 return Err(plotting_error(
6829 builtin,
6830 format!("{builtin}: {name} must contain finite coordinates"),
6831 ));
6832 }
6833 if tensor.rows == 1 || tensor.cols == 1 {
6834 return Ok(SurfaceCoordinateData::Vector(tensor.data));
6835 }
6836 let rows = tensor.rows;
6837 let cols = tensor.cols;
6838 let grid = super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin)?;
6839 Ok(SurfaceCoordinateData::Grid(grid))
6840}
6841
6842fn surface_z_grid_from_value(
6843 value: &Value,
6844 rows: usize,
6845 cols: usize,
6846 builtin: &'static str,
6847) -> BuiltinResult<Vec<Vec<f64>>> {
6848 let mut tensor = Tensor::try_from(value)
6849 .map_err(|_| plotting_error(builtin, format!("{builtin}: ZData must be numeric")))?;
6850 let expected = rows
6851 .checked_mul(cols)
6852 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: grid dimensions overflowed")))?;
6853 if tensor.data.len() != expected {
6854 return Err(plotting_error(
6855 builtin,
6856 format!("{builtin}: ZData must contain exactly {expected} values"),
6857 ));
6858 }
6859 if tensor.rows == rows && tensor.cols == cols {
6860 return super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin);
6861 }
6862 if tensor.rows == 1 || tensor.cols == 1 {
6863 tensor.rows = rows;
6864 tensor.cols = cols;
6865 tensor.shape = vec![rows, cols];
6866 return super::common::tensor_to_surface_grid_matlab_xy(tensor, rows, cols, builtin);
6867 }
6868 Err(plotting_error(
6869 builtin,
6870 format!("{builtin}: ZData must have shape {rows}x{cols}"),
6871 ))
6872}
6873
6874fn surface_grid_size(grid: &[Vec<f64>]) -> BuiltinResult<(usize, usize)> {
6875 let x_len = grid.len();
6876 let y_len = grid.first().map_or(0, Vec::len);
6877 if x_len == 0 || y_len == 0 || grid.iter().any(|row| row.len() != y_len) {
6878 return Err(plotting_error(
6879 "set",
6880 "set: surface source data is internally inconsistent",
6881 ));
6882 }
6883 Ok((x_len, y_len))
6884}
6885
6886fn validate_surface_grid_shape(
6887 grid: &[Vec<f64>],
6888 x_len: usize,
6889 y_len: usize,
6890 name: &str,
6891 builtin: &'static str,
6892) -> BuiltinResult<()> {
6893 if grid.len() != x_len || grid.iter().any(|row| row.len() != y_len) {
6894 return Err(plotting_error(
6895 builtin,
6896 format!("{builtin}: {name} matrix must have shape {y_len}x{x_len}"),
6897 ));
6898 }
6899 Ok(())
6900}
6901
6902fn axis_to_x_grid(axis: &[f64], y_len: usize) -> Vec<Vec<f64>> {
6903 axis.iter().map(|&x| vec![x; y_len]).collect()
6904}
6905
6906fn axis_to_y_grid(axis: &[f64], x_len: usize) -> Vec<Vec<f64>> {
6907 vec![axis.to_vec(); x_len]
6908}
6909
6910fn surface_face_color_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6911 match &surface.colormap {
6912 ColorMap::Custom(min, max) if (*min - *max).abs().max_element() < 1e-6 => {
6913 Value::String(color_to_short_name(*min))
6914 }
6915 _ => Value::String("flat".into()),
6916 }
6917}
6918
6919fn surface_edge_color_value(surface: &runmat_plot::plots::SurfacePlot) -> Value {
6920 Value::String(if surface.wireframe { "flat" } else { "none" }.into())
6921}
6922
6923fn surface_shading_name(shading: ShadingMode) -> &'static str {
6924 match shading {
6925 ShadingMode::Flat => "flat",
6926 ShadingMode::Smooth => "interp",
6927 ShadingMode::Faceted => "faceted",
6928 ShadingMode::None => "none",
6929 }
6930}
6931
6932fn surface_shading_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<ShadingMode> {
6933 let text = value_as_string(value)
6934 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Shading must be text")))?;
6935 match text.trim().to_ascii_lowercase().as_str() {
6936 "flat" => Ok(ShadingMode::Flat),
6937 "interp" | "gouraud" => Ok(ShadingMode::Smooth),
6938 "faceted" => Ok(ShadingMode::Faceted),
6939 "none" => Ok(ShadingMode::None),
6940 other => Err(plotting_error(
6941 builtin,
6942 format!("{builtin}: unsupported Shading `{other}`"),
6943 )),
6944 }
6945}
6946
6947fn surface_lighting_name(enabled: bool) -> &'static str {
6948 if enabled {
6949 "gouraud"
6950 } else {
6951 "none"
6952 }
6953}
6954
6955fn surface_lighting_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<bool> {
6956 if let Some(text) = value_as_string(value) {
6957 return match text.trim().to_ascii_lowercase().as_str() {
6958 "none" | "off" => Ok(false),
6959 "flat" | "gouraud" | "phong" | "auto" | "on" => Ok(true),
6960 other => Err(plotting_error(
6961 builtin,
6962 format!("{builtin}: unsupported Lighting `{other}`"),
6963 )),
6964 };
6965 }
6966 value_as_bool(value)
6967 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: Lighting must be logical")))
6968}
6969
6970fn surface_face_color_update(
6971 value: &Value,
6972 builtin: &'static str,
6973) -> BuiltinResult<SurfacePropertyUpdate> {
6974 if let Some(text) = value_as_string(value) {
6975 return match text.trim().to_ascii_lowercase().as_str() {
6976 "none" => Ok(SurfacePropertyUpdate::FaceNone),
6977 "flat" => Ok(SurfacePropertyUpdate::Shading(ShadingMode::Flat)),
6978 "interp" => Ok(SurfacePropertyUpdate::Shading(ShadingMode::Smooth)),
6979 "texturemap" => Ok(SurfacePropertyUpdate::FlattenZ(true)),
6980 _ => parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6981 .map(SurfacePropertyUpdate::FaceColor),
6982 };
6983 }
6984 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
6985 .map(SurfacePropertyUpdate::FaceColor)
6986}
6987
6988fn vertices_tensor(vertices: &[glam::Vec3]) -> Value {
6989 let rows = vertices.len();
6990 let cols = 3;
6991 let mut data = Vec::with_capacity(rows * cols);
6992 for col in 0..cols {
6993 for vertex in vertices {
6994 data.push(match col {
6995 0 => vertex.x as f64,
6996 1 => vertex.y as f64,
6997 _ => vertex.z as f64,
6998 });
6999 }
7000 }
7001 Value::Tensor(runmat_builtins::Tensor {
7002 rows,
7003 cols,
7004 shape: vec![rows, cols],
7005 data,
7006 integer_data: None,
7007 dtype: runmat_builtins::NumericDType::F64,
7008 })
7009}
7010
7011fn faces_tensor(faces: &[Vec<usize>]) -> Value {
7012 let rows = faces.len();
7013 let cols = faces.iter().map(|face| face.len()).max().unwrap_or(0);
7014 let mut data = Vec::with_capacity(rows * cols);
7015 for col in 0..cols {
7016 for face in faces {
7017 data.push(
7018 face.get(col)
7019 .map(|idx| *idx as f64 + 1.0)
7020 .unwrap_or(f64::NAN),
7021 );
7022 }
7023 }
7024 Value::Tensor(runmat_builtins::Tensor {
7025 rows,
7026 cols,
7027 shape: vec![rows, cols],
7028 data,
7029 integer_data: None,
7030 dtype: runmat_builtins::NumericDType::F64,
7031 })
7032}
7033
7034fn patch_color_property(mode: runmat_plot::plots::PatchFaceColorMode, color: glam::Vec4) -> Value {
7035 match mode {
7036 runmat_plot::plots::PatchFaceColorMode::None => Value::String("none".into()),
7037 runmat_plot::plots::PatchFaceColorMode::Flat => Value::String("flat".into()),
7038 runmat_plot::plots::PatchFaceColorMode::Color => Value::String(color_to_short_name(color)),
7039 }
7040}
7041
7042fn patch_edge_color_property(
7043 mode: runmat_plot::plots::PatchEdgeColorMode,
7044 color: glam::Vec4,
7045) -> Value {
7046 match mode {
7047 runmat_plot::plots::PatchEdgeColorMode::None => Value::String("none".into()),
7048 runmat_plot::plots::PatchEdgeColorMode::Color => Value::String(color_to_short_name(color)),
7049 }
7050}
7051
7052fn insert_line_marker_struct_props(
7053 st: &mut StructValue,
7054 marker: Option<&runmat_plot::plots::line::LineMarkerAppearance>,
7055) {
7056 if let Some(marker) = marker {
7057 st.insert(
7058 "Marker",
7059 Value::String(marker_style_name(marker.kind).into()),
7060 );
7061 st.insert("MarkerSize", Value::Num(marker.size as f64));
7062 st.insert(
7063 "MarkerFaceColor",
7064 Value::String(color_to_short_name(marker.face_color)),
7065 );
7066 st.insert(
7067 "MarkerEdgeColor",
7068 Value::String(color_to_short_name(marker.edge_color)),
7069 );
7070 st.insert("Filled", Value::Bool(marker.filled));
7071 }
7072}
7073
7074fn line_marker_property_value(
7075 marker: &Option<runmat_plot::plots::line::LineMarkerAppearance>,
7076 name: &str,
7077 builtin: &'static str,
7078) -> BuiltinResult<Value> {
7079 match name {
7080 "marker" => Ok(Value::String(
7081 marker
7082 .as_ref()
7083 .map(|m| marker_style_name(m.kind).to_string())
7084 .unwrap_or_else(|| "none".into()),
7085 )),
7086 "markersize" => Ok(Value::Num(
7087 marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
7088 )),
7089 "markerfacecolor" => Ok(Value::String(
7090 marker
7091 .as_ref()
7092 .map(|m| color_to_short_name(m.face_color))
7093 .unwrap_or_else(|| "none".into()),
7094 )),
7095 "markeredgecolor" => Ok(Value::String(
7096 marker
7097 .as_ref()
7098 .map(|m| color_to_short_name(m.edge_color))
7099 .unwrap_or_else(|| "none".into()),
7100 )),
7101 "filled" => Ok(Value::Bool(
7102 marker.as_ref().map(|m| m.filled).unwrap_or(false),
7103 )),
7104 other => Err(plotting_error(
7105 builtin,
7106 format!("{builtin}: unsupported line property `{other}`"),
7107 )),
7108 }
7109}
7110
7111fn histogram_labels_from_edges(edges: &[f64]) -> Vec<String> {
7112 edges
7113 .windows(2)
7114 .map(|pair| format!("[{:.3}, {:.3})", pair[0], pair[1]))
7115 .collect()
7116}
7117
7118pub(crate) fn validate_histogram_normalization(
7119 norm: &str,
7120 builtin: &'static str,
7121) -> BuiltinResult<()> {
7122 match norm {
7123 "count" | "probability" | "countdensity" | "pdf" | "cumcount" | "cdf" => Ok(()),
7124 other => Err(plotting_error(
7125 builtin,
7126 format!("{builtin}: unsupported histogram normalization `{other}`"),
7127 )),
7128 }
7129}
7130
7131pub(crate) fn apply_histogram_normalization(
7132 raw_counts: &[f64],
7133 edges: &[f64],
7134 norm: &str,
7135) -> Vec<f64> {
7136 let widths: Vec<f64> = edges.windows(2).map(|pair| pair[1] - pair[0]).collect();
7137 let total: f64 = raw_counts.iter().sum();
7138 match norm {
7139 "count" => raw_counts.to_vec(),
7140 "probability" => {
7141 if total > 0.0 {
7142 raw_counts.iter().map(|&c| c / total).collect()
7143 } else {
7144 vec![0.0; raw_counts.len()]
7145 }
7146 }
7147 "countdensity" => raw_counts
7148 .iter()
7149 .zip(widths.iter())
7150 .map(|(&c, &w)| if w > 0.0 { c / w } else { 0.0 })
7151 .collect(),
7152 "pdf" => {
7153 if total > 0.0 {
7154 raw_counts
7155 .iter()
7156 .zip(widths.iter())
7157 .map(|(&c, &w)| if w > 0.0 { c / (total * w) } else { 0.0 })
7158 .collect()
7159 } else {
7160 vec![0.0; raw_counts.len()]
7161 }
7162 }
7163 "cumcount" => {
7164 let mut acc = 0.0;
7165 raw_counts
7166 .iter()
7167 .map(|&c| {
7168 acc += c;
7169 acc
7170 })
7171 .collect()
7172 }
7173 "cdf" => {
7174 if total > 0.0 {
7175 let mut acc = 0.0;
7176 raw_counts
7177 .iter()
7178 .map(|&c| {
7179 acc += c;
7180 acc / total
7181 })
7182 .collect()
7183 } else {
7184 vec![0.0; raw_counts.len()]
7185 }
7186 }
7187 _ => raw_counts.to_vec(),
7188 }
7189}
7190
7191fn line_style_name(style: runmat_plot::plots::line::LineStyle) -> &'static str {
7192 match style {
7193 runmat_plot::plots::line::LineStyle::None => "none",
7194 runmat_plot::plots::line::LineStyle::Solid => "-",
7195 runmat_plot::plots::line::LineStyle::Dashed => "--",
7196 runmat_plot::plots::line::LineStyle::Dotted => ":",
7197 runmat_plot::plots::line::LineStyle::DashDot => "-.",
7198 }
7199}
7200
7201fn parse_line_style_name_for_props(name: &str) -> runmat_plot::plots::line::LineStyle {
7202 match name.trim() {
7203 "none" => runmat_plot::plots::line::LineStyle::None,
7204 "--" | "dashed" => runmat_plot::plots::line::LineStyle::Dashed,
7205 ":" | "dotted" => runmat_plot::plots::line::LineStyle::Dotted,
7206 "-." | "dashdot" => runmat_plot::plots::line::LineStyle::DashDot,
7207 _ => runmat_plot::plots::line::LineStyle::Solid,
7208 }
7209}
7210
7211fn marker_style_name(style: runmat_plot::plots::scatter::MarkerStyle) -> &'static str {
7212 match style {
7213 runmat_plot::plots::scatter::MarkerStyle::Circle => "o",
7214 runmat_plot::plots::scatter::MarkerStyle::Square => "s",
7215 runmat_plot::plots::scatter::MarkerStyle::Triangle => "^",
7216 runmat_plot::plots::scatter::MarkerStyle::Diamond => "d",
7217 runmat_plot::plots::scatter::MarkerStyle::Plus => "+",
7218 runmat_plot::plots::scatter::MarkerStyle::Cross => "x",
7219 runmat_plot::plots::scatter::MarkerStyle::Star => "*",
7220 runmat_plot::plots::scatter::MarkerStyle::Hexagon => "h",
7221 }
7222}
7223
7224fn marker_from_name(
7225 name: &str,
7226 current: Option<runmat_plot::plots::line::LineMarkerAppearance>,
7227) -> Option<runmat_plot::plots::line::LineMarkerAppearance> {
7228 let mut marker = current.unwrap_or(runmat_plot::plots::line::LineMarkerAppearance {
7229 kind: runmat_plot::plots::scatter::MarkerStyle::Circle,
7230 size: 6.0,
7231 edge_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
7232 face_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
7233 filled: false,
7234 });
7235 marker.kind = match name.trim() {
7236 "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
7237 "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
7238 "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
7239 "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
7240 "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
7241 "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
7242 "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
7243 "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
7244 "none" => return None,
7245 _ => marker.kind,
7246 };
7247 Some(marker)
7248}
7249
7250fn scatter_marker_from_name(
7251 name: &str,
7252 current: runmat_plot::plots::scatter::MarkerStyle,
7253) -> runmat_plot::plots::scatter::MarkerStyle {
7254 match name.trim() {
7255 "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
7256 "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
7257 "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
7258 "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
7259 "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
7260 "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
7261 "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
7262 "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
7263 _ => current,
7264 }
7265}
7266
7267trait Unzip3Vec<A, B, C> {
7268 fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>);
7269}
7270
7271impl<I, A, B, C> Unzip3Vec<A, B, C> for I
7272where
7273 I: Iterator<Item = (A, B, C)>,
7274{
7275 fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>) {
7276 let mut a = Vec::new();
7277 let mut b = Vec::new();
7278 let mut c = Vec::new();
7279 for (va, vb, vc) in self {
7280 a.push(va);
7281 b.push(vb);
7282 c.push(vc);
7283 }
7284 (a, b, c)
7285 }
7286}
7287
7288fn color_to_short_name(color: glam::Vec4) -> String {
7289 let candidates = [
7290 (glam::Vec4::new(1.0, 0.0, 0.0, 1.0), "r"),
7291 (glam::Vec4::new(0.0, 1.0, 0.0, 1.0), "g"),
7292 (glam::Vec4::new(0.0, 0.0, 1.0, 1.0), "b"),
7293 (glam::Vec4::new(0.0, 0.0, 0.0, 1.0), "k"),
7294 (glam::Vec4::new(1.0, 1.0, 1.0, 1.0), "w"),
7295 (glam::Vec4::new(1.0, 1.0, 0.0, 1.0), "y"),
7296 (glam::Vec4::new(1.0, 0.0, 1.0, 1.0), "m"),
7297 (glam::Vec4::new(0.0, 1.0, 1.0, 1.0), "c"),
7298 ];
7299 for (candidate, name) in candidates {
7300 if (candidate - color).abs().max_element() < 1e-6 {
7301 return name.to_string();
7302 }
7303 }
7304 format!("[{:.3},{:.3},{:.3}]", color.x, color.y, color.z)
7305}
7306
7307fn key_name(kind: PlotObjectKind) -> &'static str {
7308 match kind {
7309 PlotObjectKind::Title => "Title",
7310 PlotObjectKind::Subtitle => "Subtitle",
7311 PlotObjectKind::XLabel => "XLabel",
7312 PlotObjectKind::YLabel => "YLabel",
7313 PlotObjectKind::ZLabel => "ZLabel",
7314 PlotObjectKind::Legend => "Legend",
7315 PlotObjectKind::SuperTitle => "SGTitle",
7316 PlotObjectKind::XAxis => "XAxis",
7317 PlotObjectKind::YAxis => "YAxis",
7318 }
7319}
7320
7321trait AxesMetadataExt {
7322 fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle;
7323}
7324
7325impl AxesMetadataExt for runmat_plot::plots::AxesMetadata {
7326 fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle {
7327 match kind {
7328 PlotObjectKind::Title => self.title_style.clone(),
7329 PlotObjectKind::Subtitle => self.subtitle_style.clone(),
7330 PlotObjectKind::XLabel => self.x_label_style.clone(),
7331 PlotObjectKind::YLabel => self.y_label_style.clone(),
7332 PlotObjectKind::ZLabel => self.z_label_style.clone(),
7333 PlotObjectKind::Legend
7334 | PlotObjectKind::SuperTitle
7335 | PlotObjectKind::XAxis
7336 | PlotObjectKind::YAxis => TextStyle::default(),
7337 }
7338 }
7339}