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