Skip to main content

eulumdat_ui/widgets/
heatmap.rs

1//! Heatmap diagram widget for egui
2
3use crate::Theme;
4use egui::{pos2, vec2, Rect, Sense};
5use eulumdat::{diagram::HeatmapDiagram, Eulumdat};
6
7/// Heatmap diagram widget
8pub struct HeatmapWidget;
9
10impl HeatmapWidget {
11    /// Show the heatmap diagram
12    pub fn show(ui: &mut egui::Ui, ldt: &Eulumdat, theme: &Theme) {
13        let available_size = ui.available_size();
14        let width = available_size.x.min(800.0);
15        let height = (width * 0.5).min(available_size.y - 80.0);
16
17        let (response, painter) = ui.allocate_painter(vec2(width, height), Sense::hover());
18        let rect = response.rect;
19
20        // Margins for axes
21        let margin = vec2(60.0, 20.0);
22        let legend_width = 60.0;
23        let plot_rect = Rect::from_min_max(
24            rect.min + margin,
25            rect.max - vec2(legend_width + 20.0, 40.0),
26        );
27
28        // Background
29        painter.rect_filled(rect, 0.0, theme.background);
30
31        // Generate heatmap data
32        let heatmap = HeatmapDiagram::from_eulumdat(ldt, width as f64, height as f64);
33
34        let num_c = heatmap.c_angles.len();
35        let num_g = heatmap.g_angles.len();
36
37        if heatmap.cells.is_empty() || num_c == 0 || num_g == 0 {
38            painter.text(
39                rect.center(),
40                egui::Align2::CENTER_CENTER,
41                "No intensity data",
42                egui::FontId::proportional(14.0),
43                theme.text,
44            );
45            return;
46        }
47
48        // Draw cells
49        let cell_width = plot_rect.width() / num_c as f32;
50        let cell_height = plot_rect.height() / num_g as f32;
51
52        for cell in &heatmap.cells {
53            let x = plot_rect.left() + cell.c_index as f32 * cell_width;
54            let y = plot_rect.top() + cell.g_index as f32 * cell_height;
55            let cell_rect = Rect::from_min_size(pos2(x, y), vec2(cell_width, cell_height));
56
57            let color = theme.heatmap_color(cell.normalized);
58            painter.rect_filled(cell_rect, 0.0, color);
59        }
60
61        // Draw axes
62        Self::draw_axes(&painter, plot_rect, &heatmap, theme);
63
64        // Draw color legend
65        Self::draw_legend(&painter, rect, &heatmap, theme);
66
67        // Tooltip on hover
68        if let Some(hover_pos) = response.hover_pos() {
69            if plot_rect.contains(hover_pos) {
70                let c_idx = ((hover_pos.x - plot_rect.left()) / cell_width) as usize;
71                let g_idx = ((hover_pos.y - plot_rect.top()) / cell_height) as usize;
72
73                if let Some(cell) = heatmap
74                    .cells
75                    .iter()
76                    .find(|c| c.c_index == c_idx && c.g_index == g_idx)
77                {
78                    let tooltip_text = format!(
79                        "C: {:.0}°, γ: {:.0}°\nIntensity: {:.1} cd/klm\nCandela: {:.1} cd",
80                        cell.c_angle, cell.g_angle, cell.intensity, cell.candela
81                    );
82                    response.clone().on_hover_text(tooltip_text);
83                }
84            }
85        }
86    }
87
88    fn draw_axes(painter: &egui::Painter, rect: Rect, _heatmap: &HeatmapDiagram, theme: &Theme) {
89        // X axis label (C-planes)
90        painter.text(
91            pos2(rect.center().x, rect.bottom() + 25.0),
92            egui::Align2::CENTER_TOP,
93            "C-plane (°)",
94            egui::FontId::proportional(11.0),
95            theme.text,
96        );
97
98        // Y axis label (Gamma)
99        painter.text(
100            pos2(rect.left() - 40.0, rect.center().y),
101            egui::Align2::CENTER_CENTER,
102            "γ",
103            egui::FontId::proportional(14.0),
104            theme.text,
105        );
106
107        // X axis tick labels
108        let c_ticks = [0.0, 90.0, 180.0, 270.0, 360.0];
109        for &c in &c_ticks {
110            let x = rect.left() + (c as f32 / 360.0) * rect.width();
111            if x <= rect.right() {
112                painter.text(
113                    pos2(x, rect.bottom() + 5.0),
114                    egui::Align2::CENTER_TOP,
115                    format!("{:.0}", c),
116                    egui::FontId::proportional(9.0),
117                    theme.text,
118                );
119            }
120        }
121
122        // Y axis tick labels
123        let g_ticks = [0.0, 45.0, 90.0, 135.0, 180.0];
124        for &g in &g_ticks {
125            let y = rect.top() + (g as f32 / 180.0) * rect.height();
126            if y <= rect.bottom() {
127                painter.text(
128                    pos2(rect.left() - 5.0, y),
129                    egui::Align2::RIGHT_CENTER,
130                    format!("{:.0}", g),
131                    egui::FontId::proportional(9.0),
132                    theme.text,
133                );
134            }
135        }
136    }
137
138    fn draw_legend(painter: &egui::Painter, rect: Rect, heatmap: &HeatmapDiagram, theme: &Theme) {
139        let legend_x = rect.right() - 50.0;
140        let legend_top = rect.top() + 20.0;
141        let legend_height = rect.height() - 60.0;
142        let legend_width = 20.0;
143
144        // Draw gradient bar
145        let num_steps = 50;
146        let step_height = legend_height / num_steps as f32;
147
148        for i in 0..num_steps {
149            let normalized = 1.0 - (i as f64 / num_steps as f64);
150            let color = theme.heatmap_color(normalized);
151            let y = legend_top + i as f32 * step_height;
152            let step_rect =
153                Rect::from_min_size(pos2(legend_x, y), vec2(legend_width, step_height + 1.0));
154            painter.rect_filled(step_rect, 0.0, color);
155        }
156
157        // Border
158        painter.rect_stroke(
159            Rect::from_min_size(
160                pos2(legend_x, legend_top),
161                vec2(legend_width, legend_height),
162            ),
163            0.0,
164            egui::Stroke::new(1.0, theme.axis),
165        );
166
167        // Labels
168        painter.text(
169            pos2(legend_x + legend_width + 5.0, legend_top),
170            egui::Align2::LEFT_CENTER,
171            format!("{:.0}", heatmap.scale.max_intensity),
172            egui::FontId::proportional(9.0),
173            theme.text,
174        );
175
176        painter.text(
177            pos2(legend_x + legend_width + 5.0, legend_top + legend_height),
178            egui::Align2::LEFT_CENTER,
179            "0",
180            egui::FontId::proportional(9.0),
181            theme.text,
182        );
183
184        painter.text(
185            pos2(
186                legend_x + legend_width / 2.0,
187                legend_top + legend_height + 15.0,
188            ),
189            egui::Align2::CENTER_TOP,
190            "cd/klm",
191            egui::FontId::proportional(9.0),
192            theme.text,
193        );
194    }
195}