use super::*;
use crate::core::types::Point2f;
use crate::data::DataShaderImage;
use crate::plots::heatmap::{HeatmapData, Interpolation};
use tiny_skia::Rect;
#[derive(Clone, Copy, Debug)]
struct BucketPoint {
index: usize,
point: Point2f,
}
#[derive(Clone, Copy, Debug)]
struct ColumnBucket {
column: i32,
first: BucketPoint,
last: BucketPoint,
min_y: BucketPoint,
max_y: BucketPoint,
}
pub(super) fn should_reduce_line_series(
series: &PlotSeries,
point_count: usize,
plot_width: f32,
) -> bool {
if !matches!(series.series_type, SeriesType::Line { .. }) {
return false;
}
if series.marker_style.is_some() || series.x_errors.is_some() || series.y_errors.is_some() {
return false;
}
if series
.line_style
.as_ref()
.is_some_and(|style| !matches!(style, LineStyle::Solid))
{
return false;
}
point_count > (plot_width.max(1.0).ceil() as usize * 4)
}
pub(super) fn reduce_line_points_for_raster(
points: &[Point2f],
plot_left: f32,
plot_width: f32,
) -> Option<Vec<Point2f>> {
let column_count = plot_width.max(1.0).ceil() as usize;
if points.len() <= column_count * 4 || !points.iter().all(is_finite_point) {
return None;
}
if !is_monotonic_x(points) {
return None;
}
let max_column = column_count.saturating_sub(1) as i32;
let mut reduced = Vec::with_capacity(column_count.saturating_mul(4));
let mut active_bucket: Option<ColumnBucket> = None;
for (index, point) in points.iter().copied().enumerate() {
let column = ((point.x - plot_left).floor() as i32).clamp(0, max_column);
let bucket_point = BucketPoint { index, point };
match active_bucket.as_mut() {
Some(bucket) if bucket.column == column => update_bucket(bucket, bucket_point),
Some(bucket) => {
flush_bucket(bucket, &mut reduced);
active_bucket = Some(ColumnBucket {
column,
first: bucket_point,
last: bucket_point,
min_y: bucket_point,
max_y: bucket_point,
});
}
None => {
active_bucket = Some(ColumnBucket {
column,
first: bucket_point,
last: bucket_point,
min_y: bucket_point,
max_y: bucket_point,
});
}
}
}
if let Some(bucket) = active_bucket {
flush_bucket(&bucket, &mut reduced);
}
if reduced.len() >= points.len() {
None
} else {
Some(reduced)
}
}
pub(super) fn should_rasterize_heatmap(data: &HeatmapData) -> bool {
matches!(data.config.interpolation, Interpolation::Nearest) && !data.config.annotate
}
pub(super) fn rasterize_heatmap_surface(
data: &HeatmapData,
width: u32,
height: u32,
) -> DataShaderImage {
let width = width.max(1);
let height = height.max(1);
let mut pixels = vec![0_u8; width as usize * height as usize * 4];
for pixel_y in 0..height as usize {
let source_row = (((height as usize - 1 - pixel_y) * 2 + 1) * data.n_rows
/ (height as usize * 2))
.min(data.n_rows.saturating_sub(1));
let row = &data.values[source_row.min(data.n_rows.saturating_sub(1))];
for pixel_x in 0..width as usize {
let source_col = (((pixel_x * 2 + 1) * data.n_cols) / (width as usize * 2))
.min(data.n_cols.saturating_sub(1));
let mut color = data.get_color(row[source_col]);
if data.config.alpha < 1.0 {
color =
Color::new_rgba(color.r, color.g, color.b, (data.config.alpha * 255.0) as u8);
}
let offset = (pixel_y * width as usize + pixel_x) * 4;
pixels[offset] = color.r;
pixels[offset + 1] = color.g;
pixels[offset + 2] = color.b;
pixels[offset + 3] = color.a;
}
}
DataShaderImage::new(width as usize, height as usize, pixels)
}
pub(super) fn plot_area_surface_size(plot_area: Rect) -> (u32, u32) {
(
plot_area.width().max(1.0).round() as u32,
plot_area.height().max(1.0).round() as u32,
)
}
fn is_finite_point(point: &Point2f) -> bool {
point.x.is_finite() && point.y.is_finite()
}
fn is_monotonic_x(points: &[Point2f]) -> bool {
if points.len() < 2 {
return true;
}
let mut direction = 0_i8;
for window in points.windows(2) {
let delta = window[1].x - window[0].x;
if delta.abs() <= f32::EPSILON {
continue;
}
let current = if delta.is_sign_positive() { 1 } else { -1 };
if direction == 0 {
direction = current;
} else if direction != current {
return false;
}
}
true
}
fn update_bucket(bucket: &mut ColumnBucket, point: BucketPoint) {
bucket.last = point;
if point.point.y < bucket.min_y.point.y {
bucket.min_y = point;
}
if point.point.y > bucket.max_y.point.y {
bucket.max_y = point;
}
}
fn flush_bucket(bucket: &ColumnBucket, output: &mut Vec<Point2f>) {
let mut candidates = [bucket.first, bucket.min_y, bucket.max_y, bucket.last];
candidates.sort_by_key(|candidate| candidate.index);
let mut last_index = None;
for candidate in candidates {
if last_index == Some(candidate.index) {
continue;
}
if output
.last()
.is_some_and(|last| last.x == candidate.point.x && last.y == candidate.point.y)
{
last_index = Some(candidate.index);
continue;
}
output.push(candidate.point);
last_index = Some(candidate.index);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plots::heatmap::HeatmapConfig;
#[test]
fn test_reduce_line_points_preserves_monotonic_envelope() {
let points = vec![
Point2f::new(0.1, 10.0),
Point2f::new(0.2, 2.0),
Point2f::new(0.3, 8.0),
Point2f::new(0.4, 4.0),
Point2f::new(1.2, 5.0),
Point2f::new(1.3, 1.0),
Point2f::new(1.4, 9.0),
Point2f::new(1.5, 6.0),
Point2f::new(2.2, 3.0),
Point2f::new(2.3, 7.0),
Point2f::new(2.4, 2.0),
Point2f::new(2.5, 8.0),
];
let reduced = reduce_line_points_for_raster(&points, 0.0, 2.0).expect("expected reduction");
assert!(reduced.len() < points.len());
assert_eq!(reduced.first().copied(), points.first().copied());
assert_eq!(reduced.last().copied(), points.last().copied());
assert!(
reduced
.iter()
.any(|point| (point.y - 1.0).abs() < f32::EPSILON)
);
assert!(
reduced
.iter()
.any(|point| (point.y - 9.0).abs() < f32::EPSILON)
);
}
#[test]
fn test_reduce_line_points_rejects_non_monotonic_x() {
let points = vec![
Point2f::new(0.0, 0.0),
Point2f::new(1.0, 1.0),
Point2f::new(0.5, 2.0),
Point2f::new(1.5, 3.0),
Point2f::new(2.0, 4.0),
Point2f::new(2.5, 5.0),
Point2f::new(3.0, 6.0),
Point2f::new(3.5, 7.0),
Point2f::new(4.0, 8.0),
];
assert!(reduce_line_points_for_raster(&points, 0.0, 1.0).is_none());
}
#[test]
fn test_rasterize_heatmap_surface_matches_output_size() {
let heatmap = crate::plots::heatmap::process_heatmap(
&[vec![0.0, 1.0], vec![2.0, 3.0]],
HeatmapConfig {
colorbar: false,
..HeatmapConfig::default()
},
)
.expect("heatmap data");
let image = rasterize_heatmap_surface(&heatmap, 8, 6);
assert_eq!(image.width, 8);
assert_eq!(image.height, 6);
assert_eq!(image.pixels.len(), 8 * 6 * 4);
}
#[test]
fn test_rasterize_heatmap_surface_reaches_last_source_row_and_column() {
let heatmap = crate::plots::heatmap::process_heatmap(
&[
vec![0.0, 1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0, 7.0],
vec![8.0, 9.0, 10.0, 11.0],
vec![12.0, 13.0, 14.0, 15.0],
],
HeatmapConfig {
colorbar: false,
..HeatmapConfig::default()
},
)
.expect("heatmap data");
let image = rasterize_heatmap_surface(&heatmap, 2, 2);
let expected = heatmap.get_color(15.0);
let top_right = &image.pixels[4..8];
assert_eq!(top_right, &[expected.r, expected.g, expected.b, expected.a]);
}
}