From b580da42d5afec6094fe59865820f8d1dcb6a6d0 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:23:55 +0200 Subject: [PATCH] Add support for visible time range to the dataframe view (#6869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What - Part of #4466 - Soft-blocked by #6878 This adds support for visible time range to the dataframe. For now (likely to be iterated on soon), this mode is enabled when _any_ of the view entities have visible time range enabled (see note below). In that mode, rows are indexed by (entity, time, row_id) and can be sorted with either of the first two (asc or desc) using two new view properties. The dataframe feature is—and remains—behind an opt-in feature flag. #### Note on the current latest at vs. range switch Currently A single view entity with visible time range force the entire view into this mode. In particular, it force-opt-in *all* view entities to visible time range, setting it to `Rel(0)-Rel(0)` when not explicitly set. (It's as if the view's default visible time range switched to `Rel(0)-Rel(0)` although that's not how it's implemented.) This implicit behaviour is not ideal, and we probably should design a better way to go about it, see #4466. image ### Checklist * [x] update view help text * [x] split in multiple files * [x] clean Chunk stuff * [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 the web demo (if applicable): * Using examples from latest `main` build: [rerun.io/viewer](https://rerun.io/viewer/pr/6869?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [rerun.io/viewer](https://rerun.io/viewer/pr/6869?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! * [x] If have noted any breaking changes to the log API in `CHANGELOG.md` and the migration guide - [PR Build Summary](https://build.rerun.io/pr/6869) - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) To run all checks from `main`, comment on the PR with `@rerun-bot full-check`. --------- Co-authored-by: Clement Rey --- Cargo.lock | 4 + .../re_types/definitions/rerun/blueprint.fbs | 3 + .../archetypes/range_table_order.fbs | 23 ++ .../rerun/blueprint/components/sort_key.fbs | 20 ++ .../rerun/blueprint/components/sort_order.fbs | 20 ++ .../src/blueprint/archetypes/.gitattributes | 1 + .../re_types/src/blueprint/archetypes/mod.rs | 2 + .../blueprint/archetypes/range_table_order.rs | 201 +++++++++++++ .../src/blueprint/components/.gitattributes | 2 + .../re_types/src/blueprint/components/mod.rs | 4 + .../src/blueprint/components/sort_key.rs | 163 +++++++++++ .../src/blueprint/components/sort_order.rs | 163 +++++++++++ crates/viewer/re_edit_ui/src/lib.rs | 5 +- crates/viewer/re_selection_panel/Cargo.toml | 1 + .../src/visible_time_range_ui.rs | 2 + .../viewer/re_space_view_dataframe/Cargo.toml | 3 + .../src/latest_at_table.rs | 136 +++++++++ .../viewer/re_space_view_dataframe/src/lib.rs | 4 + .../src/space_view_class.rs | 261 ++++++----------- .../re_space_view_dataframe/src/table_ui.rs | 59 ++++ .../src/time_range_table.rs | 276 ++++++++++++++++++ .../re_space_view_dataframe/src/utils.rs | 53 ++++ .../src/blueprint/validation_gen/mod.rs | 4 + crates/viewer/re_viewer/src/reflection/mod.rs | 29 ++ .../re_viewer_context/src/query_range.rs | 12 + rerun_cpp/src/rerun/blueprint/archetypes.hpp | 1 + .../rerun/blueprint/archetypes/.gitattributes | 2 + .../archetypes/range_table_order.cpp | 38 +++ .../archetypes/range_table_order.hpp | 69 +++++ rerun_cpp/src/rerun/blueprint/components.hpp | 2 + .../rerun/blueprint/components/.gitattributes | 4 + .../rerun/blueprint/components/sort_key.cpp | 62 ++++ .../rerun/blueprint/components/sort_key.hpp | 52 ++++ .../rerun/blueprint/components/sort_order.cpp | 62 ++++ .../rerun/blueprint/components/sort_order.hpp | 52 ++++ .../rerun/blueprint/archetypes/.gitattributes | 1 + .../rerun/blueprint/archetypes/__init__.py | 2 + .../blueprint/archetypes/range_table_order.py | 82 ++++++ .../rerun/blueprint/components/.gitattributes | 2 + .../rerun/blueprint/components/__init__.py | 12 + .../rerun/blueprint/components/sort_key.py | 93 ++++++ .../rerun/blueprint/components/sort_order.py | 93 ++++++ 42 files changed, 1899 insertions(+), 181 deletions(-) create mode 100644 crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs create mode 100644 crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs create mode 100644 crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs create mode 100644 crates/store/re_types/src/blueprint/archetypes/range_table_order.rs create mode 100644 crates/store/re_types/src/blueprint/components/sort_key.rs create mode 100644 crates/store/re_types/src/blueprint/components/sort_order.rs create mode 100644 crates/viewer/re_space_view_dataframe/src/latest_at_table.rs create mode 100644 crates/viewer/re_space_view_dataframe/src/table_ui.rs create mode 100644 crates/viewer/re_space_view_dataframe/src/time_range_table.rs create mode 100644 crates/viewer/re_space_view_dataframe/src/utils.rs create mode 100644 rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.cpp create mode 100644 rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.hpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/sort_key.cpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/sort_key.hpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/sort_order.cpp create mode 100644 rerun_cpp/src/rerun/blueprint/components/sort_order.hpp create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/archetypes/range_table_order.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/components/sort_key.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/components/sort_order.py diff --git a/Cargo.lock b/Cargo.lock index cebbe37fd232..91cda6c5b686 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4854,6 +4854,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", @@ -4927,10 +4928,13 @@ dependencies = [ "re_entity_db", "re_log_types", "re_renderer", + "re_space_view", "re_tracing", + "re_types", "re_types_core", "re_ui", "re_viewer_context", + "re_viewport_blueprint", ] [[package]] diff --git a/crates/store/re_types/definitions/rerun/blueprint.fbs b/crates/store/re_types/definitions/rerun/blueprint.fbs index d6ddcb840da7..9a9ef17b3e15 100644 --- a/crates/store/re_types/definitions/rerun/blueprint.fbs +++ b/crates/store/re_types/definitions/rerun/blueprint.fbs @@ -17,6 +17,8 @@ include "./blueprint/components/panel_state.fbs"; include "./blueprint/components/query_expression.fbs"; include "./blueprint/components/root_container.fbs"; include "./blueprint/components/row_share.fbs"; +include "./blueprint/components/sort_key.fbs"; +include "./blueprint/components/sort_order.fbs"; include "./blueprint/components/space_view_class.fbs"; include "./blueprint/components/space_view_maximized.fbs"; include "./blueprint/components/space_view_origin.fbs"; @@ -41,6 +43,7 @@ include "./blueprint/archetypes/visible_time_ranges.fbs"; include "./blueprint/archetypes/visual_bounds2d.fbs"; include "./blueprint/archetypes/plot_legend.fbs"; +include "./blueprint/archetypes/range_table_order.fbs"; include "./blueprint/archetypes/scalar_axis.fbs"; include "./blueprint/views/bar_chart.fbs"; diff --git a/crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs b/crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs new file mode 100644 index 000000000000..7cdc8f0f377d --- /dev/null +++ b/crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs @@ -0,0 +1,23 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "rust/attributes.fbs"; + +include "rerun/attributes.fbs"; + +namespace rerun.blueprint.archetypes; + + +/// Configuration for the sorting of the rows of a time range table. +table RangeTableOrder ( + "attr.rerun.scope": "blueprint", + "attr.rust.derive": "Copy", + "attr.rust.generate_field_info" +) { + // --- Optional --- + + /// The primary sort key. + sort_key: rerun.blueprint.components.SortKey ("attr.rerun.component_optional", nullable, order: 1000); + + /// The sort order. + sort_order: rerun.blueprint.components.SortOrder ("attr.rerun.component_optional", nullable, order: 2000); +} diff --git a/crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs b/crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs new file mode 100644 index 000000000000..7c55b5de44bf --- /dev/null +++ b/crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs @@ -0,0 +1,20 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "rust/attributes.fbs"; + +include "rerun/datatypes.fbs"; +include "rerun/attributes.fbs"; + +namespace rerun.blueprint.components; + + +/// Primary element by which to group by in a temporal data table. +enum SortKey: byte ( + "attr.rerun.scope": "blueprint" +) { + /// Group by entity. + Entity (default), + + /// Group by instance. + Time, +} diff --git a/crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs b/crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs new file mode 100644 index 000000000000..94e2310f79cc --- /dev/null +++ b/crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs @@ -0,0 +1,20 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "rust/attributes.fbs"; + +include "rerun/datatypes.fbs"; +include "rerun/attributes.fbs"; + +namespace rerun.blueprint.components; + + +/// Sort order for data table. +enum SortOrder: byte ( + "attr.rerun.scope": "blueprint" +) { + /// Ascending + Ascending (default), + + /// Descending + Descending, +} diff --git a/crates/store/re_types/src/blueprint/archetypes/.gitattributes b/crates/store/re_types/src/blueprint/archetypes/.gitattributes index 6501c56053ed..ba0fa4bab5b8 100644 --- a/crates/store/re_types/src/blueprint/archetypes/.gitattributes +++ b/crates/store/re_types/src/blueprint/archetypes/.gitattributes @@ -4,6 +4,7 @@ background.rs linguist-generated=true mod.rs linguist-generated=true plot_legend.rs linguist-generated=true +range_table_order.rs linguist-generated=true scalar_axis.rs linguist-generated=true space_view_blueprint.rs linguist-generated=true space_view_contents.rs linguist-generated=true diff --git a/crates/store/re_types/src/blueprint/archetypes/mod.rs b/crates/store/re_types/src/blueprint/archetypes/mod.rs index c1ae16766e61..dd60566e9a65 100644 --- a/crates/store/re_types/src/blueprint/archetypes/mod.rs +++ b/crates/store/re_types/src/blueprint/archetypes/mod.rs @@ -2,6 +2,7 @@ mod background; mod plot_legend; +mod range_table_order; mod scalar_axis; mod space_view_blueprint; mod space_view_contents; @@ -14,6 +15,7 @@ mod visual_bounds2d; pub use self::background::Background; pub use self::plot_legend::PlotLegend; +pub use self::range_table_order::RangeTableOrder; pub use self::scalar_axis::ScalarAxis; pub use self::space_view_blueprint::SpaceViewBlueprint; pub use self::space_view_contents::SpaceViewContents; diff --git a/crates/store/re_types/src/blueprint/archetypes/range_table_order.rs b/crates/store/re_types/src/blueprint/archetypes/range_table_order.rs new file mode 100644 index 000000000000..f153d095270e --- /dev/null +++ b/crates/store/re_types/src/blueprint/archetypes/range_table_order.rs @@ -0,0 +1,201 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Configuration for the sorting of the rows of a time range table. +#[derive(Clone, Debug, Copy)] +pub struct RangeTableOrder { + /// The primary sort key. + pub sort_key: Option, + + /// The sort order. + pub sort_order: Option, +} + +impl ::re_types_core::SizeBytes for RangeTableOrder { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.sort_key.heap_size_bytes() + self.sort_order.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + >::is_pod() + && >::is_pod() + } +} + +static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 0usize]> = + once_cell::sync::Lazy::new(|| []); + +static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.blueprint.components.RangeTableOrderIndicator".into()]); + +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.blueprint.components.SortKey".into(), + "rerun.blueprint.components.SortOrder".into(), + ] + }); + +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 3usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.blueprint.components.RangeTableOrderIndicator".into(), + "rerun.blueprint.components.SortKey".into(), + "rerun.blueprint.components.SortOrder".into(), + ] + }); + +impl RangeTableOrder { + /// The total number of components in the archetype: 0 required, 1 recommended, 2 optional + pub const NUM_COMPONENTS: usize = 3usize; +} + +/// Indicator component for the [`RangeTableOrder`] [`::re_types_core::Archetype`] +pub type RangeTableOrderIndicator = ::re_types_core::GenericIndicatorComponent; + +impl ::re_types_core::Archetype for RangeTableOrder { + type Indicator = RangeTableOrderIndicator; + + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + "rerun.blueprint.archetypes.RangeTableOrder".into() + } + + #[inline] + fn display_name() -> &'static str { + "Range table order" + } + + #[inline] + fn indicator() -> MaybeOwnedComponentBatch<'static> { + static INDICATOR: RangeTableOrderIndicator = RangeTableOrderIndicator::DEFAULT; + MaybeOwnedComponentBatch::Ref(&INDICATOR) + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator)>, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data + .into_iter() + .map(|(name, array)| (name.full_name(), array)) + .collect(); + let sort_key = if let Some(array) = arrays_by_name.get("rerun.blueprint.components.SortKey") + { + ::from_arrow_opt(&**array) + .with_context("rerun.blueprint.archetypes.RangeTableOrder#sort_key")? + .into_iter() + .next() + .flatten() + } else { + None + }; + let sort_order = + if let Some(array) = arrays_by_name.get("rerun.blueprint.components.SortOrder") { + ::from_arrow_opt(&**array) + .with_context("rerun.blueprint.archetypes.RangeTableOrder#sort_order")? + .into_iter() + .next() + .flatten() + } else { + None + }; + Ok(Self { + sort_key, + sort_order, + }) + } +} + +impl ::re_types_core::AsComponents for RangeTableOrder { + fn as_component_batches(&self) -> Vec> { + re_tracing::profile_function!(); + use ::re_types_core::Archetype as _; + [ + Some(Self::indicator()), + self.sort_key + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), + self.sort_order + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for RangeTableOrder {} + +impl RangeTableOrder { + /// Create a new `RangeTableOrder`. + #[inline] + pub fn new() -> Self { + Self { + sort_key: None, + sort_order: None, + } + } + + /// The primary sort key. + #[inline] + pub fn with_sort_key( + mut self, + sort_key: impl Into, + ) -> Self { + self.sort_key = Some(sort_key.into()); + self + } + + /// The sort order. + #[inline] + pub fn with_sort_order( + mut self, + sort_order: impl Into, + ) -> Self { + self.sort_order = Some(sort_order.into()); + self + } +} diff --git a/crates/store/re_types/src/blueprint/components/.gitattributes b/crates/store/re_types/src/blueprint/components/.gitattributes index 6a9874206ded..653e7df988ad 100644 --- a/crates/store/re_types/src/blueprint/components/.gitattributes +++ b/crates/store/re_types/src/blueprint/components/.gitattributes @@ -12,6 +12,8 @@ mod.rs linguist-generated=true panel_state.rs linguist-generated=true query_expression.rs linguist-generated=true row_share.rs linguist-generated=true +sort_key.rs linguist-generated=true +sort_order.rs linguist-generated=true space_view_class.rs linguist-generated=true space_view_origin.rs linguist-generated=true tensor_dimension_index_slider.rs linguist-generated=true diff --git a/crates/store/re_types/src/blueprint/components/mod.rs b/crates/store/re_types/src/blueprint/components/mod.rs index 3389f6f39b39..000478b2b5f0 100644 --- a/crates/store/re_types/src/blueprint/components/mod.rs +++ b/crates/store/re_types/src/blueprint/components/mod.rs @@ -13,6 +13,8 @@ mod panel_state; mod panel_state_ext; mod query_expression; mod row_share; +mod sort_key; +mod sort_order; mod space_view_class; mod space_view_class_ext; mod space_view_origin; @@ -38,6 +40,8 @@ pub use self::lock_range_during_zoom::LockRangeDuringZoom; pub use self::panel_state::PanelState; pub use self::query_expression::QueryExpression; pub use self::row_share::RowShare; +pub use self::sort_key::SortKey; +pub use self::sort_order::SortOrder; pub use self::space_view_class::SpaceViewClass; pub use self::space_view_origin::SpaceViewOrigin; pub use self::tensor_dimension_index_slider::TensorDimensionIndexSlider; diff --git a/crates/store/re_types/src/blueprint/components/sort_key.rs b/crates/store/re_types/src/blueprint/components/sort_key.rs new file mode 100644 index 000000000000..4e809b109d55 --- /dev/null +++ b/crates/store/re_types/src/blueprint/components/sort_key.rs @@ -0,0 +1,163 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Primary element by which to group by in a temporal data table. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +pub enum SortKey { + /// Group by entity. + #[default] + Entity = 1, + + /// Group by instance. + Time = 2, +} + +impl ::re_types_core::reflection::Enum for SortKey { + #[inline] + fn variants() -> &'static [Self] { + &[Self::Entity, Self::Time] + } + + #[inline] + fn docstring_md(self) -> &'static str { + match self { + Self::Entity => "Group by entity.", + Self::Time => "Group by instance.", + } + } +} + +impl ::re_types_core::SizeBytes for SortKey { + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } + + #[inline] + fn is_pod() -> bool { + true + } +} + +impl std::fmt::Display for SortKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Entity => write!(f, "Entity"), + Self::Time => write!(f, "Time"), + } + } +} + +::re_types_core::macros::impl_into_cow!(SortKey); + +impl ::re_types_core::Loggable for SortKey { + type Name = ::re_types_core::ComponentName; + + #[inline] + fn name() -> Self::Name { + "rerun.blueprint.components.SortKey".into() + } + + #[inline] + fn arrow_datatype() -> arrow2::datatypes::DataType { + #![allow(clippy::wildcard_imports)] + use arrow2::datatypes::*; + DataType::Union( + std::sync::Arc::new(vec![ + Field::new("_null_markers", DataType::Null, true), + Field::new("Entity", DataType::Null, true), + Field::new("Time", DataType::Null, true), + ]), + Some(std::sync::Arc::new(vec![0i32, 1i32, 2i32])), + UnionMode::Sparse, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow2::{array::*, datatypes::*}; + Ok({ + // Sparse Arrow union + let data: Vec<_> = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + datum + }) + .collect(); + let num_variants = 2usize; + let types = data + .iter() + .map(|a| match a.as_deref() { + None => 0, + Some(value) => *value as i8, + }) + .collect(); + let fields: Vec<_> = + std::iter::repeat(NullArray::new(DataType::Null, data.len()).boxed()) + .take(1 + num_variants) + .collect(); + UnionArray::new(Self::arrow_datatype(), types, fields, None).boxed() + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow2::{array::*, buffer::*, datatypes::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.blueprint.components.SortKey")?; + let arrow_data_types = arrow_data.types(); + arrow_data_types + .iter() + .map(|typ| match typ { + 0 => Ok(None), + 1 => Ok(Some(Self::Entity)), + 2 => Ok(Some(Self::Time)), + _ => Err(DeserializationError::missing_union_arm( + Self::arrow_datatype(), + "", + *typ as _, + )), + }) + .collect::>>() + .with_context("rerun.blueprint.components.SortKey")? + }) + } +} diff --git a/crates/store/re_types/src/blueprint/components/sort_order.rs b/crates/store/re_types/src/blueprint/components/sort_order.rs new file mode 100644 index 000000000000..b81f37a2dc76 --- /dev/null +++ b/crates/store/re_types/src/blueprint/components/sort_order.rs @@ -0,0 +1,163 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Sort order for data table. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +pub enum SortOrder { + /// Ascending + #[default] + Ascending = 1, + + /// Descending + Descending = 2, +} + +impl ::re_types_core::reflection::Enum for SortOrder { + #[inline] + fn variants() -> &'static [Self] { + &[Self::Ascending, Self::Descending] + } + + #[inline] + fn docstring_md(self) -> &'static str { + match self { + Self::Ascending => "Ascending", + Self::Descending => "Descending", + } + } +} + +impl ::re_types_core::SizeBytes for SortOrder { + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } + + #[inline] + fn is_pod() -> bool { + true + } +} + +impl std::fmt::Display for SortOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ascending => write!(f, "Ascending"), + Self::Descending => write!(f, "Descending"), + } + } +} + +::re_types_core::macros::impl_into_cow!(SortOrder); + +impl ::re_types_core::Loggable for SortOrder { + type Name = ::re_types_core::ComponentName; + + #[inline] + fn name() -> Self::Name { + "rerun.blueprint.components.SortOrder".into() + } + + #[inline] + fn arrow_datatype() -> arrow2::datatypes::DataType { + #![allow(clippy::wildcard_imports)] + use arrow2::datatypes::*; + DataType::Union( + std::sync::Arc::new(vec![ + Field::new("_null_markers", DataType::Null, true), + Field::new("Ascending", DataType::Null, true), + Field::new("Descending", DataType::Null, true), + ]), + Some(std::sync::Arc::new(vec![0i32, 1i32, 2i32])), + UnionMode::Sparse, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow2::{array::*, datatypes::*}; + Ok({ + // Sparse Arrow union + let data: Vec<_> = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + datum + }) + .collect(); + let num_variants = 2usize; + let types = data + .iter() + .map(|a| match a.as_deref() { + None => 0, + Some(value) => *value as i8, + }) + .collect(); + let fields: Vec<_> = + std::iter::repeat(NullArray::new(DataType::Null, data.len()).boxed()) + .take(1 + num_variants) + .collect(); + UnionArray::new(Self::arrow_datatype(), types, fields, None).boxed() + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow2::{array::*, buffer::*, datatypes::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.blueprint.components.SortOrder")?; + let arrow_data_types = arrow_data.types(); + arrow_data_types + .iter() + .map(|typ| match typ { + 0 => Ok(None), + 1 => Ok(Some(Self::Ascending)), + 2 => Ok(Some(Self::Descending)), + _ => Err(DeserializationError::missing_union_arm( + Self::arrow_datatype(), + "", + *typ as _, + )), + }) + .collect::>>() + .with_context("rerun.blueprint.components.SortOrder")? + }) + } +} diff --git a/crates/viewer/re_edit_ui/src/lib.rs b/crates/viewer/re_edit_ui/src/lib.rs index af4efaf8b46e..12c33a7b27d9 100644 --- a/crates/viewer/re_edit_ui/src/lib.rs +++ b/crates/viewer/re_edit_ui/src/lib.rs @@ -15,6 +15,7 @@ use datatype_editors::{ display_name_ui, display_text_ui, edit_bool, edit_f32_min_to_max_float, edit_f32_zero_to_max, edit_f32_zero_to_one, edit_multiline_string, edit_singleline_string, edit_view_enum, }; +use re_types::blueprint::components::{SortKey, SortOrder}; use re_types::{ blueprint::components::{BackgroundKind, Corner2D, LockRangeDuringZoom, ViewFit, Visible}, components::{ @@ -68,8 +69,6 @@ pub fn register_editors(registry: &mut re_viewer_context::ComponentUiRegistry) { colormap_edit_or_view_ui(ctx.render_ctx, ui, value) }); registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); - registry - .add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); registry.add_singleline_edit_or_view(|_ctx, ui, value| { @@ -79,6 +78,8 @@ pub fn register_editors(registry: &mut re_viewer_context::ComponentUiRegistry) { edit_view_enum::(ui, value) }); registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); + registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); + registry.add_singleline_edit_or_view(|_ctx, ui, value| edit_view_enum::(ui, value)); registry.add_multiline_edit_or_view(visual_bounds2d::multiline_edit_visual_bounds2d); registry.add_singleline_edit_or_view(visual_bounds2d::singleline_edit_visual_bounds2d); diff --git a/crates/viewer/re_selection_panel/Cargo.toml b/crates/viewer/re_selection_panel/Cargo.toml index 4c6ddfba6474..2e7d11d249d4 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 c95d42bb7ab4..86b701cd01f1 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/Cargo.toml b/crates/viewer/re_space_view_dataframe/Cargo.toml index 483c6804cf68..a6ff38a428fb 100644 --- a/crates/viewer/re_space_view_dataframe/Cargo.toml +++ b/crates/viewer/re_space_view_dataframe/Cargo.toml @@ -24,10 +24,13 @@ re_data_ui.workspace = true re_entity_db.workspace = true re_log_types.workspace = true re_renderer.workspace = true +re_space_view.workspace = true re_tracing.workspace = true +re_types.workspace = true re_types_core.workspace = true re_ui.workspace = true re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true egui_extras.workspace = true egui.workspace = true diff --git a/crates/viewer/re_space_view_dataframe/src/latest_at_table.rs b/crates/viewer/re_space_view_dataframe/src/latest_at_table.rs new file mode 100644 index 000000000000..9090d9250418 --- /dev/null +++ b/crates/viewer/re_space_view_dataframe/src/latest_at_table.rs @@ -0,0 +1,136 @@ +use std::collections::BTreeSet; + +use re_data_ui::item_ui::instance_path_button; +use re_viewer_context::{UiLayout, ViewQuery, ViewerContext}; + +use crate::{ + table_ui::table_ui, + utils::{sorted_instance_paths_for, sorted_visible_entity_path}, +}; + +/// Display a "latest at" table. +/// +/// This table has entity instances as rows and components as column. That data is the result of a +/// "latest at" query based on the current timeline and time. +pub(crate) fn latest_at_table_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + query: &ViewQuery<'_>, +) { + 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(); + + // Produce a sorted list of all components that are present in one or more entities. These + // 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| { + 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, + Some(query.space_view_id), + 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], + ); + + if let Some(results) = + // This is a duplicate of the one above, but this is 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("-"); + } + }); + } + }; + + table_ui( + ui, + &sorted_components, + 1, // entity column + header_ui, + sorted_instance_paths.len(), + row_ui, + ); +} diff --git a/crates/viewer/re_space_view_dataframe/src/lib.rs b/crates/viewer/re_space_view_dataframe/src/lib.rs index 5798c5b6e90b..b3d8a9436846 100644 --- a/crates/viewer/re_space_view_dataframe/src/lib.rs +++ b/crates/viewer/re_space_view_dataframe/src/lib.rs @@ -2,7 +2,11 @@ //! //! A Space View that shows the data contained in entities in a table. +mod latest_at_table; mod space_view_class; +mod table_ui; +mod time_range_table; +mod utils; mod visualizer_system; pub use space_view_class::DataframeSpaceView; 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 d9cacbcd6b55..f7cdd171e237 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,18 +1,23 @@ -use std::collections::BTreeSet; +use egui::Ui; -use egui_extras::Column; - -use re_chunk_store::{ChunkStore, LatestAtQuery}; -use re_data_ui::item_ui::instance_path_button; -use re_entity_db::InstancePath; -use re_log_types::{EntityPath, Instance, Timeline}; +use re_log_types::EntityPath; +use re_space_view::view_property_ui; +use re_types::blueprint::{ + archetypes::RangeTableOrder, + components::{SortKey, SortOrder}, +}; use re_types_core::SpaceViewClassIdentifier; +use re_ui::list_item; use re_viewer_context::{ - SpaceViewClass, SpaceViewClassRegistryError, SpaceViewState, SpaceViewSystemExecutionError, - SystemExecutionOutput, UiLayout, ViewQuery, ViewerContext, + SpaceViewClass, SpaceViewClassRegistryError, SpaceViewId, SpaceViewState, + SpaceViewSystemExecutionError, SystemExecutionOutput, ViewQuery, ViewerContext, }; +use re_viewport_blueprint::ViewProperty; -use crate::visualizer_system::EmptySystem; +use crate::{ + latest_at_table::latest_at_table_ui, time_range_table::time_range_table_ui, + visualizer_system::EmptySystem, +}; #[derive(Default)] pub struct DataframeSpaceView; @@ -27,14 +32,29 @@ impl SpaceViewClass for DataframeSpaceView { } fn icon(&self) -> &'static re_ui::Icon { - //TODO(ab): fix that icon &re_ui::icons::SPACE_VIEW_DATAFRAME } fn help_markdown(&self, _egui_ctx: &egui::Context) -> String { "# Dataframe view -Show the data contained in entities in a table. Each entity is represented by as many rows as it has instances." +This view displays the content of the entities it contains in tabular form. Click on the view and +use the _Entity path filter_ to control which entities are displayed. + +## View types + +The Dataframe view operates in two modes: the _latest at_ mode and the _time range_ mode. + +In the _latest at_ mode, the view displays the latest data for the timeline and time set in the time +panel. A row is shown for each entity instance. + +In the _time range_ mode, the view displays all the data logged within the time range set for each +view entity. In this mode, each row corresponds to an entity and time pair. Rows are further split +if multiple `rr.log()` calls were made for the same entity/time. Static data is also displayed. + +The view switches to _time range_ mode as soon as a single one of its entities has its visible time +range set to _Override_. Each entity may have its own time range setting. (To set the same time range +for all entities, it is preferable to override the view-level visible time range.)" .to_owned() } @@ -65,188 +85,71 @@ Show the data contained in entities in a table. Each entity is represented by as Default::default() } + fn selection_ui( + &self, + ctx: &ViewerContext<'_>, + ui: &mut Ui, + state: &mut dyn SpaceViewState, + _space_origin: &EntityPath, + space_view_id: SpaceViewId, + ) -> Result<(), SpaceViewSystemExecutionError> { + list_item::list_item_scope(ui, "dataframe_view_selection_ui", |ui| { + view_property_ui::(ctx, ui, space_view_id, self, state); + }); + + Ok(()) + } + fn ui( &self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - _state: &mut dyn SpaceViewState, - + 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 - .iter_all_data_results() - .filter(|data_result| data_result.is_visible(ctx)) - .map(|data_result| &data_result.entity_path) - .cloned() - .collect(); - - 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(); - - // 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(); - } + let row_order = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + query.space_view_id, + ); + let sort_key = row_order.component_or_fallback::(ctx, self, state)?; + let sort_order = row_order.component_or_fallback::(ctx, self, state)?; - // 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| { - ui.strong(comp.short_name()); - }); - } - }; + let mode = self.table_mode(query); - // 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], - ); - - 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("-"); - } - }); - } + match mode { + TableMode::LatestAtTable => latest_at_table_ui(ctx, ui, query), + TableMode::TimeRangeTable => time_range_table_ui(ctx, ui, query, sort_key, sort_order), }; - { - 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, - ); - }); - }); - }); - } - Ok(()) } } -/// Returns a sorted, deduplicated iterator of all instance paths for a given entity. -fn sorted_instance_paths_for<'a>( - entity_path: &'a EntityPath, - store: &'a ChunkStore, - timeline: &'a Timeline, - latest_at_query: &'a LatestAtQuery, -) -> impl Iterator + 'a { - store - .all_components(timeline, entity_path) - .unwrap_or_default() - .into_iter() - .filter(|component_name| !component_name.is_indicator_component()) - .flat_map(|component_name| { - let num_instances = store - .latest_at_relevant_chunks(latest_at_query, entity_path, component_name) - .into_iter() - .filter_map(|chunk| { - let (data_time, row_id, batch) = chunk - .latest_at(latest_at_query, component_name) - .iter_rows(timeline, &component_name) - .next()?; - batch.map(|batch| (data_time, row_id, batch)) - }) - .max_by_key(|(data_time, row_id, _)| (*data_time, *row_id)) - .map_or(0, |(_, _, batch)| batch.len()); - (0..num_instances).map(|i| Instance::from(i as u64)) - }) - .collect::>() // dedup and sort - .into_iter() - .map(|instance| InstancePath::instance(entity_path.clone(), instance)) +re_viewer_context::impl_component_fallback_provider!(DataframeSpaceView => []); + +/// The two modes of the dataframe view. +enum TableMode { + LatestAtTable, + TimeRangeTable, +} + +impl DataframeSpaceView { + /// Determine which [`TableMode`] is currently active. + // TODO(ab): we probably want a less "implicit" way to switch from temporal vs. latest at tables. + #[allow(clippy::unused_self)] + fn table_mode(&self, query: &ViewQuery<'_>) -> TableMode { + let is_range_query = query + .iter_all_data_results() + .any(|data_result| data_result.property_overrides.query_range.is_time_range()); + + if is_range_query { + TableMode::TimeRangeTable + } else { + TableMode::LatestAtTable + } + } } diff --git a/crates/viewer/re_space_view_dataframe/src/table_ui.rs b/crates/viewer/re_space_view_dataframe/src/table_ui.rs new file mode 100644 index 000000000000..9735c0fa0509 --- /dev/null +++ b/crates/viewer/re_space_view_dataframe/src/table_ui.rs @@ -0,0 +1,59 @@ +use std::collections::BTreeSet; + +use egui_extras::{Column, TableRow}; + +use re_chunk_store::RowId; +use re_types_core::ComponentName; + +/// Display a nicely configured table with the provided header ui, row ui, and row count. +/// +/// The `extra_columns` are hom many more columns there are in addition to the components. +pub(crate) fn table_ui( + ui: &mut egui::Ui, + sorted_components: &BTreeSet, + extra_columns: usize, + 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); + + 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), + extra_columns + sorted_components.len(), + ) + .resizable(true) + .vscroll(true) + //TODO(ab): remove when https://github.com/emilk/egui/pull/4817 is merged/released + .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); + }); + }); + }); +} + +pub(crate) 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); +} diff --git a/crates/viewer/re_space_view_dataframe/src/time_range_table.rs b/crates/viewer/re_space_view_dataframe/src/time_range_table.rs new file mode 100644 index 000000000000..501cfb1316fe --- /dev/null +++ b/crates/viewer/re_space_view_dataframe/src/time_range_table.rs @@ -0,0 +1,276 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; + +use re_chunk_store::{Chunk, RangeQuery, RowId}; +use re_data_ui::item_ui::entity_path_button; +use re_log_types::{EntityPath, ResolvedTimeRange, TimeInt, Timeline}; +use re_types::blueprint::components::{SortKey, SortOrder}; +use re_types_core::datatypes::TimeRange; +use re_types_core::ComponentName; +use re_viewer_context::{QueryRange, UiLayout, ViewQuery, ViewerContext}; + +use crate::table_ui::{row_id_ui, table_ui}; + +/// 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). +pub(crate) fn time_range_table_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + query: &ViewQuery<'_>, + sort_key: SortKey, + sort_order: SortOrder, +) { + re_tracing::profile_function!(); + + // + // Produce a sorted list of all components we are interested id. + // + + // TODO(ab): add support for filtering components more narrowly. + 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. + query + .iter_all_data_results() + .filter(|data_result| data_result.is_visible(ctx)) + .flat_map(|data_result| { + ctx.recording_store() + .all_components(&query.timeline, &data_result.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. Rows are keyed by an + // (entity, time, row_id) tuple (see function docstring). These keys are mapped to the + // corresponding chunk that contains the actual data. + // + // We build a big, monolithic iterator for all the rows. The following code builds that by + // breaking it into several functions for sanity purpose, from innermost to outermost. + // + + type RowKey = (EntityPath, TimeInt, RowId); + + #[inline] + fn map_chunk_indices_to_key_value_iter<'a>( + indices_iter: impl Iterator + 'a, + chunk: Arc, + entity_path: EntityPath, + resolved_time_range: ResolvedTimeRange, + ) -> impl Iterator)> + 'a { + indices_iter + .filter(move |(time, _)| time.is_static() || resolved_time_range.contains(*time)) + .map(move |(time, row_id)| { + let chunk = chunk.clone(); + ((entity_path.clone(), time, row_id), chunk) + }) + } + + #[inline] + fn entity_components_to_key_value_iter<'a>( + ctx: &ViewerContext<'_>, + entity_path: &'a EntityPath, + component: &'a ComponentName, + timeline: Timeline, + resolved_time_range: ResolvedTimeRange, + ) -> impl Iterator)> + 'a { + let range_query = RangeQuery::new(timeline, resolved_time_range); + + ctx.recording_store() + .range_relevant_chunks(&range_query, entity_path, *component) + .into_iter() + // This does two things: + // 1) Filter out instances where `chunk.iter_indices()` returns `None`. + // 2) Exploit the fact that the returned iterator (if any) is *not* bound to the + // lifetime of the chunk (it has an internal Arc). + .filter_map(move |chunk| { + //TODO(ab, cmc): remove this line when a range-aware, iter_indices API is available. + let chunk = Arc::new(chunk.range(&range_query, *component)); + + chunk + .clone() + .iter_indices(&timeline) + .map(|iter_indices| (iter_indices, chunk)) + }) + .flat_map(move |(indices_iter, chunk)| { + map_chunk_indices_to_key_value_iter( + indices_iter, + chunk, + entity_path.clone(), + resolved_time_range, + ) + }) + } + + // all the rows! + 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| { + entity_components_to_key_value_iter( + ctx, + &data_result.entity_path, + component, + query.timeline, + resolved_time_range, + ) + }) + }) + .collect::>(); + + // + // Row sorting based on view properties. + // + + let mut rows = rows_to_chunk.keys().collect::>(); + + // apply sort key + match sort_key { + SortKey::Entity => {} // already correctly sorted + SortKey::Time => rows.sort_by_key(|(entity_path, time, _)| (*time, entity_path)), + }; + if sort_order == SortOrder::Descending { + rows.reverse(); + } + + // + // Drawing code. + // + + let entity_header = |ui: &mut egui::Ui| { + ui.strong("Entity"); + }; + let time_header = |ui: &mut egui::Ui| { + ui.strong("Time"); + }; + + // Draw the header row. + let header_ui = |mut row: egui_extras::TableRow<'_, '_>| { + match sort_key { + SortKey::Entity => { + row.col(entity_header); + row.col(time_header); + } + SortKey::Time => { + row.col(time_header); + row.col(entity_header); + } + } + + row.col(|ui| { + ui.strong("Row ID"); + }); + + for comp in &sorted_components { + row.col(|ui| { + ui.strong(comp.short_name()); + }); + } + }; + + let latest_at_query = query.latest_at_query(); + let entity_ui = |ui: &mut egui::Ui, entity_path: &EntityPath| { + entity_path_button( + ctx, + &latest_at_query, + ctx.recording(), + ui, + Some(query.space_view_id), + entity_path, + ); + }; + + let time_ui = |ui: &mut egui::Ui, time: &TimeInt| { + if ui + .button( + query + .timeline + .typ() + .format(*time, ctx.app_options.time_zone), + ) + .clicked() + { + ctx.rec_cfg.time_ctrl.write().set_time(*time); + } + }; + + // Draw a single line of the table. This is called for each _visible_ row. + 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[row_key]; + let (entity_path, time, row_id) = row_key; + + match sort_key { + SortKey::Entity => { + row.col(|ui| entity_ui(ui, entity_path)); + row.col(|ui| time_ui(ui, time)); + } + SortKey::Time => { + row.col(|ui| time_ui(ui, time)); + row.col(|ui| entity_ui(ui, entity_path)); + } + }; + + row.col(|ui| { + row_id_ui(ui, row_id); + }); + + for component_name in &sorted_components { + row.col(|ui| { + 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("-"); + } + }); + } + }; + + table_ui( + ui, + &sorted_components, + 3, // time, entity, row id + header_ui, + rows.len(), + row_ui, + ); +} diff --git a/crates/viewer/re_space_view_dataframe/src/utils.rs b/crates/viewer/re_space_view_dataframe/src/utils.rs new file mode 100644 index 000000000000..afcb2358cee2 --- /dev/null +++ b/crates/viewer/re_space_view_dataframe/src/utils.rs @@ -0,0 +1,53 @@ +use std::collections::BTreeSet; + +use re_chunk_store::{ChunkStore, LatestAtQuery}; +use re_entity_db::InstancePath; +use re_log_types::{EntityPath, Instance, Timeline}; +use re_viewer_context::{ViewQuery, ViewerContext}; + +/// Returns a sorted list of all entities that are visible in the view. +pub(crate) 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. +pub(crate) fn sorted_instance_paths_for<'a>( + entity_path: &'a EntityPath, + store: &'a ChunkStore, + timeline: &'a Timeline, + latest_at_query: &'a LatestAtQuery, +) -> impl Iterator + 'a { + re_tracing::profile_function!(); + + store + .all_components(timeline, entity_path) + .unwrap_or_default() + .into_iter() + .filter(|component_name| !component_name.is_indicator_component()) + .flat_map(|component_name| { + let num_instances = store + .latest_at_relevant_chunks(latest_at_query, entity_path, component_name) + .into_iter() + .filter_map(|chunk| { + let (data_time, row_id, batch) = chunk + .latest_at(latest_at_query, component_name) + .iter_rows(timeline, &component_name) + .next()?; + batch.map(|batch| (data_time, row_id, batch)) + }) + .max_by_key(|(data_time, row_id, _)| (*data_time, *row_id)) + .map_or(0, |(_, _, batch)| batch.len()); + (0..num_instances).map(|i| Instance::from(i as u64)) + }) + .collect::>() // dedup and sort + .into_iter() + .map(|instance| InstancePath::instance(entity_path.clone(), instance)) +} diff --git a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs index f7eb9218a025..81d265eac828 100644 --- a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs +++ b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs @@ -11,6 +11,8 @@ pub use re_types::blueprint::components::LockRangeDuringZoom; pub use re_types::blueprint::components::PanelState; pub use re_types::blueprint::components::QueryExpression; pub use re_types::blueprint::components::RowShare; +pub use re_types::blueprint::components::SortKey; +pub use re_types::blueprint::components::SortOrder; pub use re_types::blueprint::components::SpaceViewClass; pub use re_types::blueprint::components::SpaceViewOrigin; pub use re_types::blueprint::components::TensorDimensionIndexSlider; @@ -48,6 +50,8 @@ pub fn is_valid_blueprint(blueprint: &EntityDb) -> bool { && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) + && validate_component::(blueprint) + && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index c5c287d71e93..c4b0c3f61e50 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -146,6 +146,20 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Primary element by which to group by in a temporal data table.", + placeholder: Some(SortKey::default().to_arrow()?), + }, + ), + ( + ::name(), + ComponentReflection { + docstring_md: "Sort order for data table.", + placeholder: Some(SortOrder::default().to_arrow()?), + }, + ), ( ::name(), ComponentReflection { @@ -595,6 +609,21 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ], }, ), + ( + ArchetypeName::new("rerun.blueprint.archetypes.RangeTableOrder"), + ArchetypeReflection { + display_name: "Range table order", + docstring_md: "Configuration for the sorting of the rows of a time range table.", + fields: vec![ + ArchetypeFieldReflection { component_name : + "rerun.blueprint.components.SortKey".into(), display_name : + "Sort key", docstring_md : "The primary sort key.", }, + ArchetypeFieldReflection { component_name : + "rerun.blueprint.components.SortOrder".into(), display_name : + "Sort order", docstring_md : "The sort order.", }, + ], + }, + ), ( ArchetypeName::new("rerun.blueprint.archetypes.ScalarAxis"), ArchetypeReflection { diff --git a/crates/viewer/re_viewer_context/src/query_range.rs b/crates/viewer/re_viewer_context/src/query_range.rs index 2fcc1c89b7d3..1dd49ac69102 100644 --- a/crates/viewer/re_viewer_context/src/query_range.rs +++ b/crates/viewer/re_viewer_context/src/query_range.rs @@ -8,3 +8,15 @@ pub enum QueryRange { #[default] LatestAt, } + +impl QueryRange { + #[inline] + pub fn is_latest_at(&self) -> bool { + matches!(self, Self::LatestAt) + } + + #[inline] + pub fn is_time_range(&self) -> bool { + matches!(self, Self::TimeRange(_)) + } +} diff --git a/rerun_cpp/src/rerun/blueprint/archetypes.hpp b/rerun_cpp/src/rerun/blueprint/archetypes.hpp index 8dfd7175e565..fec6bca89b58 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes.hpp @@ -6,6 +6,7 @@ #include "blueprint/archetypes/container_blueprint.hpp" #include "blueprint/archetypes/panel_blueprint.hpp" #include "blueprint/archetypes/plot_legend.hpp" +#include "blueprint/archetypes/range_table_order.hpp" #include "blueprint/archetypes/scalar_axis.hpp" #include "blueprint/archetypes/space_view_blueprint.hpp" #include "blueprint/archetypes/space_view_contents.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes index c941889168aa..9e6c38e48b63 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes @@ -9,6 +9,8 @@ panel_blueprint.cpp linguist-generated=true panel_blueprint.hpp linguist-generated=true plot_legend.cpp linguist-generated=true plot_legend.hpp linguist-generated=true +range_table_order.cpp linguist-generated=true +range_table_order.hpp linguist-generated=true scalar_axis.cpp linguist-generated=true scalar_axis.hpp linguist-generated=true space_view_blueprint.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.cpp new file mode 100644 index 000000000000..ab6d81ed94cd --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.cpp @@ -0,0 +1,38 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs". + +#include "range_table_order.hpp" + +#include "../../collection_adapter_builtins.hpp" + +namespace rerun::blueprint::archetypes {} + +namespace rerun { + + Result> AsComponents::serialize( + const blueprint::archetypes::RangeTableOrder& archetype + ) { + using namespace blueprint::archetypes; + std::vector cells; + cells.reserve(3); + + if (archetype.sort_key.has_value()) { + auto result = DataCell::from_loggable(archetype.sort_key.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.sort_order.has_value()) { + auto result = DataCell::from_loggable(archetype.sort_order.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + { + auto indicator = RangeTableOrder::IndicatorComponent(); + auto result = DataCell::from_loggable(indicator); + RR_RETURN_NOT_OK(result.error); + cells.emplace_back(std::move(result.value)); + } + + return cells; + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.hpp new file mode 100644 index 000000000000..6df4f71f285b --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/range_table_order.hpp @@ -0,0 +1,69 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs". + +#pragma once + +#include "../../blueprint/components/sort_key.hpp" +#include "../../blueprint/components/sort_order.hpp" +#include "../../collection.hpp" +#include "../../compiler_utils.hpp" +#include "../../data_cell.hpp" +#include "../../indicator_component.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::archetypes { + /// **Archetype**: Configuration for the sorting of the rows of a time range table. + struct RangeTableOrder { + /// The primary sort key. + std::optional sort_key; + + /// The sort order. + std::optional sort_order; + + public: + static constexpr const char IndicatorComponentName[] = + "rerun.blueprint.components.RangeTableOrderIndicator"; + + /// Indicator component, used to identify the archetype when converting to a list of components. + using IndicatorComponent = rerun::components::IndicatorComponent; + + public: + RangeTableOrder() = default; + RangeTableOrder(RangeTableOrder&& other) = default; + + /// The primary sort key. + RangeTableOrder with_sort_key(rerun::blueprint::components::SortKey _sort_key) && { + sort_key = std::move(_sort_key); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + + /// The sort order. + RangeTableOrder with_sort_order(rerun::blueprint::components::SortOrder _sort_order) && { + sort_order = std::move(_sort_order); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + }; + +} // namespace rerun::blueprint::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> serialize( + const blueprint::archetypes::RangeTableOrder& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components.hpp b/rerun_cpp/src/rerun/blueprint/components.hpp index 45e6beaf7803..bcbb0d44b2ff 100644 --- a/rerun_cpp/src/rerun/blueprint/components.hpp +++ b/rerun_cpp/src/rerun/blueprint/components.hpp @@ -18,6 +18,8 @@ #include "blueprint/components/query_expression.hpp" #include "blueprint/components/root_container.hpp" #include "blueprint/components/row_share.hpp" +#include "blueprint/components/sort_key.hpp" +#include "blueprint/components/sort_order.hpp" #include "blueprint/components/space_view_class.hpp" #include "blueprint/components/space_view_maximized.hpp" #include "blueprint/components/space_view_origin.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/components/.gitattributes b/rerun_cpp/src/rerun/blueprint/components/.gitattributes index 11d00c3d1875..189ad84f442b 100644 --- a/rerun_cpp/src/rerun/blueprint/components/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/components/.gitattributes @@ -21,6 +21,10 @@ panel_state.hpp linguist-generated=true query_expression.hpp linguist-generated=true root_container.hpp linguist-generated=true row_share.hpp linguist-generated=true +sort_key.cpp linguist-generated=true +sort_key.hpp linguist-generated=true +sort_order.cpp linguist-generated=true +sort_order.hpp linguist-generated=true space_view_class.hpp linguist-generated=true space_view_maximized.hpp linguist-generated=true space_view_origin.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/components/sort_key.cpp b/rerun_cpp/src/rerun/blueprint/components/sort_key.cpp new file mode 100644 index 000000000000..f488500c1803 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/sort_key.cpp @@ -0,0 +1,62 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs". + +#include "sort_key.hpp" + +#include +#include + +namespace rerun { + const std::shared_ptr& + Loggable::arrow_datatype() { + static const auto datatype = arrow::sparse_union({ + arrow::field("_null_markers", arrow::null(), true, nullptr), + arrow::field("Entity", arrow::null(), true), + arrow::field("Time", arrow::null(), true), + }); + return datatype; + } + + Result> Loggable::to_arrow( + const blueprint::components::SortKey* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::SparseUnionBuilder* builder, const blueprint::components::SortKey* elements, + size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto variant = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append(static_cast(variant))); + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components/sort_key.hpp b/rerun_cpp/src/rerun/blueprint/components/sort_key.hpp new file mode 100644 index 000000000000..4437765fddf0 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/sort_key.hpp @@ -0,0 +1,52 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs". + +#pragma once + +#include "../../result.hpp" + +#include +#include + +namespace arrow { + class Array; + class DataType; + class SparseUnionBuilder; +} // namespace arrow + +namespace rerun::blueprint::components { + /// **Component**: Primary element by which to group by in a temporal data table. + enum class SortKey : uint8_t { + + /// Group by entity. + Entity = 1, + + /// Group by instance. + Time = 2, + }; +} // namespace rerun::blueprint::components + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.blueprint.components.SortKey"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::blueprint:: components::SortKey` into an arrow array. + static Result> to_arrow( + const blueprint::components::SortKey* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::SparseUnionBuilder* builder, const blueprint::components::SortKey* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components/sort_order.cpp b/rerun_cpp/src/rerun/blueprint/components/sort_order.cpp new file mode 100644 index 000000000000..b1a4ec0e5814 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/sort_order.cpp @@ -0,0 +1,62 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs". + +#include "sort_order.hpp" + +#include +#include + +namespace rerun { + const std::shared_ptr& + Loggable::arrow_datatype() { + static const auto datatype = arrow::sparse_union({ + arrow::field("_null_markers", arrow::null(), true, nullptr), + arrow::field("Ascending", arrow::null(), true), + arrow::field("Descending", arrow::null(), true), + }); + return datatype; + } + + Result> Loggable::to_arrow( + const blueprint::components::SortOrder* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::SparseUnionBuilder* builder, const blueprint::components::SortOrder* elements, + size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto variant = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append(static_cast(variant))); + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components/sort_order.hpp b/rerun_cpp/src/rerun/blueprint/components/sort_order.hpp new file mode 100644 index 000000000000..a292ab9bd19a --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/sort_order.hpp @@ -0,0 +1,52 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs". + +#pragma once + +#include "../../result.hpp" + +#include +#include + +namespace arrow { + class Array; + class DataType; + class SparseUnionBuilder; +} // namespace arrow + +namespace rerun::blueprint::components { + /// **Component**: Sort order for data table. + enum class SortOrder : uint8_t { + + /// Ascending + Ascending = 1, + + /// Descending + Descending = 2, + }; +} // namespace rerun::blueprint::components + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.blueprint.components.SortOrder"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::blueprint:: components::SortOrder` into an arrow array. + static Result> to_arrow( + const blueprint::components::SortOrder* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::SparseUnionBuilder* builder, const blueprint::components::SortOrder* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes index ac20f8caae1f..1ca2967b2015 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes @@ -6,6 +6,7 @@ background.py linguist-generated=true container_blueprint.py linguist-generated=true panel_blueprint.py linguist-generated=true plot_legend.py linguist-generated=true +range_table_order.py linguist-generated=true scalar_axis.py linguist-generated=true space_view_blueprint.py linguist-generated=true space_view_contents.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py index 3af18008eb45..1af293c74ee6 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py @@ -6,6 +6,7 @@ from .container_blueprint import ContainerBlueprint from .panel_blueprint import PanelBlueprint from .plot_legend import PlotLegend +from .range_table_order import RangeTableOrder from .scalar_axis import ScalarAxis from .space_view_blueprint import SpaceViewBlueprint from .space_view_contents import SpaceViewContents @@ -21,6 +22,7 @@ "ContainerBlueprint", "PanelBlueprint", "PlotLegend", + "RangeTableOrder", "ScalarAxis", "SpaceViewBlueprint", "SpaceViewContents", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/range_table_order.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/range_table_order.py new file mode 100644 index 000000000000..a1eb712e951a --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/range_table_order.py @@ -0,0 +1,82 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/range_table_order.fbs". + +# You can extend this class by creating a "RangeTableOrderExt" class in "range_table_order_ext.py". + +from __future__ import annotations + +from typing import Any + +from attrs import define, field + +from ..._baseclasses import ( + Archetype, +) +from ...blueprint import components as blueprint_components +from ...error_utils import catch_and_log_exceptions + +__all__ = ["RangeTableOrder"] + + +@define(str=False, repr=False, init=False) +class RangeTableOrder(Archetype): + """**Archetype**: Configuration for the sorting of the rows of a time range table.""" + + def __init__( + self: Any, + *, + sort_key: blueprint_components.SortKeyLike | None = None, + sort_order: blueprint_components.SortOrderLike | None = None, + ): + """ + Create a new instance of the RangeTableOrder archetype. + + Parameters + ---------- + sort_key: + The primary sort key. + sort_order: + The sort order. + + """ + + # You can define your own __init__ function as a member of RangeTableOrderExt in range_table_order_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(sort_key=sort_key, sort_order=sort_order) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + sort_key=None, # type: ignore[arg-type] + sort_order=None, # type: ignore[arg-type] + ) + + @classmethod + def _clear(cls) -> RangeTableOrder: + """Produce an empty RangeTableOrder, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + sort_key: blueprint_components.SortKeyBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=blueprint_components.SortKeyBatch._optional, # type: ignore[misc] + ) + # The primary sort key. + # + # (Docstring intentionally commented out to hide this field from the docs) + + sort_order: blueprint_components.SortOrderBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=blueprint_components.SortOrderBatch._optional, # type: ignore[misc] + ) + # The sort order. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes index e232a7f04c35..ca7cc8880260 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes @@ -18,6 +18,8 @@ panel_state.py linguist-generated=true query_expression.py linguist-generated=true root_container.py linguist-generated=true row_share.py linguist-generated=true +sort_key.py linguist-generated=true +sort_order.py linguist-generated=true space_view_class.py linguist-generated=true space_view_maximized.py linguist-generated=true space_view_origin.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py index 10f98717719d..ac06fd41efa7 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py @@ -30,6 +30,8 @@ from .query_expression import QueryExpression, QueryExpressionBatch, QueryExpressionType from .root_container import RootContainer, RootContainerBatch, RootContainerType from .row_share import RowShare, RowShareBatch, RowShareType +from .sort_key import SortKey, SortKeyArrayLike, SortKeyBatch, SortKeyLike, SortKeyType +from .sort_order import SortOrder, SortOrderArrayLike, SortOrderBatch, SortOrderLike, SortOrderType from .space_view_class import SpaceViewClass, SpaceViewClassBatch, SpaceViewClassType from .space_view_maximized import SpaceViewMaximized, SpaceViewMaximizedBatch, SpaceViewMaximizedType from .space_view_origin import SpaceViewOrigin, SpaceViewOriginBatch, SpaceViewOriginType @@ -106,6 +108,16 @@ "RowShare", "RowShareBatch", "RowShareType", + "SortKey", + "SortKeyArrayLike", + "SortKeyBatch", + "SortKeyLike", + "SortKeyType", + "SortOrder", + "SortOrderArrayLike", + "SortOrderBatch", + "SortOrderLike", + "SortOrderType", "SpaceViewClass", "SpaceViewClassBatch", "SpaceViewClassType", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/sort_key.py b/rerun_py/rerun_sdk/rerun/blueprint/components/sort_key.py new file mode 100644 index 000000000000..b4da9b031608 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/sort_key.py @@ -0,0 +1,93 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_key.fbs". + +# You can extend this class by creating a "SortKeyExt" class in "sort_key_ext.py". + +from __future__ import annotations + +from typing import Literal, Sequence, Union + +import pyarrow as pa + +from ..._baseclasses import ( + BaseBatch, + BaseExtensionType, + ComponentBatchMixin, +) + +__all__ = ["SortKey", "SortKeyArrayLike", "SortKeyBatch", "SortKeyLike", "SortKeyType"] + + +from enum import Enum + + +class SortKey(Enum): + """**Component**: Primary element by which to group by in a temporal data table.""" + + Entity = 1 + """Group by entity.""" + + Time = 2 + """Group by instance.""" + + +SortKeyLike = Union[SortKey, Literal["entity", "time"]] +SortKeyArrayLike = Union[SortKeyLike, Sequence[SortKeyLike]] + + +class SortKeyType(BaseExtensionType): + _TYPE_NAME: str = "rerun.blueprint.components.SortKey" + + def __init__(self) -> None: + pa.ExtensionType.__init__( + self, + pa.sparse_union([ + pa.field("_null_markers", pa.null(), nullable=True, metadata={}), + pa.field("Entity", pa.null(), nullable=True, metadata={}), + pa.field("Time", pa.null(), nullable=True, metadata={}), + ]), + self._TYPE_NAME, + ) + + +class SortKeyBatch(BaseBatch[SortKeyArrayLike], ComponentBatchMixin): + _ARROW_TYPE = SortKeyType() + + @staticmethod + def _native_to_pa_array(data: SortKeyArrayLike, data_type: pa.DataType) -> pa.Array: + if isinstance(data, (SortKey, int, str)): + data = [data] + + types: list[int] = [] + + for value in data: + if value is None: + types.append(0) + elif isinstance(value, SortKey): + types.append(value.value) # Actual enum value + elif isinstance(value, int): + types.append(value) # By number + elif isinstance(value, str): + if hasattr(SortKey, value): + types.append(SortKey[value].value) # fast path + elif value.lower() == "entity": + types.append(SortKey.Entity.value) + elif value.lower() == "time": + types.append(SortKey.Time.value) + else: + raise ValueError(f"Unknown SortKey kind: {value}") + else: + raise ValueError(f"Unknown SortKey kind: {value}") + + buffers = [ + None, + pa.array(types, type=pa.int8()).buffers()[1], + ] + children = (1 + 2) * [pa.nulls(len(data))] + + return pa.UnionArray.from_buffers( + type=data_type, + length=len(data), + buffers=buffers, + children=children, + ) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/sort_order.py b/rerun_py/rerun_sdk/rerun/blueprint/components/sort_order.py new file mode 100644 index 000000000000..70c932379614 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/sort_order.py @@ -0,0 +1,93 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/components/sort_order.fbs". + +# You can extend this class by creating a "SortOrderExt" class in "sort_order_ext.py". + +from __future__ import annotations + +from typing import Literal, Sequence, Union + +import pyarrow as pa + +from ..._baseclasses import ( + BaseBatch, + BaseExtensionType, + ComponentBatchMixin, +) + +__all__ = ["SortOrder", "SortOrderArrayLike", "SortOrderBatch", "SortOrderLike", "SortOrderType"] + + +from enum import Enum + + +class SortOrder(Enum): + """**Component**: Sort order for data table.""" + + Ascending = 1 + """Ascending""" + + Descending = 2 + """Descending""" + + +SortOrderLike = Union[SortOrder, Literal["ascending", "descending"]] +SortOrderArrayLike = Union[SortOrderLike, Sequence[SortOrderLike]] + + +class SortOrderType(BaseExtensionType): + _TYPE_NAME: str = "rerun.blueprint.components.SortOrder" + + def __init__(self) -> None: + pa.ExtensionType.__init__( + self, + pa.sparse_union([ + pa.field("_null_markers", pa.null(), nullable=True, metadata={}), + pa.field("Ascending", pa.null(), nullable=True, metadata={}), + pa.field("Descending", pa.null(), nullable=True, metadata={}), + ]), + self._TYPE_NAME, + ) + + +class SortOrderBatch(BaseBatch[SortOrderArrayLike], ComponentBatchMixin): + _ARROW_TYPE = SortOrderType() + + @staticmethod + def _native_to_pa_array(data: SortOrderArrayLike, data_type: pa.DataType) -> pa.Array: + if isinstance(data, (SortOrder, int, str)): + data = [data] + + types: list[int] = [] + + for value in data: + if value is None: + types.append(0) + elif isinstance(value, SortOrder): + types.append(value.value) # Actual enum value + elif isinstance(value, int): + types.append(value) # By number + elif isinstance(value, str): + if hasattr(SortOrder, value): + types.append(SortOrder[value].value) # fast path + elif value.lower() == "ascending": + types.append(SortOrder.Ascending.value) + elif value.lower() == "descending": + types.append(SortOrder.Descending.value) + else: + raise ValueError(f"Unknown SortOrder kind: {value}") + else: + raise ValueError(f"Unknown SortOrder kind: {value}") + + buffers = [ + None, + pa.array(types, type=pa.int8()).buffers()[1], + ] + children = (1 + 2) * [pa.nulls(len(data))] + + return pa.UnionArray.from_buffers( + type=data_type, + length=len(data), + buffers=buffers, + children=children, + )