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(§ion.header_left),
274 String::new(),
275 render_styled_text(§ion.header_middle),
276 ];
277 first_row.resize(n_columns, String::new());
278 first_row[n_columns - 2] = render_styled_text(§ion.header_tail);
279 builder.push_record(first_row);
280
281 let mut header_row = vec![
282 render_styled_text_single_line(§ion.contribution_header),
283 String::new(),
284 ];
285 for slot_header in §ion.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 §ion.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(§ion.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(§ion.coordinates_header()));
477 builder.push_record(header);
478
479 for group in §ion.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(§ion.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 §ion.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}