#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SeriesPattern {
#[default]
Solid,
Dashed,
Dotted,
DashDot,
ShortDash,
WideDash,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SeriesMarker {
Circle,
Square,
Triangle,
Diamond,
Cross,
Plus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SeriesHatch {
None,
Forward,
Backward,
Cross,
Horizontal,
Vertical,
}
impl SeriesPattern {
pub const ALL: [SeriesPattern; 6] = [
SeriesPattern::Solid,
SeriesPattern::Dashed,
SeriesPattern::Dotted,
SeriesPattern::DashDot,
SeriesPattern::ShortDash,
SeriesPattern::WideDash,
];
pub fn for_index(index: usize) -> Self {
Self::ALL[index % Self::ALL.len()]
}
pub fn dash(self, line_width: f32) -> Option<(f32, f32)> {
let w = line_width.max(0.5);
match self {
Self::Solid => None,
Self::Dashed => Some((w * 4.0, w * 2.5)),
Self::Dotted => Some((w * 0.9, w * 1.8)),
Self::DashDot => Some((w * 6.0, w * 2.0)),
Self::ShortDash => Some((w * 2.0, w * 1.5)),
Self::WideDash => Some((w * 3.0, w * 5.0)),
}
}
pub fn marker(self) -> SeriesMarker {
match self {
Self::Solid => SeriesMarker::Circle,
Self::Dashed => SeriesMarker::Square,
Self::Dotted => SeriesMarker::Triangle,
Self::DashDot => SeriesMarker::Diamond,
Self::ShortDash => SeriesMarker::Cross,
Self::WideDash => SeriesMarker::Plus,
}
}
pub fn hatch(self) -> SeriesHatch {
match self {
Self::Solid => SeriesHatch::None,
Self::Dashed => SeriesHatch::Forward,
Self::Dotted => SeriesHatch::Backward,
Self::DashDot => SeriesHatch::Cross,
Self::ShortDash => SeriesHatch::Horizontal,
Self::WideDash => SeriesHatch::Vertical,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_first_six_series_get_six_distinct_patterns() {
let assigned: Vec<SeriesPattern> = (0..6).map(SeriesPattern::for_index).collect();
let unique: std::collections::HashSet<_> = assigned.iter().collect();
assert_eq!(
unique.len(),
6,
"each of the first six series must be distinguishable without colour"
);
}
#[test]
fn the_pattern_cycle_does_not_share_a_period_with_the_palette() {
assert_ne!(
SeriesPattern::for_index(0),
SeriesPattern::for_index(8),
"a ninth series must not repeat the first in both channels"
);
}
#[test]
fn every_pattern_carries_all_three_renderings() {
let dashes: std::collections::HashSet<_> = SeriesPattern::ALL
.iter()
.map(|p| p.dash(2.0).map(|(d, g)| (d.to_bits(), g.to_bits())))
.collect();
let markers: std::collections::HashSet<_> =
SeriesPattern::ALL.iter().map(|p| p.marker()).collect();
let hatches: std::collections::HashSet<_> =
SeriesPattern::ALL.iter().map(|p| p.hatch()).collect();
assert_eq!(dashes.len(), 6, "line dashes must all differ");
assert_eq!(markers.len(), 6, "markers must all differ");
assert_eq!(hatches.len(), 6, "hatches must all differ");
}
#[test]
fn dashes_scale_with_the_line_width() {
let thin = SeriesPattern::Dashed.dash(1.0).unwrap();
let thick = SeriesPattern::Dashed.dash(4.0).unwrap();
assert!(thick.0 > thin.0 && thick.1 > thin.1);
}
}