From 75cb6f6d28e82496a58dbcb6f1b42e0ed5d95602 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Fri, 13 Feb 2026 14:05:49 +0100 Subject: [PATCH] Allow grouping legend entries by id rather than name 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 --- egui_plot/src/lib.rs | 1 + egui_plot/src/overlays/legend.rs | 94 ++++++++++++++++++++------------ egui_plot/src/overlays/mod.rs | 1 + 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index fafa3fcf..7ed664c2 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -68,6 +68,7 @@ pub use crate::memory::PlotMemory; pub use crate::overlays::ColorConflictHandling; pub use crate::overlays::CoordinatesFormatter; pub use crate::overlays::Legend; +pub use crate::overlays::LegendGrouping; pub use crate::placement::Corner; pub use crate::placement::HPlacement; pub use crate::placement::Placement; diff --git a/egui_plot/src/overlays/legend.rs b/egui_plot/src/overlays/legend.rs index ef8c0b6a..dba053d6 100644 --- a/egui_plot/src/overlays/legend.rs +++ b/egui_plot/src/overlays/legend.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::string::String; use egui::Align; @@ -34,6 +33,18 @@ pub enum ColorConflictHandling { RemoveColor, } +/// How to group legend entries. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum LegendGrouping { + /// Items with the same name share a single legend entry (default). + #[default] + ByName, + + /// Each item gets its own legend entry, keyed by its unique [`Id`]. + ById, +} + /// The configuration for a plot legend. #[derive(Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -44,6 +55,7 @@ pub struct Legend { pub title: Option, follow_insertion_order: bool, + grouping: LegendGrouping, color_conflict_handling: ColorConflictHandling, /// Used for overriding the `hidden_items` set in [`LegendWidget`]. @@ -58,6 +70,7 @@ impl Default for Legend { position: Corner::RightTop, title: None, follow_insertion_order: false, + grouping: LegendGrouping::default(), color_conflict_handling: ColorConflictHandling::RemoveColor, hidden_items: None, } @@ -121,6 +134,17 @@ impl Legend { self.color_conflict_handling = color_conflict_handling; self } + + /// Specifies how legend entries are grouped. Default: [`LegendGrouping::ByName`]. + /// + /// With [`LegendGrouping::ByName`], items sharing the same name are + /// merged into a single legend entry. With [`LegendGrouping::ById`], + /// each item gets its own entry keyed by its unique [`Id`]. + #[inline] + pub fn grouping(mut self, grouping: LegendGrouping) -> Self { + self.grouping = grouping; + self + } } #[derive(Clone)] @@ -228,45 +252,43 @@ impl LegendWidget { // If `config.hidden_items` is not `None`, it is used. let hidden_items = config.hidden_items.as_ref().unwrap_or(hidden_items); - // 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 keys: BTreeMap = BTreeMap::new(); - let mut entries: BTreeMap<(usize, &str), LegendEntry> = BTreeMap::new(); - items.iter().filter(|item| !item.name().is_empty()).for_each(|item| { - let next_entry = entries.len(); - let key = if config.follow_insertion_order { - *keys.entry(item.name().to_owned()).or_insert(next_entry) - } else { - // Use the same key if we don't want insertion order - 0 + // Collect the legend entries. With `ByName` grouping, items sharing the + // same name are merged into a single checkbox. With `ById` grouping, + // items sharing the same `Id` are merged instead. When colors conflict + // within a merged entry, `color_conflict_handling` decides which color + // to show. + let mut entries: Vec = Vec::new(); + let mut seen: ahash::HashMap = ahash::HashMap::default(); + for item in items.iter().filter(|item| !item.name().is_empty()) { + let dedup_key = match config.grouping { + LegendGrouping::ByName => Id::new(item.name()), + LegendGrouping::ById => item.id(), }; - entries - .entry((key, item.name())) - .and_modify(|entry| { - if entry.color != item.color() { - match config.color_conflict_handling { - ColorConflictHandling::PickFirst => (), - ColorConflictHandling::PickLast => entry.color = item.color(), - ColorConflictHandling::RemoveColor => { - // Multiple items with different colors - entry.color = Color32::TRANSPARENT; - } + if let Some(&idx) = seen.get(&dedup_key) { + let entry = &mut entries[idx]; + if entry.color != item.color() { + match config.color_conflict_handling { + ColorConflictHandling::PickFirst => (), + ColorConflictHandling::PickLast => entry.color = item.color(), + ColorConflictHandling::RemoveColor => { + entry.color = Color32::TRANSPARENT; } } - }) - .or_insert_with(|| { - let color = item.color(); - let checked = !hidden_items.contains(&item.id()); - LegendEntry::new(item.id(), item.name().to_owned(), color, checked) - }); - }); - (!entries.is_empty()).then_some(Self { - rect, - entries: entries.into_values().collect(), - config, - }) + } + } else { + seen.insert(dedup_key, entries.len()); + let color = item.color(); + let checked = !hidden_items.contains(&item.id()); + entries.push(LegendEntry::new(item.id(), item.name().to_owned(), color, checked)); + } + } + + if !config.follow_insertion_order { + entries.sort_by(|a, b| a.name.cmp(&b.name)); + } + + (!entries.is_empty()).then_some(Self { rect, entries, config }) } // Get the names of the hidden items. diff --git a/egui_plot/src/overlays/mod.rs b/egui_plot/src/overlays/mod.rs index 4ae302da..8db311f2 100644 --- a/egui_plot/src/overlays/mod.rs +++ b/egui_plot/src/overlays/mod.rs @@ -7,4 +7,5 @@ mod legend; pub use coordinates::CoordinatesFormatter; pub use legend::ColorConflictHandling; pub use legend::Legend; +pub use legend::LegendGrouping; pub use legend::LegendWidget;