diff --git a/Cargo.lock b/Cargo.lock index 16c57c4042ec6..ab5baf245f151 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4830,6 +4830,7 @@ dependencies = [ "re_log_types", "re_query", "re_space_view", + "re_space_view_dataframe", "re_space_view_spatial", "re_space_view_time_series", "re_tracing", diff --git a/crates/viewer/re_selection_panel/Cargo.toml b/crates/viewer/re_selection_panel/Cargo.toml index 4c6ddfba6474c..2e7d11d249d4f 100644 --- a/crates/viewer/re_selection_panel/Cargo.toml +++ b/crates/viewer/re_selection_panel/Cargo.toml @@ -27,6 +27,7 @@ re_entity_db.workspace = true re_log_types.workspace = true re_log.workspace = true re_query.workspace = true +re_space_view_dataframe.workspace = true re_space_view_spatial.workspace = true re_space_view_time_series.workspace = true re_space_view.workspace = true diff --git a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs index c95d42bb7ab47..86b701cd01f19 100644 --- a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs +++ b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs @@ -5,6 +5,7 @@ use egui::{NumExt as _, Response, Ui}; use re_entity_db::TimeHistogram; use re_log_types::{EntityPath, ResolvedTimeRange, TimeType, TimeZone, TimelineName}; +use re_space_view_dataframe::DataframeSpaceView; use re_space_view_spatial::{SpatialSpaceView2D, SpatialSpaceView3D}; use re_space_view_time_series::TimeSeriesSpaceView; use re_types::{ @@ -24,6 +25,7 @@ static VISIBLE_HISTORY_SUPPORTED_SPACE_VIEWS: once_cell::sync::Lazy< SpatialSpaceView3D::identifier(), SpatialSpaceView2D::identifier(), TimeSeriesSpaceView::identifier(), + DataframeSpaceView::identifier(), ] .map(Into::into) .into() diff --git a/crates/viewer/re_space_view_dataframe/src/space_view_class.rs b/crates/viewer/re_space_view_dataframe/src/space_view_class.rs index aa86130bf69d5..5f22b9b018373 100644 --- a/crates/viewer/re_space_view_dataframe/src/space_view_class.rs +++ b/crates/viewer/re_space_view_dataframe/src/space_view_class.rs @@ -1,15 +1,15 @@ -use std::collections::BTreeSet; +use egui_extras::{Column, TableRow}; +use std::collections::{BTreeMap, BTreeSet}; -use egui_extras::Column; - -use re_chunk_store::{ChunkStore, LatestAtQuery}; -use re_data_ui::item_ui::instance_path_button; +use re_chunk_store::{ChunkStore, LatestAtQuery, RangeQuery, RowId}; +use re_data_ui::item_ui::{entity_path_button, instance_path_button}; use re_entity_db::InstancePath; -use re_log_types::{EntityPath, Instance, Timeline}; -use re_types_core::SpaceViewClassIdentifier; +use re_log_types::{EntityPath, Instance, ResolvedTimeRange, Timeline}; +use re_types_core::datatypes::TimeRange; +use re_types_core::{ComponentName, SpaceViewClassIdentifier}; use re_viewer_context::{ - SpaceViewClass, SpaceViewClassRegistryError, SpaceViewState, SpaceViewSystemExecutionError, - SystemExecutionOutput, UiLayout, ViewQuery, ViewerContext, + QueryRange, SpaceViewClass, SpaceViewClassRegistryError, SpaceViewState, + SpaceViewSystemExecutionError, SystemExecutionOutput, UiLayout, ViewQuery, ViewerContext, }; use crate::visualizer_system::EmptySystem; @@ -27,7 +27,6 @@ impl SpaceViewClass for DataframeSpaceView { } fn icon(&self) -> &'static re_ui::Icon { - //TODO(ab): fix that icon &re_ui::icons::SPACE_VIEW_DATAFRAME } @@ -69,153 +68,372 @@ impl SpaceViewClass for DataframeSpaceView { ctx: &ViewerContext<'_>, ui: &mut egui::Ui, _state: &mut dyn SpaceViewState, - query: &ViewQuery<'_>, _system_output: SystemExecutionOutput, ) -> Result<(), SpaceViewSystemExecutionError> { re_tracing::profile_function!(); - // These are the entity paths whose content we must display. - let sorted_entity_paths: BTreeSet<_> = query + // TODO(ab): we probably want a less "implicit" way to switch from temporal vs. latest at tables. + let is_range_query = query .iter_all_data_results() - .filter(|data_result| data_result.is_visible(ctx)) - .map(|data_result| &data_result.entity_path) - .cloned() - .collect(); + .any(|data_result| data_result.property_overrides.query_range.is_time_range()); - let latest_at_query = query.latest_at_query(); - - let sorted_instance_paths: Vec<_>; - let sorted_components: BTreeSet<_>; - { - re_tracing::profile_scope!("query"); - - // Produce a sorted list of each entity with all their instance keys. This will be the rows - // of the table. - // - // Important: our semantics here differs from other built-in space views. "Out-of-bound" - // instance keys (aka instance keys from a secondary component that cannot be joined with a - // primary component) are not filtered out. Reasons: - // - Primary/secondary component distinction only makes sense with archetypes, which we - // ignore. TODO(#4466): make archetypes more explicit? - // - This space view is about showing all user data anyways. - // - // Note: this must be a `Vec<_>` because we need random access for `body.rows()`. - sorted_instance_paths = sorted_entity_paths - .iter() - .flat_map(|entity_path| { - sorted_instance_paths_for( - entity_path, - ctx.recording_store(), - &query.timeline, - &latest_at_query, + if is_range_query { + entity_and_time_vs_component_ui(ctx, ui, query) + } else { + entity_and_instance_vs_component_ui(ctx, ui, query) + } + } +} + +/// Show a table with entities and time as rows, and components as columns. +/// +/// Here, a "row" is a tuple of (entity_path, time, row_id). This means that both "over logging" +/// (i.e. logging multiple times the same entity/component at the same timestamp) and "split +/// logging" (i.e. multiple log calls on the same [entity, time] but with different components) +/// lead to multiple rows. In other words, no joining is done here. +/// +/// Also: +/// - View entities have their query range "forced" to a range query. If set to "latest at", they +/// are converted to Rel(0)-Rel(0). +/// - Only the data logged in the query range is displayed. There is no implicit "latest at" like +/// it's done for regular views. +/// - Static data is always shown. +/// - When both static and non-static data exist for the same entity/component, the non-static data +/// is never shown (as per our data model). +fn entity_and_time_vs_component_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + query: &ViewQuery<'_>, +) -> Result<(), SpaceViewSystemExecutionError> { + re_tracing::profile_function!(); + + // These are the entity paths whose content we must display. + let sorted_entity_paths = sorted_visible_entity_path(ctx, query); + + let sorted_components: BTreeSet<_> = { + re_tracing::profile_scope!("query components"); + + // Produce a sorted list of all components that are present in one or more entities. + // These will be the columns of the table. + sorted_entity_paths + .iter() + .flat_map(|entity_path| { + ctx.recording_store() + .all_components(&query.timeline, entity_path) + .unwrap_or_default() + }) + // TODO(#4466): make showing/hiding indicators components an explicit optional + .filter(|comp| !comp.is_indicator_component()) + .collect() + }; + + // Build the full list of rows, along with the chunk where the data is + let rows_to_chunk = query + .iter_all_data_results() + .filter(|data_result| data_result.is_visible(ctx)) + .flat_map(|data_result| { + let time_range = match &data_result.property_overrides.query_range { + QueryRange::TimeRange(time_range) => time_range.clone(), + QueryRange::LatestAt => TimeRange::AT_CURSOR, + }; + + let resolved_time_range = + ResolvedTimeRange::from_relative_time_range(&time_range, ctx.current_query().at()); + + sorted_components.iter().flat_map(move |component| { + ctx.recording_store() + .range_relevant_chunks( + &RangeQuery::new(query.timeline, resolved_time_range), + &data_result.entity_path, + *component, ) - }) - .collect(); - - // Produce a sorted list of all components that are present in one or more entities. This - // will be the columns of the table. - sorted_components = sorted_entity_paths - .iter() - .flat_map(|entity_path| { - ctx.recording_store() - .all_components(&query.timeline, entity_path) - .unwrap_or_default() - }) - // TODO(#4466): make showing/hiding indicators components an explicit optional - .filter(|comp| !comp.is_indicator_component()) - .collect(); + .into_iter() + .flat_map(move |chunk| { + chunk + .indices(&query.timeline) + .into_iter() + .flat_map(|iter| { + iter.filter(|(time, _)| { + time.is_static() || resolved_time_range.contains(*time) + }) + .map(|(time, row_id)| { + ( + (data_result.entity_path.clone(), time, row_id), + chunk.clone(), + ) + }) + }) + .collect::>() + .into_iter() + }) + }) + }) + .collect::>(); + + let rows = rows_to_chunk.keys().collect::>(); + + // Draw the header row. + let header_ui = |mut row: egui_extras::TableRow<'_, '_>| { + row.col(|ui| { + ui.strong("Entity"); + }); + + row.col(|ui| { + ui.strong("Time"); + }); + + row.col(|ui| { + ui.strong("Row ID"); + }); + + for comp in &sorted_components { + row.col(|ui| { + ui.strong(comp.short_name()); + }); } + }; - // Draw the header row. - let header_ui = |mut row: egui_extras::TableRow<'_, '_>| { + // Draw a single line of the table. This is called for each _visible_ row, so it's ok to + // duplicate some of the querying. + let latest_at_query = query.latest_at_query(); + let row_ui = |mut row: egui_extras::TableRow<'_, '_>| { + let row_key = rows[row.index()]; + let row_chunk = rows_to_chunk.get(row_key).unwrap(); + let (entity_path, time, row_id) = row_key; + + row.col(|ui| { + entity_path_button( + ctx, + &latest_at_query, + ctx.recording(), + ui, + None, + entity_path, + ); + }); + + row.col(|ui| { + ui.label( + query + .timeline + .typ() + .format(*time, ctx.app_options.time_zone), + ); + }); + + row.col(|ui| { + row_id_ui(ui, row_id); + }); + + for component_name in &sorted_components { row.col(|ui| { - ui.strong("Entity"); + let content = row_chunk.cell(*row_id, component_name); + + if let Some(content) = content { + ctx.component_ui_registry.ui_raw( + ctx, + ui, + UiLayout::List, + &latest_at_query, + ctx.recording(), + &entity_path, + *component_name, + &*content, + ); + } else { + ui.weak("-"); + } }); + } + }; - for comp in &sorted_components { - row.col(|ui| { - ui.strong(comp.short_name()); - }); - } - }; + table_ui(ui, &sorted_components, header_ui, rows.len(), row_ui); + + Ok(()) +} + +fn entity_and_instance_vs_component_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + query: &ViewQuery<'_>, +) -> Result<(), SpaceViewSystemExecutionError> { + re_tracing::profile_function!(); + + // These are the entity paths whose content we must display. + let sorted_entity_paths = sorted_visible_entity_path(ctx, query); + let latest_at_query = query.latest_at_query(); + + let sorted_instance_paths: Vec<_>; + let sorted_components: BTreeSet<_>; + { + re_tracing::profile_scope!("query"); + + // Produce a sorted list of each entity with all their instance keys. This will be the rows + // of the table. + // + // Important: our semantics here differs from other built-in space views. "Out-of-bound" + // instance keys (aka instance keys from a secondary component that cannot be joined with a + // primary component) are not filtered out. Reasons: + // - Primary/secondary component distinction only makes sense with archetypes, which we + // ignore. TODO(#4466): make archetypes more explicit? + // - This space view is about showing all user data anyways. + // + // Note: this must be a `Vec<_>` because we need random access for `body.rows()`. + sorted_instance_paths = sorted_entity_paths + .iter() + .flat_map(|entity_path| { + sorted_instance_paths_for( + entity_path, + ctx.recording_store(), + &query.timeline, + &latest_at_query, + ) + }) + .collect(); - // Draw a single line of the table. This is called for each _visible_ row, so it's ok to - // duplicate some of the querying. - let row_ui = |mut row: egui_extras::TableRow<'_, '_>| { - let instance = &sorted_instance_paths[row.index()]; + // Produce a sorted list of all components that are present in one or more entities. This + // will be the columns of the table. + sorted_components = sorted_entity_paths + .iter() + .flat_map(|entity_path| { + ctx.recording_store() + .all_components(&query.timeline, entity_path) + .unwrap_or_default() + }) + // TODO(#4466): make showing/hiding indicators components an explicit optional + .filter(|comp| !comp.is_indicator_component()) + .collect(); + } + // Draw the header row. + let header_ui = |mut row: egui_extras::TableRow<'_, '_>| { + row.col(|ui| { + ui.strong("Entity"); + }); + + for comp in &sorted_components { row.col(|ui| { - instance_path_button(ctx, &latest_at_query, ctx.recording(), ui, None, instance); + ui.strong(comp.short_name()); }); + } + }; + + // Draw a single line of the table. This is called for each _visible_ row, so it's ok to + // duplicate some of the querying. + let row_ui = |mut row: egui_extras::TableRow<'_, '_>| { + let instance = &sorted_instance_paths[row.index()]; + + row.col(|ui| { + instance_path_button(ctx, &latest_at_query, ctx.recording(), ui, None, instance); + }); + + for component_name in &sorted_components { + row.col(|ui| { + let results = ctx.recording().query_caches().latest_at( + ctx.recording_store(), + &latest_at_query, + &instance.entity_path, + [*component_name], + ); - for component_name in &sorted_components { - row.col(|ui| { - let results = ctx.recording().query_caches().latest_at( - ctx.recording_store(), + if let Some(results) = + // This is a duplicate of the one above, but this ok since this codes runs + // *only* for visible rows. + results.components.get(component_name) + { + ctx.component_ui_registry.ui( + ctx, + ui, + UiLayout::List, &latest_at_query, + ctx.recording(), &instance.entity_path, - [*component_name], + results, + &instance.instance, ); + } else { + ui.weak("-"); + } + }); + } + }; + + table_ui( + ui, + &sorted_components, + header_ui, + sorted_instance_paths.len(), + row_ui, + ); + + Ok(()) +} + +// ------------------------------------------------------------------------------------------------- +// Utilities + +fn table_ui( + ui: &mut egui::Ui, + sorted_components: &BTreeSet, + header_ui: impl FnOnce(egui_extras::TableRow<'_, '_>), + row_count: usize, + row_ui: impl FnMut(TableRow<'_, '_>), +) { + re_tracing::profile_function!(); + + egui::ScrollArea::horizontal() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - if let Some(results) = - // This is a duplicate of the one above, but this ok since this codes runs - // *only* for visible rows. - results.components.get(component_name) - { - ctx.component_ui_registry.ui( - ctx, - ui, - UiLayout::List, - &latest_at_query, - ctx.recording(), - &instance.entity_path, - results, - &instance.instance, - ); - } else { - ui.weak("-"); - } - }); + egui::Frame { + inner_margin: egui::Margin::same(5.0), + ..Default::default() } - }; - - { - re_tracing::profile_scope!("table UI"); - - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - egui::Frame { - inner_margin: egui::Margin::same(5.0), - ..Default::default() - } - .show(ui, |ui| { - egui_extras::TableBuilder::new(ui) - .columns( - Column::auto_with_initial_suggestion(200.0).clip(true), - 1 + sorted_components.len(), - ) - .resizable(true) - .vscroll(false) - .auto_shrink([false, true]) - .striped(true) - .header(re_ui::DesignTokens::table_line_height(), header_ui) - .body(|body| { - body.rows( - re_ui::DesignTokens::table_line_height(), - sorted_instance_paths.len(), - row_ui, - ); - }); + .show(ui, |ui| { + egui_extras::TableBuilder::new(ui) + .columns( + Column::auto_with_initial_suggestion(200.0).clip(true), + 3 + sorted_components.len(), + ) + .resizable(true) + .vscroll(true) + //TODO(ab): remove when https://github.com/emilk/egui/pull/4817 is merged + .max_scroll_height(f32::INFINITY) + .auto_shrink([false, false]) + .striped(true) + .header(re_ui::DesignTokens::table_line_height(), header_ui) + .body(|body| { + body.rows(re_ui::DesignTokens::table_line_height(), row_count, row_ui); }); - }); - } + }); + }); +} - Ok(()) - } +fn row_id_ui(ui: &mut egui::Ui, row_id: &RowId) { + let s = row_id.to_string(); + let split_pos = s.char_indices().nth_back(5); + + ui.label(match split_pos { + Some((pos, _)) => &s[pos..], + None => &s, + }) + .on_hover_text(s); +} + +/// Returns a sorted list of all entities that are visible in the view. +//TODO(ab): move to ViewQuery? +fn sorted_visible_entity_path( + ctx: &ViewerContext<'_>, + query: &ViewQuery<'_>, +) -> BTreeSet { + query + .iter_all_data_results() + .filter(|data_result| data_result.is_visible(ctx)) + .map(|data_result| &data_result.entity_path) + .cloned() + .collect() } /// Returns a sorted, deduplicated iterator of all instance paths for a given entity. diff --git a/crates/viewer/re_viewer_context/src/query_range.rs b/crates/viewer/re_viewer_context/src/query_range.rs index 2fcc1c89b7d35..67b15cf312e93 100644 --- a/crates/viewer/re_viewer_context/src/query_range.rs +++ b/crates/viewer/re_viewer_context/src/query_range.rs @@ -8,3 +8,13 @@ pub enum QueryRange { #[default] LatestAt, } + +impl QueryRange { + pub fn is_latest_at(&self) -> bool { + matches!(self, QueryRange::LatestAt) + } + + pub fn is_time_range(&self) -> bool { + matches!(self, QueryRange::TimeRange(_)) + } +}