Skip to content

Commit

Permalink
Optimize the scattering of balls in the time panel
Browse files Browse the repository at this point in the history
Combine close time points into one before scattering
  • Loading branch information
emilk committed May 13, 2022
1 parent bf0f040 commit ebf1675
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 73 deletions.
1 change: 1 addition & 0 deletions viewer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! rerun viewer.
mod app;
pub(crate) mod math;
mod misc;
mod remote_viewer_app;
mod ui;
Expand Down
12 changes: 12 additions & 0 deletions viewer/src/math.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use egui::emath::*;

pub fn line_segment_distance_sq_to_point([a, b]: [Pos2; 2], p: Pos2) -> f32 {
let l2 = a.distance_sq(b);
if l2 == 0.0 {
a.distance_sq(p)
} else {
let t = ((p - a).dot(b - a) / l2).clamp(0.0, 1.0);
let projection = a + t * (b - a);
p.distance_sq(projection)
}
}
185 changes: 124 additions & 61 deletions viewer/src/ui/time_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ impl TimePanel {
&tree.times
};

show_balls(
show_data_over_time(
log_db,
context,
time_area_painter,
Expand Down Expand Up @@ -374,7 +374,7 @@ fn top_row_ui(log_db: &LogDb, context: &mut ViewerContext, ui: &mut egui::Ui) {
});
}

