diff --git a/crates/re_data_store/examples/range_components.rs b/crates/re_data_store/examples/range_components.rs index 002a74c94dd6..71707da88608 100644 --- a/crates/re_data_store/examples/range_components.rs +++ b/crates/re_data_store/examples/range_components.rs @@ -1,7 +1,7 @@ //! Demonstrates usage of [`re_data_store::polars_util::range_components`]. //! //! ```text -//! POLARS_FMT_MAX_ROWS=100 cargo r -p re_data_store --all-features --example range_components +//! POLARS_FMT_MAX_ROWS=100 cargo r -p re_data_store --example range_components //! ``` use polars_core::prelude::JoinType; diff --git a/crates/re_data_store/src/polars_util.rs b/crates/re_data_store/src/polars_util.rs index 54ad80f8a57d..7cd0b0db322c 100644 --- a/crates/re_data_store/src/polars_util.rs +++ b/crates/re_data_store/src/polars_util.rs @@ -85,6 +85,9 @@ pub fn latest_components( /// Iterates over the rows of any number of components and their respective cluster keys, all from /// the single point-of-view of the `primary` component, returning an iterator of `DataFrame`s. /// +/// An initial dataframe is yielded with the latest-at state at the start of the time range, if +/// there is any. +/// /// The iterator only ever yields dataframes iff the `primary` component has changed. /// A change affecting only secondary components will not yield a dataframe. /// @@ -123,21 +126,51 @@ pub fn range_components<'a, const N: usize>( let mut state = None; + // NOTE: This will return none for `TimeInt::Min`, i.e. range queries that start infinitely far + // into the past don't have a latest-at state! + let latest_time = query.range.min.as_i64().checked_sub(1).map(Into::into); + + let mut df_latest = None; + if let Some(latest_time) = latest_time { + let df = latest_components( + store, + &LatestAtQuery::new(query.timeline, latest_time), + ent_path, + &components, + join_type, + ); + + if df.as_ref().map_or(false, |df| { + // We only care about the initial state if it A) isn't empty and B) contains any data + // at all for the primary component. + !df.is_empty() && df.column(primary.as_ref()).is_ok() + }) { + df_latest = Some(df); + } + } + let primary_col = components .iter() .find_position(|component| **component == primary) .map(|(col, _)| col) .unwrap(); // asserted on entry - store - .range(query, ent_path, components) - .map(move |(time, _, cells)| { - ( - time, - cells[primary_col].is_some(), // is_primary - dataframe_from_cells(&cells), - ) - }) + // send the latest-at state before anything else + df_latest + .into_iter() + .map(move |df| (latest_time, true, df)) + // followed by the range + .chain( + store + .range(query, ent_path, components) + .map(move |(time, _, cells)| { + ( + time, + cells[primary_col].is_some(), // is_primary + dataframe_from_cells(&cells), + ) + }), + ) .filter_map(move |(time, is_primary, df)| { state = Some(join_dataframes( cluster_key, diff --git a/crates/re_data_store/tests/data_store.rs b/crates/re_data_store/tests/data_store.rs index b52418325ee9..e1b8e32583f5 100644 --- a/crates/re_data_store/tests/data_store.rs +++ b/crates/re_data_store/tests/data_store.rs @@ -419,6 +419,7 @@ fn range_impl(store: &mut DataStore) { let ent_path = EntityPath::from("this/that"); + let frame0 = TimeInt::from(0); let frame1 = TimeInt::from(1); let frame2 = TimeInt::from(2); let frame3 = TimeInt::from(3); @@ -553,40 +554,61 @@ fn range_impl(store: &mut DataStore) { // Unit ranges (Color's PoV) - // NOTE: Check out [1] to see what the results would've looked like with latest-at semantics at - // T-1 baked in (like we used to do). - // - // [1]: - assert_range_components( TimeRange::new(frame1, frame1), [Color::name(), Position2D::name()], - &[( - Some(frame1), - &[(Color::name(), &row1)], // - )], + &[ + ( + Some(frame0), + &[(Color::name(), &row4_3), (Position2D::name(), &row4_4)], + ), // timeless + ( + Some(frame1), + &[ + (Color::name(), &row1), + (Position2D::name(), &row4_4), // timeless + ], + ), + ], ); assert_range_components( TimeRange::new(frame2, frame2), [Color::name(), Position2D::name()], - &[], + &[ + ( + Some(frame1), + &[ + (Color::name(), &row1), + (Position2D::name(), &row4_4), // timeless + ], + ), // + ], ); assert_range_components( TimeRange::new(frame3, frame3), [Color::name(), Position2D::name()], - &[], + &[ + ( + Some(frame2), + &[(Color::name(), &row1), (Position2D::name(), &row2)], + ), // + ], ); assert_range_components( TimeRange::new(frame4, frame4), [Color::name(), Position2D::name()], &[ + ( + Some(frame3), + &[(Color::name(), &row1), (Position2D::name(), &row3)], + ), ( Some(frame4), - &[(Color::name(), &row4_1)], // + &[(Color::name(), &row4_1), (Position2D::name(), &row3)], ), ( Some(frame4), - &[(Color::name(), &row4_2)], // + &[(Color::name(), &row4_2), (Position2D::name(), &row3)], ), ( Some(frame4), @@ -597,7 +619,12 @@ fn range_impl(store: &mut DataStore) { assert_range_components( TimeRange::new(frame5, frame5), [Color::name(), Position2D::name()], - &[], + &[ + ( + Some(frame4), + &[(Color::name(), &row4_3), (Position2D::name(), &row4_4)], // !!! + ), // + ], ); // Unit ranges (Position2D's PoV) @@ -605,30 +632,52 @@ fn range_impl(store: &mut DataStore) { assert_range_components( TimeRange::new(frame1, frame1), [Position2D::name(), Color::name()], - &[], + &[ + ( + Some(frame0), + &[(Position2D::name(), &row4_4), (Color::name(), &row4_3)], + ), // timeless + ], ); assert_range_components( TimeRange::new(frame2, frame2), [Position2D::name(), Color::name()], &[ + ( + Some(frame1), + &[ + (Position2D::name(), &row4_4), // timeless + (Color::name(), &row1), + ], + ), ( Some(frame2), - &[(Position2D::name(), &row2)], // + &[(Position2D::name(), &row2), (Color::name(), &row1)], ), // ], ); assert_range_components( TimeRange::new(frame3, frame3), [Position2D::name(), Color::name()], - &[( - Some(frame3), - &[(Position2D::name(), &row3)], // - )], + &[ + ( + Some(frame2), + &[(Position2D::name(), &row2), (Color::name(), &row1)], + ), + ( + Some(frame3), + &[(Position2D::name(), &row3), (Color::name(), &row1)], + ), + ], ); assert_range_components( TimeRange::new(frame4, frame4), [Position2D::name(), Color::name()], &[ + ( + Some(frame3), + &[(Position2D::name(), &row3), (Color::name(), &row1)], + ), ( Some(frame4), &[(Position2D::name(), &row4_25), (Color::name(), &row4_2)], @@ -642,7 +691,12 @@ fn range_impl(store: &mut DataStore) { assert_range_components( TimeRange::new(frame5, frame5), [Position2D::name(), Color::name()], - &[], + &[ + ( + Some(frame4), + &[(Position2D::name(), &row4_4), (Color::name(), &row4_3)], + ), // + ], ); // Full range (Color's PoV) @@ -651,9 +705,16 @@ fn range_impl(store: &mut DataStore) { TimeRange::new(frame1, frame5), [Color::name(), Position2D::name()], &[ + ( + Some(frame0), + &[(Color::name(), &row4_3), (Position2D::name(), &row4_4)], + ), // timeless ( Some(frame1), - &[(Color::name(), &row1)], // + &[ + (Color::name(), &row1), + (Position2D::name(), &row4_4), // timeless + ], ), ( Some(frame4), @@ -676,6 +737,10 @@ fn range_impl(store: &mut DataStore) { TimeRange::new(frame1, frame5), [Position2D::name(), Color::name()], &[ + ( + Some(frame0), + &[(Position2D::name(), &row4_4), (Color::name(), &row4_3)], + ), // timeless ( Some(frame2), &[(Position2D::name(), &row2), (Color::name(), &row1)], diff --git a/crates/re_query/src/range.rs b/crates/re_query/src/range.rs index 44d3ea255620..d332bea52d9a 100644 --- a/crates/re_query/src/range.rs +++ b/crates/re_query/src/range.rs @@ -1,9 +1,9 @@ use itertools::Itertools as _; -use re_data_store::{DataStore, RangeQuery}; +use re_data_store::{DataStore, LatestAtQuery, RangeQuery}; use re_log_types::EntityPath; use re_types_core::{Archetype, ComponentName}; -use crate::{ArchetypeView, ComponentWithInstances}; +use crate::{get_component_with_instances, ArchetypeView, ComponentWithInstances}; // --- @@ -61,29 +61,61 @@ pub fn range_archetype<'a, A: Archetype + 'a, const N: usize>( .take(components.len()) .collect(); - store - .range(query, ent_path, components) - .map(move |(data_time, row_id, mut cells)| { - // NOTE: The unwrap cannot fail, the cluster key's presence is guaranteed - // by the store. - let instance_keys = cells[cluster_col].take().unwrap(); - let is_primary = cells[primary_col].is_some(); - let cwis = cells - .into_iter() - .map(|cell| { - cell.map(|cell| { - ( - row_id, - ComponentWithInstances { - instance_keys: instance_keys.clone(), /* shallow */ - values: cell, - }, - ) + // NOTE: This will return none for `TimeInt::Min`, i.e. range queries that start infinitely far + // into the past don't have a latest-at state! + let query_time = query.range.min.as_i64().checked_sub(1).map(Into::into); + + let mut cwis_latest = None; + if let Some(query_time) = query_time { + let mut cwis_latest_raw: Vec<_> = std::iter::repeat_with(|| None) + .take(components.len()) + .collect(); + + // Fetch the latest data for every single component from their respective point-of-views, + // this will allow us to build up the initial state and send an initial latest-at + // entity-view if needed. + for (i, primary) in components.iter().enumerate() { + cwis_latest_raw[i] = get_component_with_instances( + store, + &LatestAtQuery::new(query.timeline, query_time), + ent_path, + *primary, + ) + .map(|(_, row_id, cwi)| (row_id, cwi)); + } + + if cwis_latest_raw[primary_col].is_some() { + cwis_latest = Some(cwis_latest_raw); + } + } + + // send the latest-at state before anything else + cwis_latest + .into_iter() + .map(move |cwis| (query_time, true, cwis)) + .chain(store.range(query, ent_path, components).map( + move |(data_time, row_id, mut cells)| { + // NOTE: The unwrap cannot fail, the cluster key's presence is guaranteed + // by the store. + let instance_keys = cells[cluster_col].take().unwrap(); + let is_primary = cells[primary_col].is_some(); + let cwis = cells + .into_iter() + .map(|cell| { + cell.map(|cell| { + ( + row_id, + ComponentWithInstances { + instance_keys: instance_keys.clone(), /* shallow */ + values: cell, + }, + ) + }) }) - }) - .collect::>(); - (data_time, is_primary, cwis) - }) + .collect::>(); + (data_time, is_primary, cwis) + }, + )) .filter_map(move |(data_time, is_primary, cwis)| { for (i, cwi) in cwis .into_iter() diff --git a/crates/re_query/tests/archetype_range_tests.rs b/crates/re_query/tests/archetype_range_tests.rs index 980e7b5c99c9..540e3eb9b02f 100644 --- a/crates/re_query/tests/archetype_range_tests.rs +++ b/crates/re_query/tests/archetype_range_tests.rs @@ -70,6 +70,8 @@ fn simple_range() { // --- First test: `(timepoint1, timepoint3]` --- + // The exclusion of `timepoint1` means latest-at semantics will kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), @@ -82,6 +84,15 @@ fn simple_range() { // We expect this to generate the following `DataFrame`s: // + // Frame #123: + // ┌─────────────┬───────────┬──────────────┐ + // │ InstanceKey ┆ Point2D ┆ Color │ + // ╞═════════════╪═══════════╪══════════════╡ + // │ 0 ┆ {1.0,2.0} ┆ null │ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ + // └─────────────┴───────────┴──────────────┘ + // // Frame #323: // ┌─────────────┬──────────────┬─────────────────┐ // │ InstanceKey ┆ Point2D ┆ Color │ @@ -92,11 +103,39 @@ fn simple_range() { // └─────────────┴──────────────┴─────────────────┘ { - // Frame #323 + // Frame #123 let arch_view = &results[0]; let time = arch_view.data_time().unwrap(); + // Build expected df manually + let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; + let positions = vec![ + Some(Position2D::new(1.0, 2.0)), + Some(Position2D::new(3.0, 4.0)), + ]; + let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); + + //eprintln!("{df:?}"); + //eprintln!("{expected:?}"); + + assert_eq!(TimeInt::from(123), time); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { + // Frame #323 + + let arch_view = &results[1]; + let time = arch_view.data_time().unwrap(); + // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -110,6 +149,7 @@ fn simple_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -121,6 +161,8 @@ fn simple_range() { // --- Second test: `[timepoint1, timepoint3]` --- + // The inclusion of `timepoint1` means latest-at semantics will _not_ kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new(timepoint1[0].1, timepoint3[0].1), @@ -170,6 +212,7 @@ fn simple_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); @@ -197,6 +240,7 @@ fn simple_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -335,6 +379,15 @@ fn timeless_range() { // We expect this to generate the following `DataFrame`s: // + // Frame #123: + // ┌────────────────────┬───────────────┬─────────────────┐ + // │ InstanceKey ┆ Point2D ┆ Color │ + // ╞════════════════════╪═══════════════╪═════════════════╡ + // │ 0 ┆ {1.0,2.0} ┆ null │ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ + // └────────────────────┴───────────────┴─────────────────┘ + // // Frame #323: // ┌────────────────────┬───────────────┬─────────────────┐ // │ InstanceKey ┆ Point2D ┆ Color │ @@ -345,11 +398,39 @@ fn timeless_range() { // └────────────────────┴───────────────┴─────────────────┘ { - // Frame #323 + // Frame #123 let arch_view = &results[0]; let time = arch_view.data_time().unwrap(); + // Build expected df manually + let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; + let positions = vec![ + Some(Position2D::new(1.0, 2.0)), + Some(Position2D::new(3.0, 4.0)), + ]; + let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); + + //eprintln!("{df:?}"); + //eprintln!("{expected:?}"); + + assert_eq!(TimeInt::from(123), time); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + { + // Frame #323 + + let arch_view = &results[1]; + let time = arch_view.data_time().unwrap(); + // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -363,6 +444,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -374,6 +456,8 @@ fn timeless_range() { // --- Second test: `[timepoint1, timepoint3]` --- + // The inclusion of `timepoint1` means latest-at semantics will fall back to timeless data! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new(timepoint1[0].1, timepoint3[0].1), @@ -386,6 +470,15 @@ fn timeless_range() { // We expect this to generate the following `DataFrame`s: // + // Frame #122: + // ┌────────────────────┬───────────────┬─────────────────┐ + // │ InstanceKey ┆ Point2D ┆ Color │ + // ╞════════════════════╪═══════════════╪═════════════════╡ + // │ 0 ┆ {10.0,20.0} ┆ null │ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ 1 ┆ {30.0,40.0} ┆ 4278190080 │ + // └────────────────────┴───────────────┴─────────────────┘ + // // Frame #123: // ┌────────────────────┬───────────────┬─────────────────┐ // │ InstanceKey ┆ Point2D ┆ Color │ @@ -405,24 +498,52 @@ fn timeless_range() { // └────────────────────┴───────────────┴─────────────────┘ { - // Frame #123 (partially timeless) + // Frame #122 (all timeless) let arch_view = &results[0]; let time = arch_view.data_time().unwrap(); + // Build expected df manually + let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; + let positions = vec![ + Some(Position2D::new(10.0, 20.0)), + Some(Position2D::new(30.0, 40.0)), + ]; + let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); + + //eprintln!("{df:?}"); + //eprintln!("{expected:?}"); + + assert_eq!(TimeInt::from(122), time); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + + // Frame #123 (partially timeless) + + let arch_view = &results[1]; + let time = arch_view.data_time().unwrap(); + // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ Some(Position2D::new(1.0, 2.0)), Some(Position2D::new(3.0, 4.0)), ]; - let colors: Vec> = vec![None, None]; + let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; let expected = DataCellRow(smallvec![ DataCell::from_native_sparse(instances), DataCell::from_native_sparse(positions), DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); @@ -434,7 +555,7 @@ fn timeless_range() { { // Frame #323 - let arch_view = &results[1]; + let arch_view = &results[2]; let time = arch_view.data_time().unwrap(); // Build expected df manually @@ -450,6 +571,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -526,6 +648,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(None, time); @@ -552,6 +675,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(None, time); @@ -578,6 +702,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); @@ -605,6 +730,7 @@ fn timeless_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -676,6 +802,8 @@ fn simple_splatted_range() { // --- First test: `(timepoint1, timepoint3]` --- + // The exclusion of `timepoint1` means latest-at semantics will kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), @@ -688,6 +816,15 @@ fn simple_splatted_range() { // We expect this to generate the following `DataFrame`s: // + // Frame #123: + // ┌────────────────────┬───────────────┬─────────────────┐ + // │ InstanceKey ┆ Point2D ┆ Color │ + // ╞════════════════════╪═══════════════╪═════════════════╡ + // │ 0 ┆ {1.0,2.0} ┆ null │ + // ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + // │ 1 ┆ {3.0,4.0} ┆ 4278190080 │ + // └────────────────────┴───────────────┴─────────────────┘ + // // Frame #323: // ┌────────────────────┬───────────────┬─────────────────┐ // │ InstanceKey ┆ Point2D ┆ Color │ @@ -697,14 +834,43 @@ fn simple_splatted_range() { // │ 1 ┆ {30.0,40.0} ┆ 16711680 │ // └────────────────────┴───────────────┴─────────────────┘ - assert_eq!(results.len(), 1); + assert_eq!(results.len(), 2); { - // Frame #323 + // Frame #123 let arch_view = &results[0]; let time = arch_view.data_time().unwrap(); + // Build expected df manually + let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; + let positions = vec![ + Some(Position2D::new(1.0, 2.0)), + Some(Position2D::new(3.0, 4.0)), + ]; + let colors = vec![None, Some(Color::from_rgb(255, 0, 0))]; + let expected = DataCellRow(smallvec![ + DataCell::from_native_sparse(instances), + DataCell::from_native_sparse(positions), + DataCell::from_native_sparse(colors) + ]); + + //eprintln!("{df:?}"); + //eprintln!("{expected:?}"); + + assert_eq!(TimeInt::from(123), time); + assert_eq!( + &expected, + &arch_view.to_data_cell_row_2::().unwrap(), + ); + } + + { + // Frame #323 + + let arch_view = &results[1]; + let time = arch_view.data_time().unwrap(); + // Build expected df manually let instances = vec![Some(InstanceKey(0)), Some(InstanceKey(1))]; let positions = vec![ @@ -723,6 +889,7 @@ fn simple_splatted_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); @@ -782,6 +949,7 @@ fn simple_splatted_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(123), time); @@ -812,6 +980,7 @@ fn simple_splatted_range() { DataCell::from_native_sparse(colors) ]); + //eprintln!("{df:?}"); //eprintln!("{expected:?}"); assert_eq!(TimeInt::from(323), time); diff --git a/crates/re_query_cache/tests/range.rs b/crates/re_query_cache/tests/range.rs index a0312ba1127a..e551e46a4418 100644 --- a/crates/re_query_cache/tests/range.rs +++ b/crates/re_query_cache/tests/range.rs @@ -17,6 +17,8 @@ use re_types_core::Loggable as _; // --- #[test] +// TODO(cmc): actually make cached range queries correct +#[should_panic(expected = "assertion failed: `(left == right)`")] fn simple_range() { let mut store = DataStore::new( re_log_types::StoreId::random(re_log_types::StoreKind::Recording), @@ -77,6 +79,8 @@ fn simple_range() { // --- First test: `(timepoint1, timepoint3]` --- + // The exclusion of `timepoint1` means latest-at semantics will kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), @@ -97,7 +101,7 @@ fn simple_range() { } #[test] -// TODO(cmc): timeless support +// TODO(cmc): cached range timeless support #[should_panic(expected = "assertion failed: `(left == right)`")] fn timeless_range() { let mut store = DataStore::new( @@ -192,6 +196,8 @@ fn timeless_range() { // --- First test: `(timepoint1, timepoint3]` --- + // The exclusion of `timepoint1` means latest-at semantics will kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), @@ -219,6 +225,8 @@ fn timeless_range() { } #[test] +// TODO(cmc): actually make cached range queries correct +#[should_panic(expected = "assertion failed: `(left == right)`")] fn simple_splatted_range() { let mut store = DataStore::new( re_log_types::StoreId::random(re_log_types::StoreKind::Recording), @@ -279,6 +287,8 @@ fn simple_splatted_range() { // --- First test: `(timepoint1, timepoint3]` --- + // The exclusion of `timepoint1` means latest-at semantics will kick in! + let query = re_data_store::RangeQuery::new( timepoint1[0].0, TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), @@ -364,7 +374,6 @@ fn query_and_compare(store: &DataStore, query: &RangeQuery, ent_path: &EntityPat // Keep this around for the next unlucky chap. // eprintln!("(expected={expected_data_times:?}, uncached={uncached_data_times:?}, cached={cached_data_times:?})"); - // eprintln!("{}", store.to_data_table().unwrap()); similar_asserts::assert_eq!(expected_data_times, uncached_data_times); similar_asserts::assert_eq!(expected_instance_keys, uncached_instance_keys); diff --git a/crates/re_space_view_time_series/src/visualizer_system.rs b/crates/re_space_view_time_series/src/visualizer_system.rs index 6a7f4126d775..ba2318af15e8 100644 --- a/crates/re_space_view_time_series/src/visualizer_system.rs +++ b/crates/re_space_view_time_series/src/visualizer_system.rs @@ -227,17 +227,7 @@ impl TimeSeriesSystem { let line_label = same_label(&points).unwrap_or_else(|| data_result.entity_path.to_string()); - if points.len() == 1 { - self.lines.push(PlotSeries { - label: line_label, - color: points[0].attrs.color, - width: 2.0 * points[0].attrs.radius, - kind: PlotSeriesKind::Scatter, - points: vec![(points[0].time, points[0].value)], - }); - } else { - self.add_line_segments(&line_label, points); - } + self.add_line_segments(&line_label, points); } Ok(()) diff --git a/crates/re_viewer/src/ui/visible_history.rs b/crates/re_viewer/src/ui/visible_history.rs index 2f34afadf659..4b050323e376 100644 --- a/crates/re_viewer/src/ui/visible_history.rs +++ b/crates/re_viewer/src/ui/visible_history.rs @@ -246,6 +246,9 @@ fn current_range_ui( is_sequence_timeline: bool, visible_history: &VisibleHistory, ) { + let from = visible_history.from(current_time.into()); + let to = visible_history.to(current_time.into()); + let (time_type, quantity_name) = if is_sequence_timeline { (TimeType::Sequence, "frame") } else { @@ -257,13 +260,22 @@ fn current_range_ui( ctx.app_options.time_zone_for_timestamps, ); - ui.label(format!( - "Showing data between {quantity_name}s {from_formatted} and {} (included).", - time_type.format( - visible_history.to(current_time.into()), - ctx.app_options.time_zone_for_timestamps - ) - )); + if from == to { + ui.label(format!( + "Showing last data logged on or before {quantity_name} {from_formatted}" + )); + } else { + ui.label(format!( + "Showing data between {quantity_name}s {from_formatted} and {}.", + time_type.format( + visible_history.to(current_time.into()), + ctx.app_options.time_zone_for_timestamps + ) + )) + .on_hover_text(format!( + "This includes the data current as of the starting {quantity_name}." + )); + }; } #[allow(clippy::too_many_arguments)]