Skip to content

Commit

Permalink
Plot legend visibility and position control (part 3): legend UI added…
Browse files Browse the repository at this point in the history
… for both timeseries and bar charts space views (#4365)

### What

Introduce UI controls to set the legend visibility and location for both
timeseries and bar chart space views

<img width="1251" alt="image"
src="https://github.com/rerun-io/rerun/assets/49431240/0ac44f67-44f8-4c98-8c41-960bc7d8015f">

* Fixes #2049

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

- [PR Build Summary](https://build.rerun.io/pr/4365)
- [Docs
preview](https://rerun.io/preview/071a456269388f0882bd379b0f88f58ccd9912c8/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/071a456269388f0882bd379b0f88f58ccd9912c8/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
abey79 authored Nov 28, 2023
1 parent 8f14f33 commit 95efb76
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 97 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/re_data_store/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ re_types_core.workspace = true

ahash.workspace = true
document-features.workspace = true
egui_plot.workspace = true
getrandom.workspace = true
itertools.workspace = true
nohash-hasher.workspace = true
Expand Down
58 changes: 58 additions & 0 deletions crates/re_data_store/src/entity_properties.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[cfg(feature = "serde")]
use re_log_types::EntityPath;
use re_log_types::TimeInt;
use std::fmt::Formatter;

#[cfg(feature = "serde")]
use crate::EditableAutoValue;
Expand Down Expand Up @@ -116,6 +117,15 @@ pub struct EntityProperties {

/// The length of the arrows in the entity's own coordinate system (space).
pub transform_3d_size: EditableAutoValue<f32>,

/// Should the legend be shown (for plot space views).
pub show_legend: EditableAutoValue<bool>,

/// The location of the legend (for plot space views).
///
/// This is an Option instead of an EditableAutoValue to let each space view class decide on
/// what's the best default.
pub legend_location: Option<LegendCorner>,
}

#[cfg(feature = "serde")]
Expand All @@ -132,6 +142,8 @@ impl Default for EntityProperties {
backproject_radius_scale: EditableAutoValue::Auto(1.0),
transform_3d_visible: EditableAutoValue::Auto(false),
transform_3d_size: EditableAutoValue::Auto(1.0),
show_legend: EditableAutoValue::Auto(true),
legend_location: None,
}
}
}
Expand Down Expand Up @@ -167,6 +179,9 @@ impl EntityProperties {
.or(&child.transform_3d_visible)
.clone(),
transform_3d_size: self.transform_3d_size.or(&child.transform_3d_size).clone(),

show_legend: self.show_legend.or(&child.show_legend).clone(),
legend_location: self.legend_location.or(child.legend_location),
}
}

Expand Down Expand Up @@ -205,6 +220,9 @@ impl EntityProperties {
.or(&self.transform_3d_visible)
.clone(),
transform_3d_size: self.transform_3d_size.or(&other.transform_3d_size).clone(),

show_legend: other.show_legend.or(&self.show_legend).clone(),
legend_location: other.legend_location.or(self.legend_location),
}
}

Expand All @@ -221,6 +239,8 @@ impl EntityProperties {
backproject_radius_scale,
transform_3d_visible,
transform_3d_size,
show_legend,
legend_location,
} = self;

visible != &other.visible
Expand All @@ -233,6 +253,8 @@ impl EntityProperties {
|| backproject_radius_scale.has_edits(&other.backproject_radius_scale)
|| transform_3d_visible.has_edits(&other.transform_3d_visible)
|| transform_3d_size.has_edits(&other.transform_3d_size)
|| show_legend.has_edits(&other.show_legend)
|| *legend_location != other.legend_location
}
}

Expand Down Expand Up @@ -387,3 +409,39 @@ impl Default for ColorMapper {
Self::Colormap(Colormap::default())
}
}

// ----------------------------------------------------------------------------

