Skip to content

Commit d7b958e

Browse files
authored
Allow grouping legend entries by id rather than name (#231)
Today, if you have several plot items sharing the same id, they get the same entry in th legend. This adds a grouping setting that allows to configure the behavior. Personally, I think today's behavior is just plain wrong, but I don't want to make this a breaking change, therefore this keeps the old behavior as the default
1 parent ed3d2c2 commit d7b958e

File tree

3 files changed

+60
-36
lines changed

3 files changed

+60
-36
lines changed

egui_plot/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub use crate::memory::PlotMemory;
6868
pub use crate::overlays::ColorConflictHandling;
6969
pub use crate::overlays::CoordinatesFormatter;
7070
pub use crate::overlays::Legend;
71+
pub use crate::overlays::LegendGrouping;
7172
pub use crate::placement::Corner;
7273
pub use crate::placement::HPlacement;
7374
pub use crate::placement::Placement;

egui_plot/src/overlays/legend.rs

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::BTreeMap;
21
use std::string::String;
32

43
use egui::Align;
@@ -34,6 +33,18 @@ pub enum ColorConflictHandling {
3433
RemoveColor,
3534
}
3635

36+
/// How to group legend entries.
37+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
39+
pub enum LegendGrouping {
40+
/// Items with the same name share a single legend entry (default).
41+
#[default]
42+
ByName,
43+
44+
/// Each item gets its own legend entry, keyed by its unique [`Id`].
45+
ById,
46+
}
47+
3748
/// The configuration for a plot legend.
3849
#[derive(Clone, PartialEq)]
3950
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -44,6 +55,7 @@ pub struct Legend {
4455
pub title: Option<String>,
4556

4657
follow_insertion_order: bool,
58+
grouping: LegendGrouping,
4759
color_conflict_handling: ColorConflictHandling,
4860

4961
/// Used for overriding the `hidden_items` set in [`LegendWidget`].
@@ -58,6 +70,7 @@ impl Default for Legend {
5870
position: Corner::RightTop,
5971
title: None,
6072
follow_insertion_order: false,
73+
grouping: LegendGrouping::default(),
6174
color_conflict_handling: ColorConflictHandling::RemoveColor,
6275
hidden_items: None,
6376
}
@@ -121,6 +134,17 @@ impl Legend {
121134
self.color_conflict_handling = color_conflict_handling;
122135
self
123136
}
137+
138+
/// Specifies how legend entries are grouped. Default: [`LegendGrouping::ByName`].
139+
///
140+
/// With [`LegendGrouping::ByName`], items sharing the same name are
141+
/// merged into a single legend entry. With [`LegendGrouping::ById`],
142+
/// each item gets its own entry keyed by its unique [`Id`].
143+
#[inline]
144+
pub fn grouping(mut self, grouping: LegendGrouping) -> Self {
145+
self.grouping = grouping;
146+
self
147+
}
124148
}
125149

126150
#[derive(Clone)]
@@ -228,45 +252,43 @@ impl LegendWidget {
228252
// If `config.hidden_items` is not `None`, it is used.
229253
let hidden_items = config.hidden_items.as_ref().unwrap_or(hidden_items);
230254

231-
// Collect the legend entries. If multiple items have the same name, they share
232-
// a checkbox. If their colors don't match, we pick a neutral color for
233-
// the checkbox.
234-
let mut keys: BTreeMap<String, usize> = BTreeMap::new();
235-
let mut entries: BTreeMap<(usize, &str), LegendEntry> = BTreeMap::new();
236-
items.iter().filter(|item| !item.name().is_empty()).for_each(|item| {
237-
let next_entry = entries.len();
238-
let key = if config.follow_insertion_order {
239-
*keys.entry(item.name().to_owned()).or_insert(next_entry)
240-
} else {
241-
// Use the same key if we don't want insertion order
242-
0
255+
// Collect the legend entries. With `ByName` grouping, items sharing the
256+
// same name are merged into a single checkbox. With `ById` grouping,
257+
// items sharing the same `Id` are merged instead. When colors conflict
258+
// within a merged entry, `color_conflict_handling` decides which color
259+
// to show.
260+
let mut entries: Vec<LegendEntry> = Vec::new();
261+
let mut seen: ahash::HashMap<Id, usize> = ahash::HashMap::default();
262+
for item in items.iter().filter(|item| !item.name().is_empty()) {
263+
let dedup_key = match config.grouping {
264+
LegendGrouping::ByName => Id::new(item.name()),
265+
LegendGrouping::ById => item.id(),
243266
};
244267

245-
entries
246-
.entry((key, item.name()))
247-
.and_modify(|entry| {
248-
if entry.color != item.color() {
249-
match config.color_conflict_handling {
250-
ColorConflictHandling::PickFirst => (),
251-
ColorConflictHandling::PickLast => entry.color = item.color(),
252-
ColorConflictHandling::RemoveColor => {
253-
// Multiple items with different colors
254-
entry.color = Color32::TRANSPARENT;
255-
}
268+
if let Some(&idx) = seen.get(&dedup_key) {
269+
let entry = &mut entries[idx];
270+
if entry.color != item.color() {
271+
match config.color_conflict_handling {
272+
ColorConflictHandling::PickFirst => (),
273+
ColorConflictHandling::PickLast => entry.color = item.color(),
274+
ColorConflictHandling::RemoveColor => {
275+
entry.color = Color32::TRANSPARENT;
256276
}
257277
}
258-
})
259-
.or_insert_with(|| {
260-
let color = item.color();
261-
let checked = !hidden_items.contains(&item.id());
262-
LegendEntry::new(item.id(), item.name().to_owned(), color, checked)
263-
});
264-
});
265-
(!entries.is_empty()).then_some(Self {
266-
rect,
267-
entries: entries.into_values().collect(),
268-
config,
269-
})
278+
}
279+
} else {
280+
seen.insert(dedup_key, entries.len());
281+
let color = item.color();
282+
let checked = !hidden_items.contains(&item.id());
283+
entries.push(LegendEntry::new(item.id(), item.name().to_owned(), color, checked));
284+
}
285+
}
286+
287+
if !config.follow_insertion_order {
288+
entries.sort_by(|a, b| a.name.cmp(&b.name));
289+
}
290+
291+
(!entries.is_empty()).then_some(Self { rect, entries, config })
270292
}
271293

272294
// Get the names of the hidden items.

egui_plot/src/overlays/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ mod legend;
77
pub use coordinates::CoordinatesFormatter;
88
pub use legend::ColorConflictHandling;
99
pub use legend::Legend;
10+
pub use legend::LegendGrouping;
1011
pub use legend::LegendWidget;

0 commit comments

Comments
 (0)