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

Add plot legends #349

Merged
merged 5 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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