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