notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }