use itertools::Itertools as _;
use re_data_store::TimeHistogram;
use re_log_types::{TimeInt, TimeRange, TimeType};
#[derive(Clone, Debug)]
pub(crate) struct TimelineAxis {
pub ranges: vec1::Vec1<TimeRange>,
}
impl TimelineAxis {
pub fn new(time_type: TimeType, times: &TimeHistogram) -> Self {
crate::profile_function!();
assert!(!times.is_empty());
let gap_threshold = gap_size_heuristic(time_type, times);
Self {
ranges: create_ranges(times, gap_threshold),
}
}
pub fn sum_time_lengths(&self) -> u64 {
self.ranges.iter().map(|t| t.abs_length()).sum()
}
pub fn min(&self) -> TimeInt {
self.ranges.first().min
}
}
fn gap_size_heuristic(time_type: TimeType, times: &TimeHistogram) -> u64 {
crate::profile_function!();
assert!(!times.is_empty());
if times.total_count() <= 2 {
return u64::MAX;
}
let total_time_span = times.min_key().unwrap().abs_diff(times.max_key().unwrap());
if total_time_span == 0 {
return u64::MAX;
}
let max_collapses = ((times.total_count() - 1) / 3).min(20) as usize;
let min_gap_size: u64 = match time_type {
TimeType::Sequence => 9,
TimeType::Time => TimeInt::from_milliseconds(100).as_i64() as _,
};
let mut gap_sizes = collect_candidate_gaps(times, min_gap_size, max_collapses);
gap_sizes.sort_unstable();
let min_collapse_fraction: f64 = (2.0 / (times.total_count() - 1) as f64).max(0.35);
let mut gap_threshold = u64::MAX;
let mut uncollapsed_time = total_time_span;
for &gap in gap_sizes.iter().rev().take(max_collapses) {
let gap_fraction = gap as f64 / uncollapsed_time as f64;
if gap_fraction > min_collapse_fraction {
gap_threshold = gap;
uncollapsed_time -= gap;
} else {
break; }
}
gap_threshold
}
fn collect_candidate_gaps(
times: &TimeHistogram,
min_gap_size: u64,
max_collapses: usize,
) -> Vec<u64> {
crate::profile_function!();
let max_gap_size = times.max_key().unwrap() - times.min_key().unwrap();
let mut granularity = max_gap_size as u64;
let mut gaps = collect_gaps_with_granularity(times, granularity, min_gap_size);
while gaps.len() < max_collapses && min_gap_size < granularity {
granularity /= 2;
gaps = collect_gaps_with_granularity(times, granularity, min_gap_size);
}
gaps
}
fn collect_gaps_with_granularity(
times: &TimeHistogram,
granularity: u64,
min_gap_size: u64,
) -> Vec<u64> {
crate::profile_function!();
times
.range(.., granularity)
.tuple_windows()
.map(|((a, _), (b, _))| a.max.abs_diff(b.min))
.filter(|&gap_size| min_gap_size < gap_size)
.collect_vec()
}
fn create_ranges(times: &TimeHistogram, gap_threshold: u64) -> vec1::Vec1<TimeRange> {
crate::profile_function!();
let mut it = times.range(.., gap_threshold);
let first_range = it.next().unwrap().0;
let mut ranges = vec1::vec1![TimeRange::new(
first_range.min.into(),
first_range.max.into()
)];
for (new_range, _count) in it {
let last_max = &mut ranges.last_mut().max;
if last_max.as_i64().abs_diff(new_range.min) < gap_threshold {
*last_max = new_range.max.into();
} else {
ranges.push(TimeRange::new(new_range.min.into(), new_range.max.into()));
}
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
use re_arrow_store::TimeRange;
fn ranges(times: &[i64]) -> vec1::Vec1<TimeRange> {
let mut time_histogram = TimeHistogram::default();
for &time in times {
time_histogram.increment(time, 1);
}
TimelineAxis::new(TimeType::Sequence, &time_histogram).ranges
}
#[test]
fn test_time_axis() {
assert_eq!(1, ranges(&[1]).len());
assert_eq!(1, ranges(&[1, 2, 3, 4]).len());
assert_eq!(1, ranges(&[10, 20, 30, 40]).len());
assert_eq!(1, ranges(&[1, 2, 3, 11, 12, 13]).len(), "Too small gap");
assert_eq!(2, ranges(&[10, 20, 30, 110, 120, 130]).len());
assert_eq!(1, ranges(&[10, 1000]).len(), "not enough numbers");
assert_eq!(
2,
ranges(&[
i64::MIN / 2,
1_000_000_000,
2_000_000_000,
3_000_000_000,
4_000_000_000,
5_000_000_000,
6_000_000_000,
])
.len()
);
assert_eq!(
3,
ranges(&[
i64::MIN / 2,
1_000_000_000,
2_000_000_000,
3_000_000_000,
4_000_000_000,
5_000_000_000,
100_000_000_000,
])
.len()
);
}
}