Skip to content

Commit

Permalink
Add support for Visible History to time series space views (#4179)
Browse files Browse the repository at this point in the history
### What

- Add support for Visible History to Timeseries space views
- Needed to introduce `DataStore::entity_min_time()` to maintain the
consistent "beginning of x axis" behaviour.
- Changes the way the Visible History feature is organised:
- Now all space view (blueprint) contain a "root entity properties"
structure that is cascaded to the enclosed groups and entities.
- The visible history part of that root entity props is editable for all
supported space view classes (now 2d, 3d, and timeseries).
- The rest is unchanged: this means that contrary to the plan it's also
possible to edit the visible history on a per-entity basis in timeseries
space views as well.

-  Closes #2547

#### Known limitations
- default value for `EntityProperty` is not ideal for entities in
timeseries space view (2x `Relative(0)`)
- #4192

#### TODO

- [x] freeze the plot display bounds while the time cursor is dragged
- [x] setting must be serialised to blueprint


https://github.com/rerun-io/rerun/assets/49431240/5bc6033d-54d4-4376-845b-81189f3c5bb7

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/4179) (if
applicable)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG

- [PR Build Summary](https://build.rerun.io/pr/4179)
- [Docs
preview](https://rerun.io/preview/8c2be673a26cff49e76942bf99b06c4b76957774/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/8c2be673a26cff49e76942bf99b06c4b76957774/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)
  • Loading branch information
abey79 authored Nov 10, 2023
1 parent 8bff997 commit a4cf644
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 94 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions crates/re_arrow_store/src/store_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ impl DataStore {
.map_or(false, |table| table.all_components.contains(component))
}

/// Find the earliest time at which something was logged for a given entity on the specified
/// timeline.
///
/// # Temporal semantics
///
/// Only considers temporal results—timeless data is ignored.
pub fn entity_min_time(&self, timeline: &Timeline, ent_path: &EntityPath) -> Option<TimeInt> {
let ent_path_hash = ent_path.hash();

let min_time = self
.tables
.get(&(*timeline, ent_path_hash))?
.buckets
.first_key_value()?
.1
.inner
.read()
.time_range
.min;

// handle case where no data was logged
if min_time == TimeInt::MIN {
None
} else {
Some(min_time)
}
}

/// Queries the datastore for the cells of the specified `components`, as seen from the point
/// of view of the so-called `primary` component.
///
Expand Down
123 changes: 122 additions & 1 deletion crates/re_arrow_store/tests/correctness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use re_arrow_store::{
};
use re_log_types::example_components::MyPoint;
use re_log_types::{
build_frame_nr, build_log_time, DataCell, DataRow, Duration, EntityPath, RowId, Time,
build_frame_nr, build_log_time, DataCell, DataRow, Duration, EntityPath, RowId, Time, TimeInt,
TimePoint, TimeType, Timeline,
};
use re_types::components::InstanceKey;
Expand Down Expand Up @@ -493,3 +493,124 @@ pub fn init_logs() {
re_log::setup_native_logging();
}
}

// ---

#[test]
fn entity_min_time_correct() -> anyhow::Result<()> {
init_logs();

for config in re_arrow_store::test_util::all_configs() {
let mut store = DataStore::new(InstanceKey::name(), config.clone());
entity_min_time_correct_impl(&mut store)?;
}

Ok(())
}

fn entity_min_time_correct_impl(store: &mut DataStore) -> anyhow::Result<()> {
let ent_path = EntityPath::from("this/that");
let wrong_ent_path = EntityPath::from("this/that/other");

let point = MyPoint::new(1.0, 1.0);
let timeline_wrong_name = Timeline::new("lag_time", TimeType::Time);
let timeline_wrong_kind = Timeline::new("log_time", TimeType::Sequence);
let timeline_frame_nr = Timeline::new("frame_nr", TimeType::Sequence);
let timeline_log_time = Timeline::log_time();

let now = Time::now();
let now_plus_one = now + Duration::from_secs(1.0);
let now_minus_one = now - Duration::from_secs(1.0);

let row = DataRow::from_component_batches(
RowId::random(),
TimePoint::from_iter([
(timeline_log_time, now.into()),
(timeline_frame_nr, 42.into()),
]),
ent_path.clone(),
[&[point] as _],
)?;

store.insert_row(&row).unwrap();

assert!(store
.entity_min_time(&timeline_wrong_name, &ent_path)
.is_none());
assert!(store
.entity_min_time(&timeline_wrong_kind, &ent_path)
.is_none());
assert_eq!(
store.entity_min_time(&timeline_frame_nr, &ent_path),
Some(TimeInt::from(42))
);
assert_eq!(
store.entity_min_time(&timeline_log_time, &ent_path),
Some(TimeInt::from(now))
);
assert!(store
.entity_min_time(&timeline_frame_nr, &wrong_ent_path)
.is_none());

// insert row in the future, these shouldn't be visible
let row = DataRow::from_component_batches(
RowId::random(),
TimePoint::from_iter([
(timeline_log_time, now_plus_one.into()),
(timeline_frame_nr, 54.into()),
]),
ent_path.clone(),
[&[point] as _],
)?;
store.insert_row(&row).unwrap();

assert!(store
.entity_min_time(&timeline_wrong_name, &ent_path)
.is_none());
assert!(store
.entity_min_time(&timeline_wrong_kind, &ent_path)
.is_none());
assert_eq!(
store.entity_min_time(&timeline_frame_nr, &ent_path),
Some(TimeInt::from(42))
);
assert_eq!(
store.entity_min_time(&timeline_log_time, &ent_path),
Some(TimeInt::from(now))
);
assert!(store
.entity_min_time(&timeline_frame_nr, &wrong_ent_path)
.is_none());

// insert row in the past, these should be visible
let row = DataRow::from_component_batches(
RowId::random(),
TimePoint::from_iter([
(timeline_log_time, now_minus_one.into()),
(timeline_frame_nr, 32.into()),
]),
ent_path.clone(),
[&[point] as _],
)?;
store.insert_row(&row).unwrap();

assert!(store
.entity_min_time(&timeline_wrong_name, &ent_path)
.is_none());
assert!(store
.entity_min_time(&timeline_wrong_kind, &ent_path)
.is_none());
assert_eq!(
store.entity_min_time(&timeline_frame_nr, &ent_path),
Some(TimeInt::from(32))
);
assert_eq!(
store.entity_min_time(&timeline_log_time, &ent_path),
Some(TimeInt::from(now_minus_one))
);
assert!(store
.entity_min_time(&timeline_frame_nr, &wrong_ent_path)
.is_none());

Ok(())
}
5 changes: 5 additions & 0 deletions crates/re_data_store/src/entity_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ impl VisibleHistory {
to: VisibleHistoryBoundary::AT_CURSOR,
};

pub const ALL: Self = Self {
from: VisibleHistoryBoundary::Infinite,
to: VisibleHistoryBoundary::Infinite,
};

pub fn from(&self, cursor: TimeInt) -> TimeInt {
match self.from {
VisibleHistoryBoundary::Absolute(value) => TimeInt::from(value),
Expand Down
4 changes: 2 additions & 2 deletions crates/re_space_view/src/space_view_contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ impl SpaceViewContents {
/// Should be called on frame start.
///
/// Propagates any data blueprint changes along the tree.
pub fn propagate_individual_to_tree(&mut self) {
pub fn propagate_individual_to_tree(&mut self, root_entity_property: &EntityProperties) {
re_tracing::profile_function!();

// NOTE: We could do this projection only when the entity properties changes
Expand Down Expand Up @@ -287,7 +287,7 @@ impl SpaceViewContents {
}
}

project_tree(self, &EntityProperties::default(), self.root_group_handle);
project_tree(self, root_entity_property, self.root_group_handle);
}

/// Adds a list of entity paths to the tree, using grouping as dictated by their entity path hierarchy.
Expand Down
4 changes: 2 additions & 2 deletions crates/re_space_view_time_series/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ all-features = true

[dependencies]
re_arrow_store.workspace = true
re_format.workspace = true
re_log_types.workspace = true
re_query.workspace = true
re_renderer.workspace = true
re_space_view.workspace = true
re_tracing.workspace = true
re_types.workspace = true
re_ui.workspace = true
re_viewer_context.workspace = true
re_query.workspace = true
re_format.workspace = true

egui_plot.workspace = true
egui.workspace = true
Expand Down
60 changes: 44 additions & 16 deletions crates/re_space_view_time_series/src/space_view_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,41 @@ use re_format::next_grid_tick_magnitude_ns;
use re_log_types::{EntityPath, TimeZone};
use re_space_view::controls;
use re_viewer_context::{
SpaceViewClass, SpaceViewClassName, SpaceViewClassRegistryError, SpaceViewId,
SpaceViewClass, SpaceViewClassName, SpaceViewClassRegistryError, SpaceViewId, SpaceViewState,
SpaceViewSystemExecutionError, ViewContextCollection, ViewPartCollection, ViewQuery,
ViewerContext,
};

use crate::view_part_system::{PlotSeriesKind, TimeSeriesSystem};

#[derive(Clone, Default)]
pub struct TimeSeriesSpaceViewState {
/// track across frames when the user moves the time cursor
is_dragging_time_cursor: bool,
}

impl SpaceViewState for TimeSeriesSpaceViewState {
fn as_any(&self) -> &dyn std::any::Any {
self
}

fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

#[derive(Default)]
pub struct TimeSeriesSpaceView;

impl TimeSeriesSpaceView {
pub const NAME: &'static str = "Time Series";
}

impl SpaceViewClass for TimeSeriesSpaceView {
type State = ();
type State = TimeSeriesSpaceViewState;

fn name(&self) -> SpaceViewClassName {
"Time Series".into()
Self::NAME.into()
}

fn icon(&self) -> &'static re_ui::Icon {
Expand Down Expand Up @@ -84,7 +104,7 @@ impl SpaceViewClass for TimeSeriesSpaceView {
&self,
ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
_state: &mut Self::State,
state: &mut Self::State,
_view_ctx: &ViewContextCollection,
parts: &ViewPartCollection,
_query: &ViewQuery<'_>,
Expand All @@ -101,16 +121,11 @@ impl SpaceViewClass for TimeSeriesSpaceView {

let time_series = parts.get::<TimeSeriesSystem>()?;

// Compute the minimum time/X value for the entire plot…
let min_time = time_series
.lines
.iter()
.flat_map(|line| line.points.iter().map(|p| p.0))
.min()
.unwrap_or(0);
// Get the minimum time/X value for the entire plot…
let min_time = time_series.min_time.unwrap_or(0);

// …then use that as an offset to avoid nasty precision issues with
// large times (nanos since epoch does not fit into an f64).
// large times (nanos since epoch does not fit into a f64).
let time_offset = if timeline.typ() == TimeType::Time {
// In order to make the tick-marks on the time axis fall on whole days, hours, minutes etc,
// we need to round to a whole day:
Expand Down Expand Up @@ -200,10 +215,20 @@ impl SpaceViewClass for TimeSeriesSpaceView {
}
}

current_time.map(|current_time| {
let time_x = (current_time - time_offset) as f64;
plot_ui.screen_from_plot([time_x, 0.0].into()).x
})
if state.is_dragging_time_cursor {
// Freeze any change to the plot boundaries to avoid weird interaction with the time
// cursor.
plot_ui.set_plot_bounds(plot_ui.plot_bounds());
}

// decide if the time cursor should be displayed, and if where
current_time
.map(|current_time| (current_time - time_offset) as f64)
.filter(|&x| {
// only display the time cursor when it's actually above the plot area
plot_ui.plot_bounds().min()[0] <= x && x <= plot_ui.plot_bounds().max()[0]
})
.map(|x| plot_ui.screen_from_plot([x, 0.0].into()).x)
});

if let Some(time_x) = time_x {
Expand All @@ -216,6 +241,7 @@ impl SpaceViewClass for TimeSeriesSpaceView {
.interact(line_rect, time_drag_id, egui::Sense::drag())
.on_hover_and_drag_cursor(egui::CursorIcon::ResizeHorizontal);

state.is_dragging_time_cursor = false;
if response.dragged() {
if let Some(pointer_pos) = ui.input(|i| i.pointer.hover_pos()) {
let time =
Expand All @@ -224,6 +250,8 @@ impl SpaceViewClass for TimeSeriesSpaceView {
let time_ctrl = &mut ctx.rec_cfg.time_ctrl;
time_ctrl.set_time(time);
time_ctrl.pause();

state.is_dragging_time_cursor = true;
}
}

Expand Down
30 changes: 25 additions & 5 deletions crates/re_space_view_time_series/src/view_part_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ pub struct PlotSeries {
pub struct TimeSeriesSystem {
pub annotation_map: AnnotationMap,
pub lines: Vec<PlotSeries>,

/// Earliest time an entity was recorded at on the current timeline.
pub min_time: Option<i64>,
}

impl NamedViewSystem for TimeSeriesSystem {
Expand Down Expand Up @@ -117,18 +120,29 @@ impl TimeSeriesSystem {

let store = ctx.store_db.store();

for (ent_path, _ent_props) in query.iter_entities_for_system(Self::name()) {
for (ent_path, ent_props) in query.iter_entities_for_system(Self::name()) {
let mut points = Vec::new();
let annotations = self.annotation_map.find(ent_path);
let annotation_info = annotations
.resolved_class_description(None)
.annotation_info();
let default_color = DefaultColor::EntityPath(ent_path);

let query = re_arrow_store::RangeQuery::new(
query.timeline,
TimeRange::new(i64::MIN.into(), i64::MAX.into()),
);
let visible_history = match query.timeline.typ() {
re_log_types::TimeType::Time => ent_props.visible_history.nanos,
re_log_types::TimeType::Sequence => ent_props.visible_history.sequences,
};

let (from, to) = if ent_props.visible_history.enabled {
(
visible_history.from(query.latest_at),
visible_history.to(query.latest_at),
)
} else {
(i64::MIN.into(), i64::MAX.into())
};

let query = re_arrow_store::RangeQuery::new(query.timeline, TimeRange::new(from, to));

let arch_views = range_archetype::<
TimeSeriesScalar,
Expand Down Expand Up @@ -171,6 +185,12 @@ impl TimeSeriesSystem {
continue;
}

let min_time = store
.entity_min_time(&query.timeline, ent_path)
.map_or(points.first().map_or(0, |p| p.time), |time| time.as_i64());

self.min_time = Some(self.min_time.map_or(min_time, |time| time.min(min_time)));

// If all points within a line share the label (and it isn't `None`), then we use it
// as the whole line label for the plot legend.
// Otherwise, we just use the entity path as-is.
Expand Down
Loading

0 comments on commit a4cf644

Please sign in to comment.