/// Where to put the legend?
///
/// This type duplicates `egui_plot::Corner` to add serialization support.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum LegendCorner {
LeftTop,
RightTop,
LeftBottom,
RightBottom,
}

impl std::fmt::Display for LegendCorner {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LegendCorner::LeftTop => write!(f, "Top Left"),
LegendCorner::RightTop => write!(f, "Top Right"),
LegendCorner::LeftBottom => write!(f, "Bottom Left"),
LegendCorner::RightBottom => write!(f, "Bottom Right"),
}
}
}

impl From<LegendCorner> for egui_plot::Corner {
fn from(value: LegendCorner) -> Self {
match value {
LegendCorner::LeftTop => egui_plot::Corner::LeftTop,
LegendCorner::RightTop => egui_plot::Corner::RightTop,
LegendCorner::LeftBottom => egui_plot::Corner::LeftBottom,
LegendCorner::RightBottom => egui_plot::Corner::RightBottom,
}
}
}
219 changes: 131 additions & 88 deletions crates/re_space_view_bar_chart/src/space_view_class.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use egui::util::hash;
use re_data_store::EntityProperties;
use re_data_store::{EditableAutoValue, EntityProperties, LegendCorner};
use re_log_types::EntityPath;
use re_space_view::controls;
use re_types::datatypes::TensorBuffer;
Expand Down Expand Up @@ -67,21 +67,69 @@ impl SpaceViewClass for BarChartSpaceView {

fn selection_ui(
&self,
_ctx: &mut ViewerContext<'_>,
_ui: &mut egui::Ui,
ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
_state: &mut Self::State,
_space_origin: &EntityPath,
_space_view_id: SpaceViewId,
_root_entity_properties: &mut EntityProperties,
root_entity_properties: &mut EntityProperties,
) {
ctx.re_ui
.selection_grid(ui, "bar_chart_selection_ui")
.show(ui, |ui| {
ctx.re_ui.grid_left_hand_label(ui, "Legend");

ui.vertical(|ui| {
let mut selected = *root_entity_properties.show_legend.get();
if ctx.re_ui.checkbox(ui, &mut selected, "Visible").changed() {
root_entity_properties.show_legend =
EditableAutoValue::UserEdited(selected);
}

let mut corner = root_entity_properties
.legend_location
.unwrap_or(LegendCorner::RightTop);

egui::ComboBox::from_id_source("legend_corner")
.selected_text(corner.to_string())
.show_ui(ui, |ui| {
ui.style_mut().wrap = Some(false);
ui.set_min_width(64.0);

ui.selectable_value(
&mut corner,
LegendCorner::LeftTop,
LegendCorner::LeftTop.to_string(),
);
ui.selectable_value(
&mut corner,
LegendCorner::RightTop,
LegendCorner::RightTop.to_string(),
);
ui.selectable_value(
&mut corner,
LegendCorner::LeftBottom,
LegendCorner::LeftBottom.to_string(),
);
ui.selectable_value(
&mut corner,
LegendCorner::RightBottom,
LegendCorner::RightBottom.to_string(),
);
});

root_entity_properties.legend_location = Some(corner);
});
ui.end_row();
});
}

fn ui(
&self,
_ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
_state: &mut Self::State,
_root_entity_properties: &EntityProperties,
root_entity_properties: &EntityProperties,
_view_ctx: &ViewContextCollection,
parts: &ViewPartCollection,
_query: &ViewQuery<'_>,
Expand All @@ -94,90 +142,85 @@ impl SpaceViewClass for BarChartSpaceView {
let zoom_both_axis = !ui.input(|i| i.modifiers.contains(controls::ASPECT_SCROLL_MODIFIER));

ui.scope(|ui| {
Plot::new("bar_chart_plot")
.legend(Legend::default())
.clamp_grid(true)
.allow_zoom(egui_plot::AxisBools {
x: true,
y: zoom_both_axis,
})
.show(ui, |plot_ui| {
fn create_bar_chart<N: Into<f64>>(
ent_path: &EntityPath,
values: impl Iterator<Item = N>,
) -> BarChart {
let color = auto_color(hash(ent_path) as _);
let fill = color.gamma_multiply(0.75).additive(); // make sure overlapping bars are obvious
BarChart::new(
values
.enumerate()
.map(|(i, value)| {
Bar::new(i as f64 + 0.5, value.into())
.width(0.95)
.name(format!("{ent_path} #{i}"))
.fill(fill)
.stroke(egui::Stroke::NONE)
})
.collect(),
)
.name(ent_path.to_string())
.color(color)
}

for (ent_path, tensor) in charts {
let chart = match &tensor.buffer {
TensorBuffer::U8(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::U16(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::U32(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::U64(data) => {
create_bar_chart(ent_path, data.iter().copied().map(|v| v as f64))
}
TensorBuffer::I8(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::I16(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::I32(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::I64(data) => {
create_bar_chart(ent_path, data.iter().copied().map(|v| v as f64))
}
TensorBuffer::F16(data) => {
create_bar_chart(ent_path, data.iter().map(|f| f.to_f32()))
}
TensorBuffer::F32(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::F64(data) => {
create_bar_chart(ent_path, data.iter().copied())
}
TensorBuffer::Jpeg(_) => {
re_log::warn_once!(
"trying to display JPEG data as a bar chart ({:?})",
ent_path
);
continue;
}
TensorBuffer::Nv12(_) => {
re_log::warn_once!(
"trying to display NV12 data as a bar chart ({:?})",
ent_path
);
continue;
}
};

plot_ui.bar_chart(chart);
}
let mut plot =
Plot::new("bar_chart_plot")
.clamp_grid(true)
.allow_zoom(egui_plot::AxisBools {
x: true,
y: zoom_both_axis,
});

if *root_entity_properties.show_legend {
plot = plot.legend(Legend {
position: root_entity_properties
.legend_location
.unwrap_or(LegendCorner::RightTop)
.into(),
..Default::default()
});
}

plot.show(ui, |plot_ui| {
fn create_bar_chart<N: Into<f64>>(
ent_path: &EntityPath,
values: impl Iterator<Item = N>,
) -> BarChart {
let color = auto_color(hash(ent_path) as _);
let fill = color.gamma_multiply(0.75).additive(); // make sure overlapping bars are obvious
BarChart::new(
values
.enumerate()
.map(|(i, value)| {
Bar::new(i as f64 + 0.5, value.into())
.width(0.95)
.name(format!("{ent_path} #{i}"))
.fill(fill)
.stroke(egui::Stroke::NONE)
})
.collect(),
)
.name(ent_path.to_string())
.color(color)
}

for (ent_path, tensor) in charts {
let chart = match &tensor.buffer {
TensorBuffer::U8(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::U16(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::U32(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::U64(data) => {
create_bar_chart(ent_path, data.iter().copied().map(|v| v as f64))
}
TensorBuffer::I8(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::I16(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::I32(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::I64(data) => {
create_bar_chart(ent_path, data.iter().copied().map(|v| v as f64))
}
TensorBuffer::F16(data) => {
create_bar_chart(ent_path, data.iter().map(|f| f.to_f32()))
}
TensorBuffer::F32(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::F64(data) => create_bar_chart(ent_path, data.iter().copied()),
TensorBuffer::Jpeg(_) => {
re_log::warn_once!(
"trying to display JPEG data as a bar chart ({:?})",
ent_path
);
continue;
}
TensorBuffer::Nv12(_) => {
re_log::warn_once!(
"trying to display NV12 data as a bar chart ({:?})",
ent_path
);
continue;
}
};

plot_ui.bar_chart(chart);
}
});
});

Ok(())
Expand Down
Loading

0 comments on commit 95efb76

Please sign in to comment.