use runmat_builtins::{Tensor, Value};
use runmat_macros::runtime_builtin;
use runmat_plot::plots::AreaPlot;
use std::cell::RefCell;
use std::rc::Rc;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::plotting::type_resolvers::handle_scalar_type;
use super::op_common::line_inputs::NumericInput;
use super::plotting_error;
use super::state::{render_active_plot, PlotRenderOptions};
use super::style::{parse_line_style_args, value_as_f64, LineStyleParseOptions};
const BUILTIN_NAME: &str = "area";
type AreaSeries = Vec<(Vec<f64>, Option<Vec<f64>>)>;
const MATLAB_COLOR_ORDER: [glam::Vec4; 7] = [
glam::Vec4::new(0.0, 0.447, 0.741, 0.4),
glam::Vec4::new(0.85, 0.325, 0.098, 0.4),
glam::Vec4::new(0.929, 0.694, 0.125, 0.4),
glam::Vec4::new(0.494, 0.184, 0.556, 0.4),
glam::Vec4::new(0.466, 0.674, 0.188, 0.4),
glam::Vec4::new(0.301, 0.745, 0.933, 0.4),
glam::Vec4::new(0.635, 0.078, 0.184, 0.4),
];
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::plotting::area")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "area",
op_kind: GpuOpKind::PlotRender,
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::InheritInputs,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes:
"area is a plotting sink; GPU inputs may remain on device when a shared WGPU context is installed.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::plotting::area")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "area",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "area performs rendering and terminates fusion graphs.",
};
#[runtime_builtin(
name = "area",
category = "plotting",
summary = "Render MATLAB-compatible area plots.",
keywords = "area,plotting,stacked,fill",
sink = true,
suppress_auto_output = true,
type_resolver(handle_scalar_type),
builtin_path = "crate::builtins::plotting::area"
)]
pub fn area_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
let (target_axes, x_value, y_value, rest) = parse_area_args(args)?;
let mut x_input = Some(NumericInput::from_value(x_value, BUILTIN_NAME)?);
let mut y_input = Some(NumericInput::from_value(y_value, BUILTIN_NAME)?);
let parsed = parse_area_style_args(&rest)?;
let plot_handles = Rc::new(RefCell::new(Vec::new()));
let plot_handles_slot = Rc::clone(&plot_handles);
let figure_handle = crate::builtins::plotting::current_figure_handle();
let render_result = render_active_plot(
BUILTIN_NAME,
PlotRenderOptions {
title: "Area",
x_label: "X",
y_label: "Y",
..Default::default()
},
move |figure, axes| {
let axes = target_axes.unwrap_or(axes);
if let Some(y_gpu) = y_input.as_ref().and_then(NumericInput::gpu_handle) {
if let Ok(plots) =
build_area_gpu_plots(x_input.as_ref().expect("x present"), y_gpu, &parsed)
{
for (idx, plot) in plots.into_iter().enumerate() {
let plot_index = figure.add_area_plot_on_axes(plot, axes);
if idx == 0 {
plot_handles_slot.borrow_mut().push((axes, plot_index));
}
}
return Ok(());
}
}
let x_tensor = x_input
.take()
.expect("x consumed")
.into_tensor(BUILTIN_NAME)?;
let y_tensor = y_input
.take()
.expect("y consumed")
.into_tensor(BUILTIN_NAME)?;
let x = vector_from_tensor(&x_tensor, BUILTIN_NAME)?;
let series = area_series_from_tensor(x.clone(), &y_tensor, BUILTIN_NAME)?;
for (idx, (upper, lower)) in series.iter().enumerate() {
let mut plot = AreaPlot::new(x.clone(), upper.clone())
.map_err(|e| plotting_error(BUILTIN_NAME, format!("area: {e}")))?;
if let Some(lower) = lower.clone() {
plot = plot.with_lower_curve(lower);
} else {
plot.baseline = parsed.base_value;
}
let color = parsed
.color
.unwrap_or(MATLAB_COLOR_ORDER[idx % MATLAB_COLOR_ORDER.len()]);
plot.color = color;
plot.label = Some(
parsed
.label
.clone()
.unwrap_or_else(|| format!("Series {}", idx + 1)),
);
let plot_index = figure.add_area_plot_on_axes(plot, axes);
plot_handles_slot.borrow_mut().push((axes, plot_index));
}
Ok(())
},
);
let first = plot_handles.borrow().first().copied();
let Some((axes, plot_index)) = first else {
return render_result.map(|_| f64::NAN);
};
let handle =
crate::builtins::plotting::state::register_area_handle(figure_handle, axes, plot_index);
if let Err(err) = render_result {
let lower = err.to_string().to_lowercase();
if lower.contains("plotting is unavailable") || lower.contains("non-main thread") {
return Ok(handle);
}
return Err(err);
}
Ok(handle)
}
fn build_area_gpu_plots(
x: &NumericInput,
y: &runmat_accelerate_api::GpuTensorHandle,
parsed: &ParsedAreaStyle,
) -> crate::BuiltinResult<Vec<AreaPlot>> {
let context = super::gpu_helpers::ensure_shared_wgpu_context(BUILTIN_NAME)?;
let y_ref = runmat_accelerate_api::export_wgpu_buffer(y)
.ok_or_else(|| plotting_error(BUILTIN_NAME, "area: unable to export GPU Y data"))?;
let (rows, cols) = area_shape_from_gpu_shape(&y_ref.shape, y_ref.len);
let scalar = runmat_plot::gpu::ScalarType::from_is_f64(
y_ref.precision == runmat_accelerate_api::ProviderPrecision::F64,
);
let (x_axis, x_bounds) = match x {
NumericInput::Gpu(handle) => {
let x_ref = runmat_accelerate_api::export_wgpu_buffer(handle)
.ok_or_else(|| plotting_error(BUILTIN_NAME, "area: unable to export GPU X data"))?;
if x_ref.len != rows {
return Err(plotting_error(
BUILTIN_NAME,
"area: X length must match rows of Y",
));
}
let bounds =
super::gpu_helpers::axis_bounds(handle, BUILTIN_NAME).unwrap_or((0.0, 0.0));
(
runmat_plot::gpu::axis::AxisData::Buffer(x_ref.buffer.clone()),
bounds,
)
}
NumericInput::Host(tensor) => {
let values = vector_from_tensor(tensor, BUILTIN_NAME)?;
if values.len() != rows {
return Err(plotting_error(
BUILTIN_NAME,
"area: X length must match rows of Y",
));
}
let axis = match scalar {
runmat_plot::gpu::ScalarType::F32 => {
let owned: Box<[f32]> = values
.iter()
.map(|v| *v as f32)
.collect::<Vec<_>>()
.into_boxed_slice();
let leaked: &'static [f32] = Box::leak(owned);
runmat_plot::gpu::axis::AxisData::F32(leaked)
}
runmat_plot::gpu::ScalarType::F64 => {
let owned: Box<[f64]> = values.clone().into_boxed_slice();
let leaked: &'static [f64] = Box::leak(owned);
runmat_plot::gpu::axis::AxisData::F64(leaked)
}
};
(
axis,
(
values.first().copied().unwrap_or(0.0) as f32,
values.last().copied().unwrap_or(0.0) as f32,
),
)
}
};
let mut plots = Vec::with_capacity(cols);
let (min_cell, max_cell) =
super::gpu_helpers::axis_bounds(y, BUILTIN_NAME).unwrap_or((0.0, 0.0));
let min_stack = if min_cell < 0.0 {
parsed.base_value as f32 + (min_cell * cols as f32)
} else {
parsed.base_value as f32
};
let max_stack = if max_cell > 0.0 {
parsed.base_value as f32 + (max_cell * cols as f32)
} else {
parsed.base_value as f32
};
for idx in 0..cols {
let gpu_vertices = runmat_plot::gpu::area::pack_vertices(
&context.device,
&context.queue,
&runmat_plot::gpu::area::AreaGpuInputs {
x_axis: x_axis.clone(),
y_buffer: y_ref.buffer.clone(),
rows: rows as u32,
cols: cols as u32,
target_col: idx as u32,
scalar,
},
&runmat_plot::gpu::area::AreaGpuParams {
color: parsed
.color
.unwrap_or(MATLAB_COLOR_ORDER[idx % MATLAB_COLOR_ORDER.len()]),
baseline: parsed.base_value as f32,
},
)
.map_err(|e| {
plotting_error(
BUILTIN_NAME,
format!("area: failed to build GPU vertices: {e}"),
)
})?;
let mut plot = AreaPlot::from_gpu_buffer(
parsed
.color
.unwrap_or(MATLAB_COLOR_ORDER[idx % MATLAB_COLOR_ORDER.len()]),
parsed.base_value,
None,
gpu_vertices,
(rows - 1) * 6,
runmat_plot::core::BoundingBox::new(
glam::Vec3::new(x_bounds.0, min_stack, 0.0),
glam::Vec3::new(x_bounds.1, max_stack, 0.0),
),
);
plot.label = Some(
parsed
.label
.clone()
.unwrap_or_else(|| format!("Series {}", idx + 1)),
);
plots.push(plot);
}
Ok(plots)
}
struct ParsedAreaStyle {
color: Option<glam::Vec4>,
label: Option<String>,
base_value: f64,
}
fn parse_area_style_args(args: &[Value]) -> crate::BuiltinResult<ParsedAreaStyle> {
let mut filtered = Vec::new();
let mut base_value = 0.0;
let mut idx = 0usize;
while idx < args.len() {
if let Some(key) = super::style::value_as_string(&args[idx]) {
if key.trim().eq_ignore_ascii_case("BaseValue") && idx + 1 < args.len() {
base_value = value_as_f64(&args[idx + 1]).ok_or_else(|| {
plotting_error(BUILTIN_NAME, "area: BaseValue must be numeric")
})?;
idx += 2;
continue;
}
}
filtered.push(args[idx].clone());
idx += 1;
}
let parsed = parse_line_style_args(&filtered, &LineStyleParseOptions::generic(BUILTIN_NAME))?;
let explicit_color = area_color_was_explicit(&filtered);
Ok(ParsedAreaStyle {
color: explicit_color.then_some(parsed.appearance.color),
label: parsed.label,
base_value,
})
}
fn area_color_was_explicit(args: &[Value]) -> bool {
if let Some(token) = args.first().and_then(super::style::value_as_string) {
let mut chars = token.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'y' | 'm' | 'c' | 'r' | 'g' | 'b' | 'w' | 'k' => return true,
'-' | '.' => {
if matches!(chars.peek(), Some('-' | '.')) {
chars.next();
}
}
_ => {}
}
}
}
let mut idx = 0usize;
while idx + 1 < args.len() {
if let Some(key) = super::style::value_as_string(&args[idx]) {
if key.trim().eq_ignore_ascii_case("Color") {
return true;
}
}
idx += 2;
}
false
}
fn parse_area_args(
args: Vec<Value>,
) -> crate::BuiltinResult<(Option<usize>, Value, Value, Vec<Value>)> {
if args.is_empty() {
return Err(plotting_error(
BUILTIN_NAME,
"area: expected Y or X,Y inputs",
));
}
let mut it = args.into_iter();
let mut target_axes = None;
let first = it.next().unwrap();
let first = if let Ok(crate::builtins::plotting::properties::PlotHandle::Axes(_, axes)) =
crate::builtins::plotting::properties::resolve_plot_handle(&first, BUILTIN_NAME)
{
target_axes = Some(axes);
it.next()
.ok_or_else(|| plotting_error(BUILTIN_NAME, "area: expected data after axes handle"))?
} else {
first
};
let Some(second) = it.next() else {
let y = Tensor::try_from(&first)
.map_err(|e| plotting_error(BUILTIN_NAME, format!("area: {e}")))?;
let (rows, _) = area_shape_from_tensor(&y);
let x = Tensor {
data: (1..=rows).map(|i| i as f64).collect(),
shape: vec![rows],
rows,
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
};
return Ok((target_axes, Value::Tensor(x), first, Vec::new()));
};
if matches!(second, Value::String(_) | Value::CharArray(_)) {
let y = Tensor::try_from(&first)
.map_err(|e| plotting_error(BUILTIN_NAME, format!("area: {e}")))?;
let (rows, _) = area_shape_from_tensor(&y);
let x = Tensor {
data: (1..=rows).map(|i| i as f64).collect(),
shape: vec![rows],
rows,
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
};
let mut rest = vec![second];
rest.extend(it);
return Ok((target_axes, Value::Tensor(x), first, rest));
}
Ok((target_axes, first, second, it.collect()))
}
fn vector_from_tensor(tensor: &Tensor, builtin: &'static str) -> crate::BuiltinResult<Vec<f64>> {
if !(tensor.rows == 1 || tensor.cols == 1 || tensor.shape.len() <= 1) {
return Err(plotting_error(
builtin,
"area: X input must be a vector matching the row count of Y",
));
}
Ok(tensor.data.clone())
}
fn area_shape_from_tensor(tensor: &Tensor) -> (usize, usize) {
if tensor.shape.len() <= 1 || tensor.rows == 1 || tensor.cols == 1 {
(tensor.data.len().max(1), 1)
} else {
(tensor.rows.max(1), tensor.cols.max(1))
}
}
fn area_shape_from_gpu_shape(shape: &[usize], len: usize) -> (usize, usize) {
let rows = shape.first().copied().unwrap_or(len).max(1);
let cols = shape.get(1).copied().unwrap_or(1).max(1);
if shape.len() <= 1 || rows == 1 || cols == 1 {
(len.max(1), 1)
} else {
(rows, cols)
}
}
fn area_series_from_tensor(
x: Vec<f64>,
y: &Tensor,
builtin: &'static str,
) -> crate::BuiltinResult<AreaSeries> {
let (rows, cols) = area_shape_from_tensor(y);
if rows != x.len() {
return Err(plotting_error(
builtin,
"area: X length must match the number of rows in Y",
));
}
let mut out: AreaSeries = Vec::with_capacity(cols);
let mut cumulative = vec![0.0; rows];
for col in 0..cols {
let mut top = Vec::with_capacity(rows);
for row in 0..rows {
let idx = if cols == 1 { row } else { col * rows + row };
cumulative[row] += y.data.get(idx).copied().unwrap_or(0.0);
top.push(cumulative[row]);
}
let lower = if col == 0 {
None
} else {
Some(out[col - 1].0.clone())
};
out.push((top, lower));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::plotting::get::get_builtin;
use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
use crate::builtins::plotting::{
clear_figure, clone_figure, current_figure_handle, reset_hold_state_for_run,
};
use runmat_plot::plots::PlotElement;
fn matrix_tensor(data: Vec<f64>, rows: usize, cols: usize) -> Tensor {
Tensor {
data,
shape: vec![rows, cols],
rows,
cols,
dtype: runmat_builtins::NumericDType::F64,
}
}
#[test]
fn area_builds_stacked_series_from_matrix() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let handle = area_builtin(vec![Value::Tensor(matrix_tensor(
vec![1.0, 2.0, 0.5, 0.5],
2,
2,
))])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
assert_eq!(fig.plots().count(), 2);
let PlotElement::Area(first) = fig.plots().next().unwrap() else {
panic!("expected area")
};
let PlotElement::Area(second) = fig.plots().nth(1).unwrap() else {
panic!("expected area")
};
assert_eq!(first.y, vec![1.0, 2.0]);
assert_eq!(second.lower_y, Some(vec![1.0, 2.0]));
assert_eq!(
get_builtin(vec![Value::Num(handle), Value::String("Type".into())]).unwrap(),
Value::String("area".into())
);
}
#[test]
fn area_accepts_explicit_x_with_matrix_series() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let handle = area_builtin(vec![
Value::Tensor(Tensor {
data: vec![1.0, 2.0, 3.0, 4.0, 5.0],
shape: vec![5],
rows: 5,
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
}),
Value::Tensor(matrix_tensor(
vec![
1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 1.0, 2.0, 3.0,
],
5,
3,
)),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
assert_eq!(fig.plots().count(), 3);
let colors = fig
.plots()
.take(3)
.map(|plot| match plot {
PlotElement::Area(area) => area.color,
_ => panic!("expected area"),
})
.collect::<Vec<_>>();
assert_eq!(colors[0], MATLAB_COLOR_ORDER[0]);
assert_eq!(colors[1], MATLAB_COLOR_ORDER[1]);
assert_eq!(colors[2], MATLAB_COLOR_ORDER[2]);
assert_eq!(
get_builtin(vec![Value::Num(handle), Value::String("Type".into())]).unwrap(),
Value::String("area".into())
);
}
#[test]
fn area_accepts_explicit_x_with_row_vector_y() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let _ = area_builtin(vec![
Value::Tensor(Tensor {
data: vec![0.0, 0.2, 0.4, 0.6],
shape: vec![4],
rows: 4,
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
}),
Value::Tensor(Tensor {
data: vec![2.0, 2.2, 2.4, 2.6],
shape: vec![1, 4],
rows: 1,
cols: 4,
dtype: runmat_builtins::NumericDType::F64,
}),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
assert_eq!(fig.plots().count(), 1);
let PlotElement::Area(plot) = fig.plots().next().unwrap() else {
panic!("expected area")
};
assert_eq!(plot.x, vec![0.0, 0.2, 0.4, 0.6]);
assert_eq!(plot.y, vec![2.0, 2.2, 2.4, 2.6]);
}
}