#[derive(Clone, Debug, PartialEq)]
pub enum LineStyle {
None,
Solid,
Dashed,
DashDot,
Dotted,
Custom { offset: f32, pattern: Vec<f32> },
}
impl LineStyle {
pub(crate) fn draws_line(&self) -> bool {
!matches!(self, LineStyle::None)
}
pub(crate) fn painter_dashes(&self, width: f32) -> Option<(Vec<f32>, Vec<f32>, f32)> {
let u = width.max(1.0);
match self {
LineStyle::None | LineStyle::Solid => None,
LineStyle::Dashed => Some((vec![5.0 * u], vec![4.0 * u], 0.0)),
LineStyle::Dotted => Some((vec![1.5 * u], vec![2.5 * u], 0.0)),
LineStyle::DashDot => Some((vec![6.0 * u, 1.5 * u], vec![3.0 * u, 3.0 * u], 0.0)),
LineStyle::Custom { offset, pattern } => {
let dashes: Vec<f32> = pattern.iter().step_by(2).copied().collect();
let gaps: Vec<f32> = pattern.iter().skip(1).step_by(2).copied().collect();
let period: f32 = dashes.iter().chain(&gaps).sum();
if dashes.is_empty() || gaps.is_empty() || period <= 0.0 {
None
} else {
Some((dashes, gaps, *offset))
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Symbol {
Circle,
Square,
Cross,
Plus,
Triangle,
Diamond,
Point,
Pixel,
VerticalLine,
HorizontalLine,
TickLeft,
TickRight,
TickUp,
TickDown,
CaretLeft,
CaretRight,
CaretUp,
CaretDown,
}
impl Symbol {
pub(crate) fn code(self) -> u32 {
match self {
Symbol::Circle => 0,
Symbol::Square => 1,
Symbol::Cross => 2,
Symbol::Plus => 3,
Symbol::Triangle => 4,
Symbol::Diamond => 5,
Symbol::Point => 6,
Symbol::Pixel => 7,
Symbol::VerticalLine => 8,
Symbol::HorizontalLine => 9,
Symbol::TickLeft => 10,
Symbol::TickRight => 11,
Symbol::TickUp => 12,
Symbol::TickDown => 13,
Symbol::CaretLeft => 14,
Symbol::CaretRight => 15,
Symbol::CaretUp => 16,
Symbol::CaretDown => 17,
}
}
pub(crate) fn render_size_px(self, marker_size: f32) -> f32 {
match self {
Symbol::Pixel => 1.0,
Symbol::Point => (0.5 * marker_size).ceil() + 1.0,
Symbol::Plus
| Symbol::VerticalLine
| Symbol::HorizontalLine
| Symbol::TickLeft
| Symbol::TickRight
| Symbol::TickUp
| Symbol::TickDown => (marker_size / 2.0).floor() * 2.0 + 1.0,
_ => marker_size,
}
}
pub fn code_str(self) -> Option<&'static str> {
Some(match self {
Symbol::Circle => "o",
Symbol::Diamond => "d",
Symbol::Square => "s",
Symbol::Plus => "+",
Symbol::Cross => "x",
Symbol::Point => ".",
Symbol::Pixel => ",",
Symbol::VerticalLine => "|",
Symbol::HorizontalLine => "_",
Symbol::TickLeft => "tickleft",
Symbol::TickRight => "tickright",
Symbol::TickUp => "tickup",
Symbol::TickDown => "tickdown",
Symbol::CaretLeft => "caretleft",
Symbol::CaretRight => "caretright",
Symbol::CaretUp => "caretup",
Symbol::CaretDown => "caretdown",
Symbol::Triangle => return None,
})
}
pub fn from_code(s: &str) -> Option<Symbol> {
let symbol = match s {
"o" => Symbol::Circle,
"d" => Symbol::Diamond,
"s" => Symbol::Square,
"+" => Symbol::Plus,
"x" => Symbol::Cross,
"." => Symbol::Point,
"," => Symbol::Pixel,
"|" => Symbol::VerticalLine,
"_" => Symbol::HorizontalLine,
"tickleft" => Symbol::TickLeft,
"tickright" => Symbol::TickRight,
"tickup" => Symbol::TickUp,
"tickdown" => Symbol::TickDown,
"caretleft" => Symbol::CaretLeft,
"caretright" => Symbol::CaretRight,
"caretup" => Symbol::CaretUp,
"caretdown" => Symbol::CaretDown,
_ => {
return match s.to_ascii_lowercase().as_str() {
"circle" => Some(Symbol::Circle),
"diamond" => Some(Symbol::Diamond),
"square" => Some(Symbol::Square),
"plus" => Some(Symbol::Plus),
"cross" => Some(Symbol::Cross),
"point" => Some(Symbol::Point),
"pixel" => Some(Symbol::Pixel),
"vertical line" => Some(Symbol::VerticalLine),
"horizontal line" => Some(Symbol::HorizontalLine),
"tick left" => Some(Symbol::TickLeft),
"tick right" => Some(Symbol::TickRight),
"tick up" => Some(Symbol::TickUp),
"tick down" => Some(Symbol::TickDown),
"caret left" => Some(Symbol::CaretLeft),
"caret right" => Some(Symbol::CaretRight),
"caret up" => Some(Symbol::CaretUp),
"caret down" => Some(Symbol::CaretDown),
"triangle" => Some(Symbol::Triangle),
_ => None,
};
}
};
Some(symbol)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Baseline {
Scalar(f64),
PerPoint(Vec<f64>),
}
impl Baseline {
pub(crate) fn values(&self, n: usize) -> Vec<f32> {
match self {
Baseline::Scalar(v) => vec![*v as f32; n],
Baseline::PerPoint(vs) => vs.iter().map(|&v| v as f32).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScalarMask {
width: usize,
height: usize,
mask: Vec<u8>,
}
impl ScalarMask {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
mask: vec![0; width.saturating_mul(height)],
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn set_mask_data(&mut self, mask: &[u8], src_width: usize) {
if src_width == self.width && mask.len() == self.width * self.height {
self.mask.clear();
self.mask.extend_from_slice(mask);
return;
}
let src_width = src_width.max(1);
let src_height = mask.len() / src_width;
let copy_w = src_width.min(self.width);
let copy_h = src_height.min(self.height);
let mut new_mask = vec![0u8; self.width * self.height];
for row in 0..copy_h {
let dst = row * self.width;
let src = row * src_width;
new_mask[dst..dst + copy_w].copy_from_slice(&mask[src..src + copy_w]);
}
self.mask = new_mask;
}
pub fn get_mask_data(&self) -> &[u8] {
&self.mask
}
pub fn is_masked(&self, col: usize, row: usize) -> bool {
if col >= self.width || row >= self.height {
return false;
}
self.mask[row * self.width + col] != 0
}
pub fn apply(&self, data: &[f32]) -> Vec<f32> {
assert_eq!(
data.len(),
self.width * self.height,
"data length must equal width * height"
);
data.iter()
.zip(&self.mask)
.map(|(&v, &m)| if m != 0 { f32::NAN } else { v })
.collect()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ErrorBars {
Symmetric(f64),
PerPoint(Vec<f64>),
Asymmetric { lower: Vec<f64>, upper: Vec<f64> },
}
impl ErrorBars {
pub(crate) fn bounds(&self, i: usize) -> (f32, f32) {
match self {
ErrorBars::Symmetric(e) => (*e as f32, *e as f32),
ErrorBars::PerPoint(es) => (es[i] as f32, es[i] as f32),
ErrorBars::Asymmetric { lower, upper } => (lower[i] as f32, upper[i] as f32),
}
}
pub(crate) fn check_len(&self, n: usize) {
match self {
ErrorBars::Symmetric(_) => {}
ErrorBars::PerPoint(es) => {
assert_eq!(
es.len(),
n,
"per-point error must have one entry per vertex"
);
}
ErrorBars::Asymmetric { lower, upper } => {
assert_eq!(
lower.len(),
n,
"asymmetric error `lower` must have one entry per vertex"
);
assert_eq!(
upper.len(),
n,
"asymmetric error `upper` must have one entry per vertex"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draws_line_false_only_for_none() {
assert!(!LineStyle::None.draws_line());
assert!(LineStyle::Solid.draws_line());
assert!(LineStyle::Dashed.draws_line());
assert!(LineStyle::DashDot.draws_line());
assert!(LineStyle::Dotted.draws_line());
}
#[test]
fn painter_dashes_solid_and_none_are_undashed() {
assert_eq!(LineStyle::Solid.painter_dashes(1.0), None);
assert_eq!(LineStyle::None.painter_dashes(1.0), None);
}
#[test]
fn painter_dashes_predefined_scale_with_width() {
assert_eq!(
LineStyle::Dashed.painter_dashes(1.0),
Some((vec![5.0], vec![4.0], 0.0))
);
assert_eq!(
LineStyle::Dashed.painter_dashes(2.0),
Some((vec![10.0], vec![8.0], 0.0))
);
assert_eq!(
LineStyle::DashDot.painter_dashes(1.0),
Some((vec![6.0, 1.5], vec![3.0, 3.0], 0.0))
);
}
const SILX_CODES: &[(&str, Symbol)] = &[
("o", Symbol::Circle),
("d", Symbol::Diamond),
("s", Symbol::Square),
("+", Symbol::Plus),
("x", Symbol::Cross),
(".", Symbol::Point),
(",", Symbol::Pixel),
("|", Symbol::VerticalLine),
("_", Symbol::HorizontalLine),
("tickleft", Symbol::TickLeft),
("tickright", Symbol::TickRight),
("tickup", Symbol::TickUp),
("tickdown", Symbol::TickDown),
("caretleft", Symbol::CaretLeft),
("caretright", Symbol::CaretRight),
("caretup", Symbol::CaretUp),
("caretdown", Symbol::CaretDown),
];
#[test]
fn from_code_maps_every_silx_code() {
for &(code, symbol) in SILX_CODES {
assert_eq!(Symbol::from_code(code), Some(symbol), "code {code:?}");
}
}
#[test]
fn code_str_round_trips_every_coded_symbol() {
for &(code, symbol) in SILX_CODES {
assert_eq!(symbol.code_str(), Some(code), "reverse of {symbol:?}");
assert_eq!(
Symbol::from_code(symbol.code_str().unwrap()),
Some(symbol),
"round-trip of {symbol:?}"
);
}
}
#[test]
fn from_code_matches_human_names_case_insensitively() {
assert_eq!(Symbol::from_code("Circle"), Some(Symbol::Circle));
assert_eq!(Symbol::from_code("DIAMOND"), Some(Symbol::Diamond));
assert_eq!(Symbol::from_code("square"), Some(Symbol::Square));
assert_eq!(Symbol::from_code("Plus"), Some(Symbol::Plus));
assert_eq!(Symbol::from_code("Cross"), Some(Symbol::Cross));
assert_eq!(Symbol::from_code("Point"), Some(Symbol::Point));
assert_eq!(Symbol::from_code("Pixel"), Some(Symbol::Pixel));
assert_eq!(
Symbol::from_code("Vertical line"),
Some(Symbol::VerticalLine)
);
assert_eq!(
Symbol::from_code("Horizontal line"),
Some(Symbol::HorizontalLine)
);
assert_eq!(Symbol::from_code("Tick left"), Some(Symbol::TickLeft));
assert_eq!(Symbol::from_code("Tick right"), Some(Symbol::TickRight));
assert_eq!(Symbol::from_code("Tick up"), Some(Symbol::TickUp));
assert_eq!(Symbol::from_code("Tick down"), Some(Symbol::TickDown));
assert_eq!(Symbol::from_code("Caret left"), Some(Symbol::CaretLeft));
assert_eq!(Symbol::from_code("Caret right"), Some(Symbol::CaretRight));
assert_eq!(Symbol::from_code("Caret up"), Some(Symbol::CaretUp));
assert_eq!(Symbol::from_code("Caret down"), Some(Symbol::CaretDown));
}
#[test]
fn triangle_has_a_name_but_no_silx_code() {
assert_eq!(Symbol::from_code("triangle"), Some(Symbol::Triangle));
assert_eq!(Symbol::from_code("Triangle"), Some(Symbol::Triangle));
assert_eq!(Symbol::Triangle.code_str(), None);
}
#[test]
fn from_code_rejects_unsupported_codes() {
assert_eq!(Symbol::from_code(""), None);
assert_eq!(Symbol::from_code("\u{2665}"), None);
assert_eq!(Symbol::from_code("heart"), None);
assert_eq!(Symbol::from_code("nope"), None);
}
#[test]
fn render_size_px_overrides_per_symbol() {
assert_eq!(Symbol::Pixel.render_size_px(7.0), 1.0);
assert_eq!(Symbol::Pixel.render_size_px(20.0), 1.0);
assert_eq!(Symbol::Point.render_size_px(7.0), 5.0);
assert_eq!(Symbol::Point.render_size_px(8.0), 5.0);
for s in [
Symbol::Plus,
Symbol::VerticalLine,
Symbol::HorizontalLine,
Symbol::TickLeft,
Symbol::TickRight,
Symbol::TickUp,
Symbol::TickDown,
] {
assert_eq!(s.render_size_px(7.0), 7.0, "odd stays odd");
assert_eq!(s.render_size_px(8.0), 9.0, "even rounds to next odd");
}
for s in [
Symbol::Circle,
Symbol::Square,
Symbol::Cross,
Symbol::Triangle,
Symbol::Diamond,
Symbol::CaretLeft,
Symbol::CaretRight,
Symbol::CaretUp,
Symbol::CaretDown,
] {
assert_eq!(s.render_size_px(7.0), 7.0);
assert_eq!(s.render_size_px(8.0), 8.0);
}
}
#[test]
fn scalar_mask_new_is_all_valid() {
let m = ScalarMask::new(2, 2);
let data = vec![1.0, 2.0, 3.0, 4.0];
assert_eq!(m.apply(&data), data);
assert!(m.get_mask_data().iter().all(|&v| v == 0));
}
#[test]
fn scalar_mask_sets_masked_pixels_to_nan_and_keeps_others() {
let mut m = ScalarMask::new(2, 2);
m.set_mask_data(&[0, 1, 1, 0], 2);
let out = m.apply(&[10.0, 20.0, 30.0, 40.0]);
assert_eq!(out[0], 10.0); assert!(out[1].is_nan()); assert!(out[2].is_nan()); assert_eq!(out[3], 40.0); assert!(m.is_masked(1, 0));
assert!(m.is_masked(0, 1));
assert!(!m.is_masked(0, 0));
}
#[test]
fn scalar_mask_any_nonzero_value_masks() {
let mut m = ScalarMask::new(3, 1);
m.set_mask_data(&[0, 7, 255], 3);
let out = m.apply(&[1.0, 2.0, 3.0]);
assert_eq!(out[0], 1.0);
assert!(out[1].is_nan());
assert!(out[2].is_nan());
}
#[test]
fn scalar_mask_clips_oversized_mask_to_image_shape() {
let mut m = ScalarMask::new(2, 2);
#[rustfmt::skip]
let big = [
1, 0, 9,
0, 1, 9,
9, 9, 9,
];
m.set_mask_data(&big, 3);
assert_eq!(m.get_mask_data(), &[1, 0, 0, 1]);
let out = m.apply(&[5.0, 6.0, 7.0, 8.0]);
assert!(out[0].is_nan());
assert_eq!(out[1], 6.0);
assert_eq!(out[2], 7.0);
assert!(out[3].is_nan());
}
#[test]
fn scalar_mask_zero_extends_undersized_mask() {
let mut m = ScalarMask::new(2, 2);
m.set_mask_data(&[1], 1);
assert_eq!(m.get_mask_data(), &[1, 0, 0, 0]);
}
#[test]
fn scalar_mask_extends_when_only_height_differs() {
let mut m = ScalarMask::new(2, 2);
m.set_mask_data(&[1, 1], 2);
assert_eq!(m.get_mask_data(), &[1, 1, 0, 0]);
}
#[test]
#[should_panic(expected = "data length must equal width * height")]
fn scalar_mask_apply_rejects_length_mismatch() {
let m = ScalarMask::new(2, 2);
m.apply(&[1.0, 2.0, 3.0]);
}
#[test]
fn painter_dashes_custom_splits_on_off_and_keeps_offset() {
let style = LineStyle::Custom {
offset: 2.0,
pattern: vec![3.0, 1.0, 2.0, 4.0],
};
assert_eq!(
style.painter_dashes(1.0),
Some((vec![3.0, 2.0], vec![1.0, 4.0], 2.0))
);
let no_gap = LineStyle::Custom {
offset: 0.0,
pattern: vec![3.0],
};
assert_eq!(no_gap.painter_dashes(1.0), None);
let empty = LineStyle::Custom {
offset: 0.0,
pattern: vec![],
};
assert_eq!(empty.painter_dashes(1.0), None);
}
}