1use runmat_plot::plots::Figure;
2
3#[cfg(not(any(feature = "gui", all(target_arch = "wasm32", feature = "plot-web"))))]
4use super::common::ERR_PLOTTING_UNAVAILABLE;
5use super::state::{clone_figure, FigureHandle};
6#[cfg(feature = "plot-core")]
7use crate::builtins::plotting::clone_geometry_scene;
8use thiserror::Error;
9
10#[cfg(feature = "plot-core")]
11use crate::builtins::common::map_control_flow_with_builtin;
12use crate::{build_runtime_error, BuiltinResult, RuntimeError};
13
14#[derive(Debug, Error)]
15#[allow(dead_code)]
16enum PlottingBackendError {
17 #[error("interactive backend error: {0}")]
18 Interactive(String),
19 #[error("static backend error: {0}")]
20 Static(String),
21 #[error("export initialization error: {0}")]
22 ImageExportInit(String),
23 #[error("export render error: {0}")]
24 ImageExport(String),
25}
26
27fn engine_error(message: impl Into<String>) -> RuntimeError {
28 build_runtime_error(message)
29 .with_identifier("RunMat:plot:EngineError")
30 .build()
31}
32
33fn engine_error_with_source(
34 message: impl Into<String>,
35 source: impl std::error::Error + Send + Sync + 'static,
36) -> RuntimeError {
37 build_runtime_error(message)
38 .with_identifier("RunMat:plot:EngineError")
39 .with_source(source)
40 .build()
41}
42
43#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
44pub fn render_figure(handle: FigureHandle, figure: Figure) -> BuiltinResult<String> {
45 #[cfg(feature = "gui")]
46 {
47 native::render(handle, figure)
48 }
49
50 #[cfg(not(feature = "gui"))]
51 {
52 let _ = handle;
53 let _ = figure;
54 Err(engine_error(ERR_PLOTTING_UNAVAILABLE))
55 }
56}
57
58#[cfg(feature = "plot-core")]
59pub async fn render_figure_png_bytes(
60 mut figure: Figure,
61 width: u32,
62 height: u32,
63) -> BuiltinResult<Vec<u8>> {
64 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
65
66 let mut settings = ImageExportSettings::default();
67 if width > 0 {
68 settings.width = width;
69 }
70 if height > 0 {
71 settings.height = height;
72 }
73
74 let mut exporter = ImageExporter::with_settings(settings)
75 .await
76 .map_err(|err| {
77 engine_error_with_source(
78 "Plot export initialization failed.",
79 PlottingBackendError::ImageExportInit(err),
80 )
81 })?;
82 exporter.set_theme_config(super::web::current_plot_theme_config());
83 exporter.render_png_bytes(&mut figure).await.map_err(|err| {
84 engine_error_with_source(
85 "Plot export failed.",
86 PlottingBackendError::ImageExport(err),
87 )
88 })
89}
90
91#[cfg(feature = "plot-core")]
92pub async fn render_figure_rgba_bytes(
93 mut figure: Figure,
94 width: u32,
95 height: u32,
96) -> BuiltinResult<Vec<u8>> {
97 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
98
99 let mut settings = ImageExportSettings::default();
100 if width > 0 {
101 settings.width = width;
102 }
103 if height > 0 {
104 settings.height = height;
105 }
106
107 let mut exporter = ImageExporter::with_settings(settings)
108 .await
109 .map_err(|err| {
110 engine_error_with_source(
111 "Plot export initialization failed.",
112 PlottingBackendError::ImageExportInit(err),
113 )
114 })?;
115 exporter.set_theme_config(super::web::current_plot_theme_config());
116 exporter
117 .render_rgba_bytes(&mut figure)
118 .await
119 .map_err(|err| {
120 engine_error_with_source(
121 "Plot export failed.",
122 PlottingBackendError::ImageExport(err),
123 )
124 })
125}
126
127#[cfg(feature = "plot-core")]
128pub async fn render_figure_png_bytes_with_camera(
129 mut figure: Figure,
130 width: u32,
131 height: u32,
132 camera: &runmat_plot::core::Camera,
133) -> BuiltinResult<Vec<u8>> {
134 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
135
136 let mut settings = ImageExportSettings::default();
137 if width > 0 {
138 settings.width = width;
139 }
140 if height > 0 {
141 settings.height = height;
142 }
143
144 let mut exporter = ImageExporter::with_settings(settings)
145 .await
146 .map_err(|err| {
147 engine_error_with_source(
148 "Plot export initialization failed.",
149 PlottingBackendError::ImageExportInit(err),
150 )
151 })?;
152 exporter.set_theme_config(super::web::current_plot_theme_config());
153 exporter
154 .render_png_bytes_with_camera(&mut figure, camera)
155 .await
156 .map_err(|err| {
157 engine_error_with_source(
158 "Plot export failed.",
159 PlottingBackendError::ImageExport(err),
160 )
161 })
162}
163
164#[cfg(feature = "plot-core")]
165pub async fn render_figure_rgba_bytes_with_camera(
166 mut figure: Figure,
167 width: u32,
168 height: u32,
169 camera: &runmat_plot::core::Camera,
170) -> BuiltinResult<Vec<u8>> {
171 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
172
173 let mut settings = ImageExportSettings::default();
174 if width > 0 {
175 settings.width = width;
176 }
177 if height > 0 {
178 settings.height = height;
179 }
180
181 let mut exporter = ImageExporter::with_settings(settings)
182 .await
183 .map_err(|err| {
184 engine_error_with_source(
185 "Plot export initialization failed.",
186 PlottingBackendError::ImageExportInit(err),
187 )
188 })?;
189 exporter.set_theme_config(super::web::current_plot_theme_config());
190 exporter
191 .render_rgba_bytes_with_camera(&mut figure, camera)
192 .await
193 .map_err(|err| {
194 engine_error_with_source(
195 "Plot export failed.",
196 PlottingBackendError::ImageExport(err),
197 )
198 })
199}
200
201#[cfg(feature = "plot-core")]
202pub async fn render_figure_png_bytes_with_axes_cameras(
203 mut figure: Figure,
204 width: u32,
205 height: u32,
206 axes_cameras: &[runmat_plot::core::Camera],
207) -> BuiltinResult<Vec<u8>> {
208 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
209
210 let mut settings = ImageExportSettings::default();
211 if width > 0 {
212 settings.width = width;
213 }
214 if height > 0 {
215 settings.height = height;
216 }
217
218 let mut exporter = ImageExporter::with_settings(settings)
219 .await
220 .map_err(|err| {
221 engine_error_with_source(
222 "Plot export initialization failed.",
223 PlottingBackendError::ImageExportInit(err),
224 )
225 })?;
226 exporter.set_theme_config(super::web::current_plot_theme_config());
227 exporter
228 .render_png_bytes_with_axes_cameras(&mut figure, axes_cameras)
229 .await
230 .map_err(|err| {
231 engine_error_with_source(
232 "Plot export failed.",
233 PlottingBackendError::ImageExport(err),
234 )
235 })
236}
237
238#[cfg(feature = "plot-core")]
239pub async fn render_figure_rgba_bytes_with_axes_cameras(
240 mut figure: Figure,
241 width: u32,
242 height: u32,
243 axes_cameras: &[runmat_plot::core::Camera],
244) -> BuiltinResult<Vec<u8>> {
245 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
246
247 let mut settings = ImageExportSettings::default();
248 if width > 0 {
249 settings.width = width;
250 }
251 if height > 0 {
252 settings.height = height;
253 }
254
255 let mut exporter = ImageExporter::with_settings(settings)
256 .await
257 .map_err(|err| {
258 engine_error_with_source(
259 "Plot export initialization failed.",
260 PlottingBackendError::ImageExportInit(err),
261 )
262 })?;
263 exporter.set_theme_config(super::web::current_plot_theme_config());
264 exporter
265 .render_rgba_bytes_with_axes_cameras(&mut figure, axes_cameras)
266 .await
267 .map_err(|err| {
268 engine_error_with_source(
269 "Plot export failed.",
270 PlottingBackendError::ImageExport(err),
271 )
272 })
273}
274
275#[cfg(feature = "plot-core")]
276pub async fn render_figure_snapshot(
277 handle: FigureHandle,
278 width: u32,
279 height: u32,
280 textmark: Option<String>,
281) -> BuiltinResult<Vec<u8>> {
282 const SNAPSHOT_CONTEXT: &str = "renderFigureImage";
283 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
284 log::debug!(
285 "runmat-runtime: render_figure_snapshot.start handle={} width={} height={} textmark={}",
286 handle.as_u32(),
287 width,
288 height,
289 textmark.as_deref().unwrap_or("")
290 );
291 let figure = clone_figure(handle).ok_or_else(|| {
292 map_control_flow_with_builtin(
293 engine_error(format!("figure handle {} does not exist", handle.as_u32())),
294 SNAPSHOT_CONTEXT,
295 )
296 })?;
297 log::debug!(
298 "runmat-runtime: render_figure_snapshot.figure_cloned handle={} axes={} elements={}",
299 handle.as_u32(),
300 figure.axes_metadata.len(),
301 figure.statistics().total_plots
302 );
303 let mut settings = ImageExportSettings::default();
304 if width > 0 {
305 settings.width = width;
306 }
307 if height > 0 {
308 settings.height = height;
309 }
310 let mut exporter = ImageExporter::with_settings(settings)
311 .await
312 .map_err(|err| {
313 map_control_flow_with_builtin(
314 engine_error_with_source(
315 "Plot export initialization failed.",
316 PlottingBackendError::ImageExportInit(err),
317 ),
318 SNAPSHOT_CONTEXT,
319 )
320 })?;
321 exporter.set_theme_config(super::web::current_plot_theme_config());
322 exporter.set_textmark(textmark.as_deref());
323
324 let mut figure = figure;
325 let bytes = exporter
326 .render_png_bytes(&mut figure)
327 .await
328 .map_err(|err| {
329 log::warn!(
330 "runmat-runtime: render_figure_snapshot.failed handle={} width={} height={} error={}",
331 handle.as_u32(),
332 width,
333 height,
334 err
335 );
336 map_control_flow_with_builtin(
337 engine_error_with_source(
338 format!("Plot export failed: {err}"),
339 PlottingBackendError::ImageExport(err),
340 ),
341 SNAPSHOT_CONTEXT,
342 )
343 })?;
344 log::debug!(
345 "runmat-runtime: render_figure_snapshot.ok handle={} bytes={}",
346 handle.as_u32(),
347 bytes.len()
348 );
349 Ok(bytes)
350}
351
352#[cfg(feature = "plot-core")]
353pub async fn render_figure_snapshot_with_camera_state(
354 handle: FigureHandle,
355 width: u32,
356 height: u32,
357 camera_state: super::web::PlotSurfaceCameraState,
358 textmark: Option<String>,
359) -> BuiltinResult<Vec<u8>> {
360 const SNAPSHOT_CONTEXT: &str = "renderFigureImage";
361 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
362 log::debug!(
363 "runmat-runtime: render_figure_snapshot_with_camera_state.start handle={} width={} height={} axes={}",
364 handle.as_u32(),
365 width,
366 height,
367 camera_state.axes.len()
368 );
369 let figure = clone_figure(handle).ok_or_else(|| {
370 map_control_flow_with_builtin(
371 engine_error(format!("figure handle {} does not exist", handle.as_u32())),
372 SNAPSHOT_CONTEXT,
373 )
374 })?;
375
376 let axes_cameras: Vec<runmat_plot::core::Camera> = camera_state
377 .axes
378 .iter()
379 .map(surface_plot_camera_to_core_camera)
380 .collect();
381
382 let mut settings = ImageExportSettings::default();
383 if width > 0 {
384 settings.width = width;
385 }
386 if height > 0 {
387 settings.height = height;
388 }
389 let mut exporter = ImageExporter::with_settings(settings)
390 .await
391 .map_err(|err| {
392 map_control_flow_with_builtin(
393 engine_error_with_source(
394 "Plot export initialization failed.",
395 PlottingBackendError::ImageExportInit(err),
396 ),
397 SNAPSHOT_CONTEXT,
398 )
399 })?;
400 exporter.set_theme_config(super::web::current_plot_theme_config());
401 exporter.set_textmark(textmark.as_deref());
402
403 let mut figure = figure;
404 let bytes = if axes_cameras.is_empty() {
405 exporter.render_png_bytes(&mut figure).await
406 } else {
407 exporter
408 .render_png_bytes_with_axes_cameras(&mut figure, &axes_cameras)
409 .await
410 }
411 .map_err(|err| {
412 log::warn!(
413 "runmat-runtime: render_figure_snapshot_with_camera_state.failed handle={} axes={} error={}",
414 handle.as_u32(),
415 axes_cameras.len(),
416 err
417 );
418 map_control_flow_with_builtin(
419 engine_error_with_source(
420 format!("Plot export failed: {err}"),
421 PlottingBackendError::ImageExport(err),
422 ),
423 SNAPSHOT_CONTEXT,
424 )
425 })?;
426 log::debug!(
427 "runmat-runtime: render_figure_snapshot_with_camera_state.ok handle={} bytes={} axes={}",
428 handle.as_u32(),
429 bytes.len(),
430 axes_cameras.len()
431 );
432 Ok(bytes)
433}
434
435#[cfg(feature = "plot-core")]
436pub async fn render_geometry_scene_snapshot(
437 handle: u32,
438 width: u32,
439 height: u32,
440 view: Option<String>,
441) -> BuiltinResult<Vec<u8>> {
442 const SNAPSHOT_CONTEXT: &str = "renderGeometrySceneImage";
443 use runmat_plot::export::image::{ImageExportSettings, ImageExporter};
444
445 log::debug!(
446 "runmat-runtime: render_geometry_scene_snapshot.start handle={} width={} height={} view={}",
447 handle,
448 width,
449 height,
450 view.as_deref().unwrap_or("")
451 );
452 let scene = clone_geometry_scene(handle).ok_or_else(|| {
453 map_control_flow_with_builtin(
454 engine_error(format!("geometry scene handle {handle} does not exist")),
455 SNAPSHOT_CONTEXT,
456 )
457 })?;
458
459 let mut settings = ImageExportSettings::default();
460 if width > 0 {
461 settings.width = width;
462 }
463 if height > 0 {
464 settings.height = height;
465 }
466 let camera = geometry_scene_snapshot_camera(
467 scene.bounds,
468 settings.width,
469 settings.height,
470 parse_camera_view_preset(view.as_deref()),
471 );
472 let mut exporter = ImageExporter::with_settings(settings)
473 .await
474 .map_err(|err| {
475 map_control_flow_with_builtin(
476 engine_error_with_source(
477 "Geometry export initialization failed.",
478 PlottingBackendError::ImageExportInit(err),
479 ),
480 SNAPSHOT_CONTEXT,
481 )
482 })?;
483 exporter.set_theme_config(super::web::current_plot_theme_config());
484
485 let bytes = exporter
486 .render_geometry_scene_png_bytes_with_camera(&scene, &camera)
487 .await
488 .map_err(|err| {
489 log::warn!(
490 "runmat-runtime: render_geometry_scene_snapshot.failed handle={} error={}",
491 handle,
492 err
493 );
494 map_control_flow_with_builtin(
495 engine_error_with_source(
496 format!("Geometry export failed: {err}"),
497 PlottingBackendError::ImageExport(err),
498 ),
499 SNAPSHOT_CONTEXT,
500 )
501 })?;
502 log::debug!(
503 "runmat-runtime: render_geometry_scene_snapshot.ok handle={} bytes={}",
504 handle,
505 bytes.len()
506 );
507 Ok(bytes)
508}
509
510#[cfg(feature = "plot-core")]
511fn parse_camera_view_preset(value: Option<&str>) -> runmat_plot::core::CameraViewPreset {
512 use runmat_plot::core::CameraViewPreset;
513
514 let Some(value) = value else {
515 return CameraViewPreset::Perspective;
516 };
517 match value.trim().to_ascii_lowercase().as_str() {
518 "perspective" | "iso" | "isometric" => CameraViewPreset::Perspective,
519 "top" | "xy" => CameraViewPreset::Top,
520 "bottom" => CameraViewPreset::Bottom,
521 "front" | "xz" => CameraViewPreset::Front,
522 "back" => CameraViewPreset::Back,
523 "left" | "yz" => CameraViewPreset::Left,
524 "right" => CameraViewPreset::Right,
525 _ => CameraViewPreset::Perspective,
526 }
527}
528
529#[cfg(feature = "plot-core")]
530fn geometry_scene_snapshot_camera(
531 bounds: runmat_plot::core::BoundingBox,
532 width: u32,
533 height: u32,
534 preset: runmat_plot::core::CameraViewPreset,
535) -> runmat_plot::core::Camera {
536 use glam::Vec3;
537 use runmat_plot::core::{BoundingBox, Camera, CameraViewPreset};
538
539 let mut camera = Camera::new();
540 camera.update_aspect_ratio(width.max(1) as f32 / height.max(1) as f32);
541 let bounds = if bounds.min.is_finite()
542 && bounds.max.is_finite()
543 && (bounds.max - bounds.min).length() > 1.0e-6
544 {
545 bounds
546 } else {
547 BoundingBox {
548 min: Vec3::splat(-1.0),
549 max: Vec3::splat(1.0),
550 }
551 };
552 let (direction, up) = match preset {
553 CameraViewPreset::Perspective => (Vec3::new(1.0, -1.0, 1.0).normalize(), Vec3::Z),
554 CameraViewPreset::Top => (Vec3::Z, Vec3::Y),
555 CameraViewPreset::Bottom => (-Vec3::Z, Vec3::Y),
556 CameraViewPreset::Front => (-Vec3::Y, Vec3::Z),
557 CameraViewPreset::Back => (Vec3::Y, Vec3::Z),
558 CameraViewPreset::Left => (-Vec3::X, Vec3::Z),
559 CameraViewPreset::Right => (Vec3::X, Vec3::Z),
560 };
561 let center = (bounds.min + bounds.max) * 0.5;
562 camera.up = up;
563 camera.target = center;
564 camera.position = center + direction;
565 camera.fit_bounds(bounds.min, bounds.max);
566 camera
567}
568
569#[cfg(feature = "plot-core")]
570fn surface_plot_camera_to_core_camera(
571 state: &super::web::PlotCameraState,
572) -> runmat_plot::core::Camera {
573 let mut camera = runmat_plot::core::Camera::new();
574 camera.position = glam::Vec3::new(state.position[0], state.position[1], state.position[2]);
575 camera.target = glam::Vec3::new(state.target[0], state.target[1], state.target[2]);
576 camera.up = glam::Vec3::new(state.up[0], state.up[1], state.up[2]);
577 camera.zoom = state.zoom;
578 camera.aspect_ratio = state.aspect_ratio.max(0.000_1);
579 camera.projection = match state.projection {
580 super::web::PlotCameraProjection::Perspective { fov, near, far } => {
581 runmat_plot::core::camera::ProjectionType::Perspective {
582 fov,
583 near: near.max(1.0e-6),
584 far: far.max(near + 1.0e-6),
585 }
586 }
587 super::web::PlotCameraProjection::Orthographic {
588 left,
589 right,
590 bottom,
591 top,
592 near,
593 far,
594 } => runmat_plot::core::camera::ProjectionType::Orthographic {
595 left,
596 right,
597 bottom,
598 top,
599 near,
600 far,
601 },
602 };
603 camera.mark_dirty();
604 camera
605}
606
607#[cfg(feature = "gui")]
608pub(crate) mod native {
609 use super::super::state::{install_figure_observer, FigureEventKind, FigureEventView};
610 use super::*;
611 use once_cell::sync::OnceCell;
612 use runmat_plot::plots::Figure;
613 use std::sync::Arc;
614 use std::sync::RwLock;
615
616 static FIGURE_EVENT_BRIDGE: OnceCell<()> = OnceCell::new();
617 static PLOTTING_MODE_OVERRIDE: OnceCell<RwLock<Option<PlottingMode>>> = OnceCell::new();
618
619 #[derive(Debug, Clone, Copy)]
620 enum PlottingMode {
621 Auto,
622 Interactive,
623 Static,
624 }
625
626 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
627 pub enum RuntimePlottingMode {
628 Auto,
629 Interactive,
630 Static,
631 }
632
633 pub fn set_runtime_plotting_mode(mode: RuntimePlottingMode) {
634 let lock = PLOTTING_MODE_OVERRIDE.get_or_init(|| RwLock::new(None));
635 let mut guard = lock.write().expect("plotting mode lock poisoned");
636 *guard = Some(match mode {
637 RuntimePlottingMode::Auto => PlottingMode::Auto,
638 RuntimePlottingMode::Interactive => PlottingMode::Interactive,
639 RuntimePlottingMode::Static => PlottingMode::Static,
640 });
641 }
642
643 pub fn render(handle: FigureHandle, mut figure: Figure) -> BuiltinResult<String> {
644 ensure_figure_event_bridge();
645 match detect_mode() {
646 PlottingMode::Interactive => interactive_export(handle, &mut figure),
647 PlottingMode::Static => static_export(&mut figure, "plot.png"),
648 PlottingMode::Auto => interactive_export(handle, &mut figure),
649 }
650 }
651
652 fn detect_mode() -> PlottingMode {
653 if let Some(lock) = PLOTTING_MODE_OVERRIDE.get() {
654 let guard = lock.read().expect("plotting mode lock poisoned");
655 if let Some(mode) = *guard {
656 return mode;
657 }
658 }
659 PlottingMode::Auto
660 }
661
662 fn interactive_export(handle: FigureHandle, figure: &mut Figure) -> BuiltinResult<String> {
663 let figure_clone = figure.clone();
664 runmat_plot::render_interactive_with_handle(handle.as_u32(), figure_clone).map_err(|err| {
665 engine_error_with_source(
666 "Interactive plotting failed. Please check GPU/GUI system setup.",
667 PlottingBackendError::Interactive(err),
668 )
669 })
670 }
671
672 fn static_export(figure: &mut Figure, filename: &str) -> BuiltinResult<String> {
673 if figure.is_empty() {
674 return Err(engine_error("No plots found in figure to export"));
675 }
676 runmat_plot::show_plot_unified(figure.clone(), Some(filename))
677 .map(|_| format!("Plot saved to {filename}"))
678 .map_err(|err| {
679 engine_error_with_source("Plot export failed.", PlottingBackendError::Static(err))
680 })
681 }
682
683 fn ensure_figure_event_bridge() {
684 FIGURE_EVENT_BRIDGE.get_or_init(|| {
685 let observer: Arc<dyn for<'a> Fn(FigureEventView<'a>) + Send + Sync> =
686 Arc::new(|event: FigureEventView<'_>| match event.kind {
687 FigureEventKind::Closed => {
688 runmat_plot::gui::lifecycle::request_close(event.handle.as_u32());
689 }
690 FigureEventKind::Updated
691 if event.figure.is_some_and(|figure| !figure.visible) =>
692 {
693 runmat_plot::gui::lifecycle::request_close(event.handle.as_u32());
694 }
695 FigureEventKind::Created
696 | FigureEventKind::Updated
697 | FigureEventKind::Cleared => {}
698 });
699 let _ = install_figure_observer(observer);
700 });
701 }
702}
703
704#[cfg(all(test, feature = "plot-core"))]
705mod tests {
706 use super::render_figure_snapshot;
707 use crate::builtins::plotting::plot::plot_builtin;
708 use crate::builtins::plotting::state::{
709 clear_figure, current_figure_handle, reset_hold_state_for_run, PlotTestLockGuard,
710 };
711 use crate::builtins::plotting::subplot::subplot_builtin;
712 use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
713 use crate::builtins::plotting::title::title_builtin;
714 use crate::builtins::plotting::xlabel::xlabel_builtin;
715 use crate::builtins::plotting::ylabel::ylabel_builtin;
716 use futures::executor::block_on;
717 use runmat_builtins::{Tensor, Value};
718
719 fn setup_plot_tests() -> PlotTestLockGuard {
720 let guard = lock_plot_registry();
721 ensure_plot_test_env();
722 reset_hold_state_for_run();
723 let _ = clear_figure(None);
724 guard
725 }
726
727 fn tensor_from(data: &[f64]) -> Tensor {
728 Tensor {
729 data: data.to_vec(),
730 shape: vec![data.len()],
731 rows: data.len(),
732 cols: 1,
733 dtype: runmat_builtins::NumericDType::F64,
734 }
735 }
736
737 #[test]
738 fn render_figure_snapshot_supports_margin_style_two_axes_lines() {
739 let _guard = setup_plot_tests();
740 let x_mm: Vec<f64> = (-30..=30).map(|i| i as f64).collect();
741 let y_mm: Vec<f64> = (-25..=25).map(|i| i as f64).collect();
742 let centerline: Vec<f64> = x_mm
743 .iter()
744 .map(|x| 25.0 + 18.0 * (-(x / 11.0).powi(2)).exp())
745 .collect();
746 let vertical: Vec<f64> = y_mm
747 .iter()
748 .map(|y| 25.0 + 20.0 * (-(y / 9.0).powi(2)).exp())
749 .collect();
750
751 subplot_builtin(Value::Num(1.0), Value::Num(2.0), Value::Num(1.0)).expect("subplot 1");
752 block_on(plot_builtin(vec![
753 Value::Tensor(tensor_from(&x_mm)),
754 Value::Tensor(tensor_from(¢erline)),
755 ]))
756 .expect("left plot");
757 title_builtin(vec![Value::String("Centerline slice".into())]).expect("left title");
758 xlabel_builtin(vec![Value::String("x (mm)".into())]).expect("left xlabel");
759 ylabel_builtin(vec![Value::String("temperature (C)".into())]).expect("left ylabel");
760
761 subplot_builtin(Value::Num(1.0), Value::Num(2.0), Value::Num(2.0)).expect("subplot 2");
762 block_on(plot_builtin(vec![
763 Value::Tensor(tensor_from(&y_mm)),
764 Value::Tensor(tensor_from(&vertical)),
765 ]))
766 .expect("right plot");
767 title_builtin(vec![Value::String("Vertical slice through source".into())])
768 .expect("right title");
769 xlabel_builtin(vec![Value::String("y (mm)".into())]).expect("right xlabel");
770 ylabel_builtin(vec![Value::String("temperature (C)".into())]).expect("right ylabel");
771
772 let handle = current_figure_handle();
773 let bytes =
774 block_on(render_figure_snapshot(handle, 1280, 720, None)).expect("snapshot render");
775 assert!(bytes.len() > 1000, "expected nontrivial PNG payload");
776 }
777}