chart.rs (7287B)
1 use egui::{Align2, Color32, FontId, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2}; 2 3 pub fn palette(i: usize) -> Color32 { 4 const P: [Color32; 10] = [ 5 Color32::from_rgb(231, 76, 60), 6 Color32::from_rgb(52, 152, 219), 7 Color32::from_rgb(46, 204, 113), 8 Color32::from_rgb(155, 89, 182), 9 Color32::from_rgb(241, 196, 15), 10 Color32::from_rgb(230, 126, 34), 11 Color32::from_rgb(26, 188, 156), 12 Color32::from_rgb(149, 165, 166), 13 Color32::from_rgb(52, 73, 94), 14 Color32::from_rgb(233, 150, 122), 15 ]; 16 P[i % P.len()] 17 } 18 19 // ---------------------- 20 // Bar chart (unchanged) 21 // ---------------------- 22 23 #[derive(Debug, Clone)] 24 pub struct Bar { 25 pub label: String, 26 pub value: f32, 27 pub color: Color32, 28 } 29 30 #[derive(Clone, Copy)] 31 pub struct BarChartStyle { 32 pub row_height: f32, 33 pub gap: f32, 34 pub rounding: f32, 35 pub show_values: bool, 36 pub value_precision: usize, 37 } 38 39 impl Default for BarChartStyle { 40 fn default() -> Self { 41 Self { 42 row_height: 18.0, 43 gap: 6.0, 44 rounding: 3.0, 45 show_values: true, 46 value_precision: 0, 47 } 48 } 49 } 50 51 /// Draws a horizontal bar chart. Returns the combined response so you can check hover/click if desired. 52 pub fn horizontal_bar_chart( 53 ui: &mut Ui, 54 title: Option<&str>, 55 bars: &[Bar], 56 style: BarChartStyle, 57 ) -> Response { 58 if let Some(t) = title { 59 ui.label(t); 60 } 61 62 if bars.is_empty() { 63 return ui.label("No data"); 64 } 65 66 let max_v = bars 67 .iter() 68 .map(|b| b.value.max(0.0)) 69 .fold(0.0_f32, f32::max); 70 71 if max_v <= 0.0 { 72 return ui.label("No data"); 73 } 74 75 // Layout: label column + bar column 76 let label_col_w = ui 77 .fonts(|f| { 78 bars.iter() 79 .map(|b| { 80 f.layout_no_wrap( 81 b.label.to_owned(), 82 FontId::proportional(14.0), 83 ui.visuals().text_color(), 84 ) 85 .size() 86 .x 87 }) 88 .fold(0.0, f32::max) 89 }) 90 .ceil() 91 + 10.0; 92 93 let avail_w = ui.available_width().max(50.0); 94 let bar_col_w = (avail_w - label_col_w).max(50.0); 95 96 let total_h = 97 bars.len() as f32 * style.row_height + (bars.len().saturating_sub(1) as f32) * style.gap; 98 let (outer_rect, outer_resp) = 99 ui.allocate_exact_size(Vec2::new(avail_w, total_h), Sense::hover()); 100 let painter = ui.painter_at(outer_rect); 101 102 // Optional: faint background 103 painter.rect_filled(outer_rect, 6.0, ui.visuals().faint_bg_color); 104 105 let mut y = outer_rect.top(); 106 107 for b in bars { 108 let row_rect = Rect::from_min_size( 109 Pos2::new(outer_rect.left(), y), 110 Vec2::new(avail_w, style.row_height), 111 ); 112 let row_resp = ui.interact( 113 row_rect, 114 ui.id().with(&b.label).with(y as i64), 115 Sense::hover(), 116 ); 117 118 // Label (left) 119 let label_pos = Pos2::new(row_rect.left() + 6.0, row_rect.center().y); 120 painter.text( 121 label_pos, 122 Align2::LEFT_CENTER, 123 &b.label, 124 FontId::proportional(14.0), 125 ui.visuals().text_color(), 126 ); 127 128 // Bar background track (right) 129 let track_rect = Rect::from_min_max( 130 Pos2::new(row_rect.left() + label_col_w, row_rect.top() + 2.0), 131 Pos2::new( 132 row_rect.left() + label_col_w + bar_col_w, 133 row_rect.bottom() - 2.0, 134 ), 135 ); 136 painter.rect_filled( 137 track_rect, 138 style.rounding, 139 ui.visuals().widgets.inactive.bg_fill, 140 ); 141 painter.rect_stroke( 142 track_rect, 143 style.rounding, 144 Stroke::new(1.0, ui.visuals().widgets.inactive.bg_stroke.color), 145 StrokeKind::Middle, 146 ); 147 148 // Filled portion 149 let frac = (b.value.max(0.0) / max_v).clamp(0.0, 1.0); 150 let fill_w = track_rect.width() * frac; 151 if fill_w > 0.0 { 152 let fill_rect = Rect::from_min_max( 153 track_rect.min, 154 Pos2::new(track_rect.min.x + fill_w, track_rect.max.y), 155 ); 156 painter.rect_filled(fill_rect, style.rounding, b.color); 157 } 158 159 // Value label (right-aligned at end of track) 160 if style.show_values { 161 let txt = if style.value_precision == 0 { 162 format!("{:.0}", b.value) 163 } else { 164 format!("{:.*}", style.value_precision, b.value) 165 }; 166 painter.text( 167 Pos2::new(track_rect.right() - 6.0, row_rect.center().y), 168 Align2::RIGHT_CENTER, 169 txt, 170 FontId::proportional(13.0), 171 ui.visuals().text_color(), 172 ); 173 } 174 175 // Tooltip on hover 176 if row_resp.hovered() { 177 let sum = bars.iter().map(|x| x.value.max(0.0)).sum::<f32>().max(1.0); 178 let pct = (b.value / sum) * 100.0; 179 row_resp.on_hover_text(format!("{}: {:.0} ({:.1}%)", b.label, b.value, pct)); 180 } 181 182 y += style.row_height + style.gap; 183 } 184 185 outer_resp 186 } 187 188 pub fn stacked_bars( 189 ui: &mut Ui, 190 size: Vec2, 191 buckets: &[Vec<(Color32, f32)>], // each bucket: vec of (color, value) 192 ) -> Response { 193 let (rect, resp) = ui.allocate_exact_size(size, Sense::hover()); 194 let painter = ui.painter_at(rect); 195 196 painter.rect_filled(rect, 6.0, ui.visuals().faint_bg_color); 197 198 if buckets.is_empty() { 199 return resp; 200 } 201 202 // find max total per bucket for scaling 203 let mut max_total = 0.0_f32; 204 for b in buckets { 205 let t: f32 = b.iter().map(|(_, v)| v.max(0.0)).sum(); 206 max_total = max_total.max(t); 207 } 208 if max_total <= 0.0 { 209 return resp; 210 } 211 212 let n = buckets.len(); 213 let bw = rect.width() / n as f32; 214 215 for (i, b) in buckets.iter().enumerate() { 216 let x0 = rect.left() + i as f32 * bw; 217 let x1 = x0 + bw; 218 219 let total: f32 = b.iter().map(|(_, v)| v.max(0.0)).sum(); 220 let h_total = rect.height() * (total / max_total); 221 222 // Optional: center bars if you want; simplest is fill from bottom with total scaling: 223 let mut y0 = rect.bottom(); 224 let y_min = rect.bottom() - h_total; 225 226 for (color, v) in b { 227 let v = v.max(0.0); 228 if v <= 0.0 { 229 continue; 230 } 231 let seg_h = h_total * (v / total.max(1.0)); 232 let seg_rect = Rect::from_min_max( 233 Pos2::new(x0 + 1.0, (y0 - seg_h).max(y_min)), 234 Pos2::new(x1 - 1.0, y0), 235 ); 236 painter.rect_filled(seg_rect, 0.0, *color); 237 y0 -= seg_h; 238 } 239 240 // faint outline 241 let outline = Rect::from_min_max( 242 Pos2::new(x0 + 1.0, y_min), 243 Pos2::new(x1 - 1.0, rect.bottom()), 244 ); 245 painter.rect_stroke( 246 outline, 247 0.0, 248 Stroke::new(1.0, ui.visuals().widgets.inactive.bg_stroke.color), 249 StrokeKind::Middle, 250 ); 251 } 252 253 resp 254 }