Skip to content

Commit

Permalink
Add plot legends (#349)
Browse files Browse the repository at this point in the history
* add plot legends

* don't show crosshairs when hovering over legend

* add a toggle for the legend

* changes based on review

* improve legend behavior when curves share names
  • Loading branch information
EmbersArc authored May 7, 2021
1 parent d862ff6 commit 838f3e4
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 30 deletions.
81 changes: 81 additions & 0 deletions egui/src/widgets/plot/legend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::string::String;

use crate::*;

pub(crate) struct LegendEntry {
pub text: String,
pub color: Color32,
pub checked: bool,
pub hovered: bool,
}

impl LegendEntry {
pub fn new(text: String, color: Color32, checked: bool) -> Self {
Self {
text,
color,
checked,
hovered: false,
}
}
}

impl Widget for &mut LegendEntry {
fn ui(self, ui: &mut Ui) -> Response {
let LegendEntry {
checked,
text,
color,
..
} = self;
let icon_width = ui.spacing().icon_width;
let icon_spacing = ui.spacing().icon_spacing;
let padding = vec2(2.0, 2.0);
let total_extra = padding + vec2(icon_width + icon_spacing, 0.0) + padding;

let text_style = TextStyle::Button;
let galley = ui.fonts().layout_no_wrap(text_style, text.clone());

let mut desired_size = total_extra + galley.size;
desired_size = desired_size.at_least(ui.spacing().interact_size);
desired_size.y = desired_size.y.at_least(icon_width);

let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
let rect = rect.shrink2(padding);

response.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, &galley.text));

let visuals = ui.style().interact(&response);

let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);

let painter = ui.painter();

painter.add(Shape::Circle {
center: big_icon_rect.center(),
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});

if *checked {
painter.add(Shape::Circle {
center: small_icon_rect.center(),
radius: small_icon_rect.width() * 0.8,
fill: *color,
stroke: Default::default(),
});
}

let text_position = pos2(
rect.left() + padding.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size.y,
);
painter.galley(text_position, galley, visuals.text_color());

self.checked ^= response.clicked_by(PointerButton::Primary);
self.hovered = response.hovered();

response
}
}
144 changes: 115 additions & 29 deletions egui/src/widgets/plot/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
//! Simple plotting library.

mod items;
mod legend;
mod transform;

use std::collections::{BTreeMap, HashSet};

pub use items::{Curve, Value};
use items::{HLine, VLine};
use transform::{Bounds, ScreenTransform};

use crate::*;
use color::Hsva;

use self::legend::LegendEntry;

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

/// Information about the plot that has to persist between frames.
Expand All @@ -18,6 +23,7 @@ use color::Hsva;
struct PlotMemory {
bounds: Bounds,
auto_bounds: bool,
hidden_curves: HashSet<String>,
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -61,6 +67,7 @@ pub struct Plot {

show_x: bool,
show_y: bool,
show_legend: bool,
}

impl Plot {
Expand Down Expand Up @@ -89,6 +96,7 @@ impl Plot {

show_x: true,
show_y: true,
show_legend: true,
}
}

Expand Down Expand Up @@ -229,6 +237,12 @@ impl Plot {
self.min_auto_bounds.extend_with_y(y.into());
self
}

/// Whether to show a legend including all named curves. Default: `true`.
pub fn show_legend(mut self, show: bool) -> Self {
self.show_legend = show;
self
}
}

