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