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