1use crate::Theme;
4use egui::{pos2, vec2, Rect, Sense};
5use eulumdat::{diagram::HeatmapDiagram, Eulumdat};
6
7pub struct HeatmapWidget;
9
10impl HeatmapWidget {
11 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 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 painter.rect_filled(rect, 0.0, theme.background);
30
31 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 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 Self::draw_axes(&painter, plot_rect, &heatmap, theme);
63
64 Self::draw_legend(&painter, rect, &heatmap, theme);
66
67 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 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 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 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 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 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 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 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}