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