use std::num::NonZeroU64;
use egui::Color32;
use egui_wgpu::wgpu;
use crate::core::items::{Baseline, ErrorBars, LineStyle, Symbol};
use crate::core::decimate::min_max_decimate;
use crate::core::transform::YAxis;
const IDENTITY: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
const INV_LN10: f32 = std::f32::consts::LOG10_E;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct CurveParams {
ortho: [[f32; 4]; 4],
color: [f32; 4],
gap_color: [f32; 4],
dash_cum: [f32; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
half_width_px: f32,
use_vertex_color: f32,
dash_offset: f32,
use_gap_color: f32,
}
impl LineStyle {
fn dash_spec(&self, width: f32) -> Option<([f32; 4], f32)> {
let u = width.max(1.0);
let from_intervals = |intervals: [f32; 4], offset: f32| -> Option<([f32; 4], f32)> {
let cum = [
intervals[0],
intervals[0] + intervals[1],
intervals[0] + intervals[1] + intervals[2],
intervals[0] + intervals[1] + intervals[2] + intervals[3],
];
if cum[3] > 0.0 {
Some((cum, offset))
} else {
None
}
};
match self {
LineStyle::None | LineStyle::Solid => None,
LineStyle::Dashed => from_intervals([5.0 * u, 4.0 * u, 0.0, 0.0], 0.0),
LineStyle::Dotted => from_intervals([1.5 * u, 2.5 * u, 0.0, 0.0], 0.0),
LineStyle::DashDot => from_intervals([6.0 * u, 3.0 * u, 1.5 * u, 3.0 * u], 0.0),
LineStyle::Custom { offset, pattern } => {
let g = |i: usize| pattern.get(i).copied().unwrap_or(0.0);
from_intervals([g(0), g(1), g(2), g(3)], *offset)
}
}
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct FillParams {
ortho: [[f32; 4]; 4],
color: [f32; 4],
axis_log: [f32; 2],
_pad: [f32; 2],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct ErrorBarParams {
ortho: [[f32; 4]; 4],
color: [f32; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
half_width_px: f32,
_pad: [f32; 3],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct MarkerParams {
ortho: [[f32; 4]; 4],
color: [f32; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
half_size_px: f32,
symbol: u32,
use_vertex_color: f32,
_pad: f32,
}
#[derive(Clone, Debug)]
pub struct CurveData {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub color: Color32,
pub colors: Option<Vec<Color32>>,
pub line_style: LineStyle,
pub gap_color: Option<Color32>,
pub fill: bool,
pub baseline: Baseline,
pub x_error: Option<ErrorBars>,
pub y_error: Option<ErrorBars>,
pub width: f32,
pub symbol: Option<Symbol>,
pub marker_size: f32,
pub y_axis: YAxis,
}
impl CurveData {
pub fn new(x: Vec<f64>, y: Vec<f64>, color: Color32) -> Self {
assert_eq!(x.len(), y.len(), "x and y must have equal length");
Self {
x,
y,
color,
colors: None,
line_style: LineStyle::Solid,
gap_color: None,
fill: false,
baseline: Baseline::Scalar(0.0),
x_error: None,
y_error: None,
width: 1.0,
symbol: None,
marker_size: 7.0,
y_axis: YAxis::Left,
}
}
pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
assert_eq!(
colors.len(),
self.x.len(),
"colors must have one entry per vertex"
);
self.colors = Some(colors);
self
}
pub fn with_line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn with_gap_color(mut self, color: Color32) -> Self {
self.gap_color = Some(color);
self
}
pub fn with_fill(mut self, baseline: Baseline) -> Self {
if let Baseline::PerPoint(vs) = &baseline {
assert_eq!(
vs.len(),
self.x.len(),
"per-point baseline must have one entry per vertex"
);
}
self.fill = true;
self.baseline = baseline;
self
}
pub fn with_x_error(mut self, error: ErrorBars) -> Self {
error.check_len(self.x.len());
self.x_error = Some(error);
self
}
pub fn with_y_error(mut self, error: ErrorBars) -> Self {
error.check_len(self.y.len());
self.y_error = Some(error);
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.width = width.max(0.0);
self
}
pub fn with_symbol(mut self, symbol: Symbol) -> Self {
self.symbol = Some(symbol);
self
}
pub fn with_marker_size(mut self, size: f32) -> Self {
self.marker_size = size.max(0.0);
self
}
pub fn with_y_axis(mut self, y_axis: YAxis) -> Self {
self.y_axis = y_axis;
self
}
}
pub struct CurvePipeline {
pub(crate) pipeline: wgpu::RenderPipeline,
pub(crate) marker_pipeline: wgpu::RenderPipeline,
pub(crate) caps_pipeline: wgpu::RenderPipeline,
pub(crate) fill_pipeline: wgpu::RenderPipeline,
pub(crate) errorbar_pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
marker_bind_group_layout: wgpu::BindGroupLayout,
fill_bind_group_layout: wgpu::BindGroupLayout,
errorbar_bind_group_layout: wgpu::BindGroupLayout,
}
impl CurvePipeline {
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot curve"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/curve.wgsl").into()),
});
let marker_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot markers"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/markers.wgsl").into()),
});
let caps_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot line caps"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/linecaps.wgsl").into()),
});
let fill_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot fill"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/fill.wgsl").into()),
});
let errorbar_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rsplot errorbars"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/errorbars.wgsl").into()),
});
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot curve bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<CurveParams>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 2]>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 4]>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<f32>() as u64),
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot curve layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let marker_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot marker bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<MarkerParams>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 2]>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 4]>() as u64
),
},
count: None,
},
],
});
let marker_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot marker layout"),
bind_group_layouts: &[Some(&marker_bind_group_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot curve pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let marker_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot marker pipeline"),
layout: Some(&marker_pipeline_layout),
vertex: wgpu::VertexState {
module: &marker_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &marker_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let caps_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot line caps pipeline"),
layout: Some(&marker_pipeline_layout),
vertex: wgpu::VertexState {
module: &caps_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &caps_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let fill_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot fill bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<FillParams>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 2]>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<f32>() as u64),
},
count: None,
},
],
});
let fill_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot fill layout"),
bind_group_layouts: &[Some(&fill_bind_group_layout)],
immediate_size: 0,
});
let fill_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot fill pipeline"),
layout: Some(&fill_pipeline_layout),
vertex: wgpu::VertexState {
module: &fill_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &fill_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let errorbar_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("rsplot errorbar bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<ErrorBarParams>() as u64,
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(
std::mem::size_of::<[f32; 4]>() as u64
),
},
count: None,
},
],
});
let errorbar_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rsplot errorbar layout"),
bind_group_layouts: &[Some(&errorbar_bind_group_layout)],
immediate_size: 0,
});
let errorbar_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rsplot errorbar pipeline"),
layout: Some(&errorbar_pipeline_layout),
vertex: wgpu::VertexState {
module: &errorbar_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &errorbar_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Self {
pipeline,
marker_pipeline,
caps_pipeline,
fill_pipeline,
errorbar_pipeline,
bind_group_layout,
marker_bind_group_layout,
fill_bind_group_layout,
errorbar_bind_group_layout,
}
}
}
pub struct GpuCurve {
points: wgpu::Buffer,
count: u32,
capacity: u32,
params: wgpu::Buffer,
bind_group: wgpu::BindGroup,
vcolors: wgpu::Buffer,
colors_capacity: u32,
vertex_color: bool,
arclen: wgpu::Buffer,
arclen_capacity: u32,
arclen_key: Option<ArclenKey>,
marker_params: wgpu::Buffer,
marker_bind_group: wgpu::BindGroup,
caps_params: wgpu::Buffer,
caps_bind_group: wgpu::BindGroup,
fill: bool,
baseline_buf: wgpu::Buffer,
baseline_capacity: u32,
fill_params: wgpu::Buffer,
fill_bind_group: wgpu::BindGroup,
errorbar_segs: wgpu::Buffer,
errorbar_count: u32,
errorbar_capacity: u32,
errorbar_params: wgpu::Buffer,
errorbar_bind_group: wgpu::BindGroup,
color: [f32; 4],
line_style: LineStyle,
gap_color: Option<[f32; 4]>,
width: f32,
symbol: Option<Symbol>,
marker_size: f32,
pub(crate) y_axis: YAxis,
src_x: Vec<f64>,
src_y: Vec<f64>,
full_count: u32,
monotonic_x: bool,
decimate_key: Option<(u64, u64, u32)>,
}
type ArclenKey = ([[f32; 4]; 4], [f32; 2], [f32; 2]);
fn is_monotonic(xs: &[f64]) -> bool {
xs.windows(2).all(|w| w[0] <= w[1])
}
fn pack(x: &[f64], y: &[f64]) -> Vec<[f32; 2]> {
x.iter()
.zip(y)
.map(|(&x, &y)| [x as f32, y as f32])
.collect()
}
fn pack_colors(colors: &[Color32]) -> Vec<[f32; 4]> {
colors
.iter()
.map(|&c| egui::Rgba::from(c).to_array())
.collect()
}
fn project_px(
x: f64,
y: f64,
ortho: &[[f32; 4]; 4],
axis_log: [f32; 2],
half_vp: [f32; 2],
) -> [f32; 2] {
let sx = if axis_log[0] > 0.5 {
(x as f32).ln() * INV_LN10
} else {
x as f32
};
let sy = if axis_log[1] > 0.5 {
(y as f32).ln() * INV_LN10
} else {
y as f32
};
let cx = ortho[0][0] * sx + ortho[1][0] * sy + ortho[3][0];
let cy = ortho[0][1] * sx + ortho[1][1] * sy + ortho[3][1];
let cw = ortho[0][3] * sx + ortho[1][3] * sy + ortho[3][3];
[cx / cw * half_vp[0], cy / cw * half_vp[1]]
}
fn cumulative_arclen(
x: &[f64],
y: &[f64],
ortho: &[[f32; 4]; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
) -> Vec<f32> {
let half_vp = [viewport_px[0] * 0.5, viewport_px[1] * 0.5];
let mut out = Vec::with_capacity(x.len());
let mut acc = 0.0f32;
let mut prev: Option<[f32; 2]> = None;
for (&px, &py) in x.iter().zip(y) {
let p = project_px(px, py, ortho, axis_log, half_vp);
if let Some(q) = prev {
let (dx, dy) = (p[0] - q[0], p[1] - q[1]);
acc += (dx * dx + dy * dy).sqrt();
}
out.push(acc);
prev = Some(p);
}
out
}
const ERRORBAR_CAP_HALF_PX: f32 = 3.0;
const ERRORBAR_WIDTH_PX: f32 = 1.0;
fn build_errorbar_segments(
x: &[f64],
y: &[f64],
x_error: Option<&ErrorBars>,
y_error: Option<&ErrorBars>,
) -> Vec<[f32; 4]> {
let cap = ERRORBAR_CAP_HALF_PX;
let mut segs = Vec::new();
let mut push = |a: [f32; 4], b: [f32; 4]| {
segs.push(a);
segs.push(b);
};
for i in 0..x.len() {
let (xi, yi) = (x[i] as f32, y[i] as f32);
if let Some(e) = y_error {
let (lo, hi) = e.bounds(i);
let (bot, top) = (yi - lo, yi + hi);
push([xi, bot, 0.0, 0.0], [xi, top, 0.0, 0.0]);
push([xi, bot, -cap, 0.0], [xi, bot, cap, 0.0]);
push([xi, top, -cap, 0.0], [xi, top, cap, 0.0]);
}
if let Some(e) = x_error {
let (lo, hi) = e.bounds(i);
let (left, right) = (xi - lo, xi + hi);
push([left, yi, 0.0, 0.0], [right, yi, 0.0, 0.0]);
push([left, yi, 0.0, -cap], [left, yi, 0.0, cap]);
push([right, yi, 0.0, -cap], [right, yi, 0.0, cap]);
}
}
segs
}
impl GpuCurve {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &CurvePipeline,
curve: &CurveData,
) -> Self {
let positions = pack(&curve.x, &curve.y);
let capacity = positions.len().max(1) as u32;
let points = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve points"),
size: (capacity as usize * std::mem::size_of::<[f32; 2]>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if !positions.is_empty() {
queue.write_buffer(&points, 0, bytemuck::cast_slice(&positions));
}
let vertex_color = curve.colors.is_some();
let colors_capacity = if vertex_color { capacity } else { 0 };
let vcolors = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve colors"),
size: (colors_capacity.max(1) as usize * std::mem::size_of::<[f32; 4]>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if let Some(colors) = &curve.colors {
let packed = pack_colors(colors);
if !packed.is_empty() {
queue.write_buffer(&vcolors, 0, bytemuck::cast_slice(&packed));
}
}
let dashed = curve.line_style.dash_spec(curve.width).is_some();
let arclen_capacity = if dashed { capacity } else { 0 };
let arclen = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve arclen"),
size: (arclen_capacity.max(1) as usize * std::mem::size_of::<f32>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve params"),
size: std::mem::size_of::<CurveParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot curve bg"),
layout: &pipeline.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: points.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vcolors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: arclen.as_entire_binding(),
},
],
});
let marker_params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot marker params"),
size: std::mem::size_of::<MarkerParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let marker_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot marker bg"),
layout: &pipeline.marker_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: marker_params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: points.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vcolors.as_entire_binding(),
},
],
});
let caps_params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot line caps params"),
size: std::mem::size_of::<MarkerParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let caps_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot line caps bg"),
layout: &pipeline.marker_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: caps_params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: points.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: vcolors.as_entire_binding(),
},
],
});
let baseline_capacity = if curve.fill { capacity } else { 0 };
let baseline_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve baseline"),
size: (baseline_capacity.max(1) as usize * std::mem::size_of::<f32>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if curve.fill {
let base = curve.baseline.values(curve.x.len());
if !base.is_empty() {
queue.write_buffer(&baseline_buf, 0, bytemuck::cast_slice(&base));
}
}
let fill_params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot fill params"),
size: std::mem::size_of::<FillParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let fill_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot fill bg"),
layout: &pipeline.fill_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: fill_params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: points.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: baseline_buf.as_entire_binding(),
},
],
});
let segs = build_errorbar_segments(
&curve.x,
&curve.y,
curve.x_error.as_ref(),
curve.y_error.as_ref(),
);
let errorbar_capacity = segs.len() as u32;
let errorbar_count = (segs.len() / 2) as u32;
let errorbar_segs = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot curve errorbar segs"),
size: (errorbar_capacity.max(1) as usize * std::mem::size_of::<[f32; 4]>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if !segs.is_empty() {
queue.write_buffer(&errorbar_segs, 0, bytemuck::cast_slice(&segs));
}
let errorbar_params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rsplot errorbar params"),
size: std::mem::size_of::<ErrorBarParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let errorbar_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rsplot errorbar bg"),
layout: &pipeline.errorbar_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: errorbar_params.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: errorbar_segs.as_entire_binding(),
},
],
});
let color = egui::Rgba::from(curve.color).to_array();
let gpu = Self {
points,
count: positions.len() as u32,
capacity,
params,
bind_group,
vcolors,
colors_capacity,
vertex_color,
arclen,
arclen_capacity,
arclen_key: None,
marker_params,
marker_bind_group,
caps_params,
caps_bind_group,
fill: curve.fill,
baseline_buf,
baseline_capacity,
fill_params,
fill_bind_group,
errorbar_segs,
errorbar_count,
errorbar_capacity,
errorbar_params,
errorbar_bind_group,
color,
line_style: curve.line_style.clone(),
gap_color: curve.gap_color.map(|c| egui::Rgba::from(c).to_array()),
width: curve.width,
symbol: curve.symbol,
marker_size: curve.marker_size,
y_axis: curve.y_axis,
monotonic_x: is_monotonic(&curve.x),
full_count: positions.len() as u32,
src_x: curve.x.clone(),
src_y: curve.y.clone(),
decimate_key: None,
};
gpu.write_uniforms(queue, IDENTITY, [0.0, 0.0], [1.0, 1.0]);
gpu
}
pub(crate) fn update(&mut self, queue: &wgpu::Queue, curve: &CurveData) -> bool {
assert_eq!(
curve.x.len(),
curve.y.len(),
"x and y must have equal length"
);
if curve.x.len() as u32 > self.capacity {
return false;
}
if let Some(colors) = &curve.colors {
assert_eq!(
colors.len(),
curve.x.len(),
"colors must have one entry per vertex"
);
if colors.len() as u32 > self.colors_capacity {
return false;
}
}
if curve.line_style.dash_spec(curve.width).is_some()
&& curve.x.len() as u32 > self.arclen_capacity
{
return false;
}
if curve.fill {
if let Baseline::PerPoint(vs) = &curve.baseline {
assert_eq!(
vs.len(),
curve.x.len(),
"per-point baseline must have one entry per vertex"
);
}
if curve.x.len() as u32 > self.baseline_capacity {
return false;
}
}
if let Some(e) = &curve.x_error {
e.check_len(curve.x.len());
}
if let Some(e) = &curve.y_error {
e.check_len(curve.y.len());
}
let segs = build_errorbar_segments(
&curve.x,
&curve.y,
curve.x_error.as_ref(),
curve.y_error.as_ref(),
);
if segs.len() as u32 > self.errorbar_capacity {
return false;
}
let positions = pack(&curve.x, &curve.y);
if !positions.is_empty() {
queue.write_buffer(&self.points, 0, bytemuck::cast_slice(&positions));
}
self.count = positions.len() as u32;
self.vertex_color = curve.colors.is_some();
if let Some(colors) = &curve.colors {
let packed = pack_colors(colors);
if !packed.is_empty() {
queue.write_buffer(&self.vcolors, 0, bytemuck::cast_slice(&packed));
}
}
self.fill = curve.fill;
if curve.fill {
let base = curve.baseline.values(curve.x.len());
if !base.is_empty() {
queue.write_buffer(&self.baseline_buf, 0, bytemuck::cast_slice(&base));
}
}
self.errorbar_count = (segs.len() / 2) as u32;
if !segs.is_empty() {
queue.write_buffer(&self.errorbar_segs, 0, bytemuck::cast_slice(&segs));
}
self.color = egui::Rgba::from(curve.color).to_array();
self.line_style = curve.line_style.clone();
self.gap_color = curve.gap_color.map(|c| egui::Rgba::from(c).to_array());
self.width = curve.width;
self.symbol = curve.symbol;
self.marker_size = curve.marker_size;
self.y_axis = curve.y_axis;
self.src_x = curve.x.clone();
self.src_y = curve.y.clone();
self.full_count = positions.len() as u32;
self.monotonic_x = is_monotonic(&curve.x);
self.decimate_key = None;
self.arclen_key = None;
true
}
pub(crate) fn ensure_decimated(
&mut self,
queue: &wgpu::Queue,
x_min: f64,
x_max: f64,
columns: u32,
) {
let beneficial = self.symbol.is_none()
&& !self.vertex_color
&& !self.fill
&& self.line_style.dash_spec(self.width).is_none()
&& self.monotonic_x
&& columns > 0
&& self.full_count as u64 > 2 * columns as u64 + 2;
if !beneficial {
if self.decimate_key.is_some() {
let positions = pack(&self.src_x, &self.src_y);
if !positions.is_empty() {
queue.write_buffer(&self.points, 0, bytemuck::cast_slice(&positions));
}
self.count = self.full_count;
self.decimate_key = None;
}
return;
}
let key = (x_min.to_bits(), x_max.to_bits(), columns);
if self.decimate_key == Some(key) {
return; }
let (dx, dy) = min_max_decimate(&self.src_x, &self.src_y, x_min, x_max, columns);
let positions = pack(&dx, &dy);
if !positions.is_empty() {
queue.write_buffer(&self.points, 0, bytemuck::cast_slice(&positions));
}
self.count = positions.len() as u32;
self.decimate_key = Some(key);
}
pub(crate) fn ensure_arclen(
&mut self,
queue: &wgpu::Queue,
ortho: [[f32; 4]; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
) {
if self.line_style.dash_spec(self.width).is_none() {
return; }
let key = (ortho, axis_log, viewport_px);
if self.arclen_key == Some(key) {
return; }
let lens = cumulative_arclen(&self.src_x, &self.src_y, &ortho, axis_log, viewport_px);
if !lens.is_empty() {
queue.write_buffer(&self.arclen, 0, bytemuck::cast_slice(&lens));
}
self.arclen_key = Some(key);
}
pub(crate) fn write_uniforms(
&self,
queue: &wgpu::Queue,
ortho: [[f32; 4]; 4],
axis_log: [f32; 2],
viewport_px: [f32; 2],
) {
let (dash_cum, dash_offset) = self
.line_style
.dash_spec(self.width)
.unwrap_or(([0.0; 4], 0.0));
let params = CurveParams {
ortho,
color: self.color,
gap_color: self.gap_color.unwrap_or([0.0; 4]),
dash_cum,
axis_log,
viewport_px,
half_width_px: 0.5 * self.width,
use_vertex_color: if self.vertex_color { 1.0 } else { 0.0 },
dash_offset,
use_gap_color: if self.gap_color.is_some() { 1.0 } else { 0.0 },
};
queue.write_buffer(&self.params, 0, bytemuck::bytes_of(¶ms));
let render_size = self
.symbol
.map_or(self.marker_size, |s| s.render_size_px(self.marker_size));
let marker = MarkerParams {
ortho,
color: self.color,
axis_log,
viewport_px,
half_size_px: 0.5 * render_size,
symbol: self.symbol.map_or(0, Symbol::code),
use_vertex_color: if self.vertex_color { 1.0 } else { 0.0 },
_pad: 0.0,
};
queue.write_buffer(&self.marker_params, 0, bytemuck::bytes_of(&marker));
let caps = MarkerParams {
ortho,
color: self.color,
axis_log,
viewport_px,
half_size_px: 0.5 * self.width,
symbol: 0,
use_vertex_color: if self.vertex_color { 1.0 } else { 0.0 },
_pad: 0.0,
};
queue.write_buffer(&self.caps_params, 0, bytemuck::bytes_of(&caps));
let fill = FillParams {
ortho,
color: self.color,
axis_log,
_pad: [0.0; 2],
};
queue.write_buffer(&self.fill_params, 0, bytemuck::bytes_of(&fill));
let errorbar = ErrorBarParams {
ortho,
color: self.color,
axis_log,
viewport_px,
half_width_px: 0.5 * ERRORBAR_WIDTH_PX,
_pad: [0.0; 3],
};
queue.write_buffer(&self.errorbar_params, 0, bytemuck::bytes_of(&errorbar));
}
pub(crate) fn draw_fill(
&self,
render_pass: &mut wgpu::RenderPass<'_>,
pipeline: &CurvePipeline,
) {
if !self.fill || self.count < 2 {
return;
}
let vertices = 6 * (self.count - 1);
render_pass.set_pipeline(&pipeline.fill_pipeline);
render_pass.set_bind_group(0, &self.fill_bind_group, &[]);
render_pass.draw(0..vertices, 0..1);
}
pub(crate) fn draw_errorbars(
&self,
render_pass: &mut wgpu::RenderPass<'_>,
pipeline: &CurvePipeline,
) {
if self.errorbar_count == 0 {
return;
}
let vertices = 6 * self.errorbar_count;
render_pass.set_pipeline(&pipeline.errorbar_pipeline);
render_pass.set_bind_group(0, &self.errorbar_bind_group, &[]);
render_pass.draw(0..vertices, 0..1);
}
pub(crate) fn draw(&self, render_pass: &mut wgpu::RenderPass<'_>, pipeline: &CurvePipeline) {
if self.count < 2 || !self.line_style.draws_line() {
return;
}
let vertices = 6 * (self.count - 1);
render_pass.set_pipeline(&pipeline.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..vertices, 0..1);
}
pub(crate) fn draw_caps(
&self,
render_pass: &mut wgpu::RenderPass<'_>,
pipeline: &CurvePipeline,
) {
if self.count < 2
|| self.width <= 0.0
|| !self.line_style.draws_line()
|| self.line_style.dash_spec(self.width).is_some()
{
return;
}
let vertices = 6 * self.count;
render_pass.set_pipeline(&pipeline.caps_pipeline);
render_pass.set_bind_group(0, &self.caps_bind_group, &[]);
render_pass.draw(0..vertices, 0..1);
}
pub(crate) fn draw_markers(
&self,
render_pass: &mut wgpu::RenderPass<'_>,
pipeline: &CurvePipeline,
) {
if self.symbol.is_none() || self.count == 0 {
return;
}
let vertices = 6 * self.count;
render_pass.set_pipeline(&pipeline.marker_pipeline);
render_pass.set_bind_group(0, &self.marker_bind_group, &[]);
render_pass.draw(0..vertices, 0..1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_codes_match_shader_switch() {
assert_eq!(Symbol::Circle.code(), 0);
assert_eq!(Symbol::Square.code(), 1);
assert_eq!(Symbol::Cross.code(), 2);
assert_eq!(Symbol::Plus.code(), 3);
assert_eq!(Symbol::Triangle.code(), 4);
assert_eq!(Symbol::Diamond.code(), 5);
assert_eq!(Symbol::Point.code(), 6);
assert_eq!(Symbol::Pixel.code(), 7);
assert_eq!(Symbol::VerticalLine.code(), 8);
assert_eq!(Symbol::HorizontalLine.code(), 9);
assert_eq!(Symbol::TickLeft.code(), 10);
assert_eq!(Symbol::TickRight.code(), 11);
assert_eq!(Symbol::TickUp.code(), 12);
assert_eq!(Symbol::TickDown.code(), 13);
assert_eq!(Symbol::CaretLeft.code(), 14);
assert_eq!(Symbol::CaretRight.code(), 15);
assert_eq!(Symbol::CaretUp.code(), 16);
assert_eq!(Symbol::CaretDown.code(), 17);
assert_eq!(Symbol::Heart.code(), 18);
}
#[test]
fn curve_data_defaults_and_builders() {
let c = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE);
assert_eq!(c.width, 1.0);
assert_eq!(c.symbol, None);
assert_eq!(c.marker_size, 7.0);
assert_eq!(c.y_axis, YAxis::Left);
assert_eq!(c.colors, None);
let c = c
.with_width(-3.0) .with_symbol(Symbol::Plus)
.with_marker_size(-1.0) .with_y_axis(YAxis::Right);
assert_eq!(c.width, 0.0);
assert_eq!(c.symbol, Some(Symbol::Plus));
assert_eq!(c.marker_size, 0.0);
assert_eq!(c.y_axis, YAxis::Right);
}
#[test]
fn with_colors_sets_per_vertex_colors() {
let c = CurveData::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0], Color32::WHITE)
.with_colors(vec![Color32::RED, Color32::GREEN, Color32::BLUE]);
assert_eq!(
c.colors,
Some(vec![Color32::RED, Color32::GREEN, Color32::BLUE])
);
}
#[test]
#[should_panic(expected = "colors must have one entry per vertex")]
fn with_colors_rejects_length_mismatch() {
CurveData::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0], Color32::WHITE)
.with_colors(vec![Color32::RED, Color32::GREEN]);
}
#[test]
fn marker_params_std140_size_unchanged() {
assert_eq!(std::mem::size_of::<MarkerParams>(), 112);
assert_eq!(std::mem::size_of::<MarkerParams>() % 16, 0);
}
#[test]
fn pack_colors_matches_single_color_conversion() {
let c = Color32::from_rgba_unmultiplied(200, 100, 50, 180);
assert_eq!(pack_colors(&[c])[0], egui::Rgba::from(c).to_array());
}
#[test]
fn line_style_default_and_builders() {
let c = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE);
assert_eq!(c.line_style, LineStyle::Solid);
assert_eq!(c.gap_color, None);
let c = c
.with_line_style(LineStyle::Dashed)
.with_gap_color(Color32::BLACK);
assert_eq!(c.line_style, LineStyle::Dashed);
assert_eq!(c.gap_color, Some(Color32::BLACK));
}
#[test]
fn dash_spec_solid_and_none_are_undashed() {
assert_eq!(LineStyle::Solid.dash_spec(1.0), None);
assert_eq!(LineStyle::None.dash_spec(1.0), None);
}
#[test]
fn dash_spec_boundaries_and_period() {
let (cum, off) = LineStyle::Dashed.dash_spec(1.0).expect("dashed");
assert_eq!(cum, [5.0, 9.0, 9.0, 9.0]);
assert_eq!(off, 0.0);
let (cum, _) = LineStyle::DashDot.dash_spec(1.0).expect("dashdot");
assert_eq!(cum, [6.0, 9.0, 10.5, 13.5]);
}
#[test]
fn dash_spec_scales_with_width() {
let (cum, _) = LineStyle::Dashed.dash_spec(2.0).expect("dashed");
assert_eq!(cum, [10.0, 18.0, 18.0, 18.0]);
}
#[test]
fn dash_spec_custom_pattern_and_offset() {
let style = LineStyle::Custom {
offset: 2.0,
pattern: vec![3.0, 1.0],
};
let (cum, off) = style.dash_spec(1.0).expect("custom");
assert_eq!(cum, [3.0, 4.0, 4.0, 4.0]);
assert_eq!(off, 2.0);
let empty = LineStyle::Custom {
offset: 0.0,
pattern: vec![],
};
assert_eq!(empty.dash_spec(1.0), None);
}
#[test]
fn cumulative_arclen_matches_pixel_distance() {
use crate::core::transform::{Axis, Transform};
use egui::{Rect, pos2};
let area = Rect::from_min_max(pos2(50.0, 30.0), pos2(450.0, 230.0));
let t = Transform::with_axes(Axis::linear(-2.0, 6.0), Axis::linear(1.0, 9.0), area);
let ortho = t.ortho_matrix();
let viewport = [area.width(), area.height()];
let x = vec![-2.0, 0.0, 3.0, 6.0];
let y = vec![1.0, 4.0, 2.0, 9.0];
let lens = cumulative_arclen(&x, &y, &ortho, [0.0, 0.0], viewport);
assert_eq!(lens.len(), 4);
assert_eq!(lens[0], 0.0);
assert!(lens.windows(2).all(|w| w[1] >= w[0]), "non-decreasing");
let mut expected = 0.0f32;
for i in 1..x.len() {
let a = t.data_to_pixel(x[i - 1], y[i - 1]);
let b = t.data_to_pixel(x[i], y[i]);
expected += (b - a).length();
}
let total = *lens.last().unwrap();
assert!(
(total - expected).abs() <= 1e-2,
"arc length {total} vs pixel distance {expected}"
);
}
#[test]
fn fill_default_and_builders() {
let c = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE);
assert!(!c.fill);
assert_eq!(c.baseline, Baseline::Scalar(0.0));
let c = c.with_fill(Baseline::Scalar(-2.0));
assert!(c.fill);
assert_eq!(c.baseline, Baseline::Scalar(-2.0));
}
#[test]
fn baseline_values_broadcasts_scalar_and_passes_through_per_point() {
assert_eq!(Baseline::Scalar(3.0).values(3), vec![3.0, 3.0, 3.0]);
assert_eq!(
Baseline::PerPoint(vec![1.0, 2.0, 3.0]).values(3),
vec![1.0, 2.0, 3.0]
);
}
#[test]
#[should_panic(expected = "per-point baseline must have one entry per vertex")]
fn with_fill_per_point_rejects_length_mismatch() {
CurveData::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0], Color32::WHITE)
.with_fill(Baseline::PerPoint(vec![0.0, 0.0]));
}
#[test]
fn error_bars_default_and_builders() {
let c = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE);
assert_eq!(c.x_error, None);
assert_eq!(c.y_error, None);
let c = c
.with_x_error(ErrorBars::Symmetric(0.5))
.with_y_error(ErrorBars::PerPoint(vec![0.1, 0.2]));
assert_eq!(c.x_error, Some(ErrorBars::Symmetric(0.5)));
assert_eq!(c.y_error, Some(ErrorBars::PerPoint(vec![0.1, 0.2])));
}
#[test]
fn error_bars_bounds_per_variant() {
assert_eq!(ErrorBars::Symmetric(0.5).bounds(0), (0.5, 0.5));
assert_eq!(ErrorBars::PerPoint(vec![0.1, 0.3]).bounds(1), (0.3, 0.3));
assert_eq!(
ErrorBars::Asymmetric {
lower: vec![0.2, 0.4],
upper: vec![1.0, 2.0],
}
.bounds(1),
(0.4, 2.0)
);
}
#[test]
#[should_panic(expected = "per-point error must have one entry per vertex")]
fn with_y_error_per_point_rejects_length_mismatch() {
CurveData::new(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0], Color32::WHITE)
.with_y_error(ErrorBars::PerPoint(vec![0.1, 0.2]));
}
#[test]
#[should_panic(expected = "asymmetric error `upper` must have one entry per vertex")]
fn with_x_error_asymmetric_rejects_length_mismatch() {
CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE).with_x_error(
ErrorBars::Asymmetric {
lower: vec![0.1, 0.2],
upper: vec![0.3],
},
);
}
#[test]
fn build_errorbar_segments_y_error_geometry() {
let cap = ERRORBAR_CAP_HALF_PX;
let segs = build_errorbar_segments(&[2.0], &[5.0], None, Some(&ErrorBars::Symmetric(1.0)));
assert_eq!(
segs,
vec![
[2.0, 4.0, 0.0, 0.0],
[2.0, 6.0, 0.0, 0.0],
[2.0, 4.0, -cap, 0.0],
[2.0, 4.0, cap, 0.0],
[2.0, 6.0, -cap, 0.0],
[2.0, 6.0, cap, 0.0],
]
);
}
#[test]
fn build_errorbar_segments_x_error_geometry() {
let cap = ERRORBAR_CAP_HALF_PX;
let segs = build_errorbar_segments(
&[2.0],
&[5.0],
Some(&ErrorBars::Asymmetric {
lower: vec![1.0],
upper: vec![2.0],
}),
None,
);
assert_eq!(
segs,
vec![
[1.0, 5.0, 0.0, 0.0],
[4.0, 5.0, 0.0, 0.0],
[1.0, 5.0, 0.0, -cap],
[1.0, 5.0, 0.0, cap],
[4.0, 5.0, 0.0, -cap],
[4.0, 5.0, 0.0, cap],
]
);
}
#[test]
fn build_errorbar_segments_counts_scale_with_active_axes() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![0.0, 1.0, 2.0];
assert!(build_errorbar_segments(&x, &y, None, None).is_empty());
let one = build_errorbar_segments(&x, &y, None, Some(&ErrorBars::Symmetric(1.0)));
assert_eq!(one.len(), 3 * 2 * x.len());
let both = build_errorbar_segments(
&x,
&y,
Some(&ErrorBars::Symmetric(0.5)),
Some(&ErrorBars::Symmetric(1.0)),
);
assert_eq!(both.len(), 6 * 2 * x.len());
}
}