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