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

Plot: Legend improvements #410

Merged
merged 63 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
2ced76e
initial work on markers
EmbersArc May 9, 2021
0d09622
clippy fix
EmbersArc May 9, 2021
10ffbfc
simplify marker
EmbersArc May 11, 2021
edd7e17
Merge branch 'master' into plot-markers
EmbersArc May 11, 2021
4ad97f3
use option for color
EmbersArc May 11, 2021
5fb8820
prepare for more demo plots
EmbersArc May 11, 2021
ee5fd49
more improvements for markers
EmbersArc May 11, 2021
99cba8a
some small adjustments
EmbersArc May 11, 2021
10e56e0
better highlighting
EmbersArc May 14, 2021
75c5c1a
don't draw transparent lines
EmbersArc May 14, 2021
90622a5
use transparent color instead of option
EmbersArc May 14, 2021
729ce17
don't brighten curves when highlighting
EmbersArc May 14, 2021
c5945f2
Merge branch 'master' into plot-markers
EmbersArc May 16, 2021
7ccc02c
Initial changes to lengend:
EmbersArc May 16, 2021
15820d7
draw legend on top of curves
EmbersArc May 16, 2021
1d7b252
update changelog
EmbersArc May 16, 2021
c5a545b
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 16, 2021
eafff6a
fix legend checkboxes
EmbersArc May 16, 2021
3cb3a14
simplify legend
EmbersArc May 16, 2021
4d4f579
remove unnecessary derives
EmbersArc May 17, 2021
ef6dc6e
remove config from legend entries
EmbersArc May 17, 2021
e62a816
avoid allocations and use line_segment
EmbersArc May 21, 2021
a4a3793
compare against transparent color
EmbersArc May 21, 2021
e4a7e56
Merge remote-tracking branch 'upstream/master' into plot-markers
EmbersArc May 21, 2021
ae00424
create new Points primitive
EmbersArc May 24, 2021
773be05
fix doctest
EmbersArc May 24, 2021
6831302
some cleanup and fix hover
EmbersArc May 24, 2021
4ee57fd
common interface for lines and points
EmbersArc May 24, 2021
4c375f0
clippy fixes
EmbersArc May 24, 2021
95bfbbf
reduce visibilities
EmbersArc May 24, 2021
0bc8abc
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 25, 2021
7867c47
update legend
EmbersArc May 25, 2021
bffaa70
clippy fix
EmbersArc May 25, 2021
9f99826
change instances of "curve" to "item"
EmbersArc May 25, 2021
30b5073
change visibility
EmbersArc May 25, 2021
88fae19
Update egui/src/widgets/plot/mod.rs
EmbersArc May 26, 2021
8bc576b
Update egui/src/widgets/plot/mod.rs
EmbersArc May 26, 2021
c6423be
Update egui_demo_lib/src/apps/demo/plot_demo.rs
EmbersArc May 26, 2021
3aeb354
Update egui_demo_lib/src/apps/demo/plot_demo.rs
EmbersArc May 26, 2021
e76a679
changes based on review
EmbersArc May 26, 2021
8576e2a
Merge remote-tracking branch 'upstream/master' into plot-markers
EmbersArc May 26, 2021
887190e
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 26, 2021
af47250
add legend to demo
EmbersArc May 26, 2021
43b4539
fix test
EmbersArc May 26, 2021
f033653
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 26, 2021
a40efbf
move highlighted items to front
EmbersArc May 26, 2021
c541952
dynamic plot size
EmbersArc May 26, 2021
36ae1af
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 26, 2021
5f29404
add legend again
EmbersArc May 27, 2021
4197bd1
remove height
EmbersArc May 27, 2021
8c9d1a0
clippy fix
EmbersArc May 27, 2021
090bbeb
Merge branch 'plot-markers' into legend-improvements
EmbersArc May 27, 2021
04ea585
Merge remote-tracking branch 'upstream/master' into legend-improvements
EmbersArc May 27, 2021
7653add
update changelog
EmbersArc May 27, 2021
96f175a
minor changes
EmbersArc May 28, 2021
eac5bf5
Update egui/src/widgets/plot/legend.rs
EmbersArc Jun 3, 2021
109a9b2
Update egui/src/widgets/plot/legend.rs
EmbersArc Jun 3, 2021
834f784
Update egui/src/widgets/plot/legend.rs
EmbersArc Jun 3, 2021
3845020
changes based on review
EmbersArc Jun 3, 2021
25bc76f
add functions to mutate legend config
EmbersArc Jun 5, 2021
490ba0a
Merge remote-tracking branch 'upstream/master' into legend-improvements
EmbersArc Jun 5, 2021
d27e0f4
Merge remote-tracking branch 'upstream/master' into legend-improvements
EmbersArc Jun 7, 2021
1fcf5c0
use horizontal_align
EmbersArc Jun 7, 2021
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
## Unreleased

