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