veloq-vis 0.4.1

Source-neutral visualization scene and SVG rendering helpers for VeloQ.
Documentation
use crate::{VizInterval, VizRole, VizScene, VizTimeWindow, VizTrack};

pub(super) struct Layout {
    pub(super) width: f64,
    pub(super) height: f64,
    pub(super) label_width: f64,
    pub(super) plot_width: f64,
    pub(super) top: f64,
    track_offsets: Vec<f64>,
    track_area_height: f64,
    pub(super) highlight_rows: usize,
}

const TRACK_BASE_HEIGHT: f64 = 22.0;
const SUBLANE_HEIGHT: f64 = 16.0;

impl Layout {
    pub(super) fn new(width_px: u32, track_lane_counts: &[usize], highlights: usize) -> Self {
        let width = f64::from(width_px.max(480));
        let label_width = 240.0;
        let top = 34.0;
        let highlight_rows = highlights.min(6);
        let mut track_offsets = Vec::with_capacity(track_lane_counts.len());
        let mut track_area_height = 0.0;
        for lane_count in track_lane_counts {
            track_offsets.push(track_area_height);
            track_area_height += track_height(*lane_count);
        }
        track_area_height = track_area_height.max(TRACK_BASE_HEIGHT);
        let height = top + track_area_height + 42.0 + highlight_rows as f64 * 16.0;
        Self {
            width,
            height,
            label_width,
            plot_width: width - label_width - 20.0,
            top,
            track_offsets,
            track_area_height,
            highlight_rows,
        }
    }

    pub(super) fn track_y(&self, row: usize) -> f64 {
        self.top + self.track_offsets.get(row).copied().unwrap_or_default() + 4.0
    }

    pub(super) fn track_lane_y(&self, row: usize, lane: usize) -> f64 {
        self.track_y(row) + lane as f64 * SUBLANE_HEIGHT
    }

    pub(super) fn plot_bottom(&self) -> f64 {
        self.top + self.track_area_height + 2.0
    }

    pub(super) fn legend_y(&self) -> f64 {
        self.height - 28.0 - self.highlight_rows as f64 * 16.0
    }

    pub(super) fn highlight_legend_y(&self, row: usize) -> f64 {
        self.legend_y() + 16.0 * (row + 1) as f64
    }
}

fn track_height(lanes: usize) -> f64 {
    TRACK_BASE_HEIGHT + lanes.saturating_sub(1) as f64 * SUBLANE_HEIGHT
}
pub(super) struct LaneLayout {
    pub(super) lanes: Vec<usize>,
    pub(super) track_lane_counts: Vec<usize>,
}

pub(super) fn assign_lanes(
    tracks: &[&VizTrack],
    items: &[&VizInterval],
    scene: &VizScene,
) -> LaneLayout {
    let mut lanes = vec![0; items.len()];
    let mut track_lane_counts = vec![1; tracks.len()];

    for (track_idx, track) in tracks.iter().enumerate() {
        if !track_uses_sublanes(track.role) {
            continue;
        }
        let mut track_items = items
            .iter()
            .enumerate()
            .filter(|(_, item)| item.track_key == track.key)
            .collect::<Vec<_>>();
        track_items.sort_by(|(a_idx, a), (b_idx, b)| {
            a.start_ns
                .cmp(&b.start_ns)
                .then_with(|| a.end_ns.cmp(&b.end_ns))
                .then_with(|| a_idx.cmp(b_idx))
        });

        let mut lane_ends: Vec<i64> = Vec::new();
        for (item_idx, item) in track_items {
            let clipped_start = item.start_ns.max(scene.time_window.start_ns);
            let clipped_end = item.end_ns.min(scene.time_window.end_ns);
            if clipped_end <= clipped_start {
                continue;
            }
            let lane = if let Some(lane) = lane_ends.iter().position(|end| *end <= clipped_start) {
                if let Some(end) = lane_ends.get_mut(lane) {
                    *end = clipped_end;
                }
                lane
            } else {
                lane_ends.push(clipped_end);
                lane_ends.len().saturating_sub(1)
            };
            if let Some(item_lane) = lanes.get_mut(item_idx) {
                *item_lane = lane;
            }
        }
        if let Some(track_lanes) = track_lane_counts.get_mut(track_idx) {
            *track_lanes = lane_ends.len().max(1);
        }
    }

    LaneLayout {
        lanes,
        track_lane_counts,
    }
}

fn track_uses_sublanes(role: VizRole) -> bool {
    matches!(role, VizRole::Detail | VizRole::Annotation)
}

pub(super) fn overlaps_window(item: &VizInterval, window: VizTimeWindow) -> bool {
    item.end_ns > window.start_ns && item.start_ns < window.end_ns && item.end_ns > item.start_ns
}