use crate::geom::Point;
use crate::view::Range;
#[derive(Debug, Clone, Copy)]
pub struct MinMax {
pub min: Point,
pub max: Point,
pub x_range: Range,
}
impl MinMax {
fn from_partial(partial: &PartialBucket) -> Self {
Self {
min: partial.min,
max: partial.max,
x_range: Range::new(partial.first_x, partial.last_x),
}
}
fn merge(a: Self, b: Self) -> Self {
let min = if a.min.y < b.min.y {
a.min
} else if b.min.y < a.min.y {
b.min
} else if a.min.x <= b.min.x {
a.min
} else {
b.min
};
let max = if a.max.y > b.max.y {
a.max
} else if b.max.y > a.max.y {
b.max
} else if a.max.x <= b.max.x {
a.max
} else {
b.max
};
Self {
min,
max,
x_range: Range::new(
a.x_range.min.min(b.x_range.min),
a.x_range.max.max(b.x_range.max),
),
}
}
pub fn push_ordered(&self, out: &mut Vec<Point>) {
if self.min == self.max {
out.push(self.min);
return;
}
if self.min.x <= self.max.x {
out.push(self.min);
out.push(self.max);
} else {
out.push(self.max);
out.push(self.min);
}
}
}
#[derive(Debug, Clone)]
struct PartialBucket {
count: usize,
min: Point,
max: Point,
first_x: f64,
last_x: f64,
}
impl PartialBucket {
fn new(point: Point) -> Self {
Self {
count: 1,
min: point,
max: point,
first_x: point.x,
last_x: point.x,
}
}
fn push(&mut self, point: Point) {
self.count += 1;
self.last_x = point.x;
if point.y < self.min.y {
self.min = point;
}
if point.y > self.max.y {
self.max = point;
}
}
}
#[derive(Debug, Clone)]
pub struct SummaryLevel {
chunk_size: usize,
buckets: Vec<MinMax>,
}
impl SummaryLevel {
fn new(chunk_size: usize) -> Self {
Self {
chunk_size,
buckets: Vec::new(),
}
}
pub(crate) fn buckets(&self) -> &[MinMax] {
&self.buckets
}
}
#[derive(Debug, Clone)]
pub struct SummaryLevels {
base_chunk: usize,
levels: Vec<SummaryLevel>,
partial: Option<PartialBucket>,
}
impl SummaryLevels {
pub fn new(base_chunk: usize) -> Self {
let base_chunk = base_chunk.max(1);
Self {
base_chunk,
levels: Vec::new(),
partial: None,
}
}
pub fn base_chunk(&self) -> usize {
self.base_chunk
}
pub fn push(&mut self, point: Point) {
match self.partial.as_mut() {
None => {
self.partial = Some(PartialBucket::new(point));
}
Some(partial) => {
partial.push(point);
if partial.count >= self.base_chunk {
let bucket = MinMax::from_partial(partial);
self.partial = None;
self.push_bucket(0, bucket);
}
}
}
}
pub fn partial_bucket(&self) -> Option<MinMax> {
self.partial.as_ref().map(MinMax::from_partial)
}
pub fn choose_level(&self, target_chunk: usize) -> Option<&SummaryLevel> {
let target_chunk = target_chunk.max(1);
for level in &self.levels {
if level.chunk_size >= target_chunk {
return Some(level);
}
}
self.levels.last()
}
fn push_bucket(&mut self, level_index: usize, bucket: MinMax) {
if self.levels.len() <= level_index {
let chunk_size = self.base_chunk.saturating_mul(1 << level_index);
self.levels.push(SummaryLevel::new(chunk_size));
}
let level = &mut self.levels[level_index];
level.buckets.push(bucket);
let len = level.buckets.len();
if len % 2 == 0 {
let merged = MinMax::merge(level.buckets[len - 2], level.buckets[len - 1]);
self.push_bucket(level_index + 1, merged);
}
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct DecimationScratch {
buckets: Vec<Bucket>,
points: Vec<Point>,
}
impl DecimationScratch {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn clear(&mut self) {
self.points.clear();
}
pub(crate) fn output(&self) -> &[Point] {
&self.points
}
pub(crate) fn output_mut(&mut self) -> &mut Vec<Point> {
&mut self.points
}
}
#[derive(Debug, Clone, Copy)]
struct Bucket {
has_data: bool,
min: Point,
max: Point,
}
impl Default for Bucket {
fn default() -> Self {
Self {
has_data: false,
min: Point::new(0.0, 0.0),
max: Point::new(0.0, 0.0),
}
}
}
impl Bucket {
fn reset(&mut self) {
self.has_data = false;
}
fn push(&mut self, point: Point) {
if !self.has_data {
self.has_data = true;
self.min = point;
self.max = point;
return;
}
if point.y < self.min.y {
self.min = point;
}
if point.y > self.max.y {
self.max = point;
}
}
fn push_ordered(&self, out: &mut Vec<Point>) {
if !self.has_data {
return;
}
if self.min == self.max {
out.push(self.min);
return;
}
if self.min.x <= self.max.x {
out.push(self.min);
out.push(self.max);
} else {
out.push(self.max);
out.push(self.min);
}
}
}
pub fn decimate_minmax<'a>(
points: &[Point],
x_range: Range,
pixel_width: usize,
scratch: &'a mut DecimationScratch,
) -> &'a [Point] {
scratch.points.clear();
if points.is_empty() || pixel_width == 0 {
return scratch.output();
}
let span = x_range.span();
if span <= 0.0 {
scratch.points.extend_from_slice(points);
return scratch.output();
}
if scratch.buckets.len() < pixel_width {
scratch.buckets.resize(pixel_width, Bucket::default());
}
for bucket in scratch.buckets.iter_mut().take(pixel_width) {
bucket.reset();
}
let width = pixel_width as f64;
for point in points {
if !point.x.is_finite() || !point.y.is_finite() {
continue;
}
let t = (point.x - x_range.min) / span;
if !(0.0..=1.0).contains(&t) {
continue;
}
let mut index = (t * width) as usize;
if index >= pixel_width {
index = pixel_width - 1;
}
scratch.buckets[index].push(*point);
}
for bucket in scratch.buckets.iter().take(pixel_width) {
bucket.push_ordered(&mut scratch.points);
}
scratch.output()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimate_preserves_extremes() {
let points = [
Point::new(0.0, 1.0),
Point::new(1.0, 5.0),
Point::new(2.0, 0.5),
Point::new(3.0, 3.0),
];
let mut scratch = DecimationScratch::new();
let out = decimate_minmax(&points, Range::new(0.0, 3.0), 1, &mut scratch);
assert_eq!(out.len(), 2);
let ys = [out[0].y, out[1].y];
assert!(ys.contains(&0.5));
assert!(ys.contains(&5.0));
}
#[test]
fn summary_levels_grow() {
let mut summary = SummaryLevels::new(2);
summary.push(Point::new(0.0, 1.0));
summary.push(Point::new(1.0, 2.0));
summary.push(Point::new(2.0, 3.0));
summary.push(Point::new(3.0, 4.0));
assert!(!summary.levels.is_empty());
let level = &summary.levels[0];
assert_eq!(level.chunk_size, 2);
assert_eq!(level.buckets.len(), 2);
}
}