1use crate::statistics::{AnalysisResults, DistributionType};
2use ratatui::widgets::TableState;
3
4#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
5pub enum AnalysisView {
6 #[default]
7 Main, DistributionDetail, CorrelationDetail, }
11
12#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
13pub enum AnalysisTool {
14 #[default]
15 Describe, DistributionAnalysis, CorrelationMatrix, }
19
20#[derive(Debug, Clone)]
22pub struct AnalysisProgress {
23 pub phase: String,
24 pub current: usize,
25 pub total: usize,
26}
27
28#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
29pub enum AnalysisFocus {
30 #[default]
31 Main, Sidebar, DistributionSelector, }
35
36#[derive(Default)]
37pub struct AnalysisModal {
38 pub active: bool,
39 pub scroll_position: usize,
40 pub selected_column: Option<usize>,
41 pub describe_column_offset: usize, pub distribution_column_offset: usize, pub correlation_column_offset: usize, pub random_seed: u64,
45 pub table_state: TableState, pub distribution_table_state: TableState, pub correlation_table_state: TableState, pub sidebar_state: TableState, pub describe_results: Option<AnalysisResults>,
51 pub distribution_results: Option<AnalysisResults>,
52 pub correlation_results: Option<AnalysisResults>,
53 pub computing: Option<AnalysisProgress>,
55 pub show_help: bool,
56 pub view: AnalysisView,
57 pub focus: AnalysisFocus,
58 pub selected_tool: Option<AnalysisTool>,
60 pub selected_distribution: Option<usize>, pub selected_correlation: Option<(usize, usize)>, pub detail_section: usize, pub selected_theoretical_distribution: DistributionType, pub distribution_selector_state: TableState, pub histogram_scale: HistogramScale, }
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum HistogramScale {
70 #[default]
71 Linear,
72 Log,
73}
74
75impl AnalysisModal {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn open(&mut self) {
81 self.active = true;
82 self.scroll_position = 0;
83 self.selected_column = None;
84 self.describe_column_offset = 0;
85 self.distribution_column_offset = 0;
86 self.correlation_column_offset = 0;
87 self.table_state.select(Some(0));
88 self.distribution_table_state.select(Some(0));
89 self.correlation_table_state.select(Some(0));
90 self.sidebar_state.select(Some(0)); self.view = AnalysisView::Main;
92 self.focus = AnalysisFocus::Sidebar; self.selected_tool = None; self.selected_distribution = Some(0);
95 self.selected_correlation = Some((0, 0));
96 self.detail_section = 0;
97 self.computing = None;
98 self.describe_results = None;
99 self.distribution_results = None;
100 self.correlation_results = None;
101 self.random_seed = std::time::SystemTime::now()
103 .duration_since(std::time::UNIX_EPOCH)
104 .unwrap_or_default()
105 .as_nanos() as u64;
106 }
107
108 pub fn close(&mut self) {
109 self.active = false;
110 self.scroll_position = 0;
111 self.selected_column = None;
112 self.describe_column_offset = 0;
113 self.distribution_column_offset = 0;
114 self.correlation_column_offset = 0;
115 self.view = AnalysisView::Main;
116 self.focus = AnalysisFocus::Main;
117 self.selected_tool = None;
118 self.selected_distribution = None;
119 self.selected_correlation = None;
120 self.detail_section = 0;
121 self.computing = None;
122 self.describe_results = None;
123 self.distribution_results = None;
124 self.correlation_results = None;
125 }
126
127 pub fn current_results(&self) -> Option<&AnalysisResults> {
129 match self.selected_tool {
130 Some(AnalysisTool::Describe) => self.describe_results.as_ref(),
131 Some(AnalysisTool::DistributionAnalysis) => self.distribution_results.as_ref(),
132 Some(AnalysisTool::CorrelationMatrix) => self.correlation_results.as_ref(),
133 None => None,
134 }
135 }
136
137 pub fn switch_focus(&mut self) {
138 if self.view == AnalysisView::DistributionDetail {
139 self.focus = match self.focus {
140 AnalysisFocus::Main => AnalysisFocus::DistributionSelector,
141 AnalysisFocus::DistributionSelector => AnalysisFocus::Main,
142 _ => AnalysisFocus::DistributionSelector,
143 };
144 } else {
145 self.focus = match self.focus {
146 AnalysisFocus::Main => AnalysisFocus::Sidebar,
147 AnalysisFocus::Sidebar => AnalysisFocus::Main,
148 _ => AnalysisFocus::Main,
149 };
150 }
151 }
152
153 pub fn select_tool(&mut self) {
154 if let Some(idx) = self.sidebar_state.selected() {
155 self.selected_tool = Some(match idx {
156 0 => AnalysisTool::Describe,
157 1 => AnalysisTool::DistributionAnalysis,
158 2 => AnalysisTool::CorrelationMatrix,
159 _ => AnalysisTool::Describe,
160 });
161 self.focus = AnalysisFocus::Main;
162 }
163 }
164
165 pub fn next_tool(&mut self) {
166 if let Some(current) = self.sidebar_state.selected() {
167 let next = (current + 1).min(2);
168 self.sidebar_state.select(Some(next));
169 }
170 }
171
172 pub fn previous_tool(&mut self) {
173 if let Some(current) = self.sidebar_state.selected() {
174 if current > 0 {
175 self.sidebar_state.select(Some(current - 1));
176 }
177 }
178 }
179
180 pub fn open_distribution_detail(&mut self) {
181 if self.focus == AnalysisFocus::Main
182 && self.selected_tool == Some(AnalysisTool::DistributionAnalysis)
183 {
184 if let Some(idx) = self.distribution_table_state.selected() {
185 if let Some(results) = &self.distribution_results {
186 if let Some(dist_analysis) = results.distribution_analyses.get(idx) {
187 self.selected_theoretical_distribution = dist_analysis.distribution_type;
188 }
189 }
190 self.view = AnalysisView::DistributionDetail;
191 self.detail_section = 0;
192 self.focus = AnalysisFocus::DistributionSelector;
193 if self.selected_theoretical_distribution == DistributionType::Unknown {
194 self.selected_theoretical_distribution = DistributionType::Normal;
195 }
196 self.distribution_selector_state.select(None);
197 }
198 }
199 }
200
201 pub fn open_correlation_detail(&mut self) {
202 if self.focus == AnalysisFocus::Main
203 && self.selected_tool == Some(AnalysisTool::CorrelationMatrix)
204 {
205 if let Some((row, col)) = self.selected_correlation {
206 if row != col {
207 self.view = AnalysisView::CorrelationDetail;
208 }
209 }
210 }
211 }
212
213 pub fn close_detail(&mut self) {
214 self.view = AnalysisView::Main;
215 self.detail_section = 0;
216 self.focus = AnalysisFocus::Main;
217 }
218
219 pub fn next_detail_section(&mut self) {
220 self.detail_section = (self.detail_section + 1) % 3;
221 }
222
223 pub fn previous_detail_section(&mut self) {
224 self.detail_section = if self.detail_section == 0 {
225 2
226 } else {
227 self.detail_section - 1
228 };
229 }
230
231 pub fn scroll_left(&mut self) {
232 match self.selected_tool {
233 Some(AnalysisTool::Describe) => {
234 if self.describe_column_offset > 0 {
235 self.describe_column_offset -= 1;
236 }
237 }
238 Some(AnalysisTool::DistributionAnalysis) => {
239 if self.distribution_column_offset > 0 {
240 self.distribution_column_offset -= 1;
241 }
242 }
243 _ => {}
244 }
245 }
246
247 pub fn scroll_right(&mut self, max_columns: usize, visible_columns: usize) {
248 match self.selected_tool {
249 Some(AnalysisTool::Describe) => {
250 let offset = &mut self.describe_column_offset;
251 if *offset + visible_columns < max_columns
252 && *offset < max_columns.saturating_sub(1)
253 {
254 *offset += 1;
255 }
256 }
257 Some(AnalysisTool::DistributionAnalysis) => {
258 let offset = &mut self.distribution_column_offset;
259 if *offset + visible_columns < max_columns
260 && *offset < max_columns.saturating_sub(1)
261 {
262 *offset += 1;
263 }
264 }
265 _ => {}
266 }
267 }
268
269 pub fn recalculate(&mut self) {
270 self.random_seed = std::time::SystemTime::now()
271 .duration_since(std::time::UNIX_EPOCH)
272 .unwrap_or_default()
273 .as_nanos() as u64;
274 }
275
276 pub fn next_row(&mut self, max_rows: usize) {
277 if self.focus == AnalysisFocus::Sidebar {
278 self.next_tool();
279 return;
280 }
281 match self.selected_tool {
282 Some(AnalysisTool::Describe) => {
283 if let Some(current) = self.table_state.selected() {
284 let next = (current + 1).min(max_rows.saturating_sub(1));
285 self.table_state.select(Some(next));
286 } else {
287 self.table_state.select(Some(0));
288 }
289 }
290 Some(AnalysisTool::DistributionAnalysis) => {
291 if let Some(current) = self.distribution_table_state.selected() {
292 let next = (current + 1).min(max_rows.saturating_sub(1));
293 self.distribution_table_state.select(Some(next));
294 self.selected_distribution = Some(next);
295 } else {
296 self.distribution_table_state.select(Some(0));
297 self.selected_distribution = Some(0);
298 }
299 }
300 Some(AnalysisTool::CorrelationMatrix) => {
301 if let Some((row, col)) = self.selected_correlation {
302 let next_row = (row + 1).min(max_rows.saturating_sub(1));
303 self.selected_correlation = Some((next_row, col));
304 self.correlation_table_state.select(Some(next_row));
305 }
306 }
307 None => {}
308 }
309 }
310
311 pub fn previous_row(&mut self) {
312 if self.focus == AnalysisFocus::Sidebar {
313 self.previous_tool();
314 return;
315 }
316 match self.selected_tool {
317 Some(AnalysisTool::Describe) => {
318 if let Some(current) = self.table_state.selected() {
319 if current > 0 {
320 self.table_state.select(Some(current - 1));
321 }
322 }
323 }
324 Some(AnalysisTool::DistributionAnalysis) => {
325 if let Some(current) = self.distribution_table_state.selected() {
326 if current > 0 {
327 let prev = current - 1;
328 self.distribution_table_state.select(Some(prev));
329 self.selected_distribution = Some(prev);
330 }
331 }
332 }
333 Some(AnalysisTool::CorrelationMatrix) => {
334 if let Some((row, col)) = self.selected_correlation {
335 if row > 0 {
336 let prev_row = row - 1;
337 self.selected_correlation = Some((prev_row, col));
338 self.correlation_table_state.select(Some(prev_row));
339 }
340 }
341 }
342 None => {}
343 }
344 }
345
346 pub fn page_down(&mut self, max_rows: usize, page_size: usize) {
347 if self.focus == AnalysisFocus::Sidebar {
348 return;
349 }
350
351 match self.selected_tool {
352 Some(AnalysisTool::Describe) => {
353 if let Some(current) = self.table_state.selected() {
354 let next = (current + page_size).min(max_rows.saturating_sub(1));
355 self.table_state.select(Some(next));
356 }
357 }
358 Some(AnalysisTool::DistributionAnalysis) => {
359 if let Some(current) = self.distribution_table_state.selected() {
360 let next = (current + page_size).min(max_rows.saturating_sub(1));
361 self.distribution_table_state.select(Some(next));
362 self.selected_distribution = Some(next);
363 }
364 }
365 Some(AnalysisTool::CorrelationMatrix) => {
366 if let Some((row, col)) = self.selected_correlation {
367 let next_row = (row + page_size).min(max_rows.saturating_sub(1));
368 self.selected_correlation = Some((next_row, col));
369 self.correlation_table_state.select(Some(next_row));
370 }
371 }
372 None => {}
373 }
374 }
375
376 pub fn page_up(&mut self, page_size: usize) {
377 if self.focus == AnalysisFocus::Sidebar {
378 return;
379 }
380
381 match self.selected_tool {
382 Some(AnalysisTool::Describe) => {
383 if let Some(current) = self.table_state.selected() {
384 let next = current.saturating_sub(page_size);
385 self.table_state.select(Some(next));
386 }
387 }
388 Some(AnalysisTool::DistributionAnalysis) => {
389 if let Some(current) = self.distribution_table_state.selected() {
390 let next = current.saturating_sub(page_size);
391 self.distribution_table_state.select(Some(next));
392 self.selected_distribution = Some(next);
393 }
394 }
395 Some(AnalysisTool::CorrelationMatrix) => {
396 if let Some((row, col)) = self.selected_correlation {
397 let prev_row = row.saturating_sub(page_size);
398 self.selected_correlation = Some((prev_row, col));
399 self.correlation_table_state.select(Some(prev_row));
400 }
401 }
402 None => {}
403 }
404 }
405
406 pub fn move_correlation_cell(
407 &mut self,
408 direction: (i32, i32),
409 max_rows: usize,
410 max_cols: usize,
411 visible_cols: usize,
412 ) {
413 if let Some((row, col)) = self.selected_correlation {
414 let new_row = ((row as i32) + direction.0)
415 .max(0)
416 .min((max_rows - 1) as i32) as usize;
417 let new_col = ((col as i32) + direction.1)
418 .max(0)
419 .min((max_cols - 1) as i32) as usize;
420 self.selected_correlation = Some((new_row, new_col));
421 self.correlation_table_state.select(Some(new_row));
422
423 if new_col < self.correlation_column_offset {
424 self.correlation_column_offset = new_col;
425 } else if new_col >= self.correlation_column_offset + visible_cols.saturating_sub(1) {
426 if new_col >= visible_cols {
427 self.correlation_column_offset =
428 new_col.saturating_sub(visible_cols.saturating_sub(1));
429 } else {
430 self.correlation_column_offset = 0;
431 }
432 }
433 }
434 }
435
436 pub fn next_distribution(&mut self) {
437 let max_idx = 13;
438
439 if let Some(current) = self.distribution_selector_state.selected() {
440 let next = (current + 1).min(max_idx);
441 self.distribution_selector_state.select(Some(next));
442 self.select_distribution();
443 } else {
444 self.distribution_selector_state.select(Some(0));
445 self.select_distribution();
446 }
447 }
448
449 pub fn previous_distribution(&mut self) {
450 if let Some(current) = self.distribution_selector_state.selected() {
451 if current > 0 {
452 self.distribution_selector_state.select(Some(current - 1));
453 self.select_distribution();
454 }
455 } else {
456 self.distribution_selector_state.select(Some(0));
457 self.select_distribution();
458 }
459 }
460
461 pub fn select_distribution(&mut self) {
462 if let Some(idx) = self.distribution_selector_state.selected() {
463 if let Some(results) = &self.distribution_results {
464 let dist_analysis_idx = self.distribution_table_state.selected().unwrap_or(0);
465 if let Some(dist_analysis) = results.distribution_analyses.get(dist_analysis_idx) {
466 let distributions = [
468 ("Normal", DistributionType::Normal),
469 ("LogNormal", DistributionType::LogNormal),
470 ("Uniform", DistributionType::Uniform),
471 ("PowerLaw", DistributionType::PowerLaw),
472 ("Exponential", DistributionType::Exponential),
473 ("Beta", DistributionType::Beta),
474 ("Gamma", DistributionType::Gamma),
475 ("Chi-Squared", DistributionType::ChiSquared),
476 ("Student's t", DistributionType::StudentsT),
477 ("Poisson", DistributionType::Poisson),
478 ("Bernoulli", DistributionType::Bernoulli),
479 ("Binomial", DistributionType::Binomial),
480 ("Geometric", DistributionType::Geometric),
481 ("Weibull", DistributionType::Weibull),
482 ];
483
484 let mut distribution_scores: Vec<(DistributionType, f64)> = distributions
485 .iter()
486 .map(|(_, dist_type)| {
487 let p_value = dist_analysis
488 .all_distribution_pvalues
489 .get(dist_type)
490 .copied()
491 .unwrap_or_else(|| {
492 if *dist_type == DistributionType::Geometric {
493 0.01
494 } else {
495 0.0
496 }
497 });
498 (*dist_type, p_value)
499 })
500 .collect();
501
502 distribution_scores
503 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
504
505 let valid_idx = idx.min(distribution_scores.len().saturating_sub(1));
506 if let Some((dist_type, _)) = distribution_scores.get(valid_idx) {
507 self.selected_theoretical_distribution = *dist_type;
508 if idx != valid_idx {
509 self.distribution_selector_state.select(Some(valid_idx));
510 }
511 }
512 }
513 }
514 }
515 }
516}