mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Add stacked chart primitives for account analytics (#45763)
## What changed Add daily stacked bar rendering in the TUI analytics module, staged for dashboard integration. - Plot three categories plus an Other remainder without renormalizing values, with separate positive and negative bands for signed credits. - Add readable numeric axes, calendar ticks, a selection cursor, and a callout showing the authoritative daily total. Distinguish missing days from zero usage. - Keep the selection visible in narrow viewports and omit labels that cannot fit without truncation. - Use terminal palette colors for series and adapt secondary text to terminal color support and background. ## Testing Add unit and snapshot coverage for axis scaling, signed bars on light and dark terminals, missing and zero values, narrow layouts, tiny segments, authoritative totals, and duplicate-date remainders. GitOrigin-RevId: 2ed58def17abd14e77929016a54fd20e91d45670
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
//! Account analytics data preparation, staged for the dashboard integration.
|
||||
//! Account analytics data and chart primitives, staged for the dashboard integration.
|
||||
|
||||
mod client;
|
||||
mod data;
|
||||
mod models;
|
||||
mod normalize;
|
||||
mod plot;
|
||||
mod render;
|
||||
mod report_data;
|
||||
mod styles;
|
||||
mod tokens;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
183
codex-rs/tui/src/analytics/plot.rs
Normal file
183
codex-rs/tui/src/analytics/plot.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
//! Daily stacked bars with fixed calendar ticks, a selection cursor, and an anchored value.
|
||||
//! Relative usage retains the server's scale; signed credits keep both bands.
|
||||
|
||||
mod annotations;
|
||||
mod layout;
|
||||
mod painting;
|
||||
|
||||
use self::layout::Layout;
|
||||
use self::painting::Band;
|
||||
use super::data;
|
||||
use super::render::parts;
|
||||
use crate::analytics::models::AccountAnalyticsDay;
|
||||
use crate::analytics::models::AccountAnalyticsUnit;
|
||||
use crate::analytics::models::AccountAnalyticsValue;
|
||||
use chrono::NaiveDate;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Share the chart's scale with the selected-day summary, including signed values.
|
||||
pub(super) fn peak(
|
||||
days: &[(NaiveDate, Option<&AccountAnalyticsDay>)],
|
||||
categories: &[AccountAnalyticsValue],
|
||||
) -> f64 {
|
||||
days.iter()
|
||||
.filter_map(|(_, day)| *day)
|
||||
.map(|day| {
|
||||
parts(&day.values, categories)
|
||||
.iter()
|
||||
.map(|value| value.abs())
|
||||
.sum::<f64>()
|
||||
.max(day.total.abs())
|
||||
})
|
||||
.fold(/*init*/ 0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Two readable intervals cover the range; counts never use fractional ticks.
|
||||
pub(super) fn axis_max(peak: f64, unit: AccountAnalyticsUnit) -> f64 {
|
||||
if peak == 0.0 {
|
||||
return if matches!(
|
||||
unit,
|
||||
AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens
|
||||
) {
|
||||
2.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
}
|
||||
let step = if matches!(
|
||||
unit,
|
||||
AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens
|
||||
) {
|
||||
(peak / 2.0).max(/*other*/ 1.0)
|
||||
} else {
|
||||
peak / 2.0
|
||||
};
|
||||
let magnitude = 10.0_f64.powf(step.log10().floor());
|
||||
let normalized = step / magnitude;
|
||||
let multiple = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0]
|
||||
.into_iter()
|
||||
.find(|multiple| {
|
||||
*multiple >= normalized
|
||||
&& (!matches!(
|
||||
unit,
|
||||
AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens
|
||||
) || (multiple * magnitude).fract() == 0.0)
|
||||
})
|
||||
.unwrap_or(/*default*/ 10.0);
|
||||
let ceiling = multiple * magnitude * 2.0;
|
||||
if ceiling.is_finite() && ceiling >= peak {
|
||||
ceiling
|
||||
} else {
|
||||
peak
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendered chart geometry exposes its positive and optional negative value bands.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Chart {
|
||||
pub(super) lines: Vec<Line<'static>>,
|
||||
pub(super) bands: usize,
|
||||
}
|
||||
|
||||
/// Render a calendar selection; the cursor must index days and dimensions fit the terminal.
|
||||
pub(super) fn chart(
|
||||
days: &[(NaiveDate, Option<&AccountAnalyticsDay>)],
|
||||
categories: &[AccountAnalyticsValue],
|
||||
cursor: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
unit: AccountAnalyticsUnit,
|
||||
) -> Chart {
|
||||
if days.is_empty() || width == 0 {
|
||||
return Chart::default();
|
||||
}
|
||||
let values = days
|
||||
.iter()
|
||||
.map(|(_, day)| {
|
||||
day.map_or(
|
||||
/*default*/ [0.0; 4],
|
||||
|day| parts(&day.values, categories),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bands = if values.iter().flatten().any(|value| *value < 0.0) {
|
||||
&[Band::Positive, Band::Negative][..]
|
||||
} else {
|
||||
&[Band::Positive][..]
|
||||
};
|
||||
let peak = axis_max(peak(days, categories), unit);
|
||||
let label_width =
|
||||
tick(peak, unit).width().max(tick(peak / 2.0, unit).width()) + bands.len() - 1 + 2;
|
||||
let layout = Layout::new(days.len(), cursor, width, height, label_width, bands);
|
||||
let renderer = Renderer {
|
||||
days,
|
||||
values,
|
||||
cursor,
|
||||
peak,
|
||||
unit,
|
||||
bands,
|
||||
layout,
|
||||
};
|
||||
let mut buf = Buffer::empty(Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
width as u16,
|
||||
(layout.marker_row + 2) as u16,
|
||||
));
|
||||
renderer.paint_bars(&mut buf);
|
||||
renderer.annotate_missing(&mut buf);
|
||||
renderer.paint_axis(&mut buf);
|
||||
renderer.annotate_selection(&mut buf);
|
||||
renderer.annotate_calendar(&mut buf);
|
||||
let lines = buf
|
||||
.content
|
||||
.chunks(width)
|
||||
.map(|row| {
|
||||
Line::from(
|
||||
row.iter()
|
||||
.map(|cell| Span::styled(cell.symbol().to_owned(), cell.style()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Chart {
|
||||
lines,
|
||||
bands: bands.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared inputs for painting and annotations; all phases use the same calendar geometry.
|
||||
struct Renderer<'a> {
|
||||
days: &'a [(NaiveDate, Option<&'a AccountAnalyticsDay>)],
|
||||
values: Vec<[f64; 4]>,
|
||||
cursor: usize,
|
||||
peak: f64,
|
||||
unit: AccountAnalyticsUnit,
|
||||
bands: &'static [Band],
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
fn amount(value: f64, unit: AccountAnalyticsUnit) -> String {
|
||||
match unit {
|
||||
AccountAnalyticsUnit::RelativeUsage => data::amount(value),
|
||||
AccountAnalyticsUnit::Count | AccountAnalyticsUnit::Tokens => data::amount(value.round()),
|
||||
AccountAnalyticsUnit::Credits => data::credit_amount(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(value: f64, unit: AccountAnalyticsUnit) -> String {
|
||||
match unit {
|
||||
AccountAnalyticsUnit::Credits => data::amount(value),
|
||||
AccountAnalyticsUnit::RelativeUsage
|
||||
| AccountAnalyticsUnit::Count
|
||||
| AccountAnalyticsUnit::Tokens => amount(value, unit),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "plot_tests.rs"]
|
||||
mod tests;
|
||||
183
codex-rs/tui/src/analytics/plot/annotations.rs
Normal file
183
codex-rs/tui/src/analytics/plot/annotations.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
//! Missing-day markers, selected-value callouts, and stable calendar labels.
|
||||
|
||||
use super::Renderer;
|
||||
use super::amount;
|
||||
use super::layout::Layout;
|
||||
use super::painting::Band;
|
||||
use crate::analytics::styles::number;
|
||||
use crate::analytics::styles::secondary_style;
|
||||
use crate::style::accent_style;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Styled;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
impl Renderer<'_> {
|
||||
pub(super) fn annotate_missing(&self, buf: &mut Buffer) {
|
||||
let Layout {
|
||||
count,
|
||||
height,
|
||||
start,
|
||||
..
|
||||
} = self.layout;
|
||||
for (index, parts) in self.values[start..start + count].iter().enumerate() {
|
||||
// The selected zero/missing value gets its own callout below; avoid repeating it.
|
||||
if start + index != self.cursor {
|
||||
let marker = match self.days[start + index].1 {
|
||||
None => Some('—'),
|
||||
Some(day) if day.total == 0.0 && parts.iter().all(|value| *value == 0.0) => {
|
||||
Some('0')
|
||||
}
|
||||
Some(_) => None,
|
||||
};
|
||||
if let Some(marker) = marker {
|
||||
buf[(self.layout.center(index) as u16, height as u16)]
|
||||
.set_char(marker)
|
||||
.set_style(Style::default().dim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn annotate_selection(&self, buf: &mut Buffer) {
|
||||
let Layout {
|
||||
axis_width,
|
||||
baseline,
|
||||
height,
|
||||
marker_row,
|
||||
plot_width,
|
||||
start,
|
||||
width,
|
||||
..
|
||||
} = self.layout;
|
||||
let selected_total = self.days[self.cursor]
|
||||
.1
|
||||
.map_or(/*default*/ 0.0, |day| day.total);
|
||||
let selected = self.days[self.cursor]
|
||||
.1
|
||||
.map_or_else(|| "—".into(), |_| amount(selected_total, self.unit));
|
||||
// A partial number can mean a different amount; leave the cursor when the value cannot fit.
|
||||
let selected = if selected.width() <= plot_width {
|
||||
selected
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let x = self.layout.center(self.cursor - start);
|
||||
let label_width = selected.width();
|
||||
let label_x = x
|
||||
.saturating_sub(label_width / 2)
|
||||
.clamp(axis_width, width - label_width);
|
||||
let below = selected_total < 0.0;
|
||||
let band = if below {
|
||||
Band::Negative
|
||||
} else {
|
||||
Band::Positive
|
||||
};
|
||||
let extent = self.values[self.cursor]
|
||||
.iter()
|
||||
.map(|value| band.amount(*value))
|
||||
.sum::<f64>();
|
||||
let rows = ((extent * height as f64 / self.peak).ceil() as usize).min(height);
|
||||
let cap = if below {
|
||||
baseline + rows
|
||||
} else {
|
||||
baseline - rows
|
||||
};
|
||||
let mut label_y = if below {
|
||||
cap + 1
|
||||
} else {
|
||||
cap.saturating_sub(/*rhs*/ 1)
|
||||
};
|
||||
// Keep the callout close without painting over a neighboring bar. A guide connects any gap.
|
||||
while (label_x..label_x + label_width)
|
||||
.any(|col| buf[(col as u16, label_y as u16)].symbol() != " ")
|
||||
{
|
||||
if below {
|
||||
label_y += 1;
|
||||
} else if label_y > 0 {
|
||||
label_y -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let guide = if below {
|
||||
cap + 1..label_y
|
||||
} else {
|
||||
label_y + 1..cap
|
||||
};
|
||||
for y in guide {
|
||||
buf[(x as u16, y as u16)]
|
||||
.set_char('┊')
|
||||
.set_style(accent_style());
|
||||
}
|
||||
buf.set_line(
|
||||
label_x as u16,
|
||||
label_y as u16,
|
||||
&Line::from(number(selected)),
|
||||
label_width as u16,
|
||||
);
|
||||
buf[(x as u16, marker_row as u16)]
|
||||
.set_char('▲')
|
||||
.set_style(accent_style());
|
||||
}
|
||||
|
||||
pub(super) fn annotate_calendar(&self, buf: &mut Buffer) {
|
||||
let Layout {
|
||||
axis_width,
|
||||
count,
|
||||
marker_row,
|
||||
plot_width,
|
||||
start,
|
||||
width,
|
||||
..
|
||||
} = self.layout;
|
||||
// Calendar ticks depend only on the range/width, never on the current selection.
|
||||
let mut occupied = vec![false; width];
|
||||
// Give the two range endpoints priority over intermediate weekly ticks.
|
||||
for index in std::iter::once(/*value*/ 0).chain((1..count).rev()) {
|
||||
if self.days.len() > 7
|
||||
&& !(start + index).is_multiple_of(/*rhs*/ 7)
|
||||
&& index + 1 != count
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let format = if self.days.len() <= 7 && plot_width / count < 7 {
|
||||
"%-d"
|
||||
} else {
|
||||
"%b %-d"
|
||||
};
|
||||
let mut label = self.days[start + index].0.format(format).to_string();
|
||||
if label.width() > plot_width {
|
||||
label = self.days[start + index].0.format("%-d").to_string();
|
||||
}
|
||||
if label.width() > plot_width {
|
||||
continue;
|
||||
}
|
||||
let offset = self
|
||||
.layout
|
||||
.center(index)
|
||||
.saturating_sub(label.len() / 2)
|
||||
.clamp(axis_width, width - label.len());
|
||||
if occupied[offset.saturating_sub(/*rhs*/ 1)..(offset + label.len() + 1).min(width)]
|
||||
.iter()
|
||||
.any(|used| *used)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
occupied[offset..offset + label.len()].fill(/*value*/ true);
|
||||
let label = if start + index == self.cursor {
|
||||
label.set_style(accent_style()).underlined()
|
||||
} else {
|
||||
label.set_style(secondary_style())
|
||||
};
|
||||
buf.set_line(
|
||||
offset as u16,
|
||||
(marker_row + 1) as u16,
|
||||
&Line::from(label),
|
||||
(width - offset) as u16,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
codex-rs/tui/src/analytics/plot/layout.rs
Normal file
55
codex-rs/tui/src/analytics/plot/layout.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Calendar viewport and shared coordinates for bars, axes, and annotations.
|
||||
|
||||
use super::painting::Band;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct Layout {
|
||||
pub(super) width: usize,
|
||||
pub(super) height: usize,
|
||||
pub(super) axis_width: usize,
|
||||
pub(super) plot_width: usize,
|
||||
pub(super) count: usize,
|
||||
pub(super) start: usize,
|
||||
pub(super) baseline: usize,
|
||||
pub(super) marker_row: usize,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub(super) fn new(
|
||||
day_count: usize,
|
||||
cursor: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
label_width: usize,
|
||||
bands: &[Band],
|
||||
) -> Self {
|
||||
// Hide the axis when a complete label would crowd out the bars.
|
||||
let axis_width = if width >= 30 && label_width <= width / 3 {
|
||||
label_width
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let plot_width = width - axis_width;
|
||||
let count = day_count.min(plot_width);
|
||||
let start = cursor.saturating_sub(count - 1).min(day_count - count);
|
||||
let baseline = height + 1;
|
||||
let marker_row = baseline + if bands.len() == 2 { height + 2 } else { 1 };
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
axis_width,
|
||||
plot_width,
|
||||
count,
|
||||
start,
|
||||
baseline,
|
||||
marker_row,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn center(self, index: usize) -> usize {
|
||||
self.axis_width
|
||||
+ (index * self.plot_width / self.count + (index + 1) * self.plot_width / self.count
|
||||
- 1)
|
||||
/ 2
|
||||
}
|
||||
}
|
||||
132
codex-rs/tui/src/analytics/plot/painting.rs
Normal file
132
codex-rs/tui/src/analytics/plot/painting.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Paint signed bar cells and numeric axes using the shared layout and scale.
|
||||
|
||||
use super::Renderer;
|
||||
use super::layout::Layout;
|
||||
use super::tick;
|
||||
use crate::analytics::styles::secondary_style;
|
||||
use crate::analytics::styles::series_colors;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Styled;
|
||||
use ratatui::text::Line;
|
||||
|
||||
/// A signed stack grows away from the baseline; amounts are nonnegative within each band.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum Band {
|
||||
Positive,
|
||||
Negative,
|
||||
}
|
||||
|
||||
impl Band {
|
||||
pub(super) fn amount(self, value: f64) -> f64 {
|
||||
match self {
|
||||
Self::Positive => value,
|
||||
Self::Negative => -value,
|
||||
}
|
||||
.max(/*other*/ 0.0)
|
||||
}
|
||||
|
||||
fn row_y(self, baseline: usize, row: usize) -> usize {
|
||||
match self {
|
||||
Self::Positive => baseline - row - 1,
|
||||
Self::Negative => baseline + row + 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn partial_glyph(self, level: usize) -> char {
|
||||
// Reversing the complementary lower block paints a top-anchored partial refund.
|
||||
let index = match self {
|
||||
Self::Negative if level < 8 => 8 - level,
|
||||
Self::Positive | Self::Negative => level,
|
||||
};
|
||||
[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<'_> {
|
||||
pub(super) fn paint_bars(&self, buf: &mut Buffer) {
|
||||
let Layout {
|
||||
axis_width,
|
||||
baseline,
|
||||
count,
|
||||
height,
|
||||
plot_width,
|
||||
start,
|
||||
..
|
||||
} = self.layout;
|
||||
let colors = series_colors();
|
||||
for (index, parts) in self.values[start..start + count].iter().enumerate() {
|
||||
let left = axis_width + index * plot_width / count;
|
||||
let step = (index + 1) * plot_width / count - index * plot_width / count;
|
||||
let bar_width = (step * 2 / 3).clamp(/*min*/ 1, /*max*/ 12);
|
||||
let x = left + (step - bar_width) / 2;
|
||||
for band in self.bands {
|
||||
let plotted = parts.iter().map(|value| band.amount(*value)).sum::<f64>();
|
||||
let scaled = ((plotted * (height * 8) as f64 / self.peak) as usize)
|
||||
.max(usize::from(plotted > 0.0));
|
||||
for row in 0..height {
|
||||
let level = scaled.saturating_sub(row * 8).min(/*other*/ 8);
|
||||
if level == 0 {
|
||||
continue;
|
||||
}
|
||||
let reverse = matches!(band, Band::Negative) && level < 8;
|
||||
let glyph = band.partial_glyph(level);
|
||||
let bottom = (row * 8) as f64 * self.peak / (height * 8) as f64;
|
||||
let top =
|
||||
((row * 8 + level) as f64 * self.peak / (height * 8) as f64).min(plotted);
|
||||
let sample = bottom + (top - bottom) / 2.0;
|
||||
let mut cumulative = 0.0;
|
||||
let color = parts
|
||||
.iter()
|
||||
.position(|value| {
|
||||
cumulative += band.amount(*value);
|
||||
sample < cumulative
|
||||
})
|
||||
.unwrap_or(/*default*/ 3);
|
||||
let y = band.row_y(baseline, row);
|
||||
for col in x..x + bar_width {
|
||||
let cell = &mut buf[(col as u16, y as u16)];
|
||||
cell.set_char(glyph).set_fg(colors[color]);
|
||||
if reverse {
|
||||
cell.set_style(Style::default().reversed());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn paint_axis(&self, buf: &mut Buffer) {
|
||||
let Layout {
|
||||
axis_width,
|
||||
baseline,
|
||||
height,
|
||||
width,
|
||||
..
|
||||
} = self.layout;
|
||||
for x in axis_width..width {
|
||||
buf[(x as u16, baseline as u16)]
|
||||
.set_char('─')
|
||||
.set_style(Style::default().dim());
|
||||
}
|
||||
if axis_width > 0 {
|
||||
let mut ticks = vec![(1, self.peak), (baseline, 0.0)];
|
||||
if height >= 4 {
|
||||
ticks.push((1 + height / 2, self.peak / 2.0));
|
||||
}
|
||||
if self.bands.len() == 2 {
|
||||
ticks.push((baseline + height, -self.peak));
|
||||
}
|
||||
for (y, value) in ticks {
|
||||
let label = tick(value, self.unit);
|
||||
let x = axis_width.saturating_sub(label.len() + 1);
|
||||
buf.set_line(
|
||||
x as u16,
|
||||
y as u16,
|
||||
&Line::from(label.set_style(secondary_style())),
|
||||
axis_width as u16,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
462
codex-rs/tui/src/analytics/plot_tests.rs
Normal file
462
codex-rs/tui/src/analytics/plot_tests.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
//! Chart scales, signed bands, missing days, narrow viewports, and terminal palettes.
|
||||
|
||||
use super::*;
|
||||
use crate::analytics::models::AccountAnalyticsHistory;
|
||||
use crate::analytics::render::categories;
|
||||
use crate::analytics::styles::series_colors;
|
||||
use crate::terminal_palette::with_test_default_colors;
|
||||
use crate::terminal_probe::DefaultColors;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn value(key: &str, value: f64) -> AccountAnalyticsValue {
|
||||
AccountAnalyticsValue {
|
||||
key: key.into(),
|
||||
label: key.into(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
fn display(chart: &Chart) -> String {
|
||||
let text = chart
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.to_string().trim_end().to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let mut palette = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
for (row, line) in chart.lines.iter().enumerate() {
|
||||
let mut previous = None;
|
||||
let mut runs = Vec::new();
|
||||
let mut column = 0;
|
||||
for span in &line.spans {
|
||||
let style = format!("{:?}", span.style);
|
||||
let index = palette
|
||||
.iter()
|
||||
.position(|candidate| *candidate == style)
|
||||
.unwrap_or_else(|| {
|
||||
palette.push(style);
|
||||
palette.len() - 1
|
||||
});
|
||||
if previous != Some(index) {
|
||||
runs.push(format!("{column}:{index}"));
|
||||
previous = Some(index);
|
||||
}
|
||||
column += span.width();
|
||||
}
|
||||
rows.push(format!("{row}: {}", runs.join(" ")));
|
||||
}
|
||||
let palette = palette
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, style)| format!("{index}: {style}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!(
|
||||
"{text}\n\nPalette:\n{palette}\nRows (column:palette):\n{}",
|
||||
rows.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_axes_use_readable_intervals() {
|
||||
use AccountAnalyticsUnit::Count;
|
||||
use AccountAnalyticsUnit::Credits;
|
||||
use AccountAnalyticsUnit::RelativeUsage;
|
||||
use AccountAnalyticsUnit::Tokens;
|
||||
let cases = [
|
||||
(82.0, Count, 100.0),
|
||||
(4_400.0, Credits, 5_000.0),
|
||||
(4_999.0, Credits, 5_000.0),
|
||||
(5_001.0, Credits, 6_000.0),
|
||||
(49.0, Count, 50.0),
|
||||
(1.0, Count, 2.0),
|
||||
(3.0, Count, 4.0),
|
||||
(1_337.0, Credits, 1_500.0),
|
||||
(0.07, Credits, 0.08),
|
||||
(0.000004, Credits, 0.000004),
|
||||
(82.0, RelativeUsage, 100.0),
|
||||
(1.0, Tokens, 2.0),
|
||||
];
|
||||
assert_eq!(
|
||||
cases.map(|(peak, unit, _)| axis_max(peak, unit)),
|
||||
cases.map(|(_, _, expected)| expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_parts_keep_the_remainder_and_full_denominator() {
|
||||
let values = vec![
|
||||
value("a", /*value*/ 4.0),
|
||||
value("b", /*value*/ 2.0),
|
||||
value("c", /*value*/ 1.0),
|
||||
value("d", /*value*/ 6.0),
|
||||
value("e", /*value*/ -1.0),
|
||||
];
|
||||
let categories = vec![values[1].clone(), values[0].clone(), values[2].clone()];
|
||||
assert_eq!(parts(&values, &categories), [2.0, 4.0, 1.0, 5.0]);
|
||||
let day = AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 20.0,
|
||||
values,
|
||||
};
|
||||
let date = day.date;
|
||||
assert_eq!(peak(&[(date, Some(&day))], &categories), 20.0);
|
||||
let refund = AccountAnalyticsDay {
|
||||
total: -1.0,
|
||||
values: vec![value("a", /*value*/ 4.0), value("b", /*value*/ -5.0)],
|
||||
..day
|
||||
};
|
||||
assert_eq!(peak(&[(date, Some(&refund))], &categories), 9.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_chart_on_light_terminal() {
|
||||
signed_chart(
|
||||
"light",
|
||||
DefaultColors {
|
||||
fg: (0, 0, 0),
|
||||
bg: (255, 255, 255),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_chart_on_dark_terminal() {
|
||||
signed_chart(
|
||||
"dark",
|
||||
DefaultColors {
|
||||
fg: (230, 230, 230),
|
||||
bg: (16, 16, 16),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn signed_chart(theme: &str, colors: DefaultColors) {
|
||||
let history = AccountAnalyticsHistory {
|
||||
unit: AccountAnalyticsUnit::Credits,
|
||||
updated_at: None,
|
||||
data: vec![
|
||||
AccountAnalyticsDay {
|
||||
date: "2026-01-13".parse().unwrap(),
|
||||
total: 8.0,
|
||||
values: vec![value("a", /*value*/ 10.0), value("b", /*value*/ -2.0)],
|
||||
},
|
||||
AccountAnalyticsDay {
|
||||
date: "2026-01-14".parse().unwrap(),
|
||||
total: -3.0,
|
||||
values: vec![value("a", /*value*/ 1.0), value("b", /*value*/ -4.0)],
|
||||
},
|
||||
AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 0.0,
|
||||
values: vec![value("a", /*value*/ 3.0), value("b", /*value*/ -3.0)],
|
||||
},
|
||||
],
|
||||
};
|
||||
let days = history
|
||||
.data
|
||||
.iter()
|
||||
.map(|day| (day.date, Some(day)))
|
||||
.collect::<Vec<_>>();
|
||||
let categories = categories(&history);
|
||||
with_test_default_colors(colors, || {
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&categories,
|
||||
/*cursor*/ 1,
|
||||
/*width*/ 44,
|
||||
/*height*/ 4,
|
||||
history.unit,
|
||||
);
|
||||
assert_eq!(rendered.bands, 2);
|
||||
insta::assert_snapshot!(format!("signed_chart_{theme}"), display(&rendered));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_zero_and_narrow_charts_keep_the_selection_visible() {
|
||||
let zero = AccountAnalyticsDay {
|
||||
date: "2026-01-14".parse().unwrap(),
|
||||
total: 0.0,
|
||||
values: vec![value("a", /*value*/ 0.0)],
|
||||
};
|
||||
let used = AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 5.0,
|
||||
values: vec![value("a", /*value*/ 5.0)],
|
||||
};
|
||||
let days = [
|
||||
("2026-01-13".parse().unwrap(), None),
|
||||
(zero.date, Some(&zero)),
|
||||
(used.date, Some(&used)),
|
||||
];
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (230, 230, 230),
|
||||
bg: (16, 16, 16),
|
||||
},
|
||||
|| {
|
||||
let mut snapshots = Vec::new();
|
||||
for (width, cursor) in [(36, 0), (36, 1), (8, 2), (1, 2)] {
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&used.values,
|
||||
cursor,
|
||||
width,
|
||||
/*height*/ 3,
|
||||
AccountAnalyticsUnit::Count,
|
||||
);
|
||||
assert_eq!(rendered.bands, 1);
|
||||
assert!(rendered.lines.iter().all(|line| line.width() <= width));
|
||||
assert_eq!(
|
||||
rendered
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|line| &line.spans)
|
||||
.filter(|span| span.content == "▲")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
if cursor == 0 {
|
||||
let dash = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.find_map(|line| line.to_string().find('—'));
|
||||
let cursor = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.find_map(|line| line.to_string().find('▲'));
|
||||
assert_eq!(dash, cursor);
|
||||
}
|
||||
snapshots.push(format!(
|
||||
"width={width}, cursor={cursor}\n{}",
|
||||
display(&rendered)
|
||||
));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_credit_history_uses_a_readable_scale() {
|
||||
let history = crate::analytics::normalize::history(
|
||||
serde_json::from_str(r#"{"data":[]}"#).unwrap(),
|
||||
crate::analytics::models::AccountAnalyticsReport::Credits,
|
||||
crate::analytics::models::AccountAnalyticsGrouping::Surface,
|
||||
"2026-01-09".parse().unwrap(),
|
||||
"2026-01-15".parse().unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let days = history
|
||||
.data
|
||||
.iter()
|
||||
.map(|day| (day.date, Some(day)))
|
||||
.collect::<Vec<_>>();
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&[],
|
||||
/*cursor*/ 6,
|
||||
/*width*/ 44,
|
||||
/*height*/ 4,
|
||||
history.unit,
|
||||
);
|
||||
assert_eq!(axis_max(peak(&days, &[]), history.unit), 1.0);
|
||||
insta::assert_snapshot!(display(&rendered));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_cells_use_the_midpoint_and_descend_from_the_baseline() {
|
||||
let categories = vec![value("a", /*value*/ 0.8), value("b", /*value*/ 0.95)];
|
||||
let mut snapshots = Vec::new();
|
||||
for sign in [1.0, -1.0] {
|
||||
let day = AccountAnalyticsDay {
|
||||
date: "2026-01-14".parse().unwrap(),
|
||||
total: 1.75 * sign,
|
||||
values: categories
|
||||
.iter()
|
||||
.map(|value| AccountAnalyticsValue {
|
||||
value: value.value * sign,
|
||||
..value.clone()
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let days = [
|
||||
(day.date, Some(&day)),
|
||||
("2026-01-15".parse().unwrap(), None),
|
||||
];
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&categories,
|
||||
/*cursor*/ 1,
|
||||
/*width*/ 12,
|
||||
/*height*/ 1,
|
||||
AccountAnalyticsUnit::Credits,
|
||||
);
|
||||
let cell = &rendered.lines[if sign > 0.0 { 1 } else { 3 }].spans[1];
|
||||
assert_eq!(cell.style.fg, Some(series_colors()[1]));
|
||||
assert_eq!(cell.content.as_ref(), if sign > 0.0 { "▇" } else { "▁" });
|
||||
assert_eq!(
|
||||
cell.style
|
||||
.add_modifier
|
||||
.contains(ratatui::style::Modifier::REVERSED),
|
||||
sign < 0.0
|
||||
);
|
||||
snapshots.push(format!("sign={sign}\n{}", display(&rendered)));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_axes_are_hidden_instead_of_truncated() {
|
||||
let day = AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 100_000_000.0,
|
||||
values: vec![value("a", /*value*/ 100_000_000.0)],
|
||||
};
|
||||
let days = [(day.date, Some(&day))];
|
||||
let mut snapshots = Vec::new();
|
||||
for width in [8, 30, 35, 44] {
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&day.values,
|
||||
/*cursor*/ 0,
|
||||
width,
|
||||
/*height*/ 4,
|
||||
AccountAnalyticsUnit::Tokens,
|
||||
);
|
||||
let baseline = rendered.lines[5].to_string();
|
||||
assert_eq!(baseline.starts_with('─'), width < 36);
|
||||
if width == 8 {
|
||||
assert!(rendered.lines[0].to_string().trim().is_empty());
|
||||
}
|
||||
snapshots.push(format!("width={width}\n{}", display(&rendered)));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_callout_keeps_the_authoritative_daily_total() {
|
||||
let day = AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 20.0,
|
||||
values: vec![value("a", /*value*/ 8.0), value("b", /*value*/ 4.0)],
|
||||
};
|
||||
let days = [(day.date, Some(&day))];
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&day.values,
|
||||
/*cursor*/ 0,
|
||||
/*width*/ 44,
|
||||
/*height*/ 4,
|
||||
AccountAnalyticsUnit::Credits,
|
||||
);
|
||||
assert!(display(&rendered).contains("20.00"));
|
||||
assert!(!display(&rendered).contains("12.00"));
|
||||
insta::assert_snapshot!(display(&rendered));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrow_long_range_charts_never_truncate_dates() {
|
||||
let start: NaiveDate = "2026-01-01".parse().unwrap();
|
||||
let history = start
|
||||
.iter_days()
|
||||
.take(/*n*/ 30)
|
||||
.map(|date| AccountAnalyticsDay {
|
||||
date,
|
||||
total: 1.0,
|
||||
values: vec![value("a", /*value*/ 1.0)],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let days = history
|
||||
.iter()
|
||||
.map(|day| (day.date, Some(day)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut snapshots = Vec::new();
|
||||
for (width, expected) in [(1, ""), (2, "15"), (5, "15"), (6, "Jan 15")] {
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&history[0].values,
|
||||
/*cursor*/ 14,
|
||||
width,
|
||||
/*height*/ 3,
|
||||
AccountAnalyticsUnit::Count,
|
||||
);
|
||||
assert_eq!(rendered.lines.last().unwrap().to_string().trim(), expected);
|
||||
snapshots.push(format!("width={width}\n{}", display(&rendered)));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_segments_keep_their_series_color_beside_a_large_day() {
|
||||
let large = AccountAnalyticsDay {
|
||||
date: "2026-01-14".parse().unwrap(),
|
||||
total: 100.0,
|
||||
values: vec![value("a", /*value*/ 100.0)],
|
||||
};
|
||||
let mut snapshots = Vec::new();
|
||||
for sign in [1.0, -1.0] {
|
||||
let tiny = AccountAnalyticsDay {
|
||||
date: "2026-01-15".parse().unwrap(),
|
||||
total: 0.01 * sign,
|
||||
values: vec![value("a", 0.01 * sign)],
|
||||
};
|
||||
let days = [(large.date, Some(&large)), (tiny.date, Some(&tiny))];
|
||||
let rendered = chart(
|
||||
&days,
|
||||
&large.values,
|
||||
/*cursor*/ 0,
|
||||
/*width*/ 44,
|
||||
/*height*/ 4,
|
||||
AccountAnalyticsUnit::Credits,
|
||||
);
|
||||
let glyph = if sign > 0.0 { "▁" } else { "▇" };
|
||||
let cells = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|line| &line.spans)
|
||||
.filter(|span| span.content == glyph)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!cells.is_empty());
|
||||
assert!(
|
||||
cells
|
||||
.iter()
|
||||
.all(|cell| cell.style.fg == Some(series_colors()[0]))
|
||||
);
|
||||
snapshots.push(format!("sign={sign}\n{}", display(&rendered)));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_message_dates_render_the_full_other_remainder() {
|
||||
use crate::analytics::models::AccountAnalyticsGrouping;
|
||||
use crate::analytics::models::AccountAnalyticsReport;
|
||||
let date = "2026-01-15".parse().unwrap();
|
||||
let response = serde_json::from_value(serde_json::json!({"data": [
|
||||
{"date": "2026-01-15", "totals": {"turns": 10}, "clients": [{"client_id": "CODEX_CLI", "turns": 8}]},
|
||||
{"date": "2026-01-15", "totals": {"turns": 10}, "clients": [{"client_id": "CODEX_CLI", "turns": 8}]}
|
||||
]})).unwrap();
|
||||
let history = crate::analytics::normalize::history(
|
||||
response,
|
||||
AccountAnalyticsReport::Messages,
|
||||
AccountAnalyticsGrouping::Surface,
|
||||
date,
|
||||
date,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let day = &history.data[0];
|
||||
let rendered = chart(
|
||||
&[(date, Some(day))],
|
||||
&day.values,
|
||||
/*cursor*/ 0,
|
||||
/*width*/ 44,
|
||||
/*height*/ 10,
|
||||
AccountAnalyticsUnit::Count,
|
||||
);
|
||||
insta::assert_snapshot!(display(&rendered));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Shared report category aggregation for token model filtering.
|
||||
//! Shared report category aggregation and stacked-chart partitioning.
|
||||
|
||||
use super::models::AccountAnalyticsHistory;
|
||||
use super::models::AccountAnalyticsValue;
|
||||
@@ -24,3 +24,20 @@ pub(super) fn categories(history: &AccountAnalyticsHistory) -> Vec<AccountAnalyt
|
||||
});
|
||||
values
|
||||
}
|
||||
|
||||
/// The last plotted category collects all remaining values without renormalizing them.
|
||||
pub(super) fn parts(
|
||||
values: &[AccountAnalyticsValue],
|
||||
categories: &[AccountAnalyticsValue],
|
||||
) -> [f64; 4] {
|
||||
let mut parts = [0.0; 4];
|
||||
for value in values {
|
||||
let index = categories
|
||||
.iter()
|
||||
.take(/*n*/ 3)
|
||||
.position(|category| category.key == value.key)
|
||||
.unwrap_or(/*default*/ 3);
|
||||
parts[index] += value.value;
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: display(&rendered)
|
||||
---
|
||||
20
|
||||
20 ████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
10 ████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
0 ────────────────────────────────────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset)
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 22:1 24:0
|
||||
1: 0:0 1:2 3:0 18:3 30:0
|
||||
2: 0:0 18:3 30:0
|
||||
3: 0:0 18:4 30:0
|
||||
4: 0:0 18:4 30:0
|
||||
5: 0:0 18:4 30:0
|
||||
6: 0:0 1:2 3:0 18:4 30:0
|
||||
7: 0:0 18:4 30:0
|
||||
8: 0:0 18:4 30:0
|
||||
9: 0:0 18:4 30:0
|
||||
10: 0:0 18:4 30:0
|
||||
11: 0:0 2:2 3:0 4:2
|
||||
12: 0:0 23:1 24:0
|
||||
13: 0:0 20:5 26:0
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
width=36, cursor=0
|
||||
|
||||
6 ▄▄▄▄▄▄▄
|
||||
███████
|
||||
— 0 ███████
|
||||
0 ─────────────────────────────────
|
||||
▲
|
||||
Jan 13 Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1 2:0 27:2 34:0
|
||||
2: 0:0 27:2 34:0
|
||||
3: 0:0 8:3 9:0 19:4 20:0 27:2 34:0
|
||||
4: 0:0 1:1 2:0 3:4
|
||||
5: 0:0 8:3 9:0
|
||||
6: 0:0 5:5 11:0 16:1 22:0 27:1 33:0
|
||||
|
||||
width=36, cursor=1
|
||||
|
||||
6 ▄▄▄▄▄▄▄
|
||||
███████
|
||||
— 0 ███████
|
||||
0 ─────────────────────────────────
|
||||
▲
|
||||
Jan 13 Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1 2:0 27:2 34:0
|
||||
2: 0:0 27:2 34:0
|
||||
3: 0:0 8:3 9:0 19:4 20:0 27:2 34:0
|
||||
4: 0:0 1:1 2:0 3:3
|
||||
5: 0:0 19:4 20:0
|
||||
6: 0:0 5:1 11:0 16:5 22:0 27:1 33:0
|
||||
|
||||
width=8, cursor=2
|
||||
5
|
||||
▄▄
|
||||
██
|
||||
— 0 ██
|
||||
────────
|
||||
▲
|
||||
13 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset)
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 6:1 7:0
|
||||
1: 0:0 5:2 7:0
|
||||
2: 0:0 5:2 7:0
|
||||
3: 0:3 1:0 3:3 4:0 5:2 7:0
|
||||
4: 0:3
|
||||
5: 0:0 6:1 7:0
|
||||
6: 0:4 2:0 5:5 7:0
|
||||
|
||||
width=1, cursor=2
|
||||
5
|
||||
▄
|
||||
█
|
||||
█
|
||||
─
|
||||
▲
|
||||
|
||||
|
||||
Palette:
|
||||
0: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:1
|
||||
2: 0:1
|
||||
3: 0:1
|
||||
4: 0:2
|
||||
5: 0:0
|
||||
6: 0:3
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
width=1
|
||||
|
||||
1
|
||||
▄
|
||||
█
|
||||
─
|
||||
▲
|
||||
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:1
|
||||
2: 0:2
|
||||
3: 0:2
|
||||
4: 0:3
|
||||
5: 0:1
|
||||
6: 0:0
|
||||
|
||||
width=2
|
||||
|
||||
1
|
||||
▄▄
|
||||
██
|
||||
──
|
||||
▲
|
||||
15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1
|
||||
2: 0:2
|
||||
3: 0:2
|
||||
4: 0:3
|
||||
5: 0:0 1:1
|
||||
6: 0:4
|
||||
|
||||
width=5
|
||||
|
||||
1
|
||||
▄▄▄▄▄
|
||||
█████
|
||||
─────
|
||||
▲
|
||||
15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 4:1
|
||||
2: 0:2
|
||||
3: 0:2
|
||||
4: 0:3
|
||||
5: 0:0 4:1
|
||||
6: 0:0 3:4
|
||||
|
||||
width=6
|
||||
|
||||
1
|
||||
▄▄▄▄▄▄
|
||||
██████
|
||||
──────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 5:1
|
||||
2: 0:2
|
||||
3: 0:2
|
||||
4: 0:3
|
||||
5: 0:0 5:1
|
||||
6: 0:4
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
width=8
|
||||
|
||||
█████
|
||||
█████
|
||||
█████
|
||||
█████
|
||||
────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1 6:0
|
||||
2: 0:0 1:1 6:0
|
||||
3: 0:0 1:1 6:0
|
||||
4: 0:0 1:1 6:0
|
||||
5: 0:2
|
||||
6: 0:0 3:3 4:0
|
||||
7: 0:4 6:0
|
||||
|
||||
width=30
|
||||
100,000,000
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
──────────────────────────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 9:1 20:0
|
||||
1: 0:0 9:2 21:0
|
||||
2: 0:0 9:2 21:0
|
||||
3: 0:0 9:2 21:0
|
||||
4: 0:0 9:2 21:0
|
||||
5: 0:3
|
||||
6: 0:0 14:1 15:0
|
||||
7: 0:0 11:4 17:0
|
||||
|
||||
width=35
|
||||
100,000,000
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
████████████
|
||||
───────────────────────────────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 12:1 23:0
|
||||
1: 0:0 11:2 23:0
|
||||
2: 0:0 11:2 23:0
|
||||
3: 0:0 11:2 23:0
|
||||
4: 0:0 11:2 23:0
|
||||
5: 0:3
|
||||
6: 0:0 17:1 18:0
|
||||
7: 0:0 14:4 20:0
|
||||
|
||||
width=44
|
||||
100,000,000
|
||||
100,000,000 ████████████
|
||||
████████████
|
||||
50,000,000 ████████████
|
||||
████████████
|
||||
0 ───────────────────────────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 23:1 34:0
|
||||
1: 0:0 1:2 12:0 22:3 34:0
|
||||
2: 0:0 22:3 34:0
|
||||
3: 0:0 2:2 12:0 22:3 34:0
|
||||
4: 0:0 22:3 34:0
|
||||
5: 0:0 11:2 12:0 13:2
|
||||
6: 0:0 28:1 29:0
|
||||
7: 0:0 25:4 31:0
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
sign=1
|
||||
|
||||
▇▇▇▇ —
|
||||
────────────
|
||||
▲
|
||||
14 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1 5:0 8:2 9:0
|
||||
2: 0:3
|
||||
3: 0:0 8:2 9:0
|
||||
4: 0:0 1:3 3:0 7:4 9:0
|
||||
|
||||
sign=-1
|
||||
|
||||
—
|
||||
────────────
|
||||
▁▁▁▁
|
||||
|
||||
▲
|
||||
14 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed()
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 8:1 9:0
|
||||
2: 0:2
|
||||
3: 0:0 1:3 5:0
|
||||
4: 0:0
|
||||
5: 0:0 8:1 9:0
|
||||
6: 0:0 1:2 3:0 7:4 9:0
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: display(&rendered)
|
||||
---
|
||||
|
||||
20 20.00
|
||||
▃▃▃▃▃▃▃▃▃▃▃▃
|
||||
10 ████████████
|
||||
████████████
|
||||
0 ────────────────────────────────────────
|
||||
▲
|
||||
Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset)
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 1:1 3:0 21:2 26:0
|
||||
2: 0:0 18:3 30:0
|
||||
3: 0:0 1:1 3:0 18:4 30:0
|
||||
4: 0:0 18:4 30:0
|
||||
5: 0:0 2:1 3:0 4:1
|
||||
6: 0:0 23:2 24:0
|
||||
7: 0:0 20:5 26:0
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: display(&rendered)
|
||||
---
|
||||
|
||||
15
|
||||
▅▅▅▅▅▅▅▅
|
||||
7.5 ████████
|
||||
████████ ▂▂▂▂▂▂▂▂ ▆▆▆▆▆▆▆▆
|
||||
0 ──────────────────────────────────────
|
||||
▄▄▄▄▄▄▄▄ ████████ ▂▂▂▂▂▂▂▂
|
||||
|
||||
-3.00
|
||||
-15
|
||||
|
||||
▲
|
||||
Jan 13 Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed()
|
||||
5: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset)
|
||||
6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
7: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 3:1 5:0
|
||||
2: 0:0 8:2 16:0
|
||||
3: 0:0 2:1 5:0 8:2 16:0
|
||||
4: 0:0 8:2 16:0 20:2 28:0 33:2 41:0
|
||||
5: 0:0 4:1 5:0 6:3
|
||||
6: 0:0 8:4 16:0 20:5 28:0 33:4 41:0
|
||||
7: 0:0
|
||||
8: 0:0 22:6 27:0
|
||||
9: 0:0 2:1 5:0
|
||||
10: 0:0
|
||||
11: 0:0 24:6 25:0
|
||||
12: 0:0 8:1 14:0 21:7 27:0 34:1 40:0
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: display(&rendered)
|
||||
---
|
||||
|
||||
15
|
||||
▅▅▅▅▅▅▅▅
|
||||
7.5 ████████
|
||||
████████ ▂▂▂▂▂▂▂▂ ▆▆▆▆▆▆▆▆
|
||||
0 ──────────────────────────────────────
|
||||
▄▄▄▄▄▄▄▄ ████████ ▂▂▂▂▂▂▂▂
|
||||
|
||||
-3.00
|
||||
-15
|
||||
|
||||
▲
|
||||
Jan 13 Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Rgb(76, 76, 76)).bg(Color::Reset).underline_color(Color::Reset)
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
4: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed()
|
||||
5: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset)
|
||||
6: Style::new().fg(Color::Rgb(0, 95, 135)).bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
7: Style::new().fg(Color::Rgb(0, 95, 135)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 3:1 5:0
|
||||
2: 0:0 8:2 16:0
|
||||
3: 0:0 2:1 5:0 8:2 16:0
|
||||
4: 0:0 8:2 16:0 20:2 28:0 33:2 41:0
|
||||
5: 0:0 4:1 5:0 6:3
|
||||
6: 0:0 8:4 16:0 20:5 28:0 33:4 41:0
|
||||
7: 0:0
|
||||
8: 0:0 22:6 27:0
|
||||
9: 0:0 2:1 5:0
|
||||
10: 0:0
|
||||
11: 0:0 24:6 25:0
|
||||
12: 0:0 8:1 14:0 21:7 27:0 34:1 40:0
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
sign=1
|
||||
100.00
|
||||
100 ████████████
|
||||
████████████
|
||||
50 ████████████
|
||||
████████████ ▁▁▁▁▁▁▁▁▁▁▁▁
|
||||
0 ───────────────────────────────────────
|
||||
▲
|
||||
Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 11:1 17:0
|
||||
1: 0:0 1:2 4:0 8:3 20:0
|
||||
2: 0:0 8:3 20:0
|
||||
3: 0:0 2:2 4:0 8:3 20:0
|
||||
4: 0:0 8:3 20:0 28:3 40:0
|
||||
5: 0:0 3:2 4:0 5:2
|
||||
6: 0:0 14:1 15:0
|
||||
7: 0:0 11:4 17:0 30:2 36:0
|
||||
|
||||
sign=-1
|
||||
100.00
|
||||
100 ████████████
|
||||
████████████
|
||||
50 ████████████
|
||||
████████████
|
||||
0 ──────────────────────────────────────
|
||||
▇▇▇▇▇▇▇▇▇▇▇▇
|
||||
|
||||
|
||||
-100
|
||||
|
||||
▲
|
||||
Jan 14 Jan 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset)
|
||||
4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).reversed()
|
||||
5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0 12:1 18:0
|
||||
1: 0:0 2:2 5:0 9:3 21:0
|
||||
2: 0:0 9:3 21:0
|
||||
3: 0:0 3:2 5:0 9:3 21:0
|
||||
4: 0:0 9:3 21:0
|
||||
5: 0:0 4:2 5:0 6:2
|
||||
6: 0:0 28:4 40:0
|
||||
7: 0:0
|
||||
8: 0:0
|
||||
9: 0:0 1:2 5:0
|
||||
10: 0:0
|
||||
11: 0:0 15:1 16:0
|
||||
12: 0:0 12:5 18:0 31:2 37:0
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: tui/src/analytics/plot_tests.rs
|
||||
expression: display(&rendered)
|
||||
---
|
||||
|
||||
1
|
||||
|
||||
0.5
|
||||
0 0 0 0 0 0 0.00
|
||||
0 ───────────────────────────────────────
|
||||
▲
|
||||
9 10 11 12 13 14 15
|
||||
|
||||
Palette:
|
||||
0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)
|
||||
1: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()
|
||||
2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold()
|
||||
3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined()
|
||||
Rows (column:palette):
|
||||
0: 0:0
|
||||
1: 0:0 3:1 4:0
|
||||
2: 0:0
|
||||
3: 0:0 1:1 4:0
|
||||
4: 0:0 7:1 8:0 12:1 13:0 18:1 19:0 23:1 24:0 29:1 30:0 34:1 35:0 38:2 42:0
|
||||
5: 0:0 3:1 4:0 5:1
|
||||
6: 0:0 40:2 41:0
|
||||
7: 0:0 7:1 8:0 11:1 13:0 17:1 19:0 22:1 24:0 28:1 30:0 33:1 35:0 39:3 41:0
|
||||
37
codex-rs/tui/src/analytics/styles.rs
Normal file
37
codex-rs/tui/src/analytics/styles.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Charts and legends share the terminal's ANSI palette.
|
||||
//! Secondary text blends probed defaults only when the terminal supports that color depth.
|
||||
|
||||
use crate::color::blend;
|
||||
use crate::style::accent_style;
|
||||
use crate::terminal_palette::StdoutColorLevel;
|
||||
use crate::terminal_palette::best_color;
|
||||
use crate::terminal_palette::default_bg;
|
||||
use crate::terminal_palette::default_fg;
|
||||
use crate::terminal_palette::effective_stdout_color_level;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
|
||||
pub(super) fn series_colors() -> [Color; 4] {
|
||||
[Color::Cyan, Color::Magenta, Color::Green, Color::Reset]
|
||||
}
|
||||
|
||||
pub(super) fn number(text: impl Into<String>) -> Span<'static> {
|
||||
Span::styled(text.into(), accent_style())
|
||||
}
|
||||
|
||||
/// Labels and instructions need more contrast than decorative rules or inactive values.
|
||||
pub(super) fn secondary_style() -> Style {
|
||||
if !matches!(
|
||||
effective_stdout_color_level(),
|
||||
StdoutColorLevel::TrueColor | StdoutColorLevel::Ansi256
|
||||
) {
|
||||
return Style::default().dim();
|
||||
}
|
||||
match (default_fg(), default_bg()) {
|
||||
(Some(fg), Some(bg)) => {
|
||||
Style::default().fg(best_color(blend(fg, bg, /*alpha*/ 0.70)))
|
||||
}
|
||||
_ => Style::default().dim(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user