Skip to main content

gammalooprs/integrate/
render_ratatui.rs

1use std::cmp::Ordering;
2use std::time::Duration;
3
4use ratatui::{
5    Frame,
6    layout::{Alignment, Constraint, Direction, Layout, Rect},
7    prelude::{Color, Line, Modifier, Span, Style},
8    symbols::Marker,
9    text::Text,
10    widgets::{
11        Axis, Block, Borders, Cell, Chart, Clear, Dataset, Gauge, Paragraph, Row, Table,
12        TableState, Tabs, Wrap,
13    },
14};
15
16use crate::utils;
17
18use super::{
19    StatusUpdate,
20    display::{StyledText, TextColor, TextStyle},
21    status_update::{
22        ComponentKind, ContributionKind, ContributionSortMode, MainResultsRow,
23        MainResultsRowGroupKind, MainTableSlotCells, StatisticsMixSegment, StatisticsScope,
24    },
25};
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28enum DashboardTab {
29    Overview,
30    Discrete,
31    MaxWeight,
32}
33
34impl DashboardTab {
35    fn all() -> [Self; 3] {
36        [Self::Overview, Self::Discrete, Self::MaxWeight]
37    }
38
39    fn index(self) -> usize {
40        match self {
41            Self::Overview => 0,
42            Self::Discrete => 1,
43            Self::MaxWeight => 2,
44        }
45    }
46
47    fn title(self) -> &'static str {
48        match self {
49            Self::Overview => "Overview",
50            Self::Discrete => "Discrete",
51            Self::MaxWeight => "Max Weight",
52        }
53    }
54
55    fn from_index(index: usize) -> Self {
56        Self::all().get(index).copied().unwrap_or(Self::Overview)
57    }
58
59    fn next(self) -> Self {
60        Self::from_index((self.index() + 1) % Self::all().len())
61    }
62
63    fn previous(self) -> Self {
64        Self::from_index((self.index() + Self::all().len() - 1) % Self::all().len())
65    }
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69enum DensityMode {
70    Compact,
71    Metrics,
72    Full,
73}
74
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76enum ChartHistoryWindow {
77    #[default]
78    Full,
79    RecentIterations(usize),
80}
81
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83enum DashboardStatisticsScope {
84    #[default]
85    Global,
86    FocusedSlot,
87}
88
89const MAX_HISTORY_POINTS: usize = 4096;
90
91impl ChartHistoryWindow {
92    fn description(self) -> String {
93        match self {
94            Self::Full => "full".to_string(),
95            Self::RecentIterations(count) => format!("last {count} iters"),
96        }
97    }
98
99    fn toggle(self) -> Self {
100        match self {
101            Self::Full => Self::RecentIterations(6),
102            Self::RecentIterations(_) => Self::Full,
103        }
104    }
105
106    fn widen(self) -> Self {
107        match self {
108            Self::Full => Self::RecentIterations(6),
109            Self::RecentIterations(count) => Self::RecentIterations((count + 1).min(128)),
110        }
111    }
112
113    fn narrow(self) -> Self {
114        match self {
115            Self::Full => Self::RecentIterations(6),
116            Self::RecentIterations(count) => Self::RecentIterations(count.saturating_sub(1).max(1)),
117        }
118    }
119}
120
121impl DensityMode {
122    fn next(self) -> Self {
123        match self {
124            Self::Compact => Self::Metrics,
125            Self::Metrics => Self::Full,
126            Self::Full => Self::Compact,
127        }
128    }
129
130    fn label(self) -> &'static str {
131        match self {
132            Self::Compact => "compact",
133            Self::Metrics => "metrics",
134            Self::Full => "full",
135        }
136    }
137}
138
139#[derive(Clone, Copy, Debug)]
140struct SlotMetricVisibility {
141    relative_error: bool,
142    chi_sq: bool,
143    max_weight_impact: bool,
144}
145
146impl Default for SlotMetricVisibility {
147    fn default() -> Self {
148        Self {
149            relative_error: true,
150            chi_sq: true,
151            max_weight_impact: true,
152        }
153    }
154}
155
156#[derive(Clone, Debug)]
157struct HistoryPoint {
158    iteration: usize,
159    samples: usize,
160    real_slot_values: Vec<Option<(f64, f64)>>,
161    imag_slot_values: Vec<Option<(f64, f64)>>,
162    completed_iteration: bool,
163}
164
165impl HistoryPoint {
166    fn slot_values(&self, component: ComponentKind) -> &[Option<(f64, f64)>] {
167        match component {
168            ComponentKind::Real => &self.real_slot_values,
169            ComponentKind::Imag => &self.imag_slot_values,
170        }
171    }
172}
173
174#[derive(Clone, Copy, Debug)]
175struct DiscreteRowRef<'a> {
176    row: &'a MainResultsRow,
177}
178
179impl<'a> DiscreteRowRef<'a> {
180    fn component(self) -> ComponentKind {
181        self.row.component.raw
182    }
183
184    fn contribution(self) -> ContributionKind {
185        self.row.contribution.raw
186    }
187
188    fn sort_value(self, mode: ContributionSortMode, slot_index: usize) -> f64 {
189        let Some(cell) = self.row.slot_cell(slot_index) else {
190            return 0.0;
191        };
192        match mode {
193            ContributionSortMode::Index => match self.contribution() {
194                ContributionKind::Bin(index) => index as f64,
195                ContributionKind::All => -1.0,
196                ContributionKind::Sum => -0.5,
197            },
198            ContributionSortMode::Integral => cell
199                .value
200                .as_ref()
201                .map(|value| value.raw.0.abs().0)
202                .unwrap_or_default(),
203            ContributionSortMode::Error => cell
204                .value
205                .as_ref()
206                .map(|value| value.raw.1.abs().0)
207                .unwrap_or_default(),
208        }
209    }
210}
211
212pub struct RatatuiDashboardState {
213    latest_update: Option<StatusUpdate>,
214    active_tab: DashboardTab,
215    density: DensityMode,
216    metric_visibility: SlotMetricVisibility,
217    focused_slot: usize,
218    statistics_scope: DashboardStatisticsScope,
219    selected_discrete_row: usize,
220    discrete_sort: ContributionSortMode,
221    discrete_descending: bool,
222    show_help: bool,
223    history: Vec<HistoryPoint>,
224    chart_history_window: ChartHistoryWindow,
225    chart_y_sigma_span: usize,
226    chart_component: Option<ComponentKind>,
227}
228
229impl Default for RatatuiDashboardState {
230    fn default() -> Self {
231        Self {
232            latest_update: None,
233            active_tab: DashboardTab::Overview,
234            density: DensityMode::Metrics,
235            metric_visibility: SlotMetricVisibility::default(),
236            focused_slot: 0,
237            statistics_scope: DashboardStatisticsScope::Global,
238            selected_discrete_row: 0,
239            discrete_sort: ContributionSortMode::Error,
240            discrete_descending: true,
241            show_help: false,
242            history: Vec::new(),
243            chart_history_window: ChartHistoryWindow::default(),
244            chart_y_sigma_span: 4,
245            chart_component: None,
246        }
247    }
248}
249
250impl RatatuiDashboardState {
251    pub fn new() -> Self {
252        Self::default()
253    }
254
255    pub fn has_update(&self) -> bool {
256        self.latest_update.is_some()
257    }
258
259    pub fn update(&mut self, update: StatusUpdate) {
260        self.push_history_point(&update);
261        self.latest_update = Some(update);
262        self.clamp_state();
263    }
264
265    pub fn next_tab(&mut self) {
266        self.active_tab = self.active_tab.next();
267    }
268
269    pub fn previous_tab(&mut self) {
270        self.active_tab = self.active_tab.previous();
271    }
272
273    pub fn select_tab(&mut self, index: usize) {
274        self.active_tab = DashboardTab::from_index(index);
275    }
276
277    pub fn cycle_density(&mut self) {
278        self.density = self.density.next();
279    }
280
281    pub fn toggle_help(&mut self) {
282        self.show_help = !self.show_help;
283    }
284
285    pub fn toggle_relative_error(&mut self) {
286        self.metric_visibility.relative_error = !self.metric_visibility.relative_error;
287    }
288
289    pub fn toggle_chi_sq(&mut self) {
290        self.metric_visibility.chi_sq = !self.metric_visibility.chi_sq;
291    }
292
293    pub fn toggle_max_weight_impact(&mut self) {
294        self.metric_visibility.max_weight_impact = !self.metric_visibility.max_weight_impact;
295    }
296
297    pub fn focus_next_slot(&mut self) {
298        let slot_count = self
299            .latest_update
300            .as_ref()
301            .map(|update| update.main_results.slot_headers.len())
302            .unwrap_or_default();
303        if slot_count > 0 {
304            self.focused_slot = (self.focused_slot + 1) % slot_count;
305        }
306        self.clamp_state();
307    }
308
309    pub fn focus_previous_slot(&mut self) {
310        let slot_count = self
311            .latest_update
312            .as_ref()
313            .map(|update| update.main_results.slot_headers.len())
314            .unwrap_or_default();
315        if slot_count > 0 {
316            self.focused_slot = (self.focused_slot + slot_count - 1) % slot_count;
317        }
318        self.clamp_state();
319    }
320
321    pub fn toggle_statistics_scope(&mut self) {
322        self.statistics_scope = match self.statistics_scope {
323            DashboardStatisticsScope::Global => DashboardStatisticsScope::FocusedSlot,
324            DashboardStatisticsScope::FocusedSlot => DashboardStatisticsScope::Global,
325        };
326    }
327
328    pub fn select_next_discrete_row(&mut self) {
329        let row_count = self.discrete_rows().len();
330        if row_count > 0 {
331            self.selected_discrete_row = (self.selected_discrete_row + 1) % row_count;
332        }
333    }
334
335    pub fn select_previous_discrete_row(&mut self) {
336        let row_count = self.discrete_rows().len();
337        if row_count > 0 {
338            self.selected_discrete_row = (self.selected_discrete_row + row_count - 1) % row_count;
339        }
340    }
341
342    pub fn cycle_discrete_sort(&mut self) {
343        self.discrete_sort = match self.discrete_sort {
344            ContributionSortMode::Index => ContributionSortMode::Integral,
345            ContributionSortMode::Integral => ContributionSortMode::Error,
346            ContributionSortMode::Error => ContributionSortMode::Index,
347        };
348        self.clamp_state();
349    }
350
351    pub fn toggle_discrete_sort_direction(&mut self) {
352        self.discrete_descending = !self.discrete_descending;
353    }
354
355    pub fn toggle_chart_history_window(&mut self) {
356        self.chart_history_window = self.chart_history_window.toggle();
357    }
358
359    pub fn widen_chart_history_window(&mut self) {
360        self.chart_history_window = self.chart_history_window.widen();
361    }
362
363    pub fn narrow_chart_history_window(&mut self) {
364        self.chart_history_window = self.chart_history_window.narrow();
365    }
366
367    pub fn widen_chart_y_sigma_span(&mut self) {
368        self.chart_y_sigma_span = (self.chart_y_sigma_span + 1).min(128);
369    }
370
371    pub fn narrow_chart_y_sigma_span(&mut self) {
372        self.chart_y_sigma_span = self.chart_y_sigma_span.saturating_sub(1).max(1);
373    }
374
375    pub fn reset_chart_y_sigma_span(&mut self) {
376        self.chart_y_sigma_span = 4;
377    }
378
379    pub fn toggle_chart_component(&mut self) {
380        let Some(update) = self.latest_update.as_ref() else {
381            return;
382        };
383        let available = self.available_chart_components(update);
384        if available.len() < 2 {
385            return;
386        }
387
388        let current = self.chart_component(update);
389        self.chart_component = match current {
390            Some(ComponentKind::Real) if available.contains(&ComponentKind::Imag) => {
391                Some(ComponentKind::Imag)
392            }
393            Some(ComponentKind::Imag) if available.contains(&ComponentKind::Real) => {
394                Some(ComponentKind::Real)
395            }
396            _ => available.first().copied(),
397        };
398    }
399
400    pub fn draw(&self, frame: &mut Frame<'_>) {
401        let area = frame.area();
402        if let Some(update) = self.latest_update.as_ref() {
403            self.draw_dashboard(frame, area, update);
404            if self.show_help {
405                self.draw_help_overlay(frame, area);
406            }
407        } else {
408            frame.render_widget(
409                Paragraph::new("Waiting for integration status updates...")
410                    .block(titled_block("Integration dashboard")),
411                area,
412            );
413        }
414    }
415
416    fn clamp_state(&mut self) {
417        if let Some(update) = self.latest_update.as_ref() {
418            let slot_count = update.main_results.slot_headers.len();
419            if slot_count == 0 {
420                self.focused_slot = 0;
421            } else {
422                self.focused_slot = self.focused_slot.min(slot_count - 1);
423            }
424        } else {
425            self.focused_slot = 0;
426        }
427
428        if let Some(update) = self.latest_update.as_ref() {
429            let available = self.available_chart_components(update);
430            if available.is_empty() {
431                self.chart_component = None;
432            } else if !self
433                .chart_component
434                .is_some_and(|component| available.contains(&component))
435            {
436                self.chart_component = update
437                    .training_component()
438                    .filter(|component| available.contains(component))
439                    .or_else(|| available.first().copied());
440            }
441        }
442
443        let row_count = self.discrete_rows().len();
444        if row_count == 0 {
445            self.selected_discrete_row = 0;
446        } else {
447            self.selected_discrete_row = self.selected_discrete_row.min(row_count - 1);
448        }
449    }
450
451    fn push_history_point(&mut self, update: &StatusUpdate) {
452        let real_slot_values = self.collect_history_slot_values(update, ComponentKind::Real);
453        let imag_slot_values = self.collect_history_slot_values(update, ComponentKind::Imag);
454        if real_slot_values.iter().all(Option::is_none)
455            && imag_slot_values.iter().all(Option::is_none)
456        {
457            return;
458        }
459
460        let point = HistoryPoint {
461            iteration: update.meta.iteration,
462            samples: update.meta.total_points,
463            real_slot_values,
464            imag_slot_values,
465            completed_iteration: !matches!(update.kind(), super::IntegrationStatusKind::Live),
466        };
467
468        if self
469            .history
470            .last()
471            .is_some_and(|previous| previous.samples == point.samples)
472        {
473            let preserve_completed_point = self.history.last().is_some_and(|previous| {
474                previous.completed_iteration
475                    && !point.completed_iteration
476                    && update.is_initial_live_status()
477            });
478            if !preserve_completed_point {
479                let _ = self.history.pop();
480            }
481        }
482        if self
483            .history
484            .last()
485            .is_some_and(|previous| previous.samples > point.samples)
486        {
487            self.history.clear();
488        }
489        self.history.push(point);
490        while self.history.len() > MAX_HISTORY_POINTS {
491            self.compact_history_preserving_span();
492        }
493    }
494
495    fn compact_history_preserving_span(&mut self) {
496        if self.history.len() <= MAX_HISTORY_POINTS {
497            return;
498        }
499
500        let retain_recent = MAX_HISTORY_POINTS / 2;
501        let split_index = self.history.len().saturating_sub(retain_recent);
502        if split_index == 0 {
503            return;
504        }
505
506        let older = &self.history[..split_index];
507        let recent = &self.history[split_index..];
508        let max_older_points = MAX_HISTORY_POINTS.saturating_sub(recent.len()).max(2);
509        let step = older.len().div_ceil(max_older_points);
510
511        let mut compacted = Vec::with_capacity(MAX_HISTORY_POINTS);
512        for (index, point) in older.iter().enumerate() {
513            if index == 0 || index + 1 == older.len() || index % step == 0 {
514                compacted.push(point.clone());
515            }
516        }
517        compacted.extend(recent.iter().cloned());
518
519        if compacted.len() > MAX_HISTORY_POINTS {
520            let keep_from_older = compacted.len().saturating_sub(recent.len());
521            let extra = compacted.len() - MAX_HISTORY_POINTS;
522            if keep_from_older > extra + 1 {
523                compacted.drain(1..=extra);
524            } else {
525                compacted.truncate(MAX_HISTORY_POINTS);
526            }
527        }
528
529        self.history = compacted;
530    }
531
532    fn collect_history_slot_values(
533        &self,
534        update: &StatusUpdate,
535        component: ComponentKind,
536    ) -> Vec<Option<(f64, f64)>> {
537        let Some(row) = update
538            .main_results
539            .find_row(ContributionKind::All, component)
540        else {
541            return vec![None; update.main_results.slot_headers.len()];
542        };
543        (0..update.main_results.slot_headers.len())
544            .map(|slot_index| {
545                row.slot_cell(slot_index)
546                    .and_then(|cell| cell.value.as_ref())
547                    .map(|value| (value.raw.0.0, value.raw.1.0.abs()))
548            })
549            .collect()
550    }
551
552    fn visible_history(&self) -> Vec<&HistoryPoint> {
553        match self.chart_history_window {
554            ChartHistoryWindow::Full => self.history.iter().collect(),
555            ChartHistoryWindow::RecentIterations(iterations) => {
556                if iterations == 0 || self.history.is_empty() {
557                    return self.history.iter().collect();
558                }
559                let latest_iteration = self
560                    .history
561                    .last()
562                    .map(|point| point.iteration)
563                    .unwrap_or_default();
564                let start_iteration = latest_iteration
565                    .saturating_add(1)
566                    .saturating_sub(iterations);
567                self.history
568                    .iter()
569                    .filter(|point| point.iteration >= start_iteration)
570                    .collect()
571            }
572        }
573    }
574
575    #[cfg(test)]
576    pub(crate) fn visible_history_sample_bounds(&self) -> Option<(usize, usize)> {
577        let visible = self.visible_history();
578        Some((visible.first()?.samples, visible.last()?.samples))
579    }
580
581    fn available_chart_components(&self, update: &StatusUpdate) -> Vec<ComponentKind> {
582        [ComponentKind::Real, ComponentKind::Imag]
583            .into_iter()
584            .filter(|component| {
585                update
586                    .main_results
587                    .find_row(ContributionKind::All, *component)
588                    .and_then(|row| row.slot_cell(self.focused_slot))
589                    .and_then(|cell| cell.value.as_ref())
590                    .is_some()
591            })
592            .collect()
593    }
594
595    fn chart_component(&self, update: &StatusUpdate) -> Option<ComponentKind> {
596        let available = self.available_chart_components(update);
597        if available.is_empty() {
598            return None;
599        }
600
601        self.chart_component
602            .filter(|component| available.contains(component))
603            .or_else(|| {
604                update
605                    .training_component_for_slot(self.focused_slot)
606                    .filter(|component| available.contains(component))
607            })
608            .or_else(|| available.first().copied())
609    }
610
611    fn draw_dashboard(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
612        let vertical = Layout::default()
613            .direction(Direction::Vertical)
614            .constraints([
615                Constraint::Length(3),
616                Constraint::Length(6),
617                Constraint::Length(update.main_results.slot_headers.len().max(1) as u16 + 2),
618                Constraint::Min(12),
619                Constraint::Length(2),
620            ])
621            .split(area);
622
623        self.draw_tabs(frame, vertical[0]);
624        self.draw_progress(frame, vertical[1], update);
625        self.draw_slot_ribbon(frame, vertical[2], update);
626
627        match self.active_tab {
628            DashboardTab::Overview => self.draw_overview_tab(frame, vertical[3], update),
629            DashboardTab::Discrete => self.draw_discrete_tab(frame, vertical[3], update),
630            DashboardTab::MaxWeight => self.draw_max_weight_tab(frame, vertical[3], update),
631        }
632
633        self.draw_footer(frame, vertical[4], update);
634    }
635
636    fn draw_tabs(&self, frame: &mut Frame<'_>, area: Rect) {
637        let titles = DashboardTab::all()
638            .into_iter()
639            .map(|tab| {
640                Line::from(vec![Span::styled(
641                    format!(" {} ", tab.title()),
642                    Style::default().add_modifier(Modifier::BOLD),
643                )])
644            })
645            .collect::<Vec<_>>();
646
647        let tabs = Tabs::new(titles)
648            .block(titled_block("GammaLoop status"))
649            .select(self.active_tab.index())
650            .highlight_style(Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD));
651        frame.render_widget(tabs, area);
652    }
653
654    fn draw_progress(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
655        let block = titled_block("Iteration progress");
656        let inner = Layout::default()
657            .direction(Direction::Vertical)
658            .constraints([
659                Constraint::Length(1),
660                Constraint::Length(1),
661                Constraint::Length(1),
662                Constraint::Length(1),
663            ])
664            .split(block.inner(area));
665        frame.render_widget(block, area);
666
667        frame.render_widget(Paragraph::new(""), inner[0]);
668
669        let mut left = StyledText::new();
670        left.push_text(" ", TextStyle::PLAIN);
671        left.push_text(
672            format!(
673                "[ {:^7} ]",
674                utils::format_wdhms(update.meta.elapsed_time.as_secs() as usize)
675            ),
676            TextStyle::PLAIN.bold(),
677        );
678        left.push_text(" | ", TextStyle::PLAIN);
679        left.push_text("# samples total ", TextStyle::green().bold());
680        left.push_text(
681            super::display::format_total_points(update.meta.total_points),
682            TextStyle::PLAIN.bold(),
683        );
684        left.push_text(" | ", TextStyle::PLAIN);
685        left.push_text("Iteration ", TextStyle::green().bold());
686        left.push_text(
687            format!("#{}", update.meta.iteration),
688            TextStyle::PLAIN.bold(),
689        );
690        left.push_text(
691            format!(
692                " ({})",
693                if matches!(update.kind(), super::IntegrationStatusKind::Live) {
694                    "running"
695                } else {
696                    "completed"
697                }
698            ),
699            TextStyle::green().bold(),
700        );
701        if matches!(update.kind(), super::IntegrationStatusKind::Live) {
702            left.push_text(" | ", TextStyle::PLAIN);
703            left.push_text("ETA ", TextStyle::green().bold());
704            left.push_text(
705                update
706                    .meta
707                    .iteration_eta()
708                    .map(|duration| utils::format_wdhms(duration.as_secs() as usize))
709                    .unwrap_or_else(|| "warming up".to_string()),
710                TextStyle::PLAIN,
711            );
712            if update.meta.show_eta_to_target {
713                left.push_text(" | ", TextStyle::PLAIN);
714                left.push_text("ETA to target ", TextStyle::green().bold());
715                if let Some(specification) = update.meta.eta_to_target_specification() {
716                    left.push_text("(", TextStyle::green().bold());
717                    left.push_text(specification, TextStyle::green().bold());
718                    left.push_text(") ", TextStyle::green().bold());
719                }
720                left.push_text(
721                    update
722                        .meta
723                        .eta_to_target()
724                        .map(|duration| {
725                            if duration == Duration::MAX {
726                                "∞".to_string()
727                            } else {
728                                utils::format_wdhms(duration.as_secs() as usize)
729                            }
730                        })
731                        .unwrap_or_else(|| "N/A".to_string()),
732                    TextStyle::PLAIN,
733                );
734            }
735        }
736
737        let mut right = StyledText::new();
738        right.push_text(
739            update
740                .meta
741                .total_sample_rate_per_second()
742                .map(format_samples_per_second)
743                .unwrap_or_else(|| "N/A".to_string()),
744            TextStyle::PLAIN.bold(),
745        );
746        right.push_text(" ", TextStyle::PLAIN);
747        right.push_text("#samples/s", TextStyle::green().bold());
748        right.push_text(" | ", TextStyle::PLAIN);
749        if let Some(sample_core_time) = update.meta.sample_core_time() {
750            right.push_text(
751                format!("{} /sample/core", sample_core_time.replace(' ', "")),
752                TextStyle::PLAIN.bold(),
753            );
754        } else {
755            right.push_text("N/A /sample/core", TextStyle::red());
756        }
757        right.push_text(" ", TextStyle::PLAIN);
758        right.push_text(
759            format!("({} cores)", update.meta.cores),
760            TextStyle::PLAIN.bold(),
761        );
762        right.push_text(" ", TextStyle::PLAIN);
763
764        let header = Layout::default()
765            .direction(Direction::Horizontal)
766            .constraints([Constraint::Percentage(68), Constraint::Percentage(32)])
767            .split(inner[1]);
768        frame.render_widget(Paragraph::new(text_from_styled_text(&left)), header[0]);
769        frame.render_widget(
770            Paragraph::new(text_from_styled_text(&right)).alignment(Alignment::Right),
771            header[1],
772        );
773
774        let progress = update.meta.iteration_progress_ratio().unwrap_or_else(|| {
775            if matches!(update.kind(), super::IntegrationStatusKind::Live) {
776                0.0
777            } else {
778                1.0
779            }
780        });
781        let gauge = Gauge::default()
782            .gauge_style(
783                Style::default()
784                    .fg(Color::Green)
785                    .bg(Color::Rgb(70, 70, 70))
786                    .add_modifier(Modifier::BOLD),
787            )
788            .ratio(progress.clamp(0.0, 1.0))
789            .label(Span::styled(
790                format!(
791                    "{} / {} ({:.1}%)",
792                    abbreviate_count(
793                        update
794                            .meta
795                            .live_progress
796                            .map(|progress| progress.completed_points)
797                            .unwrap_or(update.meta.current_iteration_points),
798                    ),
799                    abbreviate_count(
800                        update
801                            .meta
802                            .live_progress
803                            .map(|progress| progress.target_points)
804                            .unwrap_or(update.meta.current_iteration_points),
805                    ),
806                    progress * 100.0
807                ),
808                Style::default()
809                    .fg(Color::White)
810                    .add_modifier(Modifier::BOLD),
811            ));
812        frame.render_widget(gauge, inner[2]);
813        frame.render_widget(Paragraph::new(""), inner[3]);
814    }
815
816    fn draw_slot_ribbon(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
817        let slot_name_width = update
818            .main_results
819            .slot_headers
820            .iter()
821            .map(|header| header.to_plain_string().chars().count())
822            .max()
823            .unwrap_or(10)
824            .max(10) as u16;
825        let value_width = (0..update.main_results.slot_headers.len())
826            .flat_map(|slot_index| {
827                [ComponentKind::Real, ComponentKind::Imag]
828                    .into_iter()
829                    .filter_map(move |component| {
830                        update
831                            .main_results
832                            .find_row(ContributionKind::All, component)
833                            .and_then(|row| row.slot_cell(slot_index))
834                            .and_then(|cell| cell.value.as_ref())
835                            .map(|value| value.display.to_plain_string().chars().count())
836                    })
837            })
838            .max()
839            .unwrap_or(16)
840            .max(16) as u16;
841        let has_targets = (0..update.main_results.slot_headers.len()).any(|slot_index| {
842            update
843                .target_display_for_slot_component(slot_index, ComponentKind::Real)
844                .is_some()
845                || update
846                    .target_display_for_slot_component(slot_index, ComponentKind::Imag)
847                    .is_some()
848        });
849        let target_value_width = (0..update.main_results.slot_headers.len())
850            .flat_map(|slot_index| {
851                [ComponentKind::Real, ComponentKind::Imag]
852                    .into_iter()
853                    .filter_map(move |component| {
854                        update
855                            .target_display_for_slot_component(slot_index, component)
856                            .map(|value| value.to_plain_string().chars().count())
857                    })
858            })
859            .max()
860            .unwrap_or(16)
861            .max(16) as u16;
862        let rows = (0..update.main_results.slot_headers.len())
863            .map(|slot_index| {
864                let re_value = update
865                    .main_results
866                    .find_row(ContributionKind::All, ComponentKind::Real)
867                    .and_then(|row| row.slot_cell(slot_index))
868                    .and_then(|cell| cell.value.as_ref())
869                    .map(|value| &value.display);
870                let im_value = update
871                    .main_results
872                    .find_row(ContributionKind::All, ComponentKind::Imag)
873                    .and_then(|row| row.slot_cell(slot_index))
874                    .and_then(|cell| cell.value.as_ref())
875                    .map(|value| &value.display);
876                let target_re =
877                    update.target_display_for_slot_component(slot_index, ComponentKind::Real);
878                let target_im =
879                    update.target_display_for_slot_component(slot_index, ComponentKind::Imag);
880                let mut cells = vec![
881                    Cell::from(Span::styled(
882                        if slot_index == self.focused_slot {
883                            ">"
884                        } else {
885                            " "
886                        },
887                        if slot_index == self.focused_slot {
888                            Style::default()
889                                .fg(Color::White)
890                                .add_modifier(Modifier::BOLD)
891                        } else {
892                            Style::default()
893                        },
894                    )),
895                    cell_from_styled_text(&update.main_results.slot_headers[slot_index]),
896                    cell_from_styled_text(&component_label_with_colon(ComponentKind::Real)),
897                    cell_from_optional_styled_text(re_value),
898                    cell_from_styled_text(&component_label_with_colon(ComponentKind::Imag)),
899                    cell_from_optional_styled_text(im_value),
900                ];
901                if has_targets {
902                    cells.push(cell_from_styled_text(&StyledText::plain("|")));
903                    cells.push(plain_header_cell("trgt"));
904                    cells.push(cell_from_styled_text(&component_label_with_colon(
905                        ComponentKind::Real,
906                    )));
907                    cells.push(cell_from_optional_styled_text(target_re.as_ref()));
908                    cells.push(cell_from_styled_text(&component_label_with_colon(
909                        ComponentKind::Imag,
910                    )));
911                    cells.push(cell_from_optional_styled_text(target_im.as_ref()));
912                }
913                Row::new(cells)
914            })
915            .collect::<Vec<_>>();
916
917        let mut widths = vec![
918            Constraint::Length(1),
919            Constraint::Length(slot_name_width),
920            Constraint::Length(3),
921            Constraint::Length(value_width),
922            Constraint::Length(3),
923            Constraint::Length(value_width),
924        ];
925        if has_targets {
926            widths.extend([
927                Constraint::Length(1),
928                Constraint::Length(5),
929                Constraint::Length(3),
930                Constraint::Length(target_value_width),
931                Constraint::Length(3),
932                Constraint::Min(target_value_width),
933            ]);
934        }
935
936        frame.render_widget(
937            Table::new(rows, widths)
938                .block(titled_block("Integrands"))
939                .column_spacing(1),
940            area,
941        );
942    }
943
944    fn draw_overview_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
945        let vertical = if area.width >= 120 {
946            Layout::default()
947                .direction(Direction::Vertical)
948                .constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
949                .split(area)
950        } else {
951            Layout::default()
952                .direction(Direction::Vertical)
953                .constraints([
954                    Constraint::Percentage(48),
955                    Constraint::Length(11),
956                    Constraint::Min(10),
957                ])
958                .split(area)
959        };
960
961        if area.width >= 120 {
962            let top = Layout::default()
963                .direction(Direction::Horizontal)
964                .constraints([Constraint::Percentage(62), Constraint::Percentage(38)])
965                .split(vertical[0]);
966            self.draw_chart(frame, top[0], update);
967            self.draw_focused_slot_detail(frame, top[1], update);
968
969            let bottom = Layout::default()
970                .direction(Direction::Horizontal)
971                .constraints([Constraint::Percentage(56), Constraint::Percentage(44)])
972                .split(vertical[1]);
973            self.draw_results_summary_table(frame, bottom[0], update);
974            self.draw_statistics_panel(frame, bottom[1], update);
975        } else {
976            self.draw_chart(frame, vertical[0], update);
977            self.draw_focused_slot_detail(frame, vertical[1], update);
978            let bottom = Layout::default()
979                .direction(Direction::Horizontal)
980                .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
981                .split(vertical[2]);
982            self.draw_results_summary_table(frame, bottom[0], update);
983            self.draw_statistics_panel(frame, bottom[1], update);
984        }
985    }
986
987    fn draw_chart(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
988        let Some(component) = self.chart_component(update) else {
989            frame.render_widget(
990                Paragraph::new("Training phase is not a single component.")
991                    .block(titled_block("Convergence")),
992                area,
993            );
994            return;
995        };
996
997        let focused_slot = self
998            .focused_slot
999            .min(update.main_results.slot_headers.len().saturating_sub(1));
1000        let visible_history = self.visible_history();
1001        if visible_history.is_empty() {
1002            frame.render_widget(
1003                Paragraph::new("Waiting for history points...").block(titled_block("Convergence")),
1004                area,
1005            );
1006            return;
1007        }
1008
1009        let central_points = visible_history
1010            .iter()
1011            .filter_map(|point| {
1012                (*point)
1013                    .slot_values(component)
1014                    .get(focused_slot)
1015                    .copied()
1016                    .flatten()
1017                    .map(|(central, _)| (point.samples as f64, central))
1018            })
1019            .collect::<Vec<_>>();
1020        let upper_points = visible_history
1021            .iter()
1022            .filter_map(|point| {
1023                (*point)
1024                    .slot_values(component)
1025                    .get(focused_slot)
1026                    .copied()
1027                    .flatten()
1028                    .map(|(central, error)| (point.samples as f64, central + error))
1029            })
1030            .collect::<Vec<_>>();
1031        let lower_points = visible_history
1032            .iter()
1033            .filter_map(|point| {
1034                (*point)
1035                    .slot_values(component)
1036                    .get(focused_slot)
1037                    .copied()
1038                    .flatten()
1039                    .map(|(central, error)| (point.samples as f64, central - error))
1040            })
1041            .collect::<Vec<_>>();
1042        let completed_points = visible_history
1043            .iter()
1044            .filter(|point| point.completed_iteration)
1045            .filter_map(|point| {
1046                (*point)
1047                    .slot_values(component)
1048                    .get(focused_slot)
1049                    .copied()
1050                    .flatten()
1051                    .map(|(central, _)| (point.samples as f64, central))
1052            })
1053            .collect::<Vec<_>>();
1054        if central_points.is_empty() {
1055            frame.render_widget(
1056                Paragraph::new("Waiting for focused-integrand history...").block(titled_block(
1057                    format!("Convergence ({})", component.phase_name()),
1058                )),
1059                area,
1060            );
1061            return;
1062        }
1063
1064        let x_min = visible_history
1065            .first()
1066            .map(|point| point.samples as f64)
1067            .unwrap_or(0.0);
1068        let x_max = visible_history
1069            .last()
1070            .map(|point| point.samples as f64)
1071            .unwrap_or(1.0);
1072        let current_value = update
1073            .main_results
1074            .find_row(ContributionKind::All, component)
1075            .and_then(|row| row.slot_cell(focused_slot))
1076            .and_then(|cell| cell.value.as_ref())
1077            .map(|value| (value.raw.0.0, value.raw.1.0.abs()))
1078            .or_else(|| {
1079                self.history.last().and_then(|point| {
1080                    point
1081                        .slot_values(component)
1082                        .get(focused_slot)
1083                        .copied()
1084                        .flatten()
1085                })
1086            })
1087            .unwrap_or((0.0, 1.0));
1088        let sigma = current_value
1089            .1
1090            .max(current_value.0.abs().max(1.0) * 1.0e-12);
1091        let y_span = self.chart_y_sigma_span as f64;
1092        let y_min = current_value.0 - y_span * sigma;
1093        let y_max = current_value.0 + y_span * sigma;
1094
1095        let mut datasets = vec![
1096            Dataset::default()
1097                .name("central")
1098                .marker(Marker::Braille)
1099                .graph_type(ratatui::widgets::GraphType::Line)
1100                .style(
1101                    Style::default()
1102                        .fg(Color::Green)
1103                        .add_modifier(Modifier::BOLD),
1104                )
1105                .data(&central_points),
1106            Dataset::default()
1107                .name("upper")
1108                .graph_type(ratatui::widgets::GraphType::Line)
1109                .style(Style::default().fg(Color::Blue))
1110                .data(&upper_points),
1111            Dataset::default()
1112                .name("lower")
1113                .graph_type(ratatui::widgets::GraphType::Line)
1114                .style(Style::default().fg(Color::Blue))
1115                .data(&lower_points),
1116            Dataset::default()
1117                .name("iter")
1118                .marker(Marker::Bar)
1119                .graph_type(ratatui::widgets::GraphType::Scatter)
1120                .style(Style::default().fg(Color::LightMagenta))
1121                .data(&completed_points),
1122        ];
1123
1124        let target_points = update
1125            .target_for_slot_component(focused_slot, component)
1126            .map(|target| vec![(x_min, target.0), (x_max.max(x_min + 1.0), target.0)]);
1127        if let Some(target_points) = target_points.as_ref() {
1128            datasets.push(
1129                Dataset::default()
1130                    .name("target")
1131                    .graph_type(ratatui::widgets::GraphType::Line)
1132                    .style(Style::default().fg(Color::Yellow))
1133                    .data(target_points),
1134            );
1135        }
1136
1137        let selected_for_training =
1138            update.slot_component_selected_for_training(focused_slot, component);
1139        let mut title = StyledText::styled("Convergence : ", TextStyle::green().bold());
1140        title.push_text(component.phase_name(), component.text_style());
1141        title.push_text(
1142            if selected_for_training {
1143                " (selected for training)"
1144            } else {
1145                " (not selected for training)"
1146            },
1147            TextStyle::green().bold(),
1148        );
1149        title.push_text(" [", TextStyle::green().bold());
1150        title.append(update.main_results.slot_headers[focused_slot].clone());
1151        title.push_text(
1152            format!(
1153                " | {} | y ±{}σ]",
1154                self.chart_history_window.description(),
1155                self.chart_y_sigma_span
1156            ),
1157            TextStyle::green().bold(),
1158        );
1159        let x_bounds = [x_min, x_max.max(x_min + 1.0)];
1160        let chart = Chart::new(datasets)
1161            .block(titled_block_styled(&title))
1162            .x_axis(
1163                Axis::default()
1164                    .title("samples")
1165                    .bounds(x_bounds)
1166                    .labels(count_axis_labels(x_bounds[0], x_bounds[1])),
1167            )
1168            .y_axis(
1169                Axis::default()
1170                    .title("central value")
1171                    .bounds([y_min, y_max])
1172                    .labels(value_axis_labels(y_min, y_max)),
1173            );
1174        frame.render_widget(chart, area);
1175    }
1176
1177    fn draw_focused_slot_detail(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1178        let focused_slot = self
1179            .focused_slot
1180            .min(update.main_results.slot_headers.len().saturating_sub(1));
1181        let rows = self.overview_summary_rows(update);
1182        let show_target_columns = rows.iter().any(|row| {
1183            update
1184                .target_deltas_for_row_slot(row, focused_slot)
1185                .0
1186                .is_some()
1187        });
1188        let focused_column_count = 5 + if show_target_columns { 2 } else { 0 };
1189
1190        let mut table_rows = vec![blank_table_row(focused_column_count)];
1191        let mut inserted_sum_gap = false;
1192        for row in rows {
1193            if matches!(row.contribution.raw, ContributionKind::Sum) && !inserted_sum_gap {
1194                table_rows.push(blank_table_row(focused_column_count));
1195                inserted_sum_gap = true;
1196            }
1197            let Some(cell) = row.slot_cell(focused_slot) else {
1198                continue;
1199            };
1200            let (delta_sigma, delta_percent) = update.target_deltas_for_row_slot(row, focused_slot);
1201            let mut label = row.contribution.display.clone();
1202            label.push_text(" ", TextStyle::PLAIN);
1203            label.append(row.component.display.clone());
1204            let mut cells = vec![
1205                cell_from_styled_text(&label),
1206                cell_from_optional_styled_text(cell.value.as_ref().map(|value| &value.display)),
1207                cell_from_optional_styled_text(
1208                    cell.relative_error.as_ref().map(|value| &value.display),
1209                ),
1210                cell_from_optional_styled_text(cell.chi_sq.as_ref().map(|value| &value.display)),
1211                cell_from_optional_styled_text(
1212                    cell.max_weight_impact.as_ref().map(|value| &value.display),
1213                ),
1214            ];
1215            if show_target_columns {
1216                cells.push(cell_from_optional_styled_text(
1217                    delta_sigma.as_ref().map(|value| &value.display),
1218                ));
1219                cells.push(cell_from_optional_styled_text(
1220                    delta_percent.as_ref().map(|value| &value.display),
1221                ));
1222            }
1223            table_rows.push(Row::new(cells));
1224        }
1225
1226        let mut headers = vec![
1227            plain_header_cell(""),
1228            plain_header_cell("integral"),
1229            plain_header_cell("% err"),
1230            plain_header_cell("chi^2"),
1231            plain_header_cell("m.w.i"),
1232        ];
1233        if show_target_columns {
1234            headers.push(plain_header_cell("Δ [σ]"));
1235            headers.push(plain_header_cell("Δ [%]"));
1236        }
1237
1238        let mut widths = vec![
1239            Constraint::Length(9),
1240            Constraint::Length(16),
1241            Constraint::Length(8),
1242            Constraint::Length(8),
1243            Constraint::Length(10),
1244        ];
1245        if show_target_columns {
1246            widths.push(Constraint::Length(8));
1247            widths.push(Constraint::Length(8));
1248        }
1249
1250        let table = Table::new(table_rows, widths)
1251            .header(Row::new(headers))
1252            .block(titled_block_styled(&scoped_integrand_title(
1253                "Focused integrand ",
1254                &update.main_results.slot_headers[focused_slot],
1255                "",
1256            )))
1257            .column_spacing(1);
1258        frame.render_widget(table, area);
1259    }
1260
1261    fn draw_results_summary_table(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1262        let rows = self.overview_summary_rows(update);
1263        if rows.is_empty() {
1264            frame.render_widget(
1265                Paragraph::new("No overview summary rows are available.")
1266                    .block(titled_block("Results summary")),
1267                area,
1268            );
1269            return;
1270        }
1271
1272        let contribution_width = update
1273            .main_results
1274            .row_groups
1275            .iter()
1276            .flat_map(|group| group.rows.iter())
1277            .map(|row| row.contribution.display.to_plain_string().chars().count())
1278            .chain(std::iter::once(
1279                update
1280                    .main_results
1281                    .contribution_header
1282                    .to_plain_string()
1283                    .chars()
1284                    .count(),
1285            ))
1286            .max()
1287            .unwrap_or(18)
1288            .max(18) as u16;
1289        let contribution_title = styled_text_line(&update.main_results.contribution_header, 0);
1290        let contribution_detail = styled_text_line(&update.main_results.contribution_header, 1);
1291        let mut header_top = vec![cell_from_styled_text(&contribution_title), blank_cell()];
1292        let mut header_bottom = vec![cell_from_styled_text(&contribution_detail), blank_cell()];
1293        let mut widths = vec![
1294            Constraint::Length(contribution_width),
1295            Constraint::Length(3),
1296        ];
1297        for (slot_index, slot_header) in update.main_results.slot_headers.iter().enumerate() {
1298            let integral_width = rows
1299                .iter()
1300                .filter_map(|row| {
1301                    row.slot_cell(slot_index)
1302                        .and_then(|cell| cell.value.as_ref())
1303                        .map(|value| value.display.to_plain_string().chars().count())
1304                })
1305                .chain(std::iter::once(
1306                    slot_header.to_plain_string().chars().count(),
1307                ))
1308                .chain(std::iter::once("integral".chars().count()))
1309                .max()
1310                .unwrap_or(16)
1311                .max(16) as u16;
1312            header_top.push(cell_from_styled_text(slot_header));
1313            header_bottom.push(plain_header_cell("integral"));
1314            widths.push(Constraint::Length(integral_width));
1315            if self.metric_visibility.relative_error {
1316                header_top.push(blank_cell());
1317                header_bottom.push(plain_header_cell("% err"));
1318                widths.push(Constraint::Length(8));
1319            }
1320            if self.metric_visibility.chi_sq {
1321                header_top.push(blank_cell());
1322                header_bottom.push(plain_header_cell("chi^2"));
1323                widths.push(Constraint::Length(8));
1324            }
1325            if self.metric_visibility.max_weight_impact {
1326                header_top.push(blank_cell());
1327                header_bottom.push(plain_header_cell("m.w.i"));
1328                widths.push(Constraint::Length(10));
1329            }
1330        }
1331
1332        let mut table_rows = vec![
1333            blank_table_row(widths.len()),
1334            Row::new(header_top),
1335            Row::new(header_bottom),
1336            blank_table_row(widths.len()),
1337        ];
1338        for group in [MainResultsRowGroupKind::All, MainResultsRowGroupKind::Sum] {
1339            let group_rows = update
1340                .main_results
1341                .row_groups_of_kind(group)
1342                .flat_map(|row_group| row_group.rows.iter())
1343                .collect::<Vec<_>>();
1344            if group_rows.is_empty() {
1345                continue;
1346            }
1347            if table_rows.len() > 1 {
1348                table_rows.push(blank_table_row(widths.len()));
1349            }
1350            for row in group_rows {
1351                let mut cells = vec![
1352                    cell_from_styled_text(&row.contribution.display),
1353                    cell_from_styled_text(&row.component.display),
1354                ];
1355                for slot_index in 0..update.main_results.slot_headers.len() {
1356                    let cell = row.slot_cell(slot_index);
1357                    cells.push(cell_from_optional_styled_text(
1358                        cell.and_then(|cell| cell.value.as_ref())
1359                            .map(|value| &value.display),
1360                    ));
1361                    if self.metric_visibility.relative_error {
1362                        cells.push(cell_from_optional_styled_text(
1363                            cell.and_then(|cell| cell.relative_error.as_ref())
1364                                .map(|value| &value.display),
1365                        ));
1366                    }
1367                    if self.metric_visibility.chi_sq {
1368                        cells.push(cell_from_optional_styled_text(
1369                            cell.and_then(|cell| cell.chi_sq.as_ref())
1370                                .map(|value| &value.display),
1371                        ));
1372                    }
1373                    if self.metric_visibility.max_weight_impact {
1374                        cells.push(cell_from_optional_styled_text(
1375                            cell.and_then(|cell| cell.max_weight_impact.as_ref())
1376                                .map(|value| &value.display),
1377                        ));
1378                    }
1379                }
1380                table_rows.push(Row::new(cells));
1381            }
1382        }
1383
1384        let table = Table::new(table_rows, widths)
1385            .block(titled_block("Results summary"))
1386            .column_spacing(1);
1387        frame.render_widget(table, area);
1388    }
1389
1390    fn draw_statistics_panel(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1391        let Some(statistics) = update.statistics.as_ref() else {
1392            frame.render_widget(
1393                Paragraph::new("Statistics unavailable.")
1394                    .block(titled_block("Integration statistics")),
1395                area,
1396            );
1397            return;
1398        };
1399        let statistics_scope = self.selected_statistics_scope(update);
1400        let layout = Layout::default()
1401            .direction(Direction::Vertical)
1402            .constraints([
1403                Constraint::Percentage(38),
1404                Constraint::Percentage(31),
1405                Constraint::Percentage(31),
1406            ])
1407            .split(area);
1408
1409        let mut summary_rows = vec![blank_table_row(9)];
1410        summary_rows.extend(
1411            statistics
1412                .table_rows(statistics_scope)
1413                .into_iter()
1414                .map(|row| {
1415                    let mut cells = vec![cell_from_styled_text(&row.row_label)];
1416                    for entry in row.entries {
1417                        cells.push(cell_from_styled_text(&entry.label));
1418                        cells.push(cell_from_styled_text(&entry.value));
1419                    }
1420                    Row::new(cells)
1421                })
1422                .collect::<Vec<_>>(),
1423        );
1424        summary_rows.push(blank_table_row(9));
1425        let title = statistics.statistics_title(statistics_scope);
1426        let summary_table = Table::new(
1427            summary_rows,
1428            [
1429                Constraint::Length(10),
1430                Constraint::Length(12),
1431                Constraint::Length(12),
1432                Constraint::Length(12),
1433                Constraint::Length(12),
1434                Constraint::Length(12),
1435                Constraint::Length(12),
1436                Constraint::Length(13),
1437                Constraint::Min(12),
1438            ],
1439        )
1440        .block(titled_block_styled(&title))
1441        .column_spacing(1);
1442        frame.render_widget(summary_table, layout[0]);
1443
1444        self.draw_mix_panel(
1445            frame,
1446            layout[1],
1447            &statistics.timing_title(statistics_scope),
1448            &statistics.timing_mix_segments(statistics_scope),
1449        );
1450        self.draw_mix_panel(
1451            frame,
1452            layout[2],
1453            &statistics.precision_title(statistics_scope),
1454            &statistics.precision_mix_segments(statistics_scope),
1455        );
1456    }
1457
1458    fn draw_discrete_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1459        let discrete_rows = self.discrete_rows();
1460        if discrete_rows.is_empty() {
1461            frame.render_widget(
1462                Paragraph::new("No monitored discrete breakdown is available for this run.")
1463                    .block(titled_block("Discrete breakdown"))
1464                    .wrap(Wrap { trim: false }),
1465                area,
1466            );
1467            return;
1468        }
1469
1470        let focused_slot = self
1471            .focused_slot
1472            .min(update.main_results.slot_headers.len().saturating_sub(1));
1473        let horizontal = Layout::default()
1474            .direction(Direction::Horizontal)
1475            .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
1476            .split(area);
1477
1478        let contribution_header = update.main_results.contribution_header.to_plain_string();
1479        let contribution_width = discrete_rows
1480            .iter()
1481            .map(|entry| {
1482                entry
1483                    .row
1484                    .contribution
1485                    .display
1486                    .to_plain_string()
1487                    .chars()
1488                    .count()
1489            })
1490            .chain(std::iter::once(contribution_header.chars().count()))
1491            .max()
1492            .unwrap_or(24)
1493            .max(24) as u16;
1494        let integral_width = discrete_rows
1495            .iter()
1496            .filter_map(|entry| {
1497                entry
1498                    .row
1499                    .slot_cell(focused_slot)
1500                    .and_then(|cell| cell.value.as_ref())
1501                    .map(|value| value.display.to_plain_string().chars().count())
1502            })
1503            .chain(std::iter::once("integral".chars().count()))
1504            .max()
1505            .unwrap_or(14)
1506            .max(14) as u16;
1507        let mut rows = vec![blank_table_row(9)];
1508        rows.extend(
1509            discrete_rows
1510                .iter()
1511                .map(|entry| {
1512                    let cell = entry.row.slot_cell(focused_slot);
1513                    Row::new(vec![
1514                        cell_from_styled_text(&entry.row.contribution.display),
1515                        cell_from_styled_text(&entry.row.component.display),
1516                        cell_from_optional_styled_text(
1517                            cell.and_then(|cell| cell.value.as_ref())
1518                                .map(|value| &value.display),
1519                        ),
1520                        cell_from_optional_styled_text(
1521                            cell.and_then(|cell| cell.relative_error.as_ref())
1522                                .map(|value| &value.display),
1523                        ),
1524                        cell_from_optional_styled_text(
1525                            cell.and_then(|cell| cell.chi_sq.as_ref())
1526                                .map(|value| &value.display),
1527                        ),
1528                        cell_from_optional_styled_text(
1529                            cell.and_then(|cell| cell.max_weight_impact.as_ref())
1530                                .map(|value| &value.display),
1531                        ),
1532                        cell_from_optional_styled_text(
1533                            cell.and_then(|cell| cell.sample_fraction.as_ref())
1534                                .map(|value| &value.display),
1535                        ),
1536                        cell_from_optional_styled_text(
1537                            cell.and_then(|cell| cell.sample_count.as_ref())
1538                                .map(|value| &value.display),
1539                        ),
1540                        cell_from_optional_styled_text(
1541                            cell.and_then(|cell| cell.target_pdf.as_ref())
1542                                .map(|value| &value.display),
1543                        ),
1544                    ])
1545                })
1546                .collect::<Vec<_>>(),
1547        );
1548
1549        let mut state = TableState::default().with_selected(Some(self.selected_discrete_row + 1));
1550        let table = Table::new(
1551            rows,
1552            [
1553                Constraint::Length(contribution_width),
1554                Constraint::Length(3),
1555                Constraint::Length(integral_width),
1556                Constraint::Length(8),
1557                Constraint::Length(8),
1558                Constraint::Length(10),
1559                Constraint::Length(8),
1560                Constraint::Length(10),
1561                Constraint::Min(8),
1562            ],
1563        )
1564        .header(Row::new(
1565            update
1566                .main_results
1567                .discrete_headers()
1568                .iter()
1569                .map(cell_from_styled_text)
1570                .collect::<Vec<_>>(),
1571        ))
1572        .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
1573        .block(titled_block_styled(&scoped_integrand_title(
1574            "Discrete bins for focused integrand ",
1575            &update.main_results.slot_headers[focused_slot],
1576            &format!(
1577                " (sort: {} {})",
1578                discrete_sort_label(self.discrete_sort),
1579                if self.discrete_descending {
1580                    "desc"
1581                } else {
1582                    "asc"
1583                }
1584            ),
1585        )));
1586        frame.render_stateful_widget(table, horizontal[0], &mut state);
1587
1588        let selected = discrete_rows
1589            .get(self.selected_discrete_row)
1590            .copied()
1591            .unwrap_or(discrete_rows[0]);
1592        self.draw_selected_discrete_detail(frame, horizontal[1], update, selected.row);
1593    }
1594
1595    fn draw_selected_discrete_detail(
1596        &self,
1597        frame: &mut Frame<'_>,
1598        area: Rect,
1599        update: &StatusUpdate,
1600        row: &MainResultsRow,
1601    ) {
1602        let layout = Layout::default()
1603            .direction(Direction::Vertical)
1604            .constraints([Constraint::Length(3), Constraint::Min(6)])
1605            .split(area);
1606        let mut selected_line = vec![Span::raw("selected: ")];
1607        selected_line.extend(spans_from_styled_text(&row.contribution.display));
1608        selected_line.push(Span::raw(" "));
1609        selected_line.extend(spans_from_styled_text(&row.component.display));
1610        frame.render_widget(
1611            Paragraph::new(Line::from(selected_line)).block(titled_block("Selected bin")),
1612            layout[0],
1613        );
1614
1615        let slot_width = update
1616            .main_results
1617            .slot_headers
1618            .iter()
1619            .map(|slot| slot.to_plain_string().chars().count())
1620            .max()
1621            .unwrap_or(8)
1622            .max(8) as u16;
1623        let integral_width = (0..update.main_results.slot_headers.len())
1624            .filter_map(|slot_index| {
1625                row.slot_cell(slot_index)
1626                    .and_then(|cell| cell.value.as_ref())
1627                    .map(|value| value.display.to_plain_string().chars().count())
1628            })
1629            .chain(std::iter::once("integral".chars().count()))
1630            .max()
1631            .unwrap_or(14)
1632            .max(14) as u16;
1633        let mut rows = vec![blank_table_row(8)];
1634        rows.extend(
1635            (0..update.main_results.slot_headers.len())
1636                .filter_map(|slot_index| {
1637                    let cell = row.slot_cell(slot_index)?;
1638                    Some(Row::new(vec![
1639                        cell_from_styled_text(&update.main_results.slot_headers[slot_index]),
1640                        cell_from_optional_styled_text(
1641                            cell.value.as_ref().map(|value| &value.display),
1642                        ),
1643                        cell_from_optional_styled_text(
1644                            cell.relative_error.as_ref().map(|value| &value.display),
1645                        ),
1646                        cell_from_optional_styled_text(
1647                            cell.chi_sq.as_ref().map(|value| &value.display),
1648                        ),
1649                        cell_from_optional_styled_text(
1650                            cell.max_weight_impact.as_ref().map(|value| &value.display),
1651                        ),
1652                        cell_from_optional_styled_text(
1653                            cell.sample_fraction.as_ref().map(|value| &value.display),
1654                        ),
1655                        cell_from_optional_styled_text(
1656                            cell.sample_count.as_ref().map(|value| &value.display),
1657                        ),
1658                        cell_from_optional_styled_text(
1659                            cell.target_pdf.as_ref().map(|value| &value.display),
1660                        ),
1661                    ]))
1662                })
1663                .collect::<Vec<_>>(),
1664        );
1665
1666        let table = Table::new(
1667            rows,
1668            [
1669                Constraint::Length(slot_width),
1670                Constraint::Length(integral_width),
1671                Constraint::Length(10),
1672                Constraint::Length(8),
1673                Constraint::Length(10),
1674                Constraint::Length(10),
1675                Constraint::Length(10),
1676                Constraint::Min(8),
1677            ],
1678        )
1679        .header(Row::new(
1680            update
1681                .main_results
1682                .selected_bin_detail_headers()
1683                .iter()
1684                .map(cell_from_styled_text)
1685                .collect::<Vec<_>>(),
1686        ))
1687        .block(titled_block("Per integrand details"))
1688        .column_spacing(1);
1689        frame.render_widget(table, layout[1]);
1690    }
1691
1692    fn draw_max_weight_tab(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1693        let focused_slot = self
1694            .focused_slot
1695            .min(update.main_results.slot_headers.len().saturating_sub(1));
1696        let focused_slot_name = update.main_results.slot_headers[focused_slot].to_plain_string();
1697        let top_height = if area.height > 18 {
1698            update
1699                .max_weight_details
1700                .as_ref()
1701                .map(|section| {
1702                    let row_count = section
1703                        .rows_by_slot
1704                        .iter()
1705                        .map(|rows| rows.len())
1706                        .sum::<usize>() as u16;
1707                    row_count
1708                        .saturating_add(6)
1709                        .clamp(8, area.height.saturating_sub(10))
1710                })
1711                .unwrap_or(area.height.saturating_mul(2) / 5)
1712        } else {
1713            area.height.saturating_mul(2) / 5
1714        };
1715        let layout = Layout::default()
1716            .direction(Direction::Vertical)
1717            .constraints([Constraint::Length(top_height), Constraint::Min(8)])
1718            .split(area);
1719
1720        if let Some(section) = update.max_weight_details.as_ref() {
1721            let headers = section.headers();
1722            let title = section.title();
1723            let max_eval_width = max_styled_text_width(
1724                std::iter::once(&headers[2]).chain(
1725                    section
1726                        .rows_by_slot
1727                        .iter()
1728                        .flat_map(|group| group.iter().map(|row| &row.max_eval.display)),
1729                ),
1730            );
1731            let mut rows = vec![blank_table_row(4)];
1732            rows.extend(
1733                section
1734                    .rows_by_slot
1735                    .iter()
1736                    .flat_map(|group| group.iter())
1737                    .map(|row| {
1738                        Row::new(vec![
1739                            cell_from_styled_text(&row.slot.display),
1740                            cell_from_styled_text(&row.component_sign.display),
1741                            cell_from_styled_text(&row.max_eval.display),
1742                            cell_from_styled_text(&row.coordinates.display),
1743                        ])
1744                        .height(styled_text_line_count(&row.coordinates.display))
1745                    })
1746                    .collect::<Vec<_>>(),
1747            );
1748            rows.push(blank_table_row(4));
1749            let table = Table::new(
1750                rows,
1751                [
1752                    Constraint::Length(24),
1753                    Constraint::Length(10),
1754                    Constraint::Length(max_eval_width),
1755                    Constraint::Min(24),
1756                ],
1757            )
1758            .header(Row::new(
1759                headers
1760                    .iter()
1761                    .map(cell_from_styled_text)
1762                    .collect::<Vec<_>>(),
1763            ))
1764            .block(titled_block_styled(&title));
1765            frame.render_widget(table, layout[0]);
1766        } else {
1767            frame.render_widget(
1768                Paragraph::new("No overall max-weight data.")
1769                    .block(titled_block("Overall max weight")),
1770                layout[0],
1771            );
1772        }
1773
1774        if let Some(section) = update.discrete_max_weight_details.as_ref() {
1775            let sorted_rows = self.sorted_discrete_max_weight_rows(section);
1776            let lower = Layout::default()
1777                .direction(Direction::Horizontal)
1778                .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
1779                .split(layout[1]);
1780            let summary_headers = section.summary_headers();
1781            let mut summary_rows = vec![blank_table_row(section.slot_headers.len() + 2)];
1782            summary_rows.extend(
1783                sorted_rows
1784                    .iter()
1785                    .map(|row| {
1786                        let mut cells = vec![
1787                            cell_from_styled_text(&row.contribution.display),
1788                            cell_from_styled_text(&row.component_sign.display),
1789                        ];
1790                        cells.extend(
1791                            row.slot_values
1792                                .iter()
1793                                .map(|value| {
1794                                    cell_from_optional_styled_text(
1795                                        value.as_ref().map(|field| &field.display),
1796                                    )
1797                                })
1798                                .collect::<Vec<_>>(),
1799                        );
1800                        Row::new(cells)
1801                    })
1802                    .collect::<Vec<_>>(),
1803            );
1804            let summary_widths = std::iter::once(Constraint::Length(24))
1805                .chain(std::iter::once(Constraint::Length(10)))
1806                .chain(
1807                    section
1808                        .slot_headers
1809                        .iter()
1810                        .enumerate()
1811                        .map(|(slot_index, header)| {
1812                            Constraint::Min(
1813                                max_styled_text_width(std::iter::once(header).chain(
1814                                    sorted_rows.iter().filter_map(|row| {
1815                                        row.slot_values
1816                                            .get(slot_index)
1817                                            .and_then(|value| value.as_ref())
1818                                            .map(|field| &field.display)
1819                                    }),
1820                                ))
1821                                .max(16),
1822                            )
1823                        }),
1824                )
1825                .collect::<Vec<_>>();
1826            let table = Table::new(summary_rows, summary_widths)
1827                .header(Row::new(
1828                    summary_headers
1829                        .iter()
1830                        .map(cell_from_styled_text)
1831                        .collect::<Vec<_>>(),
1832                ))
1833                .block(titled_block(format!(
1834                    "Per-bin max weight (sort: {} {})",
1835                    discrete_sort_label(self.discrete_sort),
1836                    if self.discrete_descending {
1837                        "desc"
1838                    } else {
1839                        "asc"
1840                    }
1841                )))
1842                .column_spacing(1);
1843            frame.render_widget(table, lower[0]);
1844
1845            let coordinate_headers = section.coordinate_headers();
1846            let focused_slot_filter = focused_slot_name.clone();
1847            let mut coordinate_rows = vec![blank_table_row(3)];
1848            coordinate_rows.extend(
1849                sorted_rows
1850                    .iter()
1851                    .flat_map(|row| {
1852                        let focused_slot_filter = focused_slot_filter.clone();
1853                        row.slot_coordinates
1854                            .iter()
1855                            .filter(move |entry| entry.slot.raw == focused_slot_filter)
1856                            .map(move |entry| {
1857                                Row::new(vec![
1858                                    cell_from_styled_text(&row.contribution.display),
1859                                    cell_from_styled_text(&row.component_sign.display),
1860                                    cell_from_styled_text(&entry.coordinates.display),
1861                                ])
1862                                .height(styled_text_line_count(&entry.coordinates.display))
1863                            })
1864                    })
1865                    .collect::<Vec<_>>(),
1866            );
1867            let coordinates_table = Table::new(
1868                coordinate_rows,
1869                [
1870                    Constraint::Length(18),
1871                    Constraint::Length(8),
1872                    Constraint::Min(24),
1873                ],
1874            )
1875            .header(Row::new(vec![
1876                cell_from_styled_text(&coordinate_headers[0]),
1877                cell_from_styled_text(&coordinate_headers[1]),
1878                cell_from_styled_text(&coordinate_headers[3]),
1879            ]))
1880            .block(titled_block_styled(&scoped_integrand_title(
1881                "Maximum weight coordinates for integrand ",
1882                &update.main_results.slot_headers[focused_slot],
1883                "",
1884            )));
1885            frame.render_widget(coordinates_table, lower[1]);
1886        } else {
1887            frame.render_widget(
1888                Paragraph::new("No per-bin max-weight data.")
1889                    .block(titled_block("Per-bin max weight")),
1890                layout[1],
1891            );
1892        }
1893    }
1894
1895    fn draw_footer(&self, frame: &mut Frame<'_>, area: Rect, update: &StatusUpdate) {
1896        let phase = update
1897            .training_component()
1898            .map(|component| match component {
1899                ComponentKind::Real => "re",
1900                ComponentKind::Imag => "im",
1901            })
1902            .unwrap_or("n/a");
1903        let footer = Paragraph::new(Line::from(vec![
1904            Span::styled("Tabs", label_style()),
1905            Span::raw(" 1/2/3 or <- ->  "),
1906            Span::styled("Integrand", label_style()),
1907            Span::raw(" [ / ]  "),
1908            Span::styled("Bins", label_style()),
1909            Span::raw(" j/k  "),
1910            Span::styled("Metrics", label_style()),
1911            Span::raw(" r c w  "),
1912            Span::styled("Phase", label_style()),
1913            Span::raw(" p  "),
1914            Span::styled("Sort", label_style()),
1915            Span::raw(" s/v  "),
1916            Span::styled("History", label_style()),
1917            Span::raw(format!(
1918                " g +/- ({})  ",
1919                self.chart_history_window.description()
1920            )),
1921            Span::styled("Y-axis", label_style()),
1922            Span::raw(format!(" , . 0 (±{}σ)  ", self.chart_y_sigma_span)),
1923            Span::styled("Help", label_style()),
1924            Span::raw(" ?  "),
1925            Span::styled("Abort", label_style()),
1926            Span::raw(format!(" x / Ctrl-C   training {phase}")),
1927        ]))
1928        .alignment(Alignment::Left)
1929        .block(Block::default().borders(Borders::TOP));
1930        frame.render_widget(footer, area);
1931    }
1932
1933    fn draw_help_overlay(&self, frame: &mut Frame<'_>, area: Rect) {
1934        let popup = centered_rect(72, 65, area);
1935        frame.render_widget(Clear, popup);
1936        frame.render_widget(
1937            Paragraph::new(Text::from(vec![
1938                Line::from("1/2/3 or Left/Right  switch tabs"),
1939                Line::from("[ and ]             change focused integrand"),
1940                Line::from("j / k               move selected discrete row"),
1941                Line::from("s                   cycle discrete sort"),
1942                Line::from("v                   reverse discrete sort order"),
1943                Line::from("i                   toggle global/focused statistics"),
1944                Line::from("r / c / w           toggle rel err, chi^2, mwi columns"),
1945                Line::from("p                   toggle convergence real/imag phase"),
1946                Line::from("g                   toggle full/recent chart history"),
1947                Line::from("+ / -               widen or narrow recent-history window"),
1948                Line::from(", / .               tighten or widen the y-range in σ units"),
1949                Line::from("0                   reset the y-range to ±4σ"),
1950                Line::from("x                   abort the current iteration"),
1951                Line::from("Ctrl-C              stop the integration"),
1952                Line::from("?                   close help"),
1953            ]))
1954            .block(titled_block("Dashboard controls"))
1955            .alignment(Alignment::Left)
1956            .wrap(Wrap { trim: false }),
1957            popup,
1958        );
1959    }
1960
1961    fn draw_mix_panel(
1962        &self,
1963        frame: &mut Frame<'_>,
1964        area: Rect,
1965        title: &StyledText,
1966        segments: &[StatisticsMixSegment],
1967    ) {
1968        let block = titled_block_styled(title);
1969        let inner = Layout::default()
1970            .direction(Direction::Vertical)
1971            .constraints([
1972                Constraint::Length(1),
1973                Constraint::Length(1),
1974                Constraint::Length(1),
1975                Constraint::Length(1),
1976                Constraint::Length(1),
1977            ])
1978            .split(block.inner(area));
1979        frame.render_widget(block, area);
1980        frame.render_widget(Paragraph::new(""), inner[0]);
1981        frame.render_widget(
1982            Paragraph::new(mix_bar_line(segments, inner[1].width as usize)),
1983            inner[1],
1984        );
1985        frame.render_widget(Paragraph::new(""), inner[2]);
1986        if !segments.is_empty() {
1987            let constraints = (0..segments.len())
1988                .map(|_| Constraint::Percentage((100 / segments.len().max(1)) as u16))
1989                .collect::<Vec<_>>();
1990            let labels = Layout::default()
1991                .direction(Direction::Horizontal)
1992                .constraints(constraints)
1993                .split(inner[3]);
1994            for (segment, label_area) in segments.iter().zip(labels.iter().copied()) {
1995                frame.render_widget(
1996                    Paragraph::new(Line::from(vec![Span::styled(
1997                        format!(
1998                            "{} : {:.1}%",
1999                            segment.label.to_plain_string(),
2000                            segment.percentage
2001                        ),
2002                        style_from_text_style(
2003                            segment
2004                                .label
2005                                .spans
2006                                .first()
2007                                .map(|span| span.style)
2008                                .unwrap_or(TextStyle::PLAIN),
2009                        )
2010                        .add_modifier(Modifier::BOLD),
2011                    )]))
2012                    .alignment(Alignment::Center),
2013                    label_area,
2014                );
2015            }
2016        }
2017        frame.render_widget(Paragraph::new(""), inner[4]);
2018    }
2019
2020    fn discrete_rows(&self) -> Vec<DiscreteRowRef<'_>> {
2021        let Some(update) = self.latest_update.as_ref() else {
2022            return Vec::new();
2023        };
2024        let focused_slot = self
2025            .focused_slot
2026            .min(update.main_results.slot_headers.len().saturating_sub(1));
2027        self.discrete_rows_for_slot(focused_slot)
2028    }
2029
2030    fn selected_statistics_scope(&self, update: &StatusUpdate) -> StatisticsScope {
2031        match self.statistics_scope {
2032            DashboardStatisticsScope::Global => StatisticsScope::Global,
2033            DashboardStatisticsScope::FocusedSlot => StatisticsScope::Slot(
2034                self.focused_slot
2035                    .min(update.main_results.slot_headers.len().saturating_sub(1)),
2036            ),
2037        }
2038    }
2039
2040    fn discrete_rows_for_slot(&self, slot_index: usize) -> Vec<DiscreteRowRef<'_>> {
2041        let Some(update) = self.latest_update.as_ref() else {
2042            return Vec::new();
2043        };
2044        let mut rows = update
2045            .main_results
2046            .row_groups_of_kind(MainResultsRowGroupKind::Bins)
2047            .flat_map(|group| group.rows.iter())
2048            .map(|row| DiscreteRowRef { row })
2049            .collect::<Vec<_>>();
2050
2051        rows.sort_by(|lhs, rhs| {
2052            let lhs_key = lhs.sort_value(self.discrete_sort, slot_index);
2053            let rhs_key = rhs.sort_value(self.discrete_sort, slot_index);
2054            let ordering = match self.discrete_sort {
2055                ContributionSortMode::Index => match (lhs.contribution(), rhs.contribution()) {
2056                    (ContributionKind::Bin(lhs_index), ContributionKind::Bin(rhs_index)) => {
2057                        lhs_index
2058                            .cmp(&rhs_index)
2059                            .then(lhs.component().cmp(&rhs.component()))
2060                    }
2061                    _ => Ordering::Equal,
2062                },
2063                ContributionSortMode::Integral | ContributionSortMode::Error => lhs_key
2064                    .partial_cmp(&rhs_key)
2065                    .unwrap_or(Ordering::Equal)
2066                    .then(lhs.component().cmp(&rhs.component())),
2067            };
2068            if self.discrete_descending {
2069                ordering.reverse()
2070            } else {
2071                ordering
2072            }
2073        });
2074        rows
2075    }
2076
2077    fn sorted_discrete_max_weight_rows<'a>(
2078        &self,
2079        section: &'a super::status_update::DiscreteMaxWeightDetailsSection,
2080    ) -> Vec<&'a super::status_update::DiscreteMaxWeightRow> {
2081        let discrete_order = self
2082            .discrete_rows_for_slot(0)
2083            .into_iter()
2084            .map(|row| (row.contribution(), row.component()))
2085            .collect::<Vec<_>>();
2086        let mut all_rows = section
2087            .row_groups
2088            .iter()
2089            .flat_map(|group| group.iter())
2090            .collect::<Vec<_>>();
2091        all_rows.sort_by_key(|row| {
2092            self.max_weight_row_sort_rank(
2093                &discrete_order,
2094                row.contribution.raw,
2095                row.component_sign.raw.0,
2096            )
2097        });
2098        all_rows
2099    }
2100
2101    fn max_weight_row_sort_rank(
2102        &self,
2103        discrete_order: &[(ContributionKind, ComponentKind)],
2104        contribution: ContributionKind,
2105        component: ComponentKind,
2106    ) -> (usize, usize) {
2107        match contribution {
2108            ContributionKind::All => (0, component_rank(component)),
2109            ContributionKind::Bin(_) => (
2110                1,
2111                discrete_order
2112                    .iter()
2113                    .position(|entry| *entry == (contribution, component))
2114                    .unwrap_or(usize::MAX),
2115            ),
2116            ContributionKind::Sum => (2, component_rank(component)),
2117        }
2118    }
2119
2120    fn overview_value_text(&self, update: &StatusUpdate, slot_index: usize) -> Text<'static> {
2121        overview_metric_text(update, slot_index, |cell| {
2122            cell.value.as_ref().map(|value| &value.display)
2123        })
2124    }
2125
2126    fn overview_summary_rows<'a>(&self, update: &'a StatusUpdate) -> Vec<&'a MainResultsRow> {
2127        update
2128            .main_results
2129            .row_groups_of_kind(MainResultsRowGroupKind::All)
2130            .chain(
2131                update
2132                    .main_results
2133                    .row_groups_of_kind(MainResultsRowGroupKind::Sum),
2134            )
2135            .flat_map(|group| group.rows.iter())
2136            .collect()
2137    }
2138
2139    fn results_summary_cell(&self, row: &MainResultsRow, slot_index: usize) -> Text<'static> {
2140        let Some(cell) = row.slot_cell(slot_index) else {
2141            return Text::from("N/A");
2142        };
2143
2144        let mut lines = Vec::new();
2145        if let Some(value) = cell.value.as_ref() {
2146            lines.push(Line::from(spans_from_styled_text(&value.display)));
2147        } else {
2148            lines.push(Line::from("N/A"));
2149        }
2150
2151        let mut metrics = Vec::new();
2152        if self.metric_visibility.relative_error {
2153            metrics.push(labeled_value_line(
2154                "rel err",
2155                cell.relative_error.as_ref().map(|value| &value.display),
2156            ));
2157        }
2158        if self.metric_visibility.chi_sq {
2159            metrics.push(labeled_value_line(
2160                "chi^2",
2161                cell.chi_sq.as_ref().map(|value| &value.display),
2162            ));
2163        }
2164        if self.metric_visibility.max_weight_impact {
2165            metrics.push(labeled_value_line(
2166                "mwi",
2167                cell.max_weight_impact.as_ref().map(|value| &value.display),
2168            ));
2169        }
2170        if !metrics.is_empty() && !matches!(self.density, DensityMode::Compact) {
2171            let mut spans = Vec::new();
2172            for (index, metric_line) in metrics.into_iter().enumerate() {
2173                if index > 0 {
2174                    spans.push(Span::raw("   "));
2175                }
2176                spans.extend(metric_line.spans);
2177            }
2178            lines.push(Line::from(spans));
2179        }
2180
2181        Text::from(lines)
2182    }
2183}
2184
2185fn overview_metric_text(
2186    update: &StatusUpdate,
2187    slot_index: usize,
2188    select: impl Fn(&MainTableSlotCells) -> Option<&StyledText>,
2189) -> Text<'static> {
2190    let select = &select;
2191    let lines = update
2192        .main_results
2193        .row_groups_of_kind(MainResultsRowGroupKind::All)
2194        .flat_map(|group| group.rows.iter())
2195        .filter_map(|row| {
2196            let text = row.slot_cell(slot_index).and_then(select)?;
2197            let mut spans = spans_from_styled_text(&row.component.display);
2198            spans.push(Span::raw(": "));
2199            spans.extend(spans_from_styled_text(text));
2200            Some(Line::from(spans))
2201        })
2202        .collect::<Vec<_>>();
2203
2204    if lines.is_empty() {
2205        Text::from("N/A")
2206    } else {
2207        Text::from(lines)
2208    }
2209}
2210
2211fn component_label_with_colon(component: ComponentKind) -> StyledText {
2212    let mut label = component.label_display();
2213    label.push_text(":", component.text_style());
2214    label
2215}
2216
2217fn component_rank(component: ComponentKind) -> usize {
2218    match component {
2219        ComponentKind::Real => 0,
2220        ComponentKind::Imag => 1,
2221    }
2222}
2223
2224fn scoped_integrand_title(prefix: &str, slot_header: &StyledText, suffix: &str) -> StyledText {
2225    let mut title = StyledText::styled(prefix, TextStyle::green().bold());
2226    title.push_text("[", TextStyle::green().bold());
2227    title.append(slot_header.clone());
2228    title.push_text("]", TextStyle::green().bold());
2229    title.push_text(suffix, TextStyle::green().bold());
2230    title
2231}
2232
2233fn format_samples_per_second(rate: f64) -> String {
2234    abbreviate_count(rate.max(0.0).round() as usize)
2235}
2236
2237fn labeled_value_line(label: &str, value: Option<&StyledText>) -> Line<'static> {
2238    let mut spans = vec![Span::raw(format!("{label} "))];
2239    if let Some(value) = value {
2240        spans.extend(spans_from_styled_text(value));
2241    } else {
2242        spans.push(Span::raw("N/A"));
2243    }
2244    Line::from(spans)
2245}
2246
2247fn style_from_text_style(style: TextStyle) -> Style {
2248    let mut rendered = Style::default();
2249    if let Some(color) = color_from_text_style(style) {
2250        rendered = rendered.fg(color);
2251    }
2252    if style.bold {
2253        rendered = rendered.add_modifier(Modifier::BOLD);
2254    }
2255    if style.dimmed {
2256        rendered = rendered.add_modifier(Modifier::DIM);
2257    }
2258    rendered
2259}
2260
2261fn spans_from_styled_text(text: &StyledText) -> Vec<Span<'static>> {
2262    text.spans
2263        .iter()
2264        .map(|span| Span::styled(span.text.clone(), style_from_text_style(span.style)))
2265        .collect()
2266}
2267
2268fn line_from_styled_text(text: &StyledText) -> Line<'static> {
2269    Line::from(spans_from_styled_text(text))
2270}
2271
2272fn text_from_styled_text(text: &StyledText) -> Text<'static> {
2273    let mut lines = vec![Line::default()];
2274    for span in &text.spans {
2275        let style = style_from_text_style(span.style);
2276        for (index, segment) in span.text.split('\n').enumerate() {
2277            if index > 0 {
2278                lines.push(Line::default());
2279            }
2280            if !segment.is_empty() {
2281                lines
2282                    .last_mut()
2283                    .expect("at least one line")
2284                    .spans
2285                    .push(Span::styled(segment.to_string(), style));
2286            }
2287        }
2288    }
2289    Text::from(lines)
2290}
2291
2292fn styled_text_line_count(text: &StyledText) -> u16 {
2293    text.to_plain_string()
2294        .lines()
2295        .count()
2296        .max(1)
2297        .try_into()
2298        .unwrap_or(u16::MAX)
2299}
2300
2301fn styled_text_max_line_width(text: &StyledText) -> u16 {
2302    text.to_plain_string()
2303        .lines()
2304        .map(|line| line.chars().count())
2305        .max()
2306        .unwrap_or(0)
2307        .max(1)
2308        .try_into()
2309        .unwrap_or(u16::MAX)
2310}
2311
2312fn max_styled_text_width<'a>(texts: impl IntoIterator<Item = &'a StyledText>) -> u16 {
2313    texts
2314        .into_iter()
2315        .map(styled_text_max_line_width)
2316        .max()
2317        .unwrap_or(1)
2318}
2319
2320fn styled_text_line(text: &StyledText, line_index: usize) -> StyledText {
2321    let mut lines = vec![StyledText::new()];
2322    for span in &text.spans {
2323        for (index, segment) in span.text.split('\n').enumerate() {
2324            if index > 0 {
2325                lines.push(StyledText::new());
2326            }
2327            if !segment.is_empty() {
2328                lines
2329                    .last_mut()
2330                    .expect("at least one styled line")
2331                    .push_text(segment, span.style);
2332            }
2333        }
2334    }
2335    lines.get(line_index).cloned().unwrap_or_default()
2336}
2337
2338fn cell_from_styled_text(text: &StyledText) -> Cell<'static> {
2339    Cell::from(text_from_styled_text(text))
2340}
2341
2342fn cell_from_optional_styled_text(text: Option<&StyledText>) -> Cell<'static> {
2343    text.map(cell_from_styled_text).unwrap_or_else(blank_cell)
2344}
2345
2346fn plain_header_cell(text: impl Into<String>) -> Cell<'static> {
2347    Cell::from(Span::styled(text.into(), label_style()))
2348}
2349
2350fn blank_cell() -> Cell<'static> {
2351    Cell::from(String::new())
2352}
2353
2354fn blank_table_row(column_count: usize) -> Row<'static> {
2355    Row::new((0..column_count).map(|_| blank_cell()).collect::<Vec<_>>())
2356}
2357
2358fn stacked_header(title: &StyledText, subtitle: &str) -> StyledText {
2359    let mut text = title.clone();
2360    text.push_text("\n", TextStyle::PLAIN);
2361    text.push_text(subtitle, TextStyle::PLAIN.bold());
2362    text
2363}
2364
2365fn titled_block(title: impl Into<String>) -> Block<'static> {
2366    Block::default()
2367        .borders(Borders::ALL)
2368        .title(Line::from(vec![Span::styled(
2369            title.into(),
2370            Style::default()
2371                .fg(Color::Green)
2372                .add_modifier(Modifier::BOLD),
2373        )]))
2374        .border_style(Style::default().fg(Color::DarkGray))
2375}
2376
2377fn titled_block_styled(title: &StyledText) -> Block<'static> {
2378    Block::default()
2379        .borders(Borders::ALL)
2380        .title(line_from_styled_text(title))
2381        .border_style(Style::default().fg(Color::DarkGray))
2382}
2383
2384fn label_style() -> Style {
2385    Style::default().add_modifier(Modifier::BOLD)
2386}
2387
2388fn abbreviate_count(value: usize) -> String {
2389    super::display::format_abbreviated_count(value)
2390}
2391
2392fn mix_bar_line(segments: &[StatisticsMixSegment], width: usize) -> Line<'static> {
2393    let bar_width = width.max(8);
2394    let mut spans = Vec::new();
2395    let mut used = 0usize;
2396    for (index, segment) in segments.iter().enumerate() {
2397        let color = color_from_styled_text(&segment.label, Color::DarkGray);
2398        let width = if index + 1 == segments.len() {
2399            bar_width.saturating_sub(used)
2400        } else {
2401            let segment_width = ((segment.percentage / 100.0) * bar_width as f64).round() as usize;
2402            used += segment_width;
2403            segment_width
2404        };
2405        spans.push(Span::styled("█".repeat(width), Style::default().fg(color)));
2406    }
2407    Line::from(spans)
2408}
2409
2410fn discrete_sort_label(mode: ContributionSortMode) -> &'static str {
2411    match mode {
2412        ContributionSortMode::Index => "index",
2413        ContributionSortMode::Integral => "integral",
2414        ContributionSortMode::Error => "error",
2415    }
2416}
2417
2418fn color_from_text_style(style: TextStyle) -> Option<Color> {
2419    match style.color {
2420        Some(TextColor::Green) => Some(Color::Green),
2421        Some(TextColor::Blue) => Some(Color::Blue),
2422        Some(TextColor::Red) => Some(Color::Red),
2423        Some(TextColor::Yellow) => Some(Color::Yellow),
2424        Some(TextColor::Pink) => Some(Color::LightMagenta),
2425        None => None,
2426    }
2427}
2428
2429fn color_from_styled_text(text: &StyledText, fallback: Color) -> Color {
2430    text.spans
2431        .iter()
2432        .find_map(|span| color_from_text_style(span.style))
2433        .unwrap_or(fallback)
2434}
2435
2436fn count_axis_labels(min: f64, max: f64) -> Vec<Span<'static>> {
2437    evenly_spaced_values(min, max, 9)
2438        .into_iter()
2439        .map(|value| Span::raw(abbreviate_count(value.max(0.0).round() as usize)))
2440        .collect()
2441}
2442
2443fn value_axis_labels(min: f64, max: f64) -> Vec<Span<'static>> {
2444    evenly_spaced_values(min, max, 9)
2445        .into_iter()
2446        .map(|value| Span::raw(format!("{value:.3e}")))
2447        .collect()
2448}
2449
2450fn evenly_spaced_values(min: f64, max: f64, count: usize) -> Vec<f64> {
2451    if count <= 1 || !min.is_finite() || !max.is_finite() || (max - min).abs() < f64::EPSILON {
2452        return vec![min];
2453    }
2454
2455    let step = (max - min) / (count.saturating_sub(1) as f64);
2456    (0..count).map(|index| min + step * index as f64).collect()
2457}
2458
2459fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
2460    let popup_layout = Layout::default()
2461        .direction(Direction::Vertical)
2462        .constraints([
2463            Constraint::Percentage((100 - percent_y) / 2),
2464            Constraint::Percentage(percent_y),
2465            Constraint::Percentage((100 - percent_y) / 2),
2466        ])
2467        .split(area);
2468    Layout::default()
2469        .direction(Direction::Horizontal)
2470        .constraints([
2471            Constraint::Percentage((100 - percent_x) / 2),
2472            Constraint::Percentage(percent_x),
2473            Constraint::Percentage((100 - percent_x) / 2),
2474        ])
2475        .split(popup_layout[1])[1]
2476}