### Added ⭐
* [Plot legend improvements](https://github.com/emilk/egui/pull/410).
* [Line markers for plots](https://github.com/emilk/egui/pull/363).
* Add right and bottom panels (`SidePanel::right` and `Panel::bottom`).
* Add resizable panels.
Expand Down
17 changes: 14 additions & 3 deletions egui/src/widgets/plot/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub(super) trait PlotItem {
fn name(&self) -> &str;
fn color(&self) -> Color32;
fn highlight(&mut self);
fn highlighted(&self) -> bool;
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -273,7 +274,8 @@ impl Line {

/// Name of this line.
///
/// This name will show up in the plot legend, if legends are turned on.
/// This name will show up in the plot legend, if legends are turned on. Multiple lines may
/// share the same name, in which case they will also share an entry in the legend.
#[allow(clippy::needless_pass_by_value)]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
Expand Down Expand Up @@ -327,6 +329,10 @@ impl PlotItem for Line {
fn highlight(&mut self) {
self.highlight = true;
}

fn highlighted(&self) -> bool {
self.highlight
}
}

/// A set of points.
Expand Down Expand Up @@ -386,9 +392,10 @@ impl Points {
self
}

/// Name of this series of markers.
/// Name of this set of points.
///
/// This name will show up in the plot legend, if legends are turned on.
/// This name will show up in the plot legend, if legends are turned on. Multiple sets of points
/// may share the same name, in which case they will also share an entry in the legend.
#[allow(clippy::needless_pass_by_value)]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
Expand Down Expand Up @@ -556,4 +563,8 @@ impl PlotItem for Points {
fn highlight(&mut self) {
self.highlight = true;
}

fn highlighted(&self) -> bool {
self.highlight
}
}
230 changes: 193 additions & 37 deletions egui/src/widgets/plot/legend.rs
Original file line number Diff line number Diff line change
@@ -1,81 +1,237 @@
use std::string::String;
use std::{
collections::{BTreeMap, HashSet},
string::String,
};

use crate::*;

pub(crate) struct LegendEntry {
pub text: String,
pub color: Color32,
pub checked: bool,
pub hovered: bool,
use super::items::PlotItem;

/// Where to place the plot legend.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Corner {
LeftTop,
RightTop,
LeftBottom,
RightBottom,
}

impl Corner {
pub fn all() -> impl Iterator<Item = Corner> {
[
Corner::LeftTop,
Corner::RightTop,
Corner::LeftBottom,
Corner::RightBottom,
]
.iter()
.copied()
}
}

/// The configuration for a plot legend.
#[derive(Clone, Copy, PartialEq)]
pub struct Legend {
pub text_style: TextStyle,
pub position: Corner,
}

impl Default for Legend {
fn default() -> Self {
Self {
text_style: TextStyle::Body,
position: Corner::RightTop,
}
}
}

impl Legend {
pub fn text_style(mut self, style: TextStyle) -> Self {
self.text_style = style;
self
}

pub fn position(mut self, corner: Corner) -> Self {
self.position = corner;
self
}
}

#[derive(Clone)]
struct LegendEntry {
color: Color32,
checked: bool,
hovered: bool,
}

impl LegendEntry {
pub fn new(text: String, color: Color32, checked: bool) -> Self {
fn new(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,
fn ui(&mut self, ui: &mut Ui, text: String) -> Response {
let Self {
color,
..
checked,
hovered,
} = 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 galley = ui.fonts().layout_no_wrap(ui.style().body_text_style, text);

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 icon_size = galley.size.y;
let icon_spacing = icon_size / 5.0;
let total_extra = vec2(icon_size + icon_spacing, 0.0);

let desired_size = total_extra + galley.size;
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 label_on_the_left = ui.layout().horizontal_align() == Align::RIGHT;

let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
let icon_position_x = if label_on_the_left {
rect.right() - icon_size / 2.0
} else {
rect.left() + icon_size / 2.0
};
let icon_position = pos2(icon_position_x, rect.center().y);
let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size));

let painter = ui.painter();

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

if *checked {
let fill = if *color == Color32::TRANSPARENT {
ui.visuals().noninteractive().fg_stroke.color
} else {
*color
};
painter.add(Shape::Circle {
center: small_icon_rect.center(),
radius: small_icon_rect.width() * 0.8,
fill: *color,
center: icon_rect.center(),
radius: icon_size * 0.4,
fill,
stroke: Default::default(),
});
}

let text_position = pos2(
rect.left() + padding.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size.y,
);
let text_position_x = if label_on_the_left {
rect.right() - icon_size - icon_spacing - galley.size.x
} else {
rect.left() + icon_size + icon_spacing
};

let text_position = pos2(text_position_x, 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();
*checked ^= response.clicked_by(PointerButton::Primary);
*hovered = response.hovered();

response
}
}

#[derive(Clone)]
pub(super) struct LegendWidget {
rect: Rect,
entries: BTreeMap<String, LegendEntry>,
config: Legend,
}

impl LegendWidget {
/// Create a new legend from items, the names of items that are hidden and the style of the
/// text. Returns `None` if the legend has no entries.
pub(super) fn try_new(
rect: Rect,
config: Legend,
items: &[Box<dyn PlotItem>],
hidden_items: &HashSet<String>,
emilk marked this conversation as resolved.
Show resolved Hide resolved
) -> Option<Self> {
// Collect the legend entries. If multiple items have the same name, they share a
// checkbox. If their colors don't match, we pick a neutral color for the checkbox.
let mut entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
items
.iter()
.filter(|item| !item.name().is_empty())
.for_each(|item| {
entries
.entry(item.name().to_string())
.and_modify(|entry| {
if entry.color != item.color() {
// Multiple items with different colors
entry.color = Color32::TRANSPARENT;
}
})
.or_insert_with(|| {
let color = item.color();
let checked = !hidden_items.contains(item.name());
LegendEntry::new(color, checked)
});
});
(!entries.is_empty()).then(|| Self {
rect,
entries,
config,
})
}

// Get the names of the hidden items.
pub fn get_hidden_items(&self) -> HashSet<String> {
self.entries
.iter()
.filter(|(_, entry)| !entry.checked)
.map(|(name, _)| name.clone())
.collect()
}

// Get the name of the hovered items.
pub fn get_hovered_entry_name(&self) -> Option<String> {
self.entries
.iter()
.find(|(_, entry)| entry.hovered)
.map(|(name, _)| name.to_string())
}
}

impl Widget for &mut LegendWidget {
fn ui(self, ui: &mut Ui) -> Response {
let LegendWidget {
rect,
entries,
config,
} = self;

let main_dir = match config.position {
Corner::LeftTop | Corner::RightTop => Direction::TopDown,
Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp,
};
let cross_align = match config.position {
Corner::LeftTop | Corner::LeftBottom => Align::LEFT,
Corner::RightTop | Corner::RightBottom => Align::RIGHT,
};
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align);
let legend_pad = 2.0;
let legend_rect = rect.shrink(legend_pad);
let mut legend_ui = ui.child_ui(legend_rect, layout);
legend_ui
.scope(|ui| {
ui.style_mut().body_text_style = config.text_style;
entries
.iter_mut()
.map(|(name, entry)| entry.ui(ui, name.clone()))
.reduce(|r1, r2| r1.union(r2))
.unwrap()
})
.inner
}
}
Loading