impl Widget for Plot {
Expand All @@ -250,8 +264,9 @@ impl Widget for Plot {
min_size,
data_aspect,
view_aspect,
show_x,
show_y,
mut show_x,
mut show_y,
show_legend,
} = self;

let plot_id = ui.make_persistent_id(name);
Expand All @@ -260,46 +275,110 @@ impl Widget for Plot {
.id_data
.get_mut_or_insert_with(plot_id, || PlotMemory {
bounds: min_auto_bounds,
auto_bounds: true,
auto_bounds: !min_auto_bounds.is_valid(),
hidden_curves: HashSet::new(),
})
.clone();

let PlotMemory {
mut bounds,
mut auto_bounds,
mut hidden_curves,
} = memory;

// Determine the size of the plot in the UI
let size = {
let width = width.unwrap_or_else(|| {
if let (Some(height), Some(aspect)) = (height, view_aspect) {
height * aspect
} else {
ui.available_size_before_wrap_finite().x
}
});
let width = width.at_least(min_size.x);

let height = height.unwrap_or_else(|| {
if let Some(aspect) = view_aspect {
width / aspect
} else {
ui.available_size_before_wrap_finite().y
}
});
let height = height.at_least(min_size.y);
let width = width
.unwrap_or_else(|| {
if let (Some(height), Some(aspect)) = (height, view_aspect) {
height * aspect
} else {
ui.available_size_before_wrap_finite().x
}
})
.at_least(min_size.x);

let height = height
.unwrap_or_else(|| {
if let Some(aspect) = view_aspect {
width / aspect
} else {
ui.available_size_before_wrap_finite().y
}
})
.at_least(min_size.y);
vec2(width, height)
};

let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
let plot_painter = ui.painter().sub_region(rect);

// Background
ui.painter().add(Shape::Rect {
plot_painter.add(Shape::Rect {
rect,
corner_radius: 2.0,
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().window_stroke(),
});

// --- Legend ---

if show_legend {
// Collect the legend entries. If multiple curves have the same name, they share a
// checkbox. If their colors don't match, we pick a neutral color for the checkbox.
let mut legend_entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
curves
.iter()
.filter(|curve| !curve.name.is_empty())
.for_each(|curve| {
let checked = !hidden_curves.contains(&curve.name);
let text = curve.name.clone();
legend_entries
.entry(curve.name.clone())
.and_modify(|entry| {
if entry.color != curve.stroke.color {
entry.color = ui.visuals().noninteractive().fg_stroke.color
}
})
.or_insert_with(|| LegendEntry::new(text, curve.stroke.color, checked));
});

// Show the legend.
let mut legend_ui = ui.child_ui(rect, Layout::top_down(Align::LEFT));
legend_entries.values_mut().for_each(|entry| {
let response = legend_ui.add(entry);
if response.hovered() {
show_x = false;
show_y = false;
}
});

// Get the names of the hidden curves.
hidden_curves = legend_entries
.values()
.filter(|entry| !entry.checked)
.map(|entry| entry.text.clone())
.collect();

// Highlight the hovered curves.
legend_entries
.values()
.filter(|entry| entry.hovered)
.for_each(|entry| {
curves
.iter_mut()
.filter(|curve| curve.name == entry.text)
.for_each(|curve| {
curve.stroke.width *= 2.0;
});
});

// Remove deselected curves.
curves.retain(|curve| !hidden_curves.contains(&curve.name));
}

// ---

auto_bounds |= response.double_clicked_by(PointerButton::Primary);

// Set bounds automatically based on content.
Expand Down Expand Up @@ -358,13 +437,7 @@ impl Widget for Plot {
.iter_mut()
.for_each(|curve| curve.generate_points(transform.bounds().range_x()));

ui.memory().id_data.insert(
plot_id,
PlotMemory {
bounds: *transform.bounds(),
auto_bounds,
},
);
let bounds = *transform.bounds();

let prepared = Prepared {
curves,
Expand All @@ -376,7 +449,20 @@ impl Widget for Plot {
};
prepared.ui(ui, &response);

response.on_hover_cursor(CursorIcon::Crosshair)
ui.memory().id_data.insert(
plot_id,
PlotMemory {
bounds,
auto_bounds,
hidden_curves,
},
);

if show_x || show_y {
response.on_hover_cursor(CursorIcon::Crosshair)
} else {
response
}
}
}

Expand Down
1 change: 1 addition & 0 deletions egui/src/widgets/plot/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ impl Bounds {
}

/// Contains the screen rectangle and the plot bounds and provides methods to transform them.
#[derive(Clone)]
pub(crate) struct ScreenTransform {
/// The screen rectangle.
frame: Rect,
Expand Down
7 changes: 6 additions & 1 deletion egui_demo_lib/src/apps/demo/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub struct PlotDemo {
circle_radius: f64,
circle_center: Pos2,
square: bool,
legend: bool,
proportional: bool,
}

Expand All @@ -20,6 +21,7 @@ impl Default for PlotDemo {
circle_radius: 1.5,
circle_center: Pos2::new(0.0, 0.0),
square: false,
legend: true,
proportional: true,
}
}
Expand Down Expand Up @@ -54,6 +56,7 @@ impl PlotDemo {
circle_radius,
circle_center,
square,
legend,
proportional,
} = self;

Expand Down Expand Up @@ -87,6 +90,7 @@ impl PlotDemo {
ui.checkbox(animate, "animate");
ui.add_space(8.0);
ui.checkbox(square, "square view");
ui.checkbox(legend, "legend");
ui.checkbox(proportional, "proportional data axes");
});
});
Expand Down Expand Up @@ -145,7 +149,8 @@ impl super::View for PlotDemo {
.curve(self.circle())
.curve(self.sin())
.curve(self.thingy())
.min_size(Vec2::new(256.0, 200.0));
.show_legend(self.legend)
.min_size(Vec2::new(200.0, 200.0));
if self.square {
plot = plot.view_aspect(1.0);
}
Expand Down

0 comments on commit 838f3e4

Please sign in to comment.