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::Pie(plot) => get_pie_property(plot, property, builtin),
1201 super::state::PlotChildHandleState::Text(text) => {
1202 get_world_text_property(text, property, builtin)
1203 }
1204 }
1205}
1206
1207fn apply_plot_child_property(
1208 state: &super::state::PlotChildHandleState,
1209 key: &str,
1210 value: &Value,
1211 builtin: &'static str,
1212) -> BuiltinResult<()> {
1213 match state {
1214 super::state::PlotChildHandleState::Histogram(hist) => {
1215 apply_histogram_property(hist, key, value, builtin)
1216 }
1217 super::state::PlotChildHandleState::Line(plot) => {
1218 apply_line_property(plot, key, value, builtin)
1219 }
1220 super::state::PlotChildHandleState::Scatter(plot) => {
1221 apply_scatter_property(plot, key, value, builtin)
1222 }
1223 super::state::PlotChildHandleState::Bar(plot) => {
1224 apply_bar_property(plot, key, value, builtin)
1225 }
1226 super::state::PlotChildHandleState::Stem(stem) => {
1227 apply_stem_property(stem, key, value, builtin)
1228 }
1229 super::state::PlotChildHandleState::ErrorBar(errorbar) => {
1230 apply_errorbar_property(errorbar, key, value, builtin)
1231 }
1232 super::state::PlotChildHandleState::Stairs(plot) => {
1233 apply_stairs_property(plot, key, value, builtin)
1234 }
1235 super::state::PlotChildHandleState::Quiver(quiver) => {
1236 apply_quiver_property(quiver, key, value, builtin)
1237 }
1238 super::state::PlotChildHandleState::Image(image) => {
1239 apply_image_property(image, key, value, builtin)
1240 }
1241 super::state::PlotChildHandleState::Heatmap(heatmap) => {
1242 apply_heatmap_property(heatmap, key, value, builtin)
1243 }
1244 super::state::PlotChildHandleState::Area(area) => {
1245 apply_area_property(area, key, value, builtin)
1246 }
1247 super::state::PlotChildHandleState::Surface(plot) => {
1248 apply_surface_property(plot, key, value, builtin)
1249 }
1250 super::state::PlotChildHandleState::Line3(plot) => {
1251 apply_line3_property(plot, key, value, builtin)
1252 }
1253 super::state::PlotChildHandleState::Scatter3(plot) => {
1254 apply_scatter3_property(plot, key, value, builtin)
1255 }
1256 super::state::PlotChildHandleState::Contour(plot) => {
1257 apply_contour_property(plot, key, value, builtin)
1258 }
1259 super::state::PlotChildHandleState::ContourFill(plot) => {
1260 apply_contour_fill_property(plot, key, value, builtin)
1261 }
1262 super::state::PlotChildHandleState::Pie(plot) => {
1263 apply_pie_property(plot, key, value, builtin)
1264 }
1265 super::state::PlotChildHandleState::Text(text) => {
1266 apply_world_text_property(text, key, value, builtin)
1267 }
1268 }
1269}
1270
1271fn child_parent_handle(figure: FigureHandle, axes_index: usize) -> Value {
1272 Value::Num(super::state::encode_axes_handle(figure, axes_index))
1273}
1274
1275fn child_base_struct(kind: &str, figure: FigureHandle, axes_index: usize) -> StructValue {
1276 let mut st = StructValue::new();
1277 st.insert("Type", Value::String(kind.into()));
1278 st.insert("Parent", child_parent_handle(figure, axes_index));
1279 st.insert("Children", handles_value(Vec::new()));
1280 st
1281}
1282
1283fn text_position_value(position: glam::Vec3) -> Value {
1284 Value::Tensor(Tensor {
1285 rows: 1,
1286 cols: 3,
1287 shape: vec![1, 3],
1288 data: vec![position.x as f64, position.y as f64, position.z as f64],
1289 dtype: runmat_builtins::NumericDType::F64,
1290 })
1291}
1292
1293fn parse_text_position(value: &Value, builtin: &'static str) -> BuiltinResult<glam::Vec3> {
1294 match value {
1295 Value::Tensor(t) if t.data.len() == 2 || t.data.len() == 3 => Ok(glam::Vec3::new(
1296 t.data[0] as f32,
1297 t.data[1] as f32,
1298 t.data.get(2).copied().unwrap_or(0.0) as f32,
1299 )),
1300 _ => Err(plotting_error(
1301 builtin,
1302 format!("{builtin}: Position must be a 2-element or 3-element vector"),
1303 )),
1304 }
1305}
1306
1307fn get_world_text_property(
1308 handle: &super::state::TextAnnotationHandleState,
1309 property: Option<&str>,
1310 builtin: &'static str,
1311) -> BuiltinResult<Value> {
1312 let figure = super::state::clone_figure(handle.figure)
1313 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
1314 let annotation = figure
1315 .axes_text_annotation(handle.axes_index, handle.annotation_index)
1316 .cloned()
1317 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
1318 match property.map(canonical_property_name) {
1319 None => {
1320 let mut st = child_base_struct("text", handle.figure, handle.axes_index);
1321 st.insert("String", Value::String(annotation.text.clone()));
1322 st.insert("Position", text_position_value(annotation.position));
1323 if let Some(weight) = annotation.style.font_weight.clone() {
1324 st.insert("FontWeight", Value::String(weight));
1325 }
1326 if let Some(angle) = annotation.style.font_angle.clone() {
1327 st.insert("FontAngle", Value::String(angle));
1328 }
1329 if let Some(interpreter) = annotation.style.interpreter.clone() {
1330 st.insert("Interpreter", Value::String(interpreter));
1331 }
1332 if let Some(color) = annotation.style.color {
1333 st.insert("Color", Value::String(color_to_short_name(color)));
1334 }
1335 if let Some(font_size) = annotation.style.font_size {
1336 st.insert("FontSize", Value::Num(font_size as f64));
1337 }
1338 st.insert("Visible", Value::Bool(annotation.style.visible));
1339 Ok(Value::Struct(st))
1340 }
1341 Some("type") => Ok(Value::String("text".into())),
1342 Some("parent") => Ok(child_parent_handle(handle.figure, handle.axes_index)),
1343 Some("children") => Ok(handles_value(Vec::new())),
1344 Some("string") => Ok(Value::String(annotation.text)),
1345 Some("position") => Ok(text_position_value(annotation.position)),
1346 Some("fontweight") => Ok(annotation
1347 .style
1348 .font_weight
1349 .map(Value::String)
1350 .unwrap_or_else(|| Value::String(String::new()))),
1351 Some("fontangle") => Ok(annotation
1352 .style
1353 .font_angle
1354 .map(Value::String)
1355 .unwrap_or_else(|| Value::String(String::new()))),
1356 Some("interpreter") => Ok(annotation
1357 .style
1358 .interpreter
1359 .map(Value::String)
1360 .unwrap_or_else(|| Value::String(String::new()))),
1361 Some("color") => Ok(annotation
1362 .style
1363 .color
1364 .map(|c| Value::String(color_to_short_name(c)))
1365 .unwrap_or_else(|| Value::String(String::new()))),
1366 Some("fontsize") => Ok(Value::Num(
1367 annotation.style.font_size.unwrap_or_default() as f64
1368 )),
1369 Some("visible") => Ok(Value::Bool(annotation.style.visible)),
1370 Some(other) => Err(plotting_error(
1371 builtin,
1372 format!("{builtin}: unsupported text property `{other}`"),
1373 )),
1374 }
1375}
1376
1377fn apply_world_text_property(
1378 handle: &super::state::TextAnnotationHandleState,
1379 key: &str,
1380 value: &Value,
1381 builtin: &'static str,
1382) -> BuiltinResult<()> {
1383 let figure = super::state::clone_figure(handle.figure)
1384 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text figure")))?;
1385 let annotation = figure
1386 .axes_text_annotation(handle.axes_index, handle.annotation_index)
1387 .cloned()
1388 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid text handle")))?;
1389 let mut text = None;
1390 let mut position = None;
1391 let mut style = annotation.style;
1392 match canonical_property_name(key) {
1393 "string" => {
1394 text = Some(value_as_text_string(value).ok_or_else(|| {
1395 plotting_error(builtin, format!("{builtin}: String must be text"))
1396 })?);
1397 }
1398 "position" => position = Some(parse_text_position(value, builtin)?),
1399 other => apply_text_property(&mut text, &mut style, other, value, builtin)?,
1400 }
1401 set_text_annotation_properties_for_axes(
1402 handle.figure,
1403 handle.axes_index,
1404 handle.annotation_index,
1405 text,
1406 position,
1407 Some(style),
1408 )
1409 .map_err(|err| map_figure_error(builtin, err))?;
1410 Ok(())
1411}
1412
1413fn get_simple_plot(
1414 plot: &super::state::SimplePlotHandleState,
1415 builtin: &'static str,
1416) -> BuiltinResult<runmat_plot::plots::figure::PlotElement> {
1417 let figure = super::state::clone_figure(plot.figure)
1418 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot figure")))?;
1419 let resolved = figure
1420 .plots()
1421 .nth(plot.plot_index)
1422 .cloned()
1423 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid plot handle")))?;
1424 Ok(resolved)
1425}
1426
1427fn get_line_property(
1428 line_handle: &super::state::SimplePlotHandleState,
1429 property: Option<&str>,
1430 builtin: &'static str,
1431) -> BuiltinResult<Value> {
1432 let plot = get_simple_plot(line_handle, builtin)?;
1433 let runmat_plot::plots::figure::PlotElement::Line(line) = plot else {
1434 return Err(plotting_error(
1435 builtin,
1436 format!("{builtin}: invalid line handle"),
1437 ));
1438 };
1439 match property.map(canonical_property_name) {
1440 None => {
1441 let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
1442 st.insert("XData", tensor_from_vec(line.x_data.clone()));
1443 st.insert("YData", tensor_from_vec(line.y_data.clone()));
1444 st.insert("Color", Value::String(color_to_short_name(line.color)));
1445 st.insert("LineWidth", Value::Num(line.line_width as f64));
1446 st.insert(
1447 "LineStyle",
1448 Value::String(line_style_name(line.line_style).into()),
1449 );
1450 if let Some(label) = line.label.clone() {
1451 st.insert("DisplayName", Value::String(label));
1452 }
1453 insert_line_marker_struct_props(&mut st, line.marker.as_ref());
1454 Ok(Value::Struct(st))
1455 }
1456 Some("type") => Ok(Value::String("line".into())),
1457 Some("parent") => Ok(child_parent_handle(
1458 line_handle.figure,
1459 line_handle.axes_index,
1460 )),
1461 Some("children") => Ok(handles_value(Vec::new())),
1462 Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
1463 Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
1464 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
1465 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
1466 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
1467 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
1468 Some(name) => line_marker_property_value(&line.marker, name, builtin),
1469 }
1470}
1471
1472fn get_stairs_property(
1473 stairs_handle: &super::state::SimplePlotHandleState,
1474 property: Option<&str>,
1475 builtin: &'static str,
1476) -> BuiltinResult<Value> {
1477 let plot = get_simple_plot(stairs_handle, builtin)?;
1478 let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot else {
1479 return Err(plotting_error(
1480 builtin,
1481 format!("{builtin}: invalid stairs handle"),
1482 ));
1483 };
1484 match property.map(canonical_property_name) {
1485 None => {
1486 let mut st =
1487 child_base_struct("stairs", stairs_handle.figure, stairs_handle.axes_index);
1488 st.insert("XData", tensor_from_vec(stairs.x.clone()));
1489 st.insert("YData", tensor_from_vec(stairs.y.clone()));
1490 st.insert("Color", Value::String(color_to_short_name(stairs.color)));
1491 st.insert("LineWidth", Value::Num(stairs.line_width as f64));
1492 if let Some(label) = stairs.label.clone() {
1493 st.insert("DisplayName", Value::String(label));
1494 }
1495 Ok(Value::Struct(st))
1496 }
1497 Some("type") => Ok(Value::String("stairs".into())),
1498 Some("parent") => Ok(child_parent_handle(
1499 stairs_handle.figure,
1500 stairs_handle.axes_index,
1501 )),
1502 Some("children") => Ok(handles_value(Vec::new())),
1503 Some("xdata") => Ok(tensor_from_vec(stairs.x.clone())),
1504 Some("ydata") => Ok(tensor_from_vec(stairs.y.clone())),
1505 Some("color") => Ok(Value::String(color_to_short_name(stairs.color))),
1506 Some("linewidth") => Ok(Value::Num(stairs.line_width as f64)),
1507 Some("displayname") => Ok(Value::String(stairs.label.unwrap_or_default())),
1508 Some(other) => Err(plotting_error(
1509 builtin,
1510 format!("{builtin}: unsupported stairs property `{other}`"),
1511 )),
1512 }
1513}
1514
1515fn get_scatter_property(
1516 scatter_handle: &super::state::SimplePlotHandleState,
1517 property: Option<&str>,
1518 builtin: &'static str,
1519) -> BuiltinResult<Value> {
1520 let plot = get_simple_plot(scatter_handle, builtin)?;
1521 let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot else {
1522 return Err(plotting_error(
1523 builtin,
1524 format!("{builtin}: invalid scatter handle"),
1525 ));
1526 };
1527 match property.map(canonical_property_name) {
1528 None => {
1529 let mut st =
1530 child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
1531 st.insert("XData", tensor_from_vec(scatter.x_data.clone()));
1532 st.insert("YData", tensor_from_vec(scatter.y_data.clone()));
1533 st.insert(
1534 "Marker",
1535 Value::String(marker_style_name(scatter.marker_style).into()),
1536 );
1537 st.insert("SizeData", Value::Num(scatter.marker_size as f64));
1538 st.insert(
1539 "MarkerFaceColor",
1540 Value::String(color_to_short_name(scatter.color)),
1541 );
1542 st.insert(
1543 "MarkerEdgeColor",
1544 Value::String(color_to_short_name(scatter.edge_color)),
1545 );
1546 st.insert("LineWidth", Value::Num(scatter.edge_thickness as f64));
1547 if let Some(label) = scatter.label.clone() {
1548 st.insert("DisplayName", Value::String(label));
1549 }
1550 Ok(Value::Struct(st))
1551 }
1552 Some("type") => Ok(Value::String("scatter".into())),
1553 Some("parent") => Ok(child_parent_handle(
1554 scatter_handle.figure,
1555 scatter_handle.axes_index,
1556 )),
1557 Some("children") => Ok(handles_value(Vec::new())),
1558 Some("xdata") => Ok(tensor_from_vec(scatter.x_data.clone())),
1559 Some("ydata") => Ok(tensor_from_vec(scatter.y_data.clone())),
1560 Some("marker") => Ok(Value::String(
1561 marker_style_name(scatter.marker_style).into(),
1562 )),
1563 Some("sizedata") => Ok(Value::Num(scatter.marker_size as f64)),
1564 Some("markerfacecolor") => Ok(Value::String(color_to_short_name(scatter.color))),
1565 Some("markeredgecolor") => Ok(Value::String(color_to_short_name(scatter.edge_color))),
1566 Some("linewidth") => Ok(Value::Num(scatter.edge_thickness as f64)),
1567 Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
1568 Some(other) => Err(plotting_error(
1569 builtin,
1570 format!("{builtin}: unsupported scatter property `{other}`"),
1571 )),
1572 }
1573}
1574
1575fn get_bar_property(
1576 bar_handle: &super::state::SimplePlotHandleState,
1577 property: Option<&str>,
1578 builtin: &'static str,
1579) -> BuiltinResult<Value> {
1580 let plot = get_simple_plot(bar_handle, builtin)?;
1581 let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot else {
1582 return Err(plotting_error(
1583 builtin,
1584 format!("{builtin}: invalid bar handle"),
1585 ));
1586 };
1587 match property.map(canonical_property_name) {
1588 None => {
1589 let mut st = child_base_struct("bar", bar_handle.figure, bar_handle.axes_index);
1590 st.insert("FaceColor", Value::String(color_to_short_name(bar.color)));
1591 st.insert("BarWidth", Value::Num(bar.bar_width as f64));
1592 if let Some(label) = bar.label.clone() {
1593 st.insert("DisplayName", Value::String(label));
1594 }
1595 Ok(Value::Struct(st))
1596 }
1597 Some("type") => Ok(Value::String("bar".into())),
1598 Some("parent") => Ok(child_parent_handle(
1599 bar_handle.figure,
1600 bar_handle.axes_index,
1601 )),
1602 Some("children") => Ok(handles_value(Vec::new())),
1603 Some("facecolor") | Some("color") => Ok(Value::String(color_to_short_name(bar.color))),
1604 Some("barwidth") => Ok(Value::Num(bar.bar_width as f64)),
1605 Some("displayname") => Ok(Value::String(bar.label.unwrap_or_default())),
1606 Some(other) => Err(plotting_error(
1607 builtin,
1608 format!("{builtin}: unsupported bar property `{other}`"),
1609 )),
1610 }
1611}
1612
1613fn get_surface_property(
1614 surface_handle: &super::state::SimplePlotHandleState,
1615 property: Option<&str>,
1616 builtin: &'static str,
1617) -> BuiltinResult<Value> {
1618 let plot = get_simple_plot(surface_handle, builtin)?;
1619 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
1620 return Err(plotting_error(
1621 builtin,
1622 format!("{builtin}: invalid surface handle"),
1623 ));
1624 };
1625 match property.map(canonical_property_name) {
1626 None => {
1627 let mut st =
1628 child_base_struct("surface", surface_handle.figure, surface_handle.axes_index);
1629 st.insert("XData", tensor_from_vec(surface.x_data.clone()));
1630 st.insert("YData", tensor_from_vec(surface.y_data.clone()));
1631 if let Some(z) = surface.z_data.clone() {
1632 st.insert("ZData", tensor_from_matrix(z));
1633 }
1634 st.insert("FaceAlpha", Value::Num(surface.alpha as f64));
1635 if let Some(label) = surface.label.clone() {
1636 st.insert("DisplayName", Value::String(label));
1637 }
1638 Ok(Value::Struct(st))
1639 }
1640 Some("type") => Ok(Value::String("surface".into())),
1641 Some("parent") => Ok(child_parent_handle(
1642 surface_handle.figure,
1643 surface_handle.axes_index,
1644 )),
1645 Some("children") => Ok(handles_value(Vec::new())),
1646 Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
1647 Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
1648 Some("zdata") => Ok(surface
1649 .z_data
1650 .clone()
1651 .map(tensor_from_matrix)
1652 .unwrap_or_else(|| tensor_from_vec(Vec::new()))),
1653 Some("facealpha") => Ok(Value::Num(surface.alpha as f64)),
1654 Some("displayname") => Ok(Value::String(surface.label.unwrap_or_default())),
1655 Some(other) => Err(plotting_error(
1656 builtin,
1657 format!("{builtin}: unsupported surface property `{other}`"),
1658 )),
1659 }
1660}
1661
1662fn get_line3_property(
1663 line_handle: &super::state::SimplePlotHandleState,
1664 property: Option<&str>,
1665 builtin: &'static str,
1666) -> BuiltinResult<Value> {
1667 let plot = get_simple_plot(line_handle, builtin)?;
1668 let runmat_plot::plots::figure::PlotElement::Line3(line) = plot else {
1669 return Err(plotting_error(
1670 builtin,
1671 format!("{builtin}: invalid plot3 handle"),
1672 ));
1673 };
1674 match property.map(canonical_property_name) {
1675 None => {
1676 let mut st = child_base_struct("line", line_handle.figure, line_handle.axes_index);
1677 st.insert("XData", tensor_from_vec(line.x_data.clone()));
1678 st.insert("YData", tensor_from_vec(line.y_data.clone()));
1679 st.insert("ZData", tensor_from_vec(line.z_data.clone()));
1680 st.insert("Color", Value::String(color_to_short_name(line.color)));
1681 st.insert("LineWidth", Value::Num(line.line_width as f64));
1682 st.insert(
1683 "LineStyle",
1684 Value::String(line_style_name(line.line_style).into()),
1685 );
1686 if let Some(label) = line.label.clone() {
1687 st.insert("DisplayName", Value::String(label));
1688 }
1689 Ok(Value::Struct(st))
1690 }
1691 Some("type") => Ok(Value::String("line".into())),
1692 Some("parent") => Ok(child_parent_handle(
1693 line_handle.figure,
1694 line_handle.axes_index,
1695 )),
1696 Some("children") => Ok(handles_value(Vec::new())),
1697 Some("xdata") => Ok(tensor_from_vec(line.x_data.clone())),
1698 Some("ydata") => Ok(tensor_from_vec(line.y_data.clone())),
1699 Some("zdata") => Ok(tensor_from_vec(line.z_data.clone())),
1700 Some("color") => Ok(Value::String(color_to_short_name(line.color))),
1701 Some("linewidth") => Ok(Value::Num(line.line_width as f64)),
1702 Some("linestyle") => Ok(Value::String(line_style_name(line.line_style).into())),
1703 Some("displayname") => Ok(Value::String(line.label.unwrap_or_default())),
1704 Some(other) => Err(plotting_error(
1705 builtin,
1706 format!("{builtin}: unsupported plot3 property `{other}`"),
1707 )),
1708 }
1709}
1710
1711fn get_scatter3_property(
1712 scatter_handle: &super::state::SimplePlotHandleState,
1713 property: Option<&str>,
1714 builtin: &'static str,
1715) -> BuiltinResult<Value> {
1716 let plot = get_simple_plot(scatter_handle, builtin)?;
1717 let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot else {
1718 return Err(plotting_error(
1719 builtin,
1720 format!("{builtin}: invalid scatter3 handle"),
1721 ));
1722 };
1723 let (x, y, z): (Vec<f64>, Vec<f64>, Vec<f64>) = scatter
1724 .points
1725 .iter()
1726 .map(|p| (p.x as f64, p.y as f64, p.z as f64))
1727 .unzip_n_vec();
1728 match property.map(canonical_property_name) {
1729 None => {
1730 let mut st =
1731 child_base_struct("scatter", scatter_handle.figure, scatter_handle.axes_index);
1732 st.insert("XData", tensor_from_vec(x));
1733 st.insert("YData", tensor_from_vec(y));
1734 st.insert("ZData", tensor_from_vec(z));
1735 st.insert("SizeData", Value::Num(scatter.point_size as f64));
1736 if let Some(label) = scatter.label.clone() {
1737 st.insert("DisplayName", Value::String(label));
1738 }
1739 Ok(Value::Struct(st))
1740 }
1741 Some("type") => Ok(Value::String("scatter".into())),
1742 Some("parent") => Ok(child_parent_handle(
1743 scatter_handle.figure,
1744 scatter_handle.axes_index,
1745 )),
1746 Some("children") => Ok(handles_value(Vec::new())),
1747 Some("sizedata") => Ok(Value::Num(scatter.point_size as f64)),
1748 Some("displayname") => Ok(Value::String(scatter.label.unwrap_or_default())),
1749 Some(other) => Err(plotting_error(
1750 builtin,
1751 format!("{builtin}: unsupported scatter3 property `{other}`"),
1752 )),
1753 }
1754}
1755
1756fn get_pie_property(
1757 pie_handle: &super::state::SimplePlotHandleState,
1758 property: Option<&str>,
1759 builtin: &'static str,
1760) -> BuiltinResult<Value> {
1761 let plot = get_simple_plot(pie_handle, builtin)?;
1762 let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot else {
1763 return Err(plotting_error(
1764 builtin,
1765 format!("{builtin}: invalid pie handle"),
1766 ));
1767 };
1768 match property.map(canonical_property_name) {
1769 None => {
1770 let mut st = child_base_struct("pie", pie_handle.figure, pie_handle.axes_index);
1771 if let Some(label) = pie.label.clone() {
1772 st.insert("DisplayName", Value::String(label));
1773 }
1774 Ok(Value::Struct(st))
1775 }
1776 Some("type") => Ok(Value::String("pie".into())),
1777 Some("parent") => Ok(child_parent_handle(
1778 pie_handle.figure,
1779 pie_handle.axes_index,
1780 )),
1781 Some("children") => Ok(handles_value(Vec::new())),
1782 Some("displayname") => Ok(Value::String(pie.label.unwrap_or_default())),
1783 Some(other) => Err(plotting_error(
1784 builtin,
1785 format!("{builtin}: unsupported pie property `{other}`"),
1786 )),
1787 }
1788}
1789
1790fn get_contour_property(
1791 contour_handle: &super::state::SimplePlotHandleState,
1792 property: Option<&str>,
1793 builtin: &'static str,
1794) -> BuiltinResult<Value> {
1795 let plot = get_simple_plot(contour_handle, builtin)?;
1796 let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot else {
1797 return Err(plotting_error(
1798 builtin,
1799 format!("{builtin}: invalid contour handle"),
1800 ));
1801 };
1802 match property.map(canonical_property_name) {
1803 None => {
1804 let mut st =
1805 child_base_struct("contour", contour_handle.figure, contour_handle.axes_index);
1806 st.insert("ZData", Value::Num(contour.base_z as f64));
1807 if let Some(label) = contour.label.clone() {
1808 st.insert("DisplayName", Value::String(label));
1809 }
1810 Ok(Value::Struct(st))
1811 }
1812 Some("type") => Ok(Value::String("contour".into())),
1813 Some("parent") => Ok(child_parent_handle(
1814 contour_handle.figure,
1815 contour_handle.axes_index,
1816 )),
1817 Some("children") => Ok(handles_value(Vec::new())),
1818 Some("zdata") => Ok(Value::Num(contour.base_z as f64)),
1819 Some("displayname") => Ok(Value::String(contour.label.unwrap_or_default())),
1820 Some(other) => Err(plotting_error(
1821 builtin,
1822 format!("{builtin}: unsupported contour property `{other}`"),
1823 )),
1824 }
1825}
1826
1827fn get_contour_fill_property(
1828 fill_handle: &super::state::SimplePlotHandleState,
1829 property: Option<&str>,
1830 builtin: &'static str,
1831) -> BuiltinResult<Value> {
1832 let plot = get_simple_plot(fill_handle, builtin)?;
1833 let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot else {
1834 return Err(plotting_error(
1835 builtin,
1836 format!("{builtin}: invalid contourf handle"),
1837 ));
1838 };
1839 match property.map(canonical_property_name) {
1840 None => {
1841 let mut st = child_base_struct("contour", fill_handle.figure, fill_handle.axes_index);
1842 if let Some(label) = fill.label.clone() {
1843 st.insert("DisplayName", Value::String(label));
1844 }
1845 Ok(Value::Struct(st))
1846 }
1847 Some("type") => Ok(Value::String("contour".into())),
1848 Some("parent") => Ok(child_parent_handle(
1849 fill_handle.figure,
1850 fill_handle.axes_index,
1851 )),
1852 Some("children") => Ok(handles_value(Vec::new())),
1853 Some("displayname") => Ok(Value::String(fill.label.unwrap_or_default())),
1854 Some(other) => Err(plotting_error(
1855 builtin,
1856 format!("{builtin}: unsupported contourf property `{other}`"),
1857 )),
1858 }
1859}
1860
1861fn get_stem_property(
1862 stem_handle: &super::state::StemHandleState,
1863 property: Option<&str>,
1864 builtin: &'static str,
1865) -> BuiltinResult<Value> {
1866 let figure = super::state::clone_figure(stem_handle.figure)
1867 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem figure")))?;
1868 let plot = figure
1869 .plots()
1870 .nth(stem_handle.plot_index)
1871 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid stem handle")))?;
1872 let runmat_plot::plots::figure::PlotElement::Stem(stem) = plot else {
1873 return Err(plotting_error(
1874 builtin,
1875 format!("{builtin}: invalid stem handle"),
1876 ));
1877 };
1878 match property.map(canonical_property_name) {
1879 None => {
1880 let mut st = StructValue::new();
1881 st.insert("Type", Value::String("stem".into()));
1882 st.insert(
1883 "Parent",
1884 Value::Num(super::state::encode_axes_handle(
1885 stem_handle.figure,
1886 stem_handle.axes_index,
1887 )),
1888 );
1889 st.insert("Children", handles_value(Vec::new()));
1890 st.insert("BaseValue", Value::Num(stem.baseline));
1891 st.insert("BaseLine", Value::Bool(stem.baseline_visible));
1892 st.insert("LineWidth", Value::Num(stem.line_width as f64));
1893 st.insert(
1894 "LineStyle",
1895 Value::String(line_style_name(stem.line_style).into()),
1896 );
1897 st.insert("Color", Value::String(color_to_short_name(stem.color)));
1898 if let Some(marker) = &stem.marker {
1899 st.insert(
1900 "Marker",
1901 Value::String(marker_style_name(marker.kind).into()),
1902 );
1903 st.insert("MarkerSize", Value::Num(marker.size as f64));
1904 st.insert(
1905 "MarkerFaceColor",
1906 Value::String(color_to_short_name(marker.face_color)),
1907 );
1908 st.insert(
1909 "MarkerEdgeColor",
1910 Value::String(color_to_short_name(marker.edge_color)),
1911 );
1912 st.insert("Filled", Value::Bool(marker.filled));
1913 }
1914 Ok(Value::Struct(st))
1915 }
1916 Some("type") => Ok(Value::String("stem".into())),
1917 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
1918 stem_handle.figure,
1919 stem_handle.axes_index,
1920 ))),
1921 Some("children") => Ok(handles_value(Vec::new())),
1922 Some("basevalue") => Ok(Value::Num(stem.baseline)),
1923 Some("baseline") => Ok(Value::Bool(stem.baseline_visible)),
1924 Some("linewidth") => Ok(Value::Num(stem.line_width as f64)),
1925 Some("linestyle") => Ok(Value::String(line_style_name(stem.line_style).into())),
1926 Some("color") => Ok(Value::String(color_to_short_name(stem.color))),
1927 Some("marker") => Ok(Value::String(
1928 stem.marker
1929 .as_ref()
1930 .map(|m| marker_style_name(m.kind).to_string())
1931 .unwrap_or("none".into()),
1932 )),
1933 Some("markersize") => Ok(Value::Num(
1934 stem.marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
1935 )),
1936 Some("markerfacecolor") => Ok(Value::String(
1937 stem.marker
1938 .as_ref()
1939 .map(|m| color_to_short_name(m.face_color))
1940 .unwrap_or("none".into()),
1941 )),
1942 Some("markeredgecolor") => Ok(Value::String(
1943 stem.marker
1944 .as_ref()
1945 .map(|m| color_to_short_name(m.edge_color))
1946 .unwrap_or("none".into()),
1947 )),
1948 Some("filled") => Ok(Value::Bool(
1949 stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
1950 )),
1951 Some(other) => Err(plotting_error(
1952 builtin,
1953 format!("{builtin}: unsupported stem property `{other}`"),
1954 )),
1955 }
1956}
1957
1958fn get_errorbar_property(
1959 error_handle: &super::state::ErrorBarHandleState,
1960 property: Option<&str>,
1961 builtin: &'static str,
1962) -> BuiltinResult<Value> {
1963 let figure = super::state::clone_figure(error_handle.figure)
1964 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar figure")))?;
1965 let plot = figure
1966 .plots()
1967 .nth(error_handle.plot_index)
1968 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid errorbar handle")))?;
1969 let runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) = plot else {
1970 return Err(plotting_error(
1971 builtin,
1972 format!("{builtin}: invalid errorbar handle"),
1973 ));
1974 };
1975 match property.map(canonical_property_name) {
1976 None => {
1977 let mut st = StructValue::new();
1978 st.insert("Type", Value::String("errorbar".into()));
1979 st.insert(
1980 "Parent",
1981 Value::Num(super::state::encode_axes_handle(
1982 error_handle.figure,
1983 error_handle.axes_index,
1984 )),
1985 );
1986 st.insert("Children", handles_value(Vec::new()));
1987 st.insert("LineWidth", Value::Num(errorbar.line_width as f64));
1988 st.insert(
1989 "LineStyle",
1990 Value::String(line_style_name(errorbar.line_style).into()),
1991 );
1992 st.insert("Color", Value::String(color_to_short_name(errorbar.color)));
1993 st.insert("CapSize", Value::Num(errorbar.cap_size as f64));
1994 if let Some(marker) = &errorbar.marker {
1995 st.insert(
1996 "Marker",
1997 Value::String(marker_style_name(marker.kind).into()),
1998 );
1999 st.insert("MarkerSize", Value::Num(marker.size as f64));
2000 }
2001 Ok(Value::Struct(st))
2002 }
2003 Some("type") => Ok(Value::String("errorbar".into())),
2004 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2005 error_handle.figure,
2006 error_handle.axes_index,
2007 ))),
2008 Some("children") => Ok(handles_value(Vec::new())),
2009 Some("linewidth") => Ok(Value::Num(errorbar.line_width as f64)),
2010 Some("linestyle") => Ok(Value::String(line_style_name(errorbar.line_style).into())),
2011 Some("color") => Ok(Value::String(color_to_short_name(errorbar.color))),
2012 Some("capsize") => Ok(Value::Num(errorbar.cap_size as f64)),
2013 Some("marker") => Ok(Value::String(
2014 errorbar
2015 .marker
2016 .as_ref()
2017 .map(|m| marker_style_name(m.kind).to_string())
2018 .unwrap_or("none".into()),
2019 )),
2020 Some("markersize") => Ok(Value::Num(
2021 errorbar
2022 .marker
2023 .as_ref()
2024 .map(|m| m.size as f64)
2025 .unwrap_or(0.0),
2026 )),
2027 Some(other) => Err(plotting_error(
2028 builtin,
2029 format!("{builtin}: unsupported errorbar property `{other}`"),
2030 )),
2031 }
2032}
2033
2034fn get_quiver_property(
2035 quiver_handle: &super::state::QuiverHandleState,
2036 property: Option<&str>,
2037 builtin: &'static str,
2038) -> BuiltinResult<Value> {
2039 let figure = super::state::clone_figure(quiver_handle.figure)
2040 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver figure")))?;
2041 let plot = figure
2042 .plots()
2043 .nth(quiver_handle.plot_index)
2044 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid quiver handle")))?;
2045 let runmat_plot::plots::figure::PlotElement::Quiver(quiver) = plot else {
2046 return Err(plotting_error(
2047 builtin,
2048 format!("{builtin}: invalid quiver handle"),
2049 ));
2050 };
2051 match property.map(canonical_property_name) {
2052 None => {
2053 let mut st = StructValue::new();
2054 st.insert("Type", Value::String("quiver".into()));
2055 st.insert(
2056 "Parent",
2057 Value::Num(super::state::encode_axes_handle(
2058 quiver_handle.figure,
2059 quiver_handle.axes_index,
2060 )),
2061 );
2062 st.insert("Children", handles_value(Vec::new()));
2063 st.insert("Color", Value::String(color_to_short_name(quiver.color)));
2064 st.insert("LineWidth", Value::Num(quiver.line_width as f64));
2065 st.insert("AutoScaleFactor", Value::Num(quiver.scale as f64));
2066 st.insert("MaxHeadSize", Value::Num(quiver.head_size as f64));
2067 Ok(Value::Struct(st))
2068 }
2069 Some("type") => Ok(Value::String("quiver".into())),
2070 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2071 quiver_handle.figure,
2072 quiver_handle.axes_index,
2073 ))),
2074 Some("children") => Ok(handles_value(Vec::new())),
2075 Some("color") => Ok(Value::String(color_to_short_name(quiver.color))),
2076 Some("linewidth") => Ok(Value::Num(quiver.line_width as f64)),
2077 Some("autoscalefactor") => Ok(Value::Num(quiver.scale as f64)),
2078 Some("maxheadsize") => Ok(Value::Num(quiver.head_size as f64)),
2079 Some(other) => Err(plotting_error(
2080 builtin,
2081 format!("{builtin}: unsupported quiver property `{other}`"),
2082 )),
2083 }
2084}
2085
2086fn get_image_property(
2087 image_handle: &super::state::ImageHandleState,
2088 property: Option<&str>,
2089 builtin: &'static str,
2090) -> BuiltinResult<Value> {
2091 let figure = super::state::clone_figure(image_handle.figure)
2092 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image figure")))?;
2093 let plot = figure
2094 .plots()
2095 .nth(image_handle.plot_index)
2096 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid image handle")))?;
2097 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
2098 return Err(plotting_error(
2099 builtin,
2100 format!("{builtin}: invalid image handle"),
2101 ));
2102 };
2103 if !surface.image_mode {
2104 return Err(plotting_error(
2105 builtin,
2106 format!("{builtin}: handle does not reference an image plot"),
2107 ));
2108 }
2109 match property.map(canonical_property_name) {
2110 None => {
2111 let mut st = StructValue::new();
2112 st.insert("Type", Value::String("image".into()));
2113 st.insert(
2114 "Parent",
2115 Value::Num(super::state::encode_axes_handle(
2116 image_handle.figure,
2117 image_handle.axes_index,
2118 )),
2119 );
2120 st.insert("Children", handles_value(Vec::new()));
2121 st.insert("XData", tensor_from_vec(surface.x_data.clone()));
2122 st.insert("YData", tensor_from_vec(surface.y_data.clone()));
2123 st.insert(
2124 "CDataMapping",
2125 Value::String(if surface.color_grid.is_some() {
2126 "direct".into()
2127 } else {
2128 "scaled".into()
2129 }),
2130 );
2131 Ok(Value::Struct(st))
2132 }
2133 Some("type") => Ok(Value::String("image".into())),
2134 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2135 image_handle.figure,
2136 image_handle.axes_index,
2137 ))),
2138 Some("children") => Ok(handles_value(Vec::new())),
2139 Some("xdata") => Ok(tensor_from_vec(surface.x_data.clone())),
2140 Some("ydata") => Ok(tensor_from_vec(surface.y_data.clone())),
2141 Some("cdatamapping") => Ok(Value::String(if surface.color_grid.is_some() {
2142 "direct".into()
2143 } else {
2144 "scaled".into()
2145 })),
2146 Some(other) => Err(plotting_error(
2147 builtin,
2148 format!("{builtin}: unsupported image property `{other}`"),
2149 )),
2150 }
2151}
2152
2153fn get_heatmap_property(
2154 heatmap_handle: &super::state::HeatmapHandleState,
2155 property: Option<&str>,
2156 builtin: &'static str,
2157) -> BuiltinResult<Value> {
2158 let figure = super::state::clone_figure(heatmap_handle.figure)
2159 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap figure")))?;
2160 let plot = figure
2161 .plots()
2162 .nth(heatmap_handle.plot_index)
2163 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap handle")))?;
2164 let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot else {
2165 return Err(plotting_error(
2166 builtin,
2167 format!("{builtin}: invalid heatmap handle"),
2168 ));
2169 };
2170 if !surface.image_mode {
2171 return Err(plotting_error(
2172 builtin,
2173 format!("{builtin}: handle does not reference a heatmap plot"),
2174 ));
2175 }
2176 let meta = figure
2177 .axes_metadata(heatmap_handle.axes_index)
2178 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid heatmap axes")))?;
2179 match property.map(canonical_property_name) {
2180 None => {
2181 let mut st = StructValue::new();
2182 st.insert("Type", Value::String("heatmap".into()));
2183 st.insert(
2184 "Parent",
2185 Value::Num(super::state::encode_axes_handle(
2186 heatmap_handle.figure,
2187 heatmap_handle.axes_index,
2188 )),
2189 );
2190 st.insert("Children", handles_value(Vec::new()));
2191 st.insert(
2192 "Title",
2193 Value::String(meta.title.clone().unwrap_or_default()),
2194 );
2195 st.insert(
2196 "XLabel",
2197 Value::String(meta.x_label.clone().unwrap_or_default()),
2198 );
2199 st.insert(
2200 "YLabel",
2201 Value::String(meta.y_label.clone().unwrap_or_default()),
2202 );
2203 st.insert(
2204 "XDisplayLabels",
2205 string_array_from_vec(heatmap_handle.x_labels.clone())?,
2206 );
2207 st.insert(
2208 "YDisplayLabels",
2209 string_array_from_vec(heatmap_handle.y_labels.clone())?,
2210 );
2211 st.insert(
2212 "ColorData",
2213 Value::Tensor(heatmap_handle.color_data.clone()),
2214 );
2215 st.insert("ColorbarVisible", Value::Bool(meta.colorbar_enabled));
2216 st.insert(
2217 "Colormap",
2218 Value::String(format!("{:?}", meta.colormap).to_ascii_lowercase()),
2219 );
2220 Ok(Value::Struct(st))
2221 }
2222 Some("type") => Ok(Value::String("heatmap".into())),
2223 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2224 heatmap_handle.figure,
2225 heatmap_handle.axes_index,
2226 ))),
2227 Some("children") => Ok(handles_value(Vec::new())),
2228 Some("title") => Ok(Value::String(meta.title.clone().unwrap_or_default())),
2229 Some("xlabel") => Ok(Value::String(meta.x_label.clone().unwrap_or_default())),
2230 Some("ylabel") => Ok(Value::String(meta.y_label.clone().unwrap_or_default())),
2231 Some("xdisplaylabels") => string_array_from_vec(heatmap_handle.x_labels.clone()),
2232 Some("ydisplaylabels") => string_array_from_vec(heatmap_handle.y_labels.clone()),
2233 Some("colordata") => Ok(Value::Tensor(heatmap_handle.color_data.clone())),
2234 Some("colorbarvisible") | Some("colorbar") => Ok(Value::Bool(meta.colorbar_enabled)),
2235 Some("colormap") => Ok(Value::String(
2236 format!("{:?}", meta.colormap).to_ascii_lowercase(),
2237 )),
2238 Some(other) => Err(plotting_error(
2239 builtin,
2240 format!("{builtin}: unsupported heatmap property `{other}`"),
2241 )),
2242 }
2243}
2244
2245fn get_area_property(
2246 area_handle: &super::state::AreaHandleState,
2247 property: Option<&str>,
2248 builtin: &'static str,
2249) -> BuiltinResult<Value> {
2250 let figure = super::state::clone_figure(area_handle.figure)
2251 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area figure")))?;
2252 let plot = figure
2253 .plots()
2254 .nth(area_handle.plot_index)
2255 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid area handle")))?;
2256 let runmat_plot::plots::figure::PlotElement::Area(area) = plot else {
2257 return Err(plotting_error(
2258 builtin,
2259 format!("{builtin}: invalid area handle"),
2260 ));
2261 };
2262 match property.map(canonical_property_name) {
2263 None => {
2264 let mut st = StructValue::new();
2265 st.insert("Type", Value::String("area".into()));
2266 st.insert(
2267 "Parent",
2268 Value::Num(super::state::encode_axes_handle(
2269 area_handle.figure,
2270 area_handle.axes_index,
2271 )),
2272 );
2273 st.insert("Children", handles_value(Vec::new()));
2274 st.insert("XData", tensor_from_vec(area.x.clone()));
2275 st.insert("YData", tensor_from_vec(area.y.clone()));
2276 st.insert("BaseValue", Value::Num(area.baseline));
2277 st.insert("Color", Value::String(color_to_short_name(area.color)));
2278 Ok(Value::Struct(st))
2279 }
2280 Some("type") => Ok(Value::String("area".into())),
2281 Some("parent") => Ok(Value::Num(super::state::encode_axes_handle(
2282 area_handle.figure,
2283 area_handle.axes_index,
2284 ))),
2285 Some("children") => Ok(handles_value(Vec::new())),
2286 Some("xdata") => Ok(tensor_from_vec(area.x.clone())),
2287 Some("ydata") => Ok(tensor_from_vec(area.y.clone())),
2288 Some("basevalue") => Ok(Value::Num(area.baseline)),
2289 Some("color") => Ok(Value::String(color_to_short_name(area.color))),
2290 Some(other) => Err(plotting_error(
2291 builtin,
2292 format!("{builtin}: unsupported area property `{other}`"),
2293 )),
2294 }
2295}
2296
2297fn apply_histogram_property(
2298 hist: &super::state::HistogramHandleState,
2299 key: &str,
2300 value: &Value,
2301 builtin: &'static str,
2302) -> BuiltinResult<()> {
2303 match key {
2304 "normalization" => {
2305 let norm = value_as_string(value)
2306 .ok_or_else(|| {
2307 plotting_error(
2308 builtin,
2309 format!("{builtin}: Normalization must be a string"),
2310 )
2311 })?
2312 .trim()
2313 .to_ascii_lowercase();
2314 validate_histogram_normalization(&norm, builtin)?;
2315 let normalized =
2316 apply_histogram_normalization(&hist.raw_counts, &hist.bin_edges, &norm);
2317 let labels = histogram_labels_from_edges(&hist.bin_edges);
2318 super::state::update_histogram_plot_data(
2319 hist.figure,
2320 hist.plot_index,
2321 labels,
2322 normalized,
2323 )
2324 .map_err(|err| map_figure_error(builtin, err))?;
2325 super::state::update_histogram_handle_for_plot(
2326 hist.figure,
2327 hist.axes_index,
2328 hist.plot_index,
2329 norm,
2330 hist.raw_counts.clone(),
2331 )
2332 .map_err(|err| map_figure_error(builtin, err))?;
2333 Ok(())
2334 }
2335 other => Err(plotting_error(
2336 builtin,
2337 format!("{builtin}: unsupported histogram property `{other}`"),
2338 )),
2339 }
2340}
2341
2342fn apply_stem_property(
2343 stem_handle: &super::state::StemHandleState,
2344 key: &str,
2345 value: &Value,
2346 builtin: &'static str,
2347) -> BuiltinResult<()> {
2348 super::state::update_stem_plot(
2349 stem_handle.figure,
2350 stem_handle.plot_index,
2351 |stem| match key {
2352 "basevalue" => {
2353 if let Some(v) = value_as_f64(value) {
2354 stem.baseline = v;
2355 }
2356 }
2357 "baseline" => {
2358 if let Some(v) = value_as_bool(value) {
2359 stem.baseline_visible = v;
2360 }
2361 }
2362 "linewidth" => {
2363 if let Some(v) = value_as_f64(value) {
2364 stem.line_width = v as f32;
2365 }
2366 }
2367 "linestyle" => {
2368 if let Some(s) = value_as_string(value) {
2369 stem.line_style = parse_line_style_name_for_props(&s);
2370 }
2371 }
2372 "color" => {
2373 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2374 stem.color = c;
2375 }
2376 }
2377 "marker" => {
2378 if let Some(s) = value_as_string(value) {
2379 stem.marker = marker_from_name(&s, stem.marker.clone());
2380 }
2381 }
2382 "markersize" => {
2383 if let Some(v) = value_as_f64(value) {
2384 if let Some(marker) = &mut stem.marker {
2385 marker.size = v as f32;
2386 }
2387 }
2388 }
2389 "markerfacecolor" => {
2390 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2391 if let Some(marker) = &mut stem.marker {
2392 marker.face_color = c;
2393 }
2394 }
2395 }
2396 "markeredgecolor" => {
2397 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2398 if let Some(marker) = &mut stem.marker {
2399 marker.edge_color = c;
2400 }
2401 }
2402 }
2403 "filled" => {
2404 if let Some(v) = value_as_bool(value) {
2405 if let Some(marker) = &mut stem.marker {
2406 marker.filled = v;
2407 }
2408 }
2409 }
2410 _ => {}
2411 },
2412 )
2413 .map_err(|err| map_figure_error(builtin, err))?;
2414 Ok(())
2415}
2416
2417fn apply_errorbar_property(
2418 error_handle: &super::state::ErrorBarHandleState,
2419 key: &str,
2420 value: &Value,
2421 builtin: &'static str,
2422) -> BuiltinResult<()> {
2423 super::state::update_errorbar_plot(error_handle.figure, error_handle.plot_index, |errorbar| {
2424 match key {
2425 "linewidth" => {
2426 if let Some(v) = value_as_f64(value) {
2427 errorbar.line_width = v as f32;
2428 }
2429 }
2430 "linestyle" => {
2431 if let Some(s) = value_as_string(value) {
2432 errorbar.line_style = parse_line_style_name_for_props(&s);
2433 }
2434 }
2435 "color" => {
2436 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2437 errorbar.color = c;
2438 }
2439 }
2440 "capsize" => {
2441 if let Some(v) = value_as_f64(value) {
2442 errorbar.cap_size = v as f32;
2443 }
2444 }
2445 "marker" => {
2446 if let Some(s) = value_as_string(value) {
2447 errorbar.marker = marker_from_name(&s, errorbar.marker.clone());
2448 }
2449 }
2450 "markersize" => {
2451 if let Some(v) = value_as_f64(value) {
2452 if let Some(marker) = &mut errorbar.marker {
2453 marker.size = v as f32;
2454 }
2455 }
2456 }
2457 _ => {}
2458 }
2459 })
2460 .map_err(|err| map_figure_error(builtin, err))?;
2461 Ok(())
2462}
2463
2464fn apply_quiver_property(
2465 quiver_handle: &super::state::QuiverHandleState,
2466 key: &str,
2467 value: &Value,
2468 builtin: &'static str,
2469) -> BuiltinResult<()> {
2470 super::state::update_quiver_plot(quiver_handle.figure, quiver_handle.plot_index, |quiver| {
2471 match key {
2472 "color" => {
2473 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2474 quiver.color = c;
2475 }
2476 }
2477 "linewidth" => {
2478 if let Some(v) = value_as_f64(value) {
2479 quiver.line_width = v as f32;
2480 }
2481 }
2482 "autoscalefactor" => {
2483 if let Some(v) = value_as_f64(value) {
2484 quiver.scale = v as f32;
2485 }
2486 }
2487 "maxheadsize" => {
2488 if let Some(v) = value_as_f64(value) {
2489 quiver.head_size = v as f32;
2490 }
2491 }
2492 _ => {}
2493 }
2494 })
2495 .map_err(|err| map_figure_error(builtin, err))?;
2496 Ok(())
2497}
2498
2499fn apply_image_property(
2500 image_handle: &super::state::ImageHandleState,
2501 key: &str,
2502 value: &Value,
2503 builtin: &'static str,
2504) -> BuiltinResult<()> {
2505 super::state::update_image_plot(image_handle.figure, image_handle.plot_index, |surface| {
2506 match key {
2507 "xdata" => {
2508 if let Ok(tensor) = Tensor::try_from(value) {
2509 surface.x_data = tensor.data;
2510 }
2511 }
2512 "ydata" => {
2513 if let Ok(tensor) = Tensor::try_from(value) {
2514 surface.y_data = tensor.data;
2515 }
2516 }
2517 "cdatamapping" => {
2518 if let Some(text) = value_as_string(value) {
2519 if text.trim().eq_ignore_ascii_case("direct") {
2520 surface.image_mode = true;
2521 }
2522 }
2523 }
2524 _ => {}
2525 }
2526 })
2527 .map_err(|err| map_figure_error(builtin, err))?;
2528 Ok(())
2529}
2530
2531fn apply_heatmap_property(
2532 heatmap_handle: &super::state::HeatmapHandleState,
2533 key: &str,
2534 value: &Value,
2535 builtin: &'static str,
2536) -> BuiltinResult<()> {
2537 match key {
2538 "title" => apply_axes_property(
2539 heatmap_handle.figure,
2540 heatmap_handle.axes_index,
2541 "title",
2542 value,
2543 builtin,
2544 ),
2545 "xlabel" => apply_axes_property(
2546 heatmap_handle.figure,
2547 heatmap_handle.axes_index,
2548 "xlabel",
2549 value,
2550 builtin,
2551 ),
2552 "ylabel" => apply_axes_property(
2553 heatmap_handle.figure,
2554 heatmap_handle.axes_index,
2555 "ylabel",
2556 value,
2557 builtin,
2558 ),
2559 "colorbar" | "colorbarvisible" => apply_axes_property(
2560 heatmap_handle.figure,
2561 heatmap_handle.axes_index,
2562 "colorbar",
2563 value,
2564 builtin,
2565 ),
2566 "colormap" => apply_axes_property(
2567 heatmap_handle.figure,
2568 heatmap_handle.axes_index,
2569 "colormap",
2570 value,
2571 builtin,
2572 ),
2573 "xdisplaylabels" => {
2574 let labels = label_strings_from_value(value, builtin, "labels")?;
2575 if labels.len() != heatmap_handle.x_labels.len() {
2576 return Err(plotting_error(
2577 builtin,
2578 format!("{builtin}: XDisplayLabels length must match heatmap columns"),
2579 ));
2580 }
2581 super::state::set_heatmap_display_labels(
2582 heatmap_handle.figure,
2583 heatmap_handle.axes_index,
2584 heatmap_handle.plot_index,
2585 Some(labels),
2586 None,
2587 )
2588 .map_err(|err| map_figure_error(builtin, err))
2589 }
2590 "ydisplaylabels" => {
2591 let labels = label_strings_from_value(value, builtin, "labels")?;
2592 if labels.len() != heatmap_handle.y_labels.len() {
2593 return Err(plotting_error(
2594 builtin,
2595 format!("{builtin}: YDisplayLabels length must match heatmap rows"),
2596 ));
2597 }
2598 super::state::set_heatmap_display_labels(
2599 heatmap_handle.figure,
2600 heatmap_handle.axes_index,
2601 heatmap_handle.plot_index,
2602 None,
2603 Some(labels),
2604 )
2605 .map_err(|err| map_figure_error(builtin, err))
2606 }
2607 other => Err(plotting_error(
2608 builtin,
2609 format!("{builtin}: unsupported heatmap property `{other}`"),
2610 )),
2611 }
2612}
2613
2614fn apply_area_property(
2615 area_handle: &super::state::AreaHandleState,
2616 key: &str,
2617 value: &Value,
2618 builtin: &'static str,
2619) -> BuiltinResult<()> {
2620 super::state::update_area_plot(
2621 area_handle.figure,
2622 area_handle.plot_index,
2623 |area| match key {
2624 "color" => {
2625 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2626 area.color = c;
2627 }
2628 }
2629 "basevalue" => {
2630 if let Some(v) = value_as_f64(value) {
2631 area.baseline = v;
2632 area.lower_y = None;
2633 }
2634 }
2635 _ => {}
2636 },
2637 )
2638 .map_err(|err| map_figure_error(builtin, err))?;
2639 Ok(())
2640}
2641
2642fn apply_line_property(
2643 line_handle: &super::state::SimplePlotHandleState,
2644 key: &str,
2645 value: &Value,
2646 builtin: &'static str,
2647) -> BuiltinResult<()> {
2648 super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
2649 if let runmat_plot::plots::figure::PlotElement::Line(line) = plot {
2650 apply_line_plot_properties(line, key, value, builtin);
2651 }
2652 })
2653 .map_err(|err| map_figure_error(builtin, err))?;
2654 Ok(())
2655}
2656
2657fn apply_stairs_property(
2658 stairs_handle: &super::state::SimplePlotHandleState,
2659 key: &str,
2660 value: &Value,
2661 builtin: &'static str,
2662) -> BuiltinResult<()> {
2663 super::state::update_plot_element(stairs_handle.figure, stairs_handle.plot_index, |plot| {
2664 if let runmat_plot::plots::figure::PlotElement::Stairs(stairs) = plot {
2665 match key {
2666 "color" => {
2667 if let Ok(c) =
2668 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2669 {
2670 stairs.color = c;
2671 }
2672 }
2673 "linewidth" => {
2674 if let Some(v) = value_as_f64(value) {
2675 stairs.line_width = v as f32;
2676 }
2677 }
2678 "displayname" => {
2679 stairs.label = value_as_string(value).map(|s| s.to_string());
2680 }
2681 _ => {}
2682 }
2683 }
2684 })
2685 .map_err(|err| map_figure_error(builtin, err))?;
2686 Ok(())
2687}
2688
2689fn apply_scatter_property(
2690 scatter_handle: &super::state::SimplePlotHandleState,
2691 key: &str,
2692 value: &Value,
2693 builtin: &'static str,
2694) -> BuiltinResult<()> {
2695 super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
2696 if let runmat_plot::plots::figure::PlotElement::Scatter(scatter) = plot {
2697 match key {
2698 "marker" => {
2699 if let Some(s) = value_as_string(value) {
2700 scatter.marker_style = scatter_marker_from_name(&s, scatter.marker_style);
2701 }
2702 }
2703 "sizedata" => {
2704 if let Some(v) = value_as_f64(value) {
2705 scatter.marker_size = v as f32;
2706 }
2707 }
2708 "markerfacecolor" => {
2709 if let Ok(c) =
2710 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2711 {
2712 scatter.set_face_color(c);
2713 }
2714 }
2715 "markeredgecolor" => {
2716 if let Ok(c) =
2717 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2718 {
2719 scatter.set_edge_color(c);
2720 }
2721 }
2722 "linewidth" => {
2723 if let Some(v) = value_as_f64(value) {
2724 scatter.set_edge_thickness(v as f32);
2725 }
2726 }
2727 "displayname" => {
2728 scatter.label = value_as_string(value).map(|s| s.to_string());
2729 }
2730 _ => {}
2731 }
2732 }
2733 })
2734 .map_err(|err| map_figure_error(builtin, err))?;
2735 Ok(())
2736}
2737
2738fn apply_bar_property(
2739 bar_handle: &super::state::SimplePlotHandleState,
2740 key: &str,
2741 value: &Value,
2742 builtin: &'static str,
2743) -> BuiltinResult<()> {
2744 super::state::update_plot_element(bar_handle.figure, bar_handle.plot_index, |plot| {
2745 if let runmat_plot::plots::figure::PlotElement::Bar(bar) = plot {
2746 match key {
2747 "facecolor" | "color" => {
2748 if let Ok(c) =
2749 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2750 {
2751 bar.color = c;
2752 }
2753 }
2754 "barwidth" => {
2755 if let Some(v) = value_as_f64(value) {
2756 bar.bar_width = v as f32;
2757 }
2758 }
2759 "displayname" => {
2760 bar.label = value_as_string(value).map(|s| s.to_string());
2761 }
2762 _ => {}
2763 }
2764 }
2765 })
2766 .map_err(|err| map_figure_error(builtin, err))?;
2767 Ok(())
2768}
2769
2770fn apply_surface_property(
2771 surface_handle: &super::state::SimplePlotHandleState,
2772 key: &str,
2773 value: &Value,
2774 builtin: &'static str,
2775) -> BuiltinResult<()> {
2776 super::state::update_plot_element(surface_handle.figure, surface_handle.plot_index, |plot| {
2777 if let runmat_plot::plots::figure::PlotElement::Surface(surface) = plot {
2778 match key {
2779 "facealpha" => {
2780 if let Some(v) = value_as_f64(value) {
2781 surface.alpha = v as f32;
2782 }
2783 }
2784 "displayname" => {
2785 surface.label = value_as_string(value).map(|s| s.to_string());
2786 }
2787 _ => {}
2788 }
2789 }
2790 })
2791 .map_err(|err| map_figure_error(builtin, err))?;
2792 Ok(())
2793}
2794
2795fn apply_line3_property(
2796 line_handle: &super::state::SimplePlotHandleState,
2797 key: &str,
2798 value: &Value,
2799 builtin: &'static str,
2800) -> BuiltinResult<()> {
2801 super::state::update_plot_element(line_handle.figure, line_handle.plot_index, |plot| {
2802 if let runmat_plot::plots::figure::PlotElement::Line3(line) = plot {
2803 match key {
2804 "color" => {
2805 if let Ok(c) =
2806 parse_color_value(&LineStyleParseOptions::generic(builtin), value)
2807 {
2808 line.color = c;
2809 }
2810 }
2811 "linewidth" => {
2812 if let Some(v) = value_as_f64(value) {
2813 line.line_width = v as f32;
2814 }
2815 }
2816 "linestyle" => {
2817 if let Some(s) = value_as_string(value) {
2818 line.line_style = parse_line_style_name_for_props(&s);
2819 }
2820 }
2821 "displayname" => {
2822 line.label = value_as_string(value).map(|s| s.to_string());
2823 }
2824 _ => {}
2825 }
2826 }
2827 })
2828 .map_err(|err| map_figure_error(builtin, err))?;
2829 Ok(())
2830}
2831
2832fn apply_scatter3_property(
2833 scatter_handle: &super::state::SimplePlotHandleState,
2834 key: &str,
2835 value: &Value,
2836 builtin: &'static str,
2837) -> BuiltinResult<()> {
2838 super::state::update_plot_element(scatter_handle.figure, scatter_handle.plot_index, |plot| {
2839 if let runmat_plot::plots::figure::PlotElement::Scatter3(scatter) = plot {
2840 match key {
2841 "sizedata" => {
2842 if let Some(v) = value_as_f64(value) {
2843 scatter.point_size = v as f32;
2844 }
2845 }
2846 "displayname" => {
2847 scatter.label = value_as_string(value).map(|s| s.to_string());
2848 }
2849 _ => {}
2850 }
2851 }
2852 })
2853 .map_err(|err| map_figure_error(builtin, err))?;
2854 Ok(())
2855}
2856
2857fn apply_pie_property(
2858 pie_handle: &super::state::SimplePlotHandleState,
2859 key: &str,
2860 value: &Value,
2861 builtin: &'static str,
2862) -> BuiltinResult<()> {
2863 super::state::update_plot_element(pie_handle.figure, pie_handle.plot_index, |plot| {
2864 if let runmat_plot::plots::figure::PlotElement::Pie(pie) = plot {
2865 if key == "displayname" {
2866 pie.label = value_as_string(value).map(|s| s.to_string());
2867 }
2868 }
2869 })
2870 .map_err(|err| map_figure_error(builtin, err))?;
2871 Ok(())
2872}
2873
2874fn apply_contour_property(
2875 contour_handle: &super::state::SimplePlotHandleState,
2876 key: &str,
2877 value: &Value,
2878 builtin: &'static str,
2879) -> BuiltinResult<()> {
2880 super::state::update_plot_element(contour_handle.figure, contour_handle.plot_index, |plot| {
2881 if let runmat_plot::plots::figure::PlotElement::Contour(contour) = plot {
2882 if key == "displayname" {
2883 contour.label = value_as_string(value).map(|s| s.to_string());
2884 }
2885 }
2886 })
2887 .map_err(|err| map_figure_error(builtin, err))?;
2888 Ok(())
2889}
2890
2891fn apply_contour_fill_property(
2892 fill_handle: &super::state::SimplePlotHandleState,
2893 key: &str,
2894 value: &Value,
2895 builtin: &'static str,
2896) -> BuiltinResult<()> {
2897 super::state::update_plot_element(fill_handle.figure, fill_handle.plot_index, |plot| {
2898 if let runmat_plot::plots::figure::PlotElement::ContourFill(fill) = plot {
2899 if key == "displayname" {
2900 fill.label = value_as_string(value).map(|s| s.to_string());
2901 }
2902 }
2903 })
2904 .map_err(|err| map_figure_error(builtin, err))?;
2905 Ok(())
2906}
2907
2908fn apply_line_plot_properties(
2909 line: &mut runmat_plot::plots::LinePlot,
2910 key: &str,
2911 value: &Value,
2912 builtin: &'static str,
2913) {
2914 match key {
2915 "color" => {
2916 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2917 line.color = c;
2918 }
2919 }
2920 "linewidth" => {
2921 if let Some(v) = value_as_f64(value) {
2922 line.line_width = v as f32;
2923 }
2924 }
2925 "linestyle" => {
2926 if let Some(s) = value_as_string(value) {
2927 line.line_style = parse_line_style_name_for_props(&s);
2928 }
2929 }
2930 "displayname" => {
2931 line.label = value_as_string(value).map(|s| s.to_string());
2932 }
2933 "marker" => {
2934 if let Some(s) = value_as_string(value) {
2935 line.marker = marker_from_name(&s, line.marker.clone());
2936 }
2937 }
2938 "markersize" => {
2939 if let Some(v) = value_as_f64(value) {
2940 if let Some(marker) = &mut line.marker {
2941 marker.size = v as f32;
2942 }
2943 }
2944 }
2945 "markerfacecolor" => {
2946 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2947 if let Some(marker) = &mut line.marker {
2948 marker.face_color = c;
2949 }
2950 }
2951 }
2952 "markeredgecolor" => {
2953 if let Ok(c) = parse_color_value(&LineStyleParseOptions::generic(builtin), value) {
2954 if let Some(marker) = &mut line.marker {
2955 marker.edge_color = c;
2956 }
2957 }
2958 }
2959 "filled" => {
2960 if let Some(v) = value_as_bool(value) {
2961 if let Some(marker) = &mut line.marker {
2962 marker.filled = v;
2963 }
2964 }
2965 }
2966 _ => {}
2967 }
2968}
2969
2970fn limits_from_optional_value(
2971 value: &Value,
2972 builtin: &'static str,
2973) -> BuiltinResult<Option<(f64, f64)>> {
2974 if let Some(text) = value_as_string(value) {
2975 let norm = text.trim().to_ascii_lowercase();
2976 if matches!(norm.as_str(), "auto" | "tight") {
2977 return Ok(None);
2978 }
2979 }
2980 Ok(Some(
2981 crate::builtins::plotting::op_common::limits::limits_from_value(value, builtin)?,
2982 ))
2983}
2984
2985fn parse_colormap_name(
2986 name: &str,
2987 builtin: &'static str,
2988) -> BuiltinResult<runmat_plot::plots::surface::ColorMap> {
2989 match name.trim().to_ascii_lowercase().as_str() {
2990 "parula" => Ok(runmat_plot::plots::surface::ColorMap::Parula),
2991 "viridis" => Ok(runmat_plot::plots::surface::ColorMap::Viridis),
2992 "plasma" => Ok(runmat_plot::plots::surface::ColorMap::Plasma),
2993 "inferno" => Ok(runmat_plot::plots::surface::ColorMap::Inferno),
2994 "magma" => Ok(runmat_plot::plots::surface::ColorMap::Magma),
2995 "turbo" => Ok(runmat_plot::plots::surface::ColorMap::Turbo),
2996 "jet" => Ok(runmat_plot::plots::surface::ColorMap::Jet),
2997 "hot" => Ok(runmat_plot::plots::surface::ColorMap::Hot),
2998 "cool" => Ok(runmat_plot::plots::surface::ColorMap::Cool),
2999 "spring" => Ok(runmat_plot::plots::surface::ColorMap::Spring),
3000 "summer" => Ok(runmat_plot::plots::surface::ColorMap::Summer),
3001 "autumn" => Ok(runmat_plot::plots::surface::ColorMap::Autumn),
3002 "winter" => Ok(runmat_plot::plots::surface::ColorMap::Winter),
3003 "gray" | "grey" => Ok(runmat_plot::plots::surface::ColorMap::Gray),
3004 "bone" => Ok(runmat_plot::plots::surface::ColorMap::Bone),
3005 "copper" => Ok(runmat_plot::plots::surface::ColorMap::Copper),
3006 "pink" => Ok(runmat_plot::plots::surface::ColorMap::Pink),
3007 "lines" => Ok(runmat_plot::plots::surface::ColorMap::Lines),
3008 other => Err(plotting_error(
3009 builtin,
3010 format!("{builtin}: unknown colormap '{other}'"),
3011 )),
3012 }
3013}
3014
3015fn apply_axes_text_alias(
3016 handle: FigureHandle,
3017 axes_index: usize,
3018 kind: PlotObjectKind,
3019 value: &Value,
3020 builtin: &'static str,
3021) -> BuiltinResult<()> {
3022 if let Some(text) = value_as_string(value) {
3023 set_text_properties_for_axes(handle, axes_index, kind, Some(text), None)
3024 .map_err(|err| map_figure_error(builtin, err))?;
3025 return Ok(());
3026 }
3027
3028 let scalar = handle_scalar(value, builtin)?;
3029 let (src_handle, src_axes, src_kind) =
3030 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3031 if src_kind != kind {
3032 return Err(plotting_error(
3033 builtin,
3034 format!(
3035 "{builtin}: expected a matching text handle for `{}`",
3036 key_name(kind)
3037 ),
3038 ));
3039 }
3040 let meta = axes_metadata_snapshot(src_handle, src_axes)
3041 .map_err(|err| map_figure_error(builtin, err))?;
3042 let (text, style) = match kind {
3043 PlotObjectKind::Title => (meta.title, meta.title_style),
3044 PlotObjectKind::XLabel => (meta.x_label, meta.x_label_style),
3045 PlotObjectKind::YLabel => (meta.y_label, meta.y_label_style),
3046 PlotObjectKind::ZLabel => (meta.z_label, meta.z_label_style),
3047 PlotObjectKind::Legend => unreachable!(),
3048 PlotObjectKind::SuperTitle => unreachable!(),
3049 };
3050 set_text_properties_for_axes(handle, axes_index, kind, text, Some(style))
3051 .map_err(|err| map_figure_error(builtin, err))?;
3052 Ok(())
3053}
3054
3055fn validate_axes_text_alias(
3056 kind: PlotObjectKind,
3057 value: &Value,
3058 builtin: &'static str,
3059) -> BuiltinResult<()> {
3060 if value_as_string(value).is_some() {
3061 return Ok(());
3062 }
3063
3064 let scalar = handle_scalar(value, builtin)?;
3065 let (src_handle, src_axes, src_kind) =
3066 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3067 if src_kind != kind {
3068 return Err(plotting_error(
3069 builtin,
3070 format!(
3071 "{builtin}: expected a matching text handle for `{}`",
3072 key_name(kind)
3073 ),
3074 ));
3075 }
3076 axes_metadata_snapshot(src_handle, src_axes).map_err(|err| map_figure_error(builtin, err))?;
3077 Ok(())
3078}
3079
3080fn apply_figure_text_alias(
3081 handle: FigureHandle,
3082 kind: PlotObjectKind,
3083 value: &Value,
3084 builtin: &'static str,
3085) -> BuiltinResult<()> {
3086 if let Some(text) = value_as_text_string(value) {
3087 match kind {
3088 PlotObjectKind::SuperTitle => {
3089 set_sg_title_properties_for_figure(handle, Some(text), None)
3090 .map_err(|err| map_figure_error(builtin, err))?;
3091 }
3092 _ => unreachable!(),
3093 }
3094 return Ok(());
3095 }
3096
3097 let scalar = handle_scalar(value, builtin)?;
3098 let (src_handle, _src_axes, src_kind) =
3099 decode_plot_object_handle(scalar).map_err(|err| map_figure_error(builtin, err))?;
3100 if src_kind != kind {
3101 return Err(plotting_error(
3102 builtin,
3103 format!(
3104 "{builtin}: expected a matching text handle for `{}`",
3105 key_name(kind)
3106 ),
3107 ));
3108 }
3109
3110 let figure = super::state::clone_figure(src_handle)
3111 .ok_or_else(|| plotting_error(builtin, format!("{builtin}: invalid figure handle")))?;
3112 let (text, style) = match kind {
3113 PlotObjectKind::SuperTitle => (figure.sg_title, figure.sg_title_style),
3114 _ => unreachable!(),
3115 };
3116 set_sg_title_properties_for_figure(handle, text, Some(style))
3117 .map_err(|err| map_figure_error(builtin, err))?;
3118 Ok(())
3119}
3120
3121fn collect_label_strings(builtin: &'static str, args: &[Value]) -> BuiltinResult<Vec<String>> {
3122 let mut labels = Vec::new();
3123 for arg in args {
3124 match arg {
3125 Value::StringArray(arr) => labels.extend(arr.data.iter().cloned()),
3126 Value::Cell(cell) => {
3127 for row in 0..cell.rows {
3128 for col in 0..cell.cols {
3129 let value = cell.get(row, col).map_err(|err| {
3130 plotting_error(builtin, format!("legend: invalid label cell: {err}"))
3131 })?;
3132 labels.push(value_as_string(&value).ok_or_else(|| {
3133 plotting_error(builtin, "legend: labels must be strings or char arrays")
3134 })?);
3135 }
3136 }
3137 }
3138 _ => labels.push(value_as_string(arg).ok_or_else(|| {
3139 plotting_error(builtin, "legend: labels must be strings or char arrays")
3140 })?),
3141 }
3142 }
3143 Ok(labels)
3144}
3145
3146fn handle_scalar(value: &Value, builtin: &'static str) -> BuiltinResult<f64> {
3147 match value {
3148 Value::Num(v) => Ok(*v),
3149 Value::Int(i) => Ok(i.to_f64()),
3150 Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
3151 _ => Err(plotting_error(
3152 builtin,
3153 format!("{builtin}: expected plotting handle"),
3154 )),
3155 }
3156}
3157
3158fn legend_labels_value(labels: Vec<String>) -> Value {
3159 Value::StringArray(StringArray {
3160 rows: 1,
3161 cols: labels.len().max(1),
3162 shape: vec![1, labels.len().max(1)],
3163 data: labels,
3164 })
3165}
3166
3167fn text_value(text: Option<String>) -> Value {
3168 match text {
3169 Some(text) if text.contains('\n') => {
3170 let lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
3171 Value::StringArray(StringArray {
3172 rows: 1,
3173 cols: lines.len().max(1),
3174 shape: vec![1, lines.len().max(1)],
3175 data: lines,
3176 })
3177 }
3178 Some(text) => Value::String(text),
3179 None => Value::String(String::new()),
3180 }
3181}
3182
3183fn handles_value(handles: Vec<f64>) -> Value {
3184 Value::Tensor(runmat_builtins::Tensor {
3185 rows: 1,
3186 cols: handles.len(),
3187 shape: vec![1, handles.len()],
3188 data: handles,
3189 dtype: runmat_builtins::NumericDType::F64,
3190 })
3191}
3192
3193fn tensor_from_vec(data: Vec<f64>) -> Value {
3194 Value::Tensor(runmat_builtins::Tensor {
3195 rows: 1,
3196 cols: data.len(),
3197 shape: vec![1, data.len()],
3198 data,
3199 dtype: runmat_builtins::NumericDType::F64,
3200 })
3201}
3202
3203fn string_array_from_vec(data: Vec<String>) -> BuiltinResult<Value> {
3204 let cols = data.len();
3205 let array = StringArray::new(data, vec![1, cols])
3206 .map_err(|e| plotting_error("get", format!("get: {e}")))?;
3207 Ok(Value::StringArray(array))
3208}
3209
3210pub(crate) fn label_strings_from_value(
3211 value: &Value,
3212 builtin: &'static str,
3213 label_context: &str,
3214) -> BuiltinResult<Vec<String>> {
3215 match value {
3216 Value::StringArray(array) => Ok(array.data.clone()),
3217 Value::Cell(cell) => cell
3218 .data
3219 .iter()
3220 .map(|item| {
3221 value_as_text_string(item).ok_or_else(|| {
3222 plotting_error(
3223 builtin,
3224 format!("{builtin}: {label_context} must contain text values"),
3225 )
3226 })
3227 })
3228 .collect(),
3229 Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
3230 Value::String(text) => Ok(vec![text.clone()]),
3231 Value::Tensor(tensor) => Ok(tensor.data.iter().map(|v| v.to_string()).collect()),
3232 Value::Int(i) => Ok(vec![i.to_i64().to_string()]),
3233 Value::Num(v) => Ok(vec![v.to_string()]),
3234 other => Err(plotting_error(
3235 builtin,
3236 format!("{builtin}: unsupported {label_context} value {other:?}"),
3237 )),
3238 }
3239}
3240
3241fn tensor_from_matrix(data: Vec<Vec<f64>>) -> Value {
3242 let rows = data.len();
3243 let cols = data.first().map(|row| row.len()).unwrap_or(0);
3244 let flat = data.into_iter().flat_map(|row| row.into_iter()).collect();
3245 Value::Tensor(runmat_builtins::Tensor {
3246 rows,
3247 cols,
3248 shape: vec![rows, cols],
3249 data: flat,
3250 dtype: runmat_builtins::NumericDType::F64,
3251 })
3252}
3253
3254fn insert_line_marker_struct_props(
3255 st: &mut StructValue,
3256 marker: Option<&runmat_plot::plots::line::LineMarkerAppearance>,
3257) {
3258 if let Some(marker) = marker {
3259 st.insert(
3260 "Marker",
3261 Value::String(marker_style_name(marker.kind).into()),
3262 );
3263 st.insert("MarkerSize", Value::Num(marker.size as f64));
3264 st.insert(
3265 "MarkerFaceColor",
3266 Value::String(color_to_short_name(marker.face_color)),
3267 );
3268 st.insert(
3269 "MarkerEdgeColor",
3270 Value::String(color_to_short_name(marker.edge_color)),
3271 );
3272 st.insert("Filled", Value::Bool(marker.filled));
3273 }
3274}
3275
3276fn line_marker_property_value(
3277 marker: &Option<runmat_plot::plots::line::LineMarkerAppearance>,
3278 name: &str,
3279 builtin: &'static str,
3280) -> BuiltinResult<Value> {
3281 match name {
3282 "marker" => Ok(Value::String(
3283 marker
3284 .as_ref()
3285 .map(|m| marker_style_name(m.kind).to_string())
3286 .unwrap_or_else(|| "none".into()),
3287 )),
3288 "markersize" => Ok(Value::Num(
3289 marker.as_ref().map(|m| m.size as f64).unwrap_or(0.0),
3290 )),
3291 "markerfacecolor" => Ok(Value::String(
3292 marker
3293 .as_ref()
3294 .map(|m| color_to_short_name(m.face_color))
3295 .unwrap_or_else(|| "none".into()),
3296 )),
3297 "markeredgecolor" => Ok(Value::String(
3298 marker
3299 .as_ref()
3300 .map(|m| color_to_short_name(m.edge_color))
3301 .unwrap_or_else(|| "none".into()),
3302 )),
3303 "filled" => Ok(Value::Bool(
3304 marker.as_ref().map(|m| m.filled).unwrap_or(false),
3305 )),
3306 other => Err(plotting_error(
3307 builtin,
3308 format!("{builtin}: unsupported line property `{other}`"),
3309 )),
3310 }
3311}
3312
3313fn histogram_labels_from_edges(edges: &[f64]) -> Vec<String> {
3314 edges
3315 .windows(2)
3316 .map(|pair| format!("[{:.3}, {:.3})", pair[0], pair[1]))
3317 .collect()
3318}
3319
3320fn validate_histogram_normalization(norm: &str, builtin: &'static str) -> BuiltinResult<()> {
3321 match norm {
3322 "count" | "probability" | "countdensity" | "pdf" | "cumcount" | "cdf" => Ok(()),
3323 other => Err(plotting_error(
3324 builtin,
3325 format!("{builtin}: unsupported histogram normalization `{other}`"),
3326 )),
3327 }
3328}
3329
3330fn apply_histogram_normalization(raw_counts: &[f64], edges: &[f64], norm: &str) -> Vec<f64> {
3331 let widths: Vec<f64> = edges.windows(2).map(|pair| pair[1] - pair[0]).collect();
3332 let total: f64 = raw_counts.iter().sum();
3333 match norm {
3334 "count" => raw_counts.to_vec(),
3335 "probability" => {
3336 if total > 0.0 {
3337 raw_counts.iter().map(|&c| c / total).collect()
3338 } else {
3339 vec![0.0; raw_counts.len()]
3340 }
3341 }
3342 "countdensity" => raw_counts
3343 .iter()
3344 .zip(widths.iter())
3345 .map(|(&c, &w)| if w > 0.0 { c / w } else { 0.0 })
3346 .collect(),
3347 "pdf" => {
3348 if total > 0.0 {
3349 raw_counts
3350 .iter()
3351 .zip(widths.iter())
3352 .map(|(&c, &w)| if w > 0.0 { c / (total * w) } else { 0.0 })
3353 .collect()
3354 } else {
3355 vec![0.0; raw_counts.len()]
3356 }
3357 }
3358 "cumcount" => {
3359 let mut acc = 0.0;
3360 raw_counts
3361 .iter()
3362 .map(|&c| {
3363 acc += c;
3364 acc
3365 })
3366 .collect()
3367 }
3368 "cdf" => {
3369 if total > 0.0 {
3370 let mut acc = 0.0;
3371 raw_counts
3372 .iter()
3373 .map(|&c| {
3374 acc += c;
3375 acc / total
3376 })
3377 .collect()
3378 } else {
3379 vec![0.0; raw_counts.len()]
3380 }
3381 }
3382 _ => raw_counts.to_vec(),
3383 }
3384}
3385
3386fn line_style_name(style: runmat_plot::plots::line::LineStyle) -> &'static str {
3387 match style {
3388 runmat_plot::plots::line::LineStyle::Solid => "-",
3389 runmat_plot::plots::line::LineStyle::Dashed => "--",
3390 runmat_plot::plots::line::LineStyle::Dotted => ":",
3391 runmat_plot::plots::line::LineStyle::DashDot => "-.",
3392 }
3393}
3394
3395fn parse_line_style_name_for_props(name: &str) -> runmat_plot::plots::line::LineStyle {
3396 match name.trim() {
3397 "--" | "dashed" => runmat_plot::plots::line::LineStyle::Dashed,
3398 ":" | "dotted" => runmat_plot::plots::line::LineStyle::Dotted,
3399 "-." | "dashdot" => runmat_plot::plots::line::LineStyle::DashDot,
3400 _ => runmat_plot::plots::line::LineStyle::Solid,
3401 }
3402}
3403
3404fn marker_style_name(style: runmat_plot::plots::scatter::MarkerStyle) -> &'static str {
3405 match style {
3406 runmat_plot::plots::scatter::MarkerStyle::Circle => "o",
3407 runmat_plot::plots::scatter::MarkerStyle::Square => "s",
3408 runmat_plot::plots::scatter::MarkerStyle::Triangle => "^",
3409 runmat_plot::plots::scatter::MarkerStyle::Diamond => "d",
3410 runmat_plot::plots::scatter::MarkerStyle::Plus => "+",
3411 runmat_plot::plots::scatter::MarkerStyle::Cross => "x",
3412 runmat_plot::plots::scatter::MarkerStyle::Star => "*",
3413 runmat_plot::plots::scatter::MarkerStyle::Hexagon => "h",
3414 }
3415}
3416
3417fn marker_from_name(
3418 name: &str,
3419 current: Option<runmat_plot::plots::line::LineMarkerAppearance>,
3420) -> Option<runmat_plot::plots::line::LineMarkerAppearance> {
3421 let mut marker = current.unwrap_or(runmat_plot::plots::line::LineMarkerAppearance {
3422 kind: runmat_plot::plots::scatter::MarkerStyle::Circle,
3423 size: 6.0,
3424 edge_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
3425 face_color: glam::Vec4::new(0.0, 0.447, 0.741, 1.0),
3426 filled: false,
3427 });
3428 marker.kind = match name.trim() {
3429 "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
3430 "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
3431 "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
3432 "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
3433 "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
3434 "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
3435 "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
3436 "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
3437 "none" => return None,
3438 _ => marker.kind,
3439 };
3440 Some(marker)
3441}
3442
3443fn scatter_marker_from_name(
3444 name: &str,
3445 current: runmat_plot::plots::scatter::MarkerStyle,
3446) -> runmat_plot::plots::scatter::MarkerStyle {
3447 match name.trim() {
3448 "o" => runmat_plot::plots::scatter::MarkerStyle::Circle,
3449 "s" => runmat_plot::plots::scatter::MarkerStyle::Square,
3450 "^" => runmat_plot::plots::scatter::MarkerStyle::Triangle,
3451 "d" => runmat_plot::plots::scatter::MarkerStyle::Diamond,
3452 "+" => runmat_plot::plots::scatter::MarkerStyle::Plus,
3453 "x" => runmat_plot::plots::scatter::MarkerStyle::Cross,
3454 "*" => runmat_plot::plots::scatter::MarkerStyle::Star,
3455 "h" => runmat_plot::plots::scatter::MarkerStyle::Hexagon,
3456 _ => current,
3457 }
3458}
3459
3460trait Unzip3Vec<A, B, C> {
3461 fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>);
3462}
3463
3464impl<I, A, B, C> Unzip3Vec<A, B, C> for I
3465where
3466 I: Iterator<Item = (A, B, C)>,
3467{
3468 fn unzip_n_vec(self) -> (Vec<A>, Vec<B>, Vec<C>) {
3469 let mut a = Vec::new();
3470 let mut b = Vec::new();
3471 let mut c = Vec::new();
3472 for (va, vb, vc) in self {
3473 a.push(va);
3474 b.push(vb);
3475 c.push(vc);
3476 }
3477 (a, b, c)
3478 }
3479}
3480
3481fn color_to_short_name(color: glam::Vec4) -> String {
3482 let candidates = [
3483 (glam::Vec4::new(1.0, 0.0, 0.0, 1.0), "r"),
3484 (glam::Vec4::new(0.0, 1.0, 0.0, 1.0), "g"),
3485 (glam::Vec4::new(0.0, 0.0, 1.0, 1.0), "b"),
3486 (glam::Vec4::new(0.0, 0.0, 0.0, 1.0), "k"),
3487 (glam::Vec4::new(1.0, 1.0, 1.0, 1.0), "w"),
3488 (glam::Vec4::new(1.0, 1.0, 0.0, 1.0), "y"),
3489 (glam::Vec4::new(1.0, 0.0, 1.0, 1.0), "m"),
3490 (glam::Vec4::new(0.0, 1.0, 1.0, 1.0), "c"),
3491 ];
3492 for (candidate, name) in candidates {
3493 if (candidate - color).abs().max_element() < 1e-6 {
3494 return name.to_string();
3495 }
3496 }
3497 format!("[{:.3},{:.3},{:.3}]", color.x, color.y, color.z)
3498}
3499
3500fn key_name(kind: PlotObjectKind) -> &'static str {
3501 match kind {
3502 PlotObjectKind::Title => "Title",
3503 PlotObjectKind::XLabel => "XLabel",
3504 PlotObjectKind::YLabel => "YLabel",
3505 PlotObjectKind::ZLabel => "ZLabel",
3506 PlotObjectKind::Legend => "Legend",
3507 PlotObjectKind::SuperTitle => "SGTitle",
3508 }
3509}
3510
3511trait AxesMetadataExt {
3512 fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle;
3513}
3514
3515impl AxesMetadataExt for runmat_plot::plots::AxesMetadata {
3516 fn text_style_for(&self, kind: PlotObjectKind) -> TextStyle {
3517 match kind {
3518 PlotObjectKind::Title => self.title_style.clone(),
3519 PlotObjectKind::XLabel => self.x_label_style.clone(),
3520 PlotObjectKind::YLabel => self.y_label_style.clone(),
3521 PlotObjectKind::ZLabel => self.z_label_style.clone(),
3522 PlotObjectKind::Legend => TextStyle::default(),
3523 PlotObjectKind::SuperTitle => TextStyle::default(),
3524 }
3525 }
3526}