Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Scalar, SeriesLine, and SeriesPoint archetypes with their own visualizers #4875

Merged
merged 37 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
11cc4f2
Introduce SeriesLine and SeriesPoint archetypes
jleibs Jan 19, 2024
aacd520
Add a trimmed down Scalar archetype
jleibs Jan 30, 2024
9d49bcd
codegen
jleibs Jan 19, 2024
a385c17
Refactor aggregation out of visualizer_system
jleibs Jan 30, 2024
2825e1c
Refactor override utilities out of visualizer_system
jleibs Jan 30, 2024
3dfd3bb
Make the existing TimeSeriesSystem legacy
jleibs Jan 30, 2024
a1529ac
Copy-pasta a LineVisualizerSystem
jleibs Jan 30, 2024
0916895
defend against inverted time ranges
teh-cmc Jan 31, 2024
8c8ff28
More copy-pasta
jleibs Jan 31, 2024
37d89ba
New blueprint component for specifying visualizers
jleibs Jan 31, 2024
9e40916
codegen
jleibs Jan 31, 2024
1a99f36
Use the Visualizers override to drive with visualizers are run
jleibs Jan 31, 2024
20446a4
Don't show components that aren't relevant to the visualizers
jleibs Jan 31, 2024
bc9391b
dont forbid it entirely, there are still good reasons to do this
teh-cmc Feb 1, 2024
24967b7
pushing boundaries, literally
teh-cmc Feb 1, 2024
c099d5a
review
teh-cmc Feb 1, 2024
c0acdee
fmt
teh-cmc Feb 1, 2024
507e716
Pre-merge #4994
jleibs Feb 1, 2024
1824fd5
Factor out common plot operations to utils
jleibs Feb 1, 2024
e3e84d5
Merge branch 'main' into jleibs/series_style
jleibs Feb 1, 2024
a0f20dd
Fix codegen
jleibs Feb 1, 2024
cacd6e2
Naming the legacy_visualizer_system back to visualizer_system to avoi…
jleibs Feb 1, 2024
60530c3
Fix use statements from rename
jleibs Feb 1, 2024
4b7aef9
Lots of lines->series renames
jleibs Feb 1, 2024
b5ccb44
Clean up and move around archetypes
jleibs Feb 1, 2024
697030c
codegen
jleibs Feb 1, 2024
b959504
Factor out determination of plot bounds and delta
jleibs Feb 1, 2024
faafcbe
Reference code that handles the overriding
jleibs Feb 1, 2024
6bf2f99
Merge branch 'main' into jleibs/series_style
jleibs Feb 1, 2024
c6756b2
More doc cleanup
jleibs Feb 1, 2024
7687334
Merge branch 'main' into jleibs/series_style
jleibs Feb 1, 2024
009b153
spell
jleibs Feb 1, 2024
457842b
Fix the spawn heuristic
jleibs Feb 1, 2024
2ecadab
lint
jleibs Feb 1, 2024
4fbb760
pydoc index
jleibs Feb 1, 2024
edadbb8
Opt out of roundtrips
jleibs Feb 1, 2024
ff87c9e
Awkward doc reference
jleibs Feb 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 18 additions & 1 deletion crates/re_space_view/src/data_query_blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use re_entity_db::{
use re_log_types::{
path::RuleEffect, DataRow, EntityPath, EntityPathFilter, EntityPathRule, RowId, StoreKind,
};
use re_types_core::{archetypes::Clear, ComponentName};
use re_types_core::{archetypes::Clear, components::VisualizerOverrides, ComponentName};
use re_viewer_context::{
blueprint_timepoint_for_writes, DataQueryId, DataQueryResult, DataResult, DataResultHandle,
DataResultNode, DataResultTree, IndicatedEntities, PerVisualizer, PropertyOverrides,
Expand Down Expand Up @@ -254,6 +254,9 @@ impl<'a> QueryExpressionEvaluator<'a> {

// Only populate visualizers if this is a match
// Note that allowed prefixes that aren't matches can still create groups
// TODO(jleibs): It would be nice to lookup the override queries here, but we don't have
// access to a query context. Also the entity-override-path-joining is expensive and we don't want
// to do it during heuristic evaluation.
jleibs marked this conversation as resolved.
Show resolved Hide resolved
let visualizers: SmallVec<_> = if any_match {
self.visualizable_entities_for_visualizer_systems
.iter()
Expand Down Expand Up @@ -467,6 +470,20 @@ impl DataQueryPropertyResolver<'_> {
.individual_override_root
.join(&node.data_result.entity_path);

// If the user has overridden the visualizers, update which visualizers are used.
{
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
re_tracing::profile_scope!("Update visualizers from overrides");

if let Some(viz_override) = ctx
.blueprint
.store()
.query_latest_component::<VisualizerOverrides>(&override_path, query)
.map(|c| c.value)
{
node.data_result.visualizers =
viz_override.0.iter().map(|v| v.as_str().into()).collect();
}
}
let mut component_overrides: HashMap<ComponentName, (StoreKind, EntityPath)> =
Default::default();

Expand Down
202 changes: 202 additions & 0 deletions crates/re_space_view_time_series/src/aggregation.rs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just moving all the aggregation pieces out of visualizer_system.

Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use crate::{PlotPoint, PlotPointAttrs};

/// Implements aggregation behavior corresponding to [`TimeSeriesAggregator::Average`].
pub struct AverageAggregator;

impl AverageAggregator {
#[inline]
pub fn aggregate(aggregation_factor: f64, points: &[PlotPoint]) -> Vec<PlotPoint> {
let min_time = points.first().map_or(i64::MIN, |p| p.time);
let max_time = points.last().map_or(i64::MAX, |p| p.time);

let mut aggregated =
Vec::with_capacity((points.len() as f64 / aggregation_factor) as usize);

// NOTE: `floor()` since we handle fractional tails separately.
let window_size = usize::max(1, aggregation_factor.floor() as usize);
let aggregation_factor_fract = aggregation_factor.fract();

let mut i = 0;
while i < points.len() {
let mut j = 0;

let mut ratio = 1.0;
let mut acc = points[i + j].clone();
j += 1;

while j < window_size
&& i + j < points.len()
&& are_aggregatable(&points[i], &points[i + j], window_size)
{
let point = &points[i + j];

acc.value += point.value;
acc.attrs.radius += point.attrs.radius;

ratio += 1.0;
j += 1;
}

// Do a weighted average for the fractional tail.
if aggregation_factor_fract > 0.0
&& i + j < points.len()
&& are_aggregatable(&points[i], &points[i + j], window_size)
{
let point = &points[i + j];

let w = aggregation_factor_fract;
acc.value += point.value * w;
acc.attrs.radius += (point.attrs.radius as f64 * w) as f32;

ratio += aggregation_factor_fract;
j += 1;
}

acc.value /= ratio;
acc.attrs.radius = (acc.attrs.radius as f64 / ratio) as _;

aggregated.push(acc);

i += j;
}

// Force align the start and end timestamps to prevent jarring visual glitches.
if let Some(p) = aggregated.first_mut() {
p.time = min_time;
}
if let Some(p) = aggregated.last_mut() {
p.time = max_time;
}

aggregated
}
}

/// Implements aggregation behaviors corresponding to [`TimeSeriesAggregator::Max`],
/// [`TimeSeriesAggregator::Min`], [`TimeSeriesAggregator::MinMax`] and
/// [`TimeSeriesAggregator::MinMaxAverage`], .
pub enum MinMaxAggregator {
/// Keep only the maximum values in the range.
Max,

/// Keep only the minimum values in the range.
Min,

/// Keep both the minimum and maximum values in the range.
///
/// This will yield two aggregated points instead of one, effectively creating a vertical line.
MinMax,

/// Find both the minimum and maximum values in the range, then use the average of those.
MinMaxAverage,
}

impl MinMaxAggregator {
#[inline]
pub fn aggregate(&self, aggregation_factor: f64, points: &[PlotPoint]) -> Vec<PlotPoint> {
let min_time = points.first().map_or(i64::MIN, |p| p.time);
let max_time = points.last().map_or(i64::MAX, |p| p.time);

let capacity = (points.len() as f64 / aggregation_factor) as usize;
let mut aggregated = match self {
MinMaxAggregator::MinMax => Vec::with_capacity(capacity * 2),
_ => Vec::with_capacity(capacity),
};

// NOTE: `round()` since this can only handle discrete window sizes.
let window_size = usize::max(1, aggregation_factor.round() as usize);

let mut i = 0;
while i < points.len() {
let mut j = 0;

let mut acc_min = points[i + j].clone();
let mut acc_max = points[i + j].clone();
j += 1;

while j < window_size
&& i + j < points.len()
&& are_aggregatable(&points[i], &points[i + j], window_size)
{
let point = &points[i + j];

match self {
MinMaxAggregator::MinMax | MinMaxAggregator::MinMaxAverage => {
acc_min.value = f64::min(acc_min.value, point.value);
acc_min.attrs.radius = f32::min(acc_min.attrs.radius, point.attrs.radius);
acc_max.value = f64::max(acc_max.value, point.value);
acc_max.attrs.radius = f32::max(acc_max.attrs.radius, point.attrs.radius);
}
MinMaxAggregator::Min => {
acc_min.value = f64::min(acc_min.value, point.value);
acc_min.attrs.radius = f32::min(acc_min.attrs.radius, point.attrs.radius);
}
MinMaxAggregator::Max => {
acc_max.value = f64::max(acc_max.value, point.value);
acc_max.attrs.radius = f32::max(acc_max.attrs.radius, point.attrs.radius);
}
}

j += 1;
}

match self {
MinMaxAggregator::MinMax => {
aggregated.push(acc_min);
// Don't push the same point twice.
if j > 1 {
aggregated.push(acc_max);
}
}
MinMaxAggregator::MinMaxAverage => {
// Don't average a single point with itself.
if j > 1 {
acc_min.value = (acc_min.value + acc_max.value) * 0.5;
acc_min.attrs.radius = (acc_min.attrs.radius + acc_max.attrs.radius) * 0.5;
}
aggregated.push(acc_min);
}
MinMaxAggregator::Min => {
aggregated.push(acc_min);
}
MinMaxAggregator::Max => {
aggregated.push(acc_max);
}
}

i += j;
}

// Force align the start and end timestamps to prevent jarring visual glitches.
if let Some(p) = aggregated.first_mut() {
p.time = min_time;
}
if let Some(p) = aggregated.last_mut() {
p.time = max_time;
}

aggregated
}
}

/// Are two [`PlotPoint`]s safe to aggregate?
fn are_aggregatable(point1: &PlotPoint, point2: &PlotPoint, window_size: usize) -> bool {
let PlotPoint {
time,
value: _,
attrs,
} = point1;
let PlotPointAttrs {
label,
color,
radius: _,
kind,
} = attrs;

// We cannot aggregate two points that don't live in the same aggregation window to start with.
// This is very common with e.g. sparse datasets.
time.abs_diff(point2.time) <= window_size as u64
&& *label == point2.attrs.label
&& *color == point2.attrs.color
&& *kind == point2.attrs.kind
}
69 changes: 68 additions & 1 deletion crates/re_space_view_time_series/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
//!
//! A Space View that shows plots over Rerun timelines.

mod aggregation;
mod legacy_visualizer_system;
jleibs marked this conversation as resolved.
Show resolved Hide resolved
mod line_visualizer_system;
mod overrides;
mod point_visualizer_system;
mod space_view_class;
mod visualizer_system;
mod util;

use re_log_types::EntityPath;
use re_viewer_context::external::re_entity_db::TimeSeriesAggregator;
pub use space_view_class::TimeSeriesSpaceView;

/// Computes a deterministic, globally unique ID for the plot based on the ID of the space view
Expand All @@ -18,3 +25,63 @@ pub use space_view_class::TimeSeriesSpaceView;
pub(crate) fn plot_id(space_view_id: re_viewer_context::SpaceViewId) -> egui::Id {
egui::Id::new(("plot", space_view_id))
}

// ---

#[derive(Clone, Debug)]
pub struct PlotPointAttrs {
pub label: Option<String>,
pub color: egui::Color32,
pub radius: f32,
pub kind: PlotSeriesKind,
}

impl PartialEq for PlotPointAttrs {
fn eq(&self, rhs: &Self) -> bool {
let Self {
label,
color,
radius,
kind,
} = self;
label.eq(&rhs.label)
&& color.eq(&rhs.color)
&& radius.total_cmp(&rhs.radius).is_eq()
&& kind.eq(&rhs.kind)
}
}

impl Eq for PlotPointAttrs {}

#[derive(Clone, Debug)]
struct PlotPoint {
time: i64,
value: f64,
attrs: PlotPointAttrs,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PlotSeriesKind {
Continuous,
Scatter,
Clear,
}

#[derive(Clone, Debug)]
pub struct PlotSeries {
pub label: String,
pub color: egui::Color32,
pub width: f32,
pub kind: PlotSeriesKind,
pub points: Vec<(i64, f64)>,
pub entity_path: EntityPath,

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

/// What kind of aggregation was used to compute the graph?
pub aggregator: TimeSeriesAggregator,

/// `1.0` for raw data.
pub aggregation_factor: f64,
}