btop-style Braille terminal graphs for ratatui | MSRV: 1.74.0
[dependencies]
bgraph = "0.1"
ratatui = "0.29"| Type | Purpose | Pattern |
|---|---|---|
Graph<'a> |
Static data visualization | Widget |
TimeSeries |
Scrolling time-series | StatefulWidget + TimeSeriesState |
DualGraph |
Side-by-side graphs | Widget |
DualTimeSeries |
Dual scrolling series | StatefulWidget + DualTimeSeriesState |
MultiGraph |
Overlaid series | Widget |
MultiTimeSeries |
Multi scrolling series | StatefulWidget + MultiTimeSeriesState |
Meter |
Horizontal percentage bar | Widget |
MiniGraph<'a> |
1-row braille sparkline | Widget |
MiniTimeSeries |
Scrolling 1-row sparkline | StatefulWidget + MiniTimeSeriesState |
VerticalGauge |
Vertical percentage bar | Widget |
BrailleVerticalGauge |
Braille vertical gauge (4x/8x) | Widget |
use bgraph::{Graph, DataSource};
struct Sine;
impl DataSource for Sine {
fn sample(&self, x: f32) -> f32 { (x * 6.28).sin() }
}
let graph = Graph::new(&Sine)
.x_range(0.0, 1.0)
.y_range(-1.0, 1.0)
.style(Style::default().fg(Color::Cyan));
frame.render_widget(graph, area);use bgraph::{Graph, FnDataSource};
let data = vec![0.2, 0.5, 0.8, 1.0, 0.7];
let source = FnDataSource::new(move |x| {
let i = (x * (data.len() - 1) as f32) as usize;
data.get(i).copied().unwrap_or(0.0)
});
frame.render_widget(Graph::new(&source), area);use bgraph::{TimeSeries, TimeSeriesState};
// Create state (once)
let mut state = TimeSeriesState::with_range(100, 0.0, 100.0);
// Update loop
state.push(new_value);
// Render
frame.render_stateful_widget(TimeSeries::new(), area, &mut state);use bgraph::{DualTimeSeries, DualTimeSeriesState, SplitDirection};
let mut state = DualTimeSeriesState::with_ranges(
100,
(0.0, 100.0),
(0.0, 100.0),
);
state.push(val1, val2);
// Horizontal: left | right (default)
let dual = DualTimeSeries::new().split_ratio(0.5);
// Vertical: top / bottom
let dual = DualTimeSeries::new()
.split_direction(SplitDirection::Vertical)
.split_ratio(0.5);
frame.render_stateful_widget(dual, area, &mut state);use bgraph::{MultiTimeSeries, MultiTimeSeriesState, LegendPosition};
let mut state = MultiTimeSeriesState::with_range(4, 100, 0.0, 100.0);
for i in 0..4 { state.set_label(i, format!("Core {}", i)); }
// Update each series
state.push(0, core0); state.push(1, core1); // ...
let multi = MultiTimeSeries::new()
.show_legend(true)
.legend_position(LegendPosition::TopRight);
frame.render_stateful_widget(multi, area, &mut state);use bgraph::{Meter, ColorGradient};
// Simple percentage bar with gradient coloring
let meter = Meter::new(75) // 75%
.gradient(ColorGradient::three_point(
Color::Green, Color::Yellow, Color::Red
))
.show_percentage(true);
frame.render_widget(meter, area);use bgraph::{MiniTimeSeries, MiniTimeSeriesState, ColorGradient};
// Create state for 1-row sparkline
let mut state = MiniTimeSeriesState::with_range(60, 0.0, 100.0);
// Push values (O(1) ring buffer)
state.push(cpu_percent);
// Render with gradient
let sparkline = MiniTimeSeries::new()
.gradient(ColorGradient::three_point(
Color::Green, Color::Yellow, Color::Red
));
frame.render_stateful_widget(sparkline, area, &mut state);TimeSeriesState::new(capacity) // Auto Y-range
TimeSeriesState::with_range(cap, min, max) // Fixed Y-range
state.push(value) // O(1) append
state.clear() // Reset buffer
state.len() / state.is_empty() // Buffer status// Convenience constructors
let gradient = ColorGradient::three_point(Color::Blue, Color::Yellow, Color::Red);
let gradient = ColorGradient::two_point(Color::Green, Color::Red);
// Or build manually
let gradient = ColorGradient::new()
.add_stop(0.0, Color::Blue)
.add_stop(0.5, Color::Yellow)
.add_stop(1.0, Color::Red);
let color = gradient.get_color(0.5); // Returns interpolated color
// Pre-compute for performance
let table: [Color; 101] = gradient.precompute();
let color = table[75]; // Fast lookupuse bgraph::GradientMode;
// Value mode (default): color by data value
Graph::new(&data).gradient(g.clone()).gradient_mode(GradientMode::Value);
// Position mode (btop-style): color by row Y position
// Top rows = hot, bottom rows = cool
Graph::new(&data).gradient(g.clone()).gradient_mode(GradientMode::Position);.render_mode(RenderMode::Braille) // High-res (default)
.render_mode(RenderMode::Block) // Fallbackuse bgraph::{snap_height, snap_graph_area};
// Snap raw height to clean division (multiples of 50, 20, 10, or 4)
let snapped = snap_height(12); // Returns 10
// Snap a Rect and center it vertically
let graph_area = snap_graph_area(area);
// Custom base for non-percentage ranges
let snapped = snap_height_with_base(15, 8); // Returns 8
let graph_area = snap_graph_area_with_base(area, 8);Why snap? Braille uses 5 sub-levels per row. Snapping to nice heights (4, 10, 20, 50) ensures:
- Each row = round percentage (25%, 10%, 5%, 2%)
- Each braille level = clean value (5%, 2%, 1%, 0.4%)
- 50% mark always lands on row boundary
| Guarantee | Details |
|---|---|
| O(1) push | VecDeque ring buffer; no allocations after capacity reached |
| Y-range | .with_range() = fixed; .new() = auto-calculated per frame |
| Thread-safe | State structs are Send + Sync |
| No unsafe | #![deny(unsafe_code)] enforced |
Heat-map gradient:
let heatmap = ColorGradient::new()
.add_stop(0.0, Color::Rgb(0, 0, 255)) // Blue (cold)
.add_stop(0.5, Color::Rgb(255, 255, 0)) // Yellow
.add_stop(1.0, Color::Rgb(255, 0, 0)); // Red (hot)Custom series styles:
MultiTimeSeries::new().styles(vec![
Style::default().fg(Color::Red),
Style::default().fg(Color::Green),
Style::default().fg(Color::Blue),
])Zero-line reference:
Graph::new(&source).zero_line(true)use bgraph::{
// Core
Graph, DataSource, FnDataSource, RenderMode, ColorGradient, GradientMode,
// Time Series
TimeSeries, TimeSeriesState,
// Dual
DualGraph, DualTimeSeries, DualTimeSeriesState, SplitDirection,
// Multi
Series, MultiGraph, MultiTimeSeries, MultiTimeSeriesState, LegendPosition,
// btop-style Widgets
Meter, MiniGraph, MiniTimeSeries, MiniTimeSeriesState,
// Layout Utilities
snap_height, snap_height_with_base, snap_graph_area, snap_graph_area_with_base,
};