fn show_balls(
fn show_data_over_time(
log_db: &LogDb,
context: &mut ViewerContext,
time_area_painter: &egui::Painter,
Expand All @@ -385,14 +385,13 @@ fn show_balls(
) {
crate::profile_function!();

let (top_y, bottom_y) = (full_width_rect.top(), full_width_rect.bottom());
// painting each data point as a separate circle is slow (too many circles!)
// so we join time points that are close together.
let points_per_time = time_ranges_ui.points_per_time().unwrap_or(f32::INFINITY);
let max_stretch_length_in_time = 1.0 / points_per_time as f64; // TODO

let pointer_pos = ui.input().pointer.hover_pos();

let mut hovered_messages = vec![];

let mut scatter = BallScatterer::default();

let hovered_color = ui.visuals().widgets.hovered.text_color();
let inactive_color = ui
.visuals()
Expand All @@ -406,81 +405,131 @@ fn show_balls(
} else {
context.time_control.time_selection()
};
let time_source = context.time_control.source();
let time_source = context.time_control.source().to_owned();

struct Stretch<'a> {
start_x: f32,
start_time: TimeValue,
stop_time: TimeValue,
selected: bool,
log_ids: Vec<&'a BTreeSet<LogId>>,
}

// TODO: optimize this, a lot
let mut shapes = vec![];
for (time, log_ids) in source {
if let Some(time) = time.0.get(time_source).copied() {
if let Some(x) = time_ranges_ui.x_from_time(time) {
let radius_multiplier = (log_ids.len() as f32).cbrt().at_most(3.0);
let base_radius = 2.0 * radius_multiplier;
let mut scatter = BallScatterer::default();
let mut hovered_messages = vec![];

if x + base_radius < full_width_rect.min.x
|| full_width_rect.max.x < x - base_radius
{
continue;
}
let mut paint_stretch = |stretch: &Stretch<'_>| {
let stop_x = time_ranges_ui
.x_from_time(stretch.stop_time)
.unwrap_or(stretch.start_x);

let pos = scatter.add(x, base_radius, (top_y, bottom_y));
let num_messages: usize = stretch.log_ids.iter().map(|l| l.len()).sum();
let radius = 2.5 * (1.0 + 0.5 * (num_messages as f32).log10());
let radius = radius.at_most(full_width_rect.height() / 3.0);

let is_hovered = pointer_pos.map_or(false, |pointer_pos| {
pos.distance(pointer_pos) < base_radius + 1.0
});
let x = (stretch.start_x + stop_x) * 0.5;
let pos = scatter.add(x, radius, (full_width_rect.top(), full_width_rect.bottom()));

let is_selected = selected_time_range.map_or(true, |range| range.contains(time));
let is_hovered = pointer_pos.map_or(false, |pointer_pos| {
pos.distance(pointer_pos) < radius + 1.0
});

let mut color = if is_hovered {
hovered_color
} else {
inactive_color
};
let mut color = if is_hovered {
hovered_color
} else {
inactive_color
};
if ui.visuals().dark_mode {
color = color.additive();
}

if ui.visuals().dark_mode {
color = color.additive();
}
let radius = if is_hovered {
1.75 * radius
} else if stretch.selected {
1.25 * radius
} else {
radius
};

shapes.push(Shape::circle_filled(pos, radius, color));

let radius = if is_hovered {
3.0
} else if is_selected {
1.75
if is_hovered && !ui.ctx().memory().is_anything_being_dragged() {
hovered_messages.extend(stretch.log_ids.iter().copied().flatten().copied());
}
};

let mut stretch: Option<Stretch<'_>> = None;

for (time, log_ids) in source {
// TODO: avoid this lookup by pre-partitioning on time source
if let Some(time) = time.0.get(&time_source).copied() {
let selected = selected_time_range.map_or(true, |range| range.contains(time));

if let Some(current_stretch) = &mut stretch {
if current_stretch.selected == selected
&& TimeRange::new(current_stretch.start_time, time)
.span()
.unwrap_or_default()
< max_stretch_length_in_time
{
// extend:
current_stretch.stop_time = time;
current_stretch.log_ids.push(log_ids);
} else {
1.25
};
let radius = radius * radius_multiplier;
// stop the previous…
paint_stretch(current_stretch);

shapes.push(Shape::circle_filled(pos, radius, color));
stretch = None;
}
}

if is_hovered && !ui.ctx().memory().is_anything_being_dragged() {
hovered_messages.extend(log_ids.iter().copied());
if stretch.is_none() {
if let Some(x) = time_ranges_ui.x_from_time(time) {
stretch = Some(Stretch {
start_x: x,
start_time: time,
stop_time: time,
selected,
log_ids: vec![log_ids],
});
}
}
}
}

if let Some(stretch) = stretch {
paint_stretch(&stretch);
}

time_area_painter.extend(shapes);

if !hovered_messages.is_empty() {
egui::containers::popup::show_tooltip_at_pointer(ui.ctx(), Id::new("data_tooltip"), |ui| {
// TODO: show as a table?
if hovered_messages.len() == 1 {
let log_id = hovered_messages[0];
if let Some(msg) = log_db.get_msg(&log_id) {
ui.push_id(log_id, |ui| {
ui.group(|ui| {
crate::space_view::show_log_msg(
context,
ui,
msg,
crate::Preview::Small,
);
});
show_log_ids_tooltip(log_db, context, ui.ctx(), &hovered_messages);
}
}

fn show_log_ids_tooltip(
log_db: &LogDb,
context: &mut ViewerContext,
ctx: &egui::Context,
log_ids: &[LogId],
) {
egui::containers::popup::show_tooltip_at_pointer(ctx, Id::new("data_tooltip"), |ui| {
// TODO: show as a table?
if log_ids.len() == 1 {
let log_id = log_ids[0];
if let Some(msg) = log_db.get_msg(&log_id) {
ui.push_id(log_id, |ui| {
ui.group(|ui| {
crate::space_view::show_log_msg(context, ui, msg, crate::Preview::Small);
});
}
} else {
ui.label(format!("{} log messages", hovered_messages.len()));
});
}
});
}
} else {
ui.label(format!("{} log messages", log_ids.len()));
}
});
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -1030,6 +1079,7 @@ fn view_everything(x_range: &RangeInclusive<f32>, time_source_axis: &TimeSourceA
struct Segment {
/// Matches [`Self::time`] (linear transform).
x: RangeInclusive<f32>,

/// Matches [`Self::x`] (linear transform).
time: TimeRange,

Expand Down Expand Up @@ -1260,6 +1310,19 @@ impl TimeRangesUi {
time_spanned: self.time_view.time_spanned / zoom_factor as f64,
})
}

/// How many egui points for each time unit?
fn points_per_time(&self) -> Option<f32> {
for segment in &self.segments {
let dx = *segment.x.end() - *segment.x.start();
if let Some(dt) = segment.time.span() {
if dx > 0.0 && dt >= 0.0 {
return Some(dx / dt as f32);
}
}
}
None
}
}

fn paint_time_range_ticks(
Expand Down
13 changes: 1 addition & 12 deletions viewer/src/ui/view2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ fn hovered(
let a = to_screen.transform_pos(a.into());
let b = to_screen.transform_pos(b.into());
let line_segment_distance_sq =
line_segment_distance_sq_to_point([a, b], pointer_pos);
crate::math::line_segment_distance_sq_to_point([a, b], pointer_pos);
min_dist_sq = min_dist_sq.min(line_segment_distance_sq);
}
min_dist_sq.sqrt()
Expand All @@ -229,14 +229,3 @@ fn hovered(

closest_id
}

fn line_segment_distance_sq_to_point([a, b]: [Pos2; 2], p: Pos2) -> f32 {
let l2 = a.distance_sq(b);
if l2 == 0.0 {
a.distance_sq(p)
} else {
let t = ((p - a).dot(b - a) / l2).clamp(0.0, 1.0);
let projection = a + t * (b - a);
p.distance_sq(projection)
}
}

0 comments on commit ebf1675

Please sign in to comment.