| 36 |
36 |
|
//! it did it would be a layout engine with one consumer's flow baked into it.
|
| 37 |
37 |
|
|
| 38 |
38 |
|
use makeover_layout::{
|
| 39 |
|
- |
Act, Awaiting, Field, FieldKind, Figure, Heading, Meter, ThemeVariant, Token, Tone,
|
|
39 |
+ |
Act, Awaiting, Bar, Chart, Field, FieldKind, Figure, Heading, Meter, ThemeVariant, Token, Tone,
|
| 40 |
40 |
|
};
|
| 41 |
41 |
|
use ratatui::buffer::Buffer;
|
| 42 |
42 |
|
use ratatui::layout::Rect;
|
| 991 |
991 |
|
|
| 992 |
992 |
|
#[cfg(test)]
|
| 993 |
993 |
|
mod tests;
|
|
994 |
+ |
|
|
995 |
+ |
/// A chart, one line per bar.
|
|
996 |
+ |
///
|
|
997 |
+ |
/// # Why the bars lie down here
|
|
998 |
+ |
///
|
|
999 |
+ |
/// A webview draws a chart as columns standing on an axis, and a terminal has
|
|
1000 |
+ |
/// one glyph per cell and a handful of rows. Standing the bars up would mean
|
|
1001 |
+ |
/// drawing each one as a stack of partial blocks and giving up the labels,
|
|
1002 |
+ |
/// which are the half a reader actually reads. Laid down, every bar keeps its
|
|
1003 |
+ |
/// place on the axis, its magnitude and its reading, and the drawing is
|
|
1004 |
+ |
/// [`meter`]'s repeated -- which is the honest answer for the same reason
|
|
1005 |
+ |
/// `quasi-tui`'s timeline draws no gridlines: a terminal draws what a terminal
|
|
1006 |
+ |
/// draws rather than an impression of the other renderer.
|
|
1007 |
+ |
///
|
|
1008 |
+ |
/// The axis is not drawn as a rule or a scale, for that same reason. It is
|
|
1009 |
+ |
/// stated instead: every bar is `meter_cells` wide and full means
|
|
1010 |
+ |
/// [`Chart::most`], so the widths are comparable across the run, which is the
|
|
1011 |
+ |
/// one thing a chart has to get right.
|
|
1012 |
+ |
///
|
|
1013 |
+ |
/// # What is left out
|
|
1014 |
+ |
///
|
|
1015 |
+ |
/// [`Chart::label`] is not drawn. It names what the magnitudes are and every
|
|
1016 |
+ |
/// bar's own [`Bar::reading`] already carries the units, so drawing it would be
|
|
1017 |
+ |
/// a heading this function does not own the room for. A caller that wants it
|
|
1018 |
+ |
/// says it as a heading, which is what a description does anyway.
|
|
1019 |
+ |
///
|
|
1020 |
+ |
/// Labels are padded to the widest, so the bars line up. That is measured in
|
|
1021 |
+ |
/// characters rather than in display cells, which is wrong for a label holding
|
|
1022 |
+ |
/// a wide glyph and is what [`crate::text`] would cost to bring in for a case
|
|
1023 |
+ |
/// that has not turned up.
|
|
1024 |
+ |
#[must_use]
|
|
1025 |
+ |
pub fn chart(style: &PieceStyle, chart: &Chart<'_>, bars: &[Bar<'_>]) -> Vec<Line<'static>> {
|
|
1026 |
+ |
let widest = bars
|
|
1027 |
+ |
.iter()
|
|
1028 |
+ |
.map(|bar| bar.at.chars().count())
|
|
1029 |
+ |
.max()
|
|
1030 |
+ |
.unwrap_or(0);
|
|
1031 |
+ |
bars.iter()
|
|
1032 |
+ |
.map(|bar| chart_line(style, chart, bar, widest))
|
|
1033 |
+ |
.collect()
|
|
1034 |
+ |
}
|
|
1035 |
+ |
|
|
1036 |
+ |
/// One bar's line: where it sits, how far it reaches, and what it says.
|
|
1037 |
+ |
fn chart_line(
|
|
1038 |
+ |
style: &PieceStyle,
|
|
1039 |
+ |
chart: &Chart<'_>,
|
|
1040 |
+ |
bar: &Bar<'_>,
|
|
1041 |
+ |
widest: usize,
|
|
1042 |
+ |
) -> Line<'static> {
|
|
1043 |
+ |
let cells = usize::from(style.meter_cells);
|
|
1044 |
+ |
// Rounded rather than truncated, so a bar that is nearly full does not read
|
|
1045 |
+ |
// as one cell short of every other. The multiplication is done before the
|
|
1046 |
+ |
// division for the reason it is in `meter`: in integers, the other order is
|
|
1047 |
+ |
// zero.
|
|
1048 |
+ |
let filled = if chart.most == 0 {
|
|
1049 |
+ |
0
|
|
1050 |
+ |
} else {
|
|
1051 |
+ |
let scaled = (bar.value as u128 * cells as u128).div_ceil(chart.most as u128);
|
|
1052 |
+ |
(scaled as usize).min(cells)
|
|
1053 |
+ |
};
|
|
1054 |
+ |
|
|
1055 |
+ |
let mut spans = vec![Span::styled(
|
|
1056 |
+ |
format!("{:width$} ", bar.at, width = widest),
|
|
1057 |
+ |
style.secondary,
|
|
1058 |
+ |
)];
|
|
1059 |
+ |
spans.push(Span::styled(
|
|
1060 |
+ |
format!(
|
|
1061 |
+ |
"{}{}",
|
|
1062 |
+ |
style.meter_full.to_string().repeat(filled),
|
|
1063 |
+ |
style.meter_empty.to_string().repeat(cells - filled)
|
|
1064 |
+ |
),
|
|
1065 |
+ |
style.tone(chart.tone),
|
|
1066 |
+ |
));
|
|
1067 |
+ |
if let Some(reading) = chart_reading(bar) {
|
|
1068 |
+ |
spans.push(Span::styled(reading, style.muted));
|
|
1069 |
+ |
}
|
|
1070 |
+ |
Line::from(spans)
|
|
1071 |
+ |
}
|
|
1072 |
+ |
|
|
1073 |
+ |
/// What a bar says beside its own drawing, or nothing.
|
|
1074 |
+ |
///
|
|
1075 |
+ |
/// The webview's `bar_text` in this renderer's spelling. Both facts joined the
|
|
1076 |
+ |
/// same way, and both left out when the description carried neither.
|
|
1077 |
+ |
fn chart_reading(bar: &Bar<'_>) -> Option<String> {
|
|
1078 |
+ |
match (bar.reading, bar.note) {
|
|
1079 |
+ |
(Some(reading), Some(note)) => Some(format!(" {reading} / {note}")),
|
|
1080 |
+ |
(Some(only), None) | (None, Some(only)) => Some(format!(" {only}")),
|
|
1081 |
+ |
(None, None) => None,
|
|
1082 |
+ |
}
|
|
1083 |
+ |
}
|