Skip to main content

gammalooprs/integrate/
render_tabled.rs

1use colored::Colorize;
2use itertools::Itertools;
3use tabled::{
4    Table,
5    builder::Builder,
6    settings::{
7        Alignment, Modify, Panel, Span,
8        object::{Cell, Columns, Object, Rows},
9        style::{HorizontalLine, Style},
10        themes::BorderCorrection,
11        width::Width,
12    },
13};
14
15use crate::utils::normalize_tabled_separator_rows;
16
17use super::{
18    display::{StyledText, TextColor},
19    status_update::{
20        DiscreteMaxWeightDetailsSection, IntegralView, MainResultsRowGroupKind, MainResultsSection,
21        MaxWeightDetailsSection, StatisticsScope, StatisticsSection, StatusUpdate,
22    },
23};
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct TabledRenderOptions {
27    pub max_table_width: usize,
28    pub show_statistics: bool,
29    pub show_max_weight_details: bool,
30    pub show_top_discrete_grid: bool,
31    pub show_discrete_contributions_sum: bool,
32    pub show_max_weight_info_for_discrete_bins: bool,
33}
34
35impl Default for TabledRenderOptions {
36    fn default() -> Self {
37        Self {
38            max_table_width: 250,
39            show_statistics: true,
40            show_max_weight_details: true,
41            show_top_discrete_grid: false,
42            show_discrete_contributions_sum: false,
43            show_max_weight_info_for_discrete_bins: false,
44        }
45    }
46}
47
48struct StatusTable {
49    table: Table,
50    separator_after_rows: Vec<usize>,
51    hidden_vertical_boundaries: Vec<usize>,
52    full_row_vertical_count: usize,
53    suppress_header_middle_separator: bool,
54    suppress_header_tail_separator: bool,
55}
56
57fn render_styled_text(text: &StyledText) -> String {
58    text.spans
59        .iter()
60        .map(|span| {
61            let mut styled = match span.style.color {
62                Some(TextColor::Green) => span.text.green(),
63                Some(TextColor::Blue) => span.text.blue(),
64                Some(TextColor::Red) => span.text.red(),
65                Some(TextColor::Yellow) => span.text.yellow(),
66                Some(TextColor::Pink) => span.text.bright_magenta(),
67                None => span.text.normal(),
68            };
69            if span.style.dimmed {
70                styled = styled.dimmed();
71            }
72            if span.style.bold {
73                styled = styled.bold();
74            }
75            styled.to_string()
76        })
77        .collect()
78}
79
80fn render_styled_text_single_line(text: &StyledText) -> String {
81    render_styled_text(text).replace('\n', " ")
82}
83
84fn maybe_render<T>(value: &Option<super::display::DisplayField<T>>) -> String {
85    value
86        .as_ref()
87        .map(|value| render_styled_text(&value.display))
88        .unwrap_or_default()
89}
90
91fn status_group_separator() -> tabled::settings::style::VerticalLine<
92    tabled::settings::style::On,
93    tabled::settings::style::On,
94    (),
95> {
96    tabled::settings::style::VerticalLine::new('│')
97        .top('┬')
98        .bottom('┴')
99}
100
101fn suppress_iteration_header_separators(
102    rendered: &str,
103    suppress_middle_separator: bool,
104    suppress_tail_separator: bool,
105) -> String {
106    rendered
107        .lines()
108        .enumerate()
109        .map(|(line_index, line)| {
110            if line_index != 1 {
111                return line.to_string();
112            }
113
114            let vertical_positions = line.match_indices('│').map(|(idx, _)| idx).collect_vec();
115            if vertical_positions.len() < 3 {
116                return line.to_string();
117            }
118
119            let mut updated = line.to_string();
120            if suppress_tail_separator {
121                let suppressed_index = vertical_positions[vertical_positions.len() - 2];
122                updated.replace_range(suppressed_index..suppressed_index + '│'.len_utf8(), " ");
123            }
124            if suppress_middle_separator && vertical_positions.len() >= 4 {
125                let suppressed_index = vertical_positions[1];
126                updated.replace_range(suppressed_index..suppressed_index + '│'.len_utf8(), " ");
127            }
128            updated
129        })
130        .join("\n")
131}
132
133fn hide_hidden_vertical_boundaries(
134    rendered: &str,
135    hidden_vertical_boundaries: &[usize],
136    full_row_vertical_count: usize,
137) -> String {
138    rendered
139        .lines()
140        .map(|line| {
141            let vertical_positions = line.match_indices('│').map(|(idx, _)| idx).collect_vec();
142            if vertical_positions.len() != full_row_vertical_count {
143                return line.to_string();
144            }
145
146            let mut updated = line.to_string();
147            for boundary in hidden_vertical_boundaries.iter().rev() {
148                let Some(position) = vertical_positions.get(boundary + 1).copied() else {
149                    continue;
150                };
151                updated.replace_range(position..position + '│'.len_utf8(), " ");
152            }
153            updated
154        })
155        .join("\n")
156}
157
158fn insert_separator_rows(rendered: &str, separator_after_rows: &[usize]) -> String {
159    if separator_after_rows.is_empty() {
160        return rendered.to_string();
161    }
162
163    let mut lines = rendered.lines().map(str::to_string).collect_vec();
164    let width = lines
165        .first()
166        .map(|line| line.chars().count())
167        .unwrap_or_default();
168    if width < 2 {
169        return rendered.to_string();
170    }
171    let separator = format!("├{}┤", "─".repeat(width - 2));
172
173    for (inserted, row) in separator_after_rows.iter().enumerate() {
174        let insert_at = *row + 2 + inserted;
175        lines.insert(insert_at, separator.clone());
176    }
177
178    lines.join("\n")
179}
180
181fn suppress_spanned_metadata_header_separators(rendered: &str) -> String {
182    rendered
183        .lines()
184        .map(|line| {
185            if !line.contains("χ²/dof") || !line.contains("mwi") {
186                return line.to_string();
187            }
188
189            let mut updated = line.to_string();
190            let separators_to_remove = [("χ²/dof", "mwi"), ("Δ [σ]", "Δ [%]")];
191
192            for (left_label, right_label) in separators_to_remove {
193                let Some(left_start) = updated.find(left_label) else {
194                    continue;
195                };
196                let Some(right_start) = updated.find(right_label) else {
197                    continue;
198                };
199                let left_end = left_start + left_label.len();
200                if let Some((separator_index, _)) = updated[left_end..right_start]
201                    .match_indices('│')
202                    .next_back()
203                {
204                    let separator_index = left_end + separator_index;
205                    updated.replace_range(separator_index..separator_index + '│'.len_utf8(), " ");
206                }
207            }
208
209            updated
210        })
211        .join("\n")
212}
213
214fn render_tables_with_shared_width(mut tables: Vec<StatusTable>, max_table_width: usize) -> String {
215    let max_width = tables
216        .iter()
217        .map(|table| table.table.total_width())
218        .max()
219        .unwrap_or(0)
220        .min(max_table_width.max(1));
221
222    for table in &mut tables {
223        if table.table.total_width() < max_width {
224            table.table.with(Width::increase(max_width));
225        }
226    }
227
228    tables
229        .into_iter()
230        .map(|table| {
231            let mut rendered = table.table.to_string();
232            rendered = hide_hidden_vertical_boundaries(
233                &rendered,
234                &table.hidden_vertical_boundaries,
235                table.full_row_vertical_count,
236            );
237            if table.suppress_header_middle_separator || table.suppress_header_tail_separator {
238                rendered = suppress_iteration_header_separators(
239                    &rendered,
240                    table.suppress_header_middle_separator,
241                    table.suppress_header_tail_separator,
242                );
243            }
244            rendered = suppress_spanned_metadata_header_separators(&rendered);
245            rendered = insert_separator_rows(&rendered, &table.separator_after_rows);
246            normalize_tabled_separator_rows(&rendered)
247        })
248        .collect_vec()
249        .join("\n")
250}
251
252fn build_main_results_table(
253    section: &MainResultsSection,
254    options: &TabledRenderOptions,
255) -> StatusTable {
256    let visible_row_groups = section
257        .row_groups
258        .iter()
259        .filter(|group| match group.kind {
260            MainResultsRowGroupKind::All => true,
261            MainResultsRowGroupKind::Sum => options.show_discrete_contributions_sum,
262            MainResultsRowGroupKind::Bins => options.show_top_discrete_grid,
263        })
264        .collect_vec();
265    let show_discrete_columns = section.has_discrete_columns
266        && (options.show_top_discrete_grid || options.show_discrete_contributions_sum);
267    let slot_block_width = if show_discrete_columns { 5 } else { 2 };
268    let metadata_columns = if section.has_target_columns { 4 } else { 2 };
269    let n_columns = 2 + section.slot_headers.len() * slot_block_width + metadata_columns;
270
271    let mut builder = Builder::new();
272    let mut first_row = vec![
273        render_styled_text(&section.header_left),
274        String::new(),
275        render_styled_text(&section.header_middle),
276    ];
277    first_row.resize(n_columns, String::new());
278    first_row[n_columns - 2] = render_styled_text(&section.header_tail);
279    builder.push_record(first_row);
280
281    let mut header_row = vec![
282        render_styled_text_single_line(&section.contribution_header),
283        String::new(),
284    ];
285    for slot_header in &section.slot_headers {
286        header_row.push(render_styled_text(slot_header));
287        header_row.extend(std::iter::repeat_n(String::new(), slot_block_width - 1));
288    }
289    header_row.extend(
290        section
291            .metadata_headers()
292            .into_iter()
293            .map(|header| render_styled_text(&header)),
294    );
295    builder.push_record(header_row);
296
297    for group in &visible_row_groups {
298        for row in &group.rows {
299            let mut record = vec![
300                render_styled_text(&row.contribution.display),
301                render_styled_text(&row.component.display),
302            ];
303            for slot_cells in &row.slot_cells {
304                let statistics = slot_cells.statistics(IntegralView::Signed);
305                record.push(maybe_render(&statistics.value));
306                record.push(maybe_render(&statistics.relative_error));
307                if show_discrete_columns {
308                    record.push(maybe_render(&slot_cells.sample_fraction));
309                    record.push(maybe_render(&slot_cells.sample_count));
310                    record.push(maybe_render(&slot_cells.target_pdf));
311                }
312            }
313            let slot0_statistics = row
314                .slot_cells
315                .first()
316                .map(|cell| cell.statistics(IntegralView::Signed));
317            record.push(
318                slot0_statistics
319                    .and_then(|statistics| statistics.chi_sq.as_ref())
320                    .map(|field| render_styled_text(&field.display))
321                    .unwrap_or_default(),
322            );
323            record.push(
324                slot0_statistics
325                    .and_then(|statistics| statistics.max_weight_impact.as_ref())
326                    .map(|field| render_styled_text(&field.display))
327                    .unwrap_or_default(),
328            );
329            if section.has_target_columns {
330                record.push(maybe_render(&row.delta_sigma));
331                record.push(maybe_render(&row.delta_percent));
332            }
333            builder.push_record(record);
334
335            if group.kind == MainResultsRowGroupKind::All {
336                let mut continuation = vec![
337                    String::new(),
338                    render_styled_text(
339                        &row.component
340                            .raw
341                            .label_display_for_view(IntegralView::Absolute),
342                    ),
343                ];
344                for slot_cells in &row.slot_cells {
345                    continuation.push(maybe_render(
346                        &slot_cells.statistics(IntegralView::Absolute).value,
347                    ));
348                    continuation.push(String::new());
349                    if show_discrete_columns {
350                        continuation.extend(std::iter::repeat_n(String::new(), 3));
351                    }
352                }
353                continuation.extend(std::iter::repeat_n(String::new(), metadata_columns));
354                builder.push_record(continuation);
355            }
356        }
357    }
358
359    let mut table = builder.build();
360    table.modify((0, 0), Span::column(2));
361    table.modify((0, 2), Span::column((n_columns - 4) as isize));
362    table.modify((0, n_columns - 2), Span::column(2));
363    table.modify((1, 0), Span::column(2));
364    for (slot_index, _) in section.slot_headers.iter().enumerate() {
365        table.modify(
366            (1, 2 + slot_index * slot_block_width),
367            Span::column(slot_block_width as isize),
368        );
369    }
370
371    let first_metadata_column = 2 + section.slot_headers.len() * slot_block_width;
372
373    let mut separator_rows = vec![1usize, 2usize];
374    let mut row_offset = 2usize;
375    for (group_index, group) in visible_row_groups.iter().enumerate() {
376        row_offset += group.rows.len()
377            * if group.kind == MainResultsRowGroupKind::All {
378                2
379            } else {
380                1
381            };
382        if group_index + 1 < visible_row_groups.len() {
383            separator_rows.push(row_offset);
384        }
385    }
386    separator_rows.sort_unstable();
387    separator_rows.dedup();
388
389    table.with(Style::rounded().remove_horizontals());
390    table.with(BorderCorrection::span());
391    table.with(Modify::new(Rows::new(0..)).with(Alignment::left()));
392    table.with(Modify::new(Cell::new(0, 2)).with(Alignment::center()));
393    table.with(Modify::new(Cell::new(0, n_columns - 2)).with(Alignment::center()));
394    table.with(Modify::new(Rows::new(1..2)).with(Alignment::center()));
395
396    let mut hidden_vertical_boundaries = vec![0usize];
397    for slot_index in 0..section.slot_headers.len() {
398        let block_start = 2 + slot_index * slot_block_width;
399        hidden_vertical_boundaries.extend(block_start..(block_start + slot_block_width - 1));
400    }
401    hidden_vertical_boundaries.push(first_metadata_column);
402    if section.has_target_columns {
403        hidden_vertical_boundaries.push(first_metadata_column + 2);
404    }
405
406    StatusTable {
407        table,
408        separator_after_rows: separator_rows
409            .into_iter()
410            .map(|row| row.saturating_sub(1))
411            .collect(),
412        hidden_vertical_boundaries,
413        full_row_vertical_count: n_columns + 1,
414        suppress_header_middle_separator: true,
415        suppress_header_tail_separator: true,
416    }
417}
418
419fn build_max_weight_details_table(section: &MaxWeightDetailsSection) -> StatusTable {
420    let mut builder = Builder::new();
421    let headers = section.headers();
422    builder.push_record([
423        render_styled_text(&headers[0]),
424        render_styled_text(&headers[1]),
425        render_styled_text(&headers[2]),
426        render_styled_text(&headers[3]),
427    ]);
428
429    for group in &section.rows_by_slot {
430        for row in group {
431            builder.push_record([
432                render_styled_text(&row.slot.display),
433                render_styled_text(&row.component_sign.display),
434                render_styled_text(&row.max_eval.display),
435                render_styled_text(&row.coordinates.display),
436            ]);
437        }
438    }
439
440    let mut table = builder.build();
441    table.modify((0, 0), Span::column(2));
442    table.with(Panel::header(render_styled_text(&section.title())));
443    table.with(Style::rounded().remove_horizontals());
444    table.with(BorderCorrection::span());
445    table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
446    table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
447
448    let mut separator_rows = vec![0usize, 1usize];
449    let mut row_offset = 1usize;
450    for (group_index, group) in section.rows_by_slot.iter().enumerate() {
451        row_offset += group.len();
452        if group_index + 1 < section.rows_by_slot.len() {
453            separator_rows.push(row_offset);
454        }
455    }
456
457    StatusTable {
458        table,
459        separator_after_rows: separator_rows,
460        hidden_vertical_boundaries: vec![0],
461        full_row_vertical_count: 5,
462        suppress_header_middle_separator: false,
463        suppress_header_tail_separator: false,
464    }
465}
466
467fn build_discrete_max_weight_details_table(
468    section: &DiscreteMaxWeightDetailsSection,
469) -> StatusTable {
470    let mut builder = Builder::new();
471    let mut header = section
472        .summary_headers()
473        .iter()
474        .map(render_styled_text_single_line)
475        .collect_vec();
476    header.push(render_styled_text(&section.coordinates_header()));
477    builder.push_record(header);
478
479    for group in &section.row_groups {
480        for row in group {
481            let mut record = vec![
482                render_styled_text(&row.contribution.display),
483                render_styled_text(&row.component_sign.display),
484            ];
485            record.extend(row.slot_values.iter().map(|value| {
486                value
487                    .as_ref()
488                    .map(|value| render_styled_text(&value.display))
489                    .unwrap_or_default()
490            }));
491            record.push(
492                row.slot_coordinates
493                    .iter()
494                    .map(|entry| {
495                        format!(
496                            "{}: {}",
497                            render_styled_text(&entry.slot.display),
498                            render_styled_text(&entry.coordinates.display),
499                        )
500                    })
501                    .join("\n"),
502            );
503            builder.push_record(record);
504        }
505    }
506
507    let mut table = builder.build();
508    table.modify((0, 0), Span::column(2));
509    table.with(Panel::header(render_styled_text(&section.title())));
510
511    let mut separator_rows = vec![1usize, 2usize];
512    let mut row_offset = 1usize;
513    for (group_index, group) in section.row_groups.iter().enumerate() {
514        row_offset += group.len();
515        if group_index + 1 < section.row_groups.len() {
516            separator_rows.push(row_offset + 1);
517        }
518    }
519
520    table.with(Style::rounded().remove_horizontals());
521    table.with(BorderCorrection::span());
522    table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
523    table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
524
525    StatusTable {
526        table,
527        separator_after_rows: separator_rows
528            .into_iter()
529            .map(|row| row.saturating_sub(1))
530            .collect(),
531        hidden_vertical_boundaries: vec![0],
532        full_row_vertical_count: section.slot_headers.len() + 4,
533        suppress_header_middle_separator: false,
534        suppress_header_tail_separator: false,
535    }
536}
537
538fn build_statistics_table(section: &StatisticsSection) -> StatusTable {
539    let rows = section.table_rows(StatisticsScope::Global);
540    let mut builder = Builder::new();
541    for row in &rows {
542        let mut record = vec![render_styled_text(&row.row_label)];
543        for entry in &row.entries {
544            record.push(render_styled_text(&entry.label));
545            record.push(render_styled_text(&entry.value));
546        }
547        builder.push_record(record);
548    }
549
550    let mut table = builder.build();
551    table.with(Panel::header(render_styled_text(
552        &section.statistics_title(StatisticsScope::Global),
553    )));
554    table.with(
555        Style::rounded()
556            .remove_horizontals()
557            .verticals([
558                (1, status_group_separator()),
559                (3, status_group_separator()),
560                (5, status_group_separator()),
561                (7, status_group_separator()),
562            ])
563            .horizontals([(
564                1,
565                HorizontalLine::new('─')
566                    .intersection('┬')
567                    .left('├')
568                    .right('┤'),
569            )])
570            .remove_vertical(),
571    );
572    table.with(BorderCorrection::span());
573    table.with(Modify::new(Rows::new(0..1)).with(Alignment::center()));
574    table.with(Modify::new(Rows::new(1..)).with(Alignment::left()));
575    for column in [1usize, 3, 5, 7] {
576        table.with(
577            Modify::new(Rows::new(1..).intersect(Columns::one(column))).with(Alignment::right()),
578        );
579    }
580
581    StatusTable {
582        table,
583        separator_after_rows: Vec::new(),
584        hidden_vertical_boundaries: Vec::new(),
585        full_row_vertical_count: 0,
586        suppress_header_middle_separator: false,
587        suppress_header_tail_separator: false,
588    }
589}
590
591pub(crate) fn render_status_update(update: &StatusUpdate, options: &TabledRenderOptions) -> String {
592    let mut tables = vec![build_main_results_table(&update.main_results, options)];
593    if options.show_max_weight_details
594        && let Some(section) = update.max_weight_details(IntegralView::Signed)
595    {
596        tables.push(build_max_weight_details_table(section));
597    }
598    if options.show_max_weight_details
599        && options.show_max_weight_info_for_discrete_bins
600        && let Some(section) = update.discrete_max_weight_details(IntegralView::Signed)
601    {
602        tables.push(build_discrete_max_weight_details_table(section));
603    }
604    if options.show_statistics
605        && let Some(section) = update.statistics.as_ref()
606    {
607        tables.push(build_statistics_table(section));
608    }
609
610    render_tables_with_shared_width(tables, options.max_table_width)
611}