flowsurface-data 0.9.0

Data aggregation, indexing, and configuration utilities for Flowsurface
Documentation
use super::{
    ticks::{
        Y_LABEL_DENSITY,
        y::{TickValue, YAxisScale},
        y_labels_that_fit,
    },
    value::{PanelValueLabelPolicy, PanelValueScale, YUnit, YUnitRange},
};

const MAX_AUTOSCALE_SPAN_FACTOR: f64 = 1_000.0;
pub const ZOOM_SENSITIVITY: f32 = 30.0;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PanelYValueRange {
    min: f64,
    max: f64,
}

impl PanelYValueRange {
    fn new(min: f64, max: f64) -> Option<Self> {
        (min.is_finite() && max.is_finite() && min < max).then_some(Self { min, max })
    }

    pub fn from_units(range: YUnitRange, scale: PanelValueScale) -> Option<Self> {
        let (min, max) = scale.values(range);
        Self::new(min, max)
    }

    pub fn to_units(self, scale: PanelValueScale) -> YUnitRange {
        YUnitRange::new(scale.value_to_unit(self.min), scale.value_to_unit(self.max))
    }

    pub fn axis_labels(
        self,
        scale: PanelValueScale,
        policy: PanelValueLabelPolicy,
        top: f32,
        height: f32,
        text_size: f32,
    ) -> Vec<(String, f32)> {
        let ticks = YAxisScale::Quantized {
            step: scale.unit_to_value(YUnit(1)),
        }
        .ticks(
            self.min,
            self.max,
            y_labels_that_fit(height, text_size, Y_LABEL_DENSITY).max(2) as i32,
        );
        let step = ticks.step.unwrap_or(1.0);

        let mut labels: Vec<_> = ticks
            .ticks
            .into_iter()
            .filter_map(|tick| {
                let TickValue::Float(value) = tick.value else {
                    return None;
                };
                let y = f64::from(top) + (1.0 - tick.ratio) * f64::from(height);
                y.is_finite()
                    .then_some(y as f32)
                    .filter(|y| y.is_finite())
                    .map(|y| {
                        (
                            policy.format_axis_value(scale, value, step),
                            y.clamp(top, top + height),
                        )
                    })
            })
            .collect();
        labels.dedup_by(|left, right| left.0 == right.0);
        labels
    }

    pub fn normalized(min_value: f64, max_value: f64) -> Option<Self> {
        if !min_value.is_finite() || !max_value.is_finite() {
            return None;
        }
        let (mut min_value, mut max_value) = if min_value <= max_value {
            (min_value, max_value)
        } else {
            (max_value, min_value)
        };
        if (max_value - min_value).abs() <= 1e-8 {
            let pad = min_value.abs().max(1.0) * 0.01;
            min_value -= pad;
            max_value += pad;
        }
        Self::new(min_value, max_value)
    }

    pub fn min_value(self) -> f64 {
        self.min
    }

    pub fn max_value(self) -> f64 {
        self.max
    }

    pub fn span(self) -> f64 {
        (self.max - self.min).abs()
    }

    pub fn zoom_around_ratio(
        self,
        anchor_ratio: f64,
        zoom_scale: f64,
        zoom_in: bool,
    ) -> Option<Self> {
        if !zoom_scale.is_finite() {
            return None;
        }
        let anchor_ratio = anchor_ratio.clamp(0.0, 1.0);
        let span = self.span().max(1e-8);
        let zoom_scale = zoom_scale.abs().max(1.0);
        let new_span = if zoom_in {
            span / zoom_scale
        } else if span <= f64::MAX / zoom_scale {
            span * zoom_scale
        } else {
            f64::MAX
        };
        if !new_span.is_finite() {
            return None;
        }
        let anchor_value = self.min + anchor_ratio * span;
        let new_min = anchor_value - anchor_ratio * new_span;
        Self::new(new_min, new_min + new_span)
    }

    fn clamp_span(self, min_span: f64, max_span: f64) -> Option<Self> {
        if !self.min.is_finite()
            || !self.max.is_finite()
            || !min_span.is_finite()
            || !max_span.is_finite()
        {
            return None;
        }

        let min_span = min_span.abs().max(1e-8);
        let max_span = max_span.abs().max(min_span);
        let span = self.span();
        let target_span = span.clamp(min_span, max_span);
        if target_span == span {
            return Some(self);
        }
        let center = self.min * 0.5 + self.max * 0.5;
        let min_value = center - target_span * 0.5;
        Self::new(min_value, min_value + target_span)
    }

    fn clamp_to_bounds(self, lower: f64, upper: f64) -> Option<Self> {
        if !lower.is_finite() || !upper.is_finite() || lower >= upper {
            return None;
        }
        if self.min >= lower && self.max <= upper {
            return Some(self);
        }

        let span = self.span().min(upper - lower);
        let mut min_value = self.min.max(lower);
        let mut max_value = min_value + span;
        if max_value > upper {
            max_value = upper;
            min_value = max_value - span;
        }

        Self::new(min_value, max_value)
    }

