1- use std:: collections:: BTreeMap ;
21use std:: string:: String ;
32
43use 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.
0 commit comments