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, 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                record.push(maybe_render(&slot_cells.value));
305                record.push(maybe_render(&slot_cells.relative_error));
306                if show_discrete_columns {
307                    record.push(maybe_render(&slot_cells.sample_fraction));
308                    record.push(maybe_render(&slot_cells.sample_count));
309                    record.push(maybe_render(&slot_cells.target_pdf));
310                }
311            }
312            record.push(maybe_render(&row.chi_sq));
313            record.push(maybe_render(&row.max_weight_impact));
314            if section.has_target_columns {
315                record.push(maybe_render(&row.delta_sigma));
316                record.push(maybe_render(&row.delta_percent));
317            }
318            builder.push_record(record);
319        }
320    }
321
322    let mut table = builder.build();
323    table.modify((0, 0), Span::column(2));
324    table.modify((0, 2), Span::column((n_columns - 4) as isize));
325    table.modify((0, n_columns - 2), Span::column(2));
326    table.modify((1, 0), Span::column(2));
327    for (slot_index, _) in section.slot_headers.iter().enumerate() {
328        table.modify(
329            (1, 2 + slot_index * slot_block_width),
330            Span::column(slot_block_width as isize),
331        );
332    }
333
334    let first_metadata_column = 2 + section.slot_headers.len() * slot_block_width;
335
336    let mut separator_rows = vec![1usize, 2usize];
337    let mut row_offset = 2usize;
338    for (group_index, group) in visible_row_groups.iter().enumerate() {
339        row_offset += group.rows.len();
340        if group_index + 1 < visible_row_groups.len() {
341            separator_rows.push(row_offset);
342        }
343    }
344    separator_rows.sort_unstable();
345    separator_rows.dedup();
346
347    table.with(Style::rounded().remove_horizontals());
348    table.with(BorderCorrection::span());
349    table.with(Modify::new(Rows::new(0..)).with(Alignment::left()));
350    table.with(Modify::new(Cell::new(0, 2)).with(Alignment::center()));
351    table.with(Modify::new(Cell::new(0, n_columns - 2)).with(Alignment::center()));
352    table.with(Modify::new(Rows::new(1..2)).with(Alignment::center()));
353
354    let mut hidden_vertical_boundaries = vec![0usize];
355    for slot_index in 0..section.slot_headers.len() {
356        let block_start = 2 + slot_index * slot_block_width;
357        hidden_vertical_boundaries.extend(block_start..(block_start + slot_block_width - 1));
358    }
359    hidden_vertical_boundaries.push(first_metadata_column);
360    if section.has_target_columns {
361        hidden_vertical_boundaries.push(first_metadata_column + 2);
362    }
363
364    StatusTable {
365        table,
366        separator_after_rows: separator_rows
367            .into_iter()
368            .map(|row| row.saturating_sub(1))
369            .collect(),
370        hidden_vertical_boundaries,
371        full_row_vertical_count: n_columns + 1,
372        suppress_header_middle_separator: true,
373        suppress_header_tail_separator: true,
374    }
375}
376
377fn build_max_weight_details_table(section: &MaxWeightDetailsSection) -> StatusTable {
378    let mut builder = Builder::new();
379    let headers = section.headers();
380    builder.push_record([
381        render_styled_text(&headers[0]),
382        render_styled_text(&headers[1]),
383        render_styled_text(&headers[2]),
384        render_styled_text(&headers[3]),
385    ]);
386
387    for group in &section.rows_by_slot {
388        for row in group {
389            builder.push_record([
390                render_styled_text(&row.slot.display),
391                render_styled_text(&row.component_sign.display),
392                render_styled_text(&row.max_eval.display),
393                render_styled_text(&row.coordinates.display),
394            ]);
395        }
396    }
397
398    let mut table = builder.build();
399    table.modify((0, 0), Span::column(2));
400    table.with(Panel::header(render_styled_text(&section.title())));
401    table.with(Style::rounded().remove_horizontals());
402    table.with(BorderCorrection::span());
403    table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
404    table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
405
406    let mut separator_rows = vec![0usize, 1usize];
407    let mut row_offset = 1usize;
408    for (group_index, group) in section.rows_by_slot.iter().enumerate() {
409        row_offset += group.len();
410        if group_index + 1 < section.rows_by_slot.len() {
411            separator_rows.push(row_offset);
412        }
413    }
414
415    StatusTable {
416        table,
417        separator_after_rows: separator_rows,
418        hidden_vertical_boundaries: vec![0],
419        full_row_vertical_count: 5,
420        suppress_header_middle_separator: false,
421        suppress_header_tail_separator: false,
422    }
423}
424
425fn build_discrete_max_weight_details_table(
426    section: &DiscreteMaxWeightDetailsSection,
427) -> StatusTable {
428    let mut builder = Builder::new();
429    let mut header = section
430        .summary_headers()
431        .iter()
432        .map(render_styled_text_single_line)
433        .collect_vec();
434    header.push(render_styled_text(&section.coordinates_header()));
435    builder.push_record(header);
436
437    for group in &section.row_groups {
438        for row in group {
439            let mut record = vec![
440                render_styled_text(&row.contribution.display),
441                render_styled_text(&row.component_sign.display),
442            ];
443            record.extend(row.slot_values.iter().map(|value| {
444                value
445                    .as_ref()
446                    .map(|value| render_styled_text(&value.display))
447                    .unwrap_or_default()
448            }));
449            record.push(
450                row.slot_coordinates
451                    .iter()
452                    .map(|entry| {
453                        format!(
454                            "{}: {}",
455                            render_styled_text(&entry.slot.display),
456                            render_styled_text(&entry.coordinates.display),
457                        )
458                    })
459                    .join("\n"),
460            );
461            builder.push_record(record);
462        }
463    }
464
465    let mut table = builder.build();
466    table.modify((0, 0), Span::column(2));
467    table.with(Panel::header(render_styled_text(&section.title())));
468
469    let mut separator_rows = vec![1usize, 2usize];
470    let mut row_offset = 1usize;
471    for (group_index, group) in section.row_groups.iter().enumerate() {
472        row_offset += group.len();
473        if group_index + 1 < section.row_groups.len() {
474            separator_rows.push(row_offset + 1);
475        }
476    }
477
478    table.with(Style::rounded().remove_horizontals());
479    table.with(BorderCorrection::span());
480    table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
481    table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
482
483    StatusTable {
484        table,
485        separator_after_rows: separator_rows
486            .into_iter()
487            .map(|row| row.saturating_sub(1))
488            .collect(),
489        hidden_vertical_boundaries: vec![0],
490        full_row_vertical_count: section.slot_headers.len() + 4,
491        suppress_header_middle_separator: false,
492        suppress_header_tail_separator: false,
493    }
494}
495
496fn build_statistics_table(section: &StatisticsSection) -> StatusTable {
497    let rows = section.table_rows(StatisticsScope::Global);
498    let mut builder = Builder::new();
499    for row in &rows {
500        let mut record = vec![render_styled_text(&row.row_label)];
501        for entry in &row.entries {
502            record.push(render_styled_text(&entry.label));
503            record.push(render_styled_text(&entry.value));
504        }
505        builder.push_record(record);
506    }
507
508    let mut table = builder.build();
509    table.with(Panel::header(render_styled_text(
510        &section.statistics_title(StatisticsScope::Global),
511    )));
512    table.with(
513        Style::rounded()
514            .remove_horizontals()
515            .verticals([
516                (1, status_group_separator()),
517                (3, status_group_separator()),
518                (5, status_group_separator()),
519                (7, status_group_separator()),
520            ])
521            .horizontals([(
522                1,
523                HorizontalLine::new('─')
524                    .intersection('┬')
525                    .left('├')
526                    .right('┤'),
527            )])
528            .remove_vertical(),
529    );
530    table.with(BorderCorrection::span());
531    table.with(Modify::new(Rows::new(0..1)).with(Alignment::center()));
532    table.with(Modify::new(Rows::new(1..)).with(Alignment::left()));
533    for column in [1usize, 3, 5, 7] {
534        table.with(
535            Modify::new(Rows::new(1..).intersect(Columns::one(column))).with(Alignment::right()),
536        );
537    }
538
539    StatusTable {
540        table,
541        separator_after_rows: Vec::new(),
542        hidden_vertical_boundaries: Vec::new(),
543        full_row_vertical_count: 0,
544        suppress_header_middle_separator: false,
545        suppress_header_tail_separator: false,
546    }
547}
548
549pub(crate) fn render_status_update(update: &StatusUpdate, options: &TabledRenderOptions) -> String {
550    let mut tables = vec![build_main_results_table(&update.main_results, options)];
551    if options.show_max_weight_details
552        && let Some(section) = update.max_weight_details.as_ref()
553    {
554        tables.push(build_max_weight_details_table(section));
555    }
556    if options.show_max_weight_details
557        && options.show_max_weight_info_for_discrete_bins
558        && let Some(section) = update.discrete_max_weight_details.as_ref()
559    {
560        tables.push(build_discrete_max_weight_details_table(section));
561    }
562    if options.show_statistics
563        && let Some(section) = update.statistics.as_ref()
564    {
565        tables.push(build_statistics_table(section));
566    }
567
568    render_tables_with_shared_width(tables, options.max_table_width)
569}