    pub fn pan_by_ratio(self, delta_ratio: f64) -> Option<Self> {
        if !delta_ratio.is_finite() {
            return None;
        }
        let span = self.span().max(1e-8);
        let shift = delta_ratio * span;
        Self::normalized(self.min + shift, self.max + shift)
    }
}

#[derive(Debug, Clone, Copy)]
pub struct PanelYViewportLimits {
    pub min_span: f64,
    pub max_span: f64,
    pub bounds: (f64, f64),
}

impl PanelYViewportLimits {
    pub fn for_scale(
        scale: PanelValueScale,
        autoscale_span: f64,
        physical_height: f64,
    ) -> Option<Self> {
        let (min_span, max_span) = autoscale_span_limits(
            autoscale_span,
            physical_height,
            scale.minimum_span().min(autoscale_span),
        )?;
        let bounds = scale.value_bounds();
        let max_span = max_span.min(bounds.1 - bounds.0);
        (max_span >= min_span).then_some(Self {
            min_span,
            max_span,
            bounds,
        })
    }

    pub fn zoom(
        self,
        viewport: PanelYValueRange,
        anchor_ratio: f64,
        delta: f32,
    ) -> Option<PanelYValueRange> {
        if !delta.is_finite() {
            return None;
        }
        let zoom_in = delta > 0.0;
        let zoom_scale = (f64::from(delta.abs()) / f64::from(ZOOM_SENSITIVITY))
            .min(f64::MAX.ln())
            .exp();
        let scale_limit = if zoom_in {
            viewport.span() / self.min_span
        } else {
            self.max_span / viewport.span()
        };
        let zoom_scale = zoom_scale.min(scale_limit.max(1.0));
        let viewport = viewport.zoom_around_ratio(anchor_ratio, zoom_scale, zoom_in)?;
        self.apply(viewport)
    }

    pub fn apply(self, viewport: PanelYValueRange) -> Option<PanelYValueRange> {
        let viewport = viewport.clamp_span(self.min_span, self.max_span)?;
        viewport.clamp_to_bounds(self.bounds.0, self.bounds.1)
    }
}

pub fn autoscale_span_limits(
    autoscale_span: f64,
    physical_height: f64,
    min_span: f64,
) -> Option<(f64, f64)> {
    if autoscale_span.is_nan()
        || autoscale_span < 0.0
        || !physical_height.is_finite()
        || physical_height <= 0.0
        || !min_span.is_finite()
    {
        return None;
    }

    let min_span = min_span.abs().max(1e-8);
    let factor = physical_height.clamp(1.0, MAX_AUTOSCALE_SPAN_FACTOR);
    let autoscale_span = autoscale_span.min(f64::MAX).max(min_span);
    let max_span = if autoscale_span > f64::MAX / factor {
        f64::MAX
    } else {
        autoscale_span * factor
    };
    Some((min_span, max_span.max(min_span)))
}

pub fn place_non_overlapping_labels(
    desired_centers: &[f32],
    min_y: f32,
    max_y: f32,
    label_height: f32,
    gap: f32,
) -> Vec<Option<f32>> {
    if desired_centers.is_empty() {
        return Vec::new();
    }

    let label_height = label_height.max(1.0);
    let min_center = min_y + label_height * 0.5;
    let max_center = (max_y - label_height * 0.5).max(min_center);
    let spacing = (label_height + gap.max(0.0)).max(label_height);
    let capacity = ((max_center - min_center) / spacing).floor() as usize + 1;
    let visible_count = desired_centers.len().min(capacity.max(1));

    let mut candidates: Vec<(usize, f32)> = desired_centers.iter().copied().enumerate().collect();
    candidates.sort_by(|(_, left), (_, right)| left.total_cmp(right));

    let selected = if visible_count == candidates.len() {
        candidates
    } else {
        (0..visible_count)
            .map(|slot| {
                let source_index = if visible_count == 1 {
                    candidates.len() / 2
                } else {
                    slot * (candidates.len() - 1) / (visible_count - 1)
                };
                candidates[source_index]
            })
            .collect()
    };

    let mut centers: Vec<f32> = selected
        .iter()
        .map(|(_, center)| center.clamp(min_center, max_center))
        .collect();

    if centers.len() > 1 {
        centers[0] = centers[0].max(min_center);

        for index in 1..centers.len() {
            centers[index] = centers[index].max(centers[index - 1] + spacing);
        }

        if centers[centers.len() - 1] > max_center {
            let last = centers.len() - 1;
            centers[last] = max_center;

            for index in (0..last).rev() {
                centers[index] = centers[index].min(centers[index + 1] - spacing);
            }
        }
    }

    let mut placements = vec![None; desired_centers.len()];
    for ((source_index, _), center) in selected.into_iter().zip(centers) {
        placements[source_index] = Some(center);
    }

    placements
}