SparklineControl
Compact time-series graph — a scrolling history of values rendered as vertical bars or braille dots.
Overview
SparklineControl keeps a rolling window of double values and draws them as a dense little chart.
Push a value with AddDataPoint and the oldest one falls off once the buffer is full, giving you a
live "last N samples" view in a handful of terminal rows.
Four render modes cover the common shapes: Block (the classic ▁▂▃▄▅▆▇█ ramp, 8 levels per cell),
Braille (denser and smoother), and the two bidirectional variants that draw a second series
downward from a centre line — the natural fit for upload/download or read/write pairs.
It is a display control: no focus, no keyboard or mouse handling. Colors can be flat, role-based, or interpolated across a gradient, and an optional title, baseline, and X-axis can be layered on.
For a single value against a maximum use BarGraphControl; for multi-series plots with axes use LineGraphControl.
Quick Start
var spark = Controls.Sparkline()
.WithHeight(4)
.WithAutoFitDataPoints()
.WithBarColor(Color.Cyan1)
.Build();
window.AddControl(spark);
// Push samples as they arrive — the control scrolls and repaints itself
spark.AddDataPoint(cpuPercent);
Builder API
Create a builder with Controls.Sparkline() (or new SparklineBuilder()).
Data
.WithData(IEnumerable<double> dataPoints) // Seed the primary series
.WithMaxDataPoints(int maxPoints) // Buffer size (default 50)
.WithAutoFitDataPoints(bool autoFit = true) // Size the buffer to the control width
.WithMinValue(double minValue) // Fixed scale floor
.WithMaxValue(double maxValue) // Fixed scale ceiling
Leaving min/max unset auto-scales to the data currently in the buffer.
Appearance
.WithHeight(int height) // Graph height in rows (default 8)
.WithMode(SparklineMode mode) // Block, Braille, Bidirectional, BidirectionalBraille
.WithBarColor(Color color)
.WithBackgroundColor(Color color)
.WithColorRole(ColorRole role, ThemeMode? mode = null)
.Outline(bool outline = true)
.WithBorder(BorderStyle style)
.WithBorder(BorderStyle style, Color color)
.WithBorderColor(Color color)
Gradients
.WithGradient(ColorGradient gradient)
.WithGradient(string gradientSpec) // "cool", "warm", "blue→cyan→green"
.WithGradient(params Color[] colors)
Title and baseline
.WithTitle(string title)
.WithTitle(string title, Color color)
.WithTitleColor(Color color)
.WithTitlePosition(TitlePosition position) // Top (default) or Bottom
.WithBaseline(bool show = true, char baselineChar = '┈',
Color? color = null, TitlePosition position = TitlePosition.Bottom)
.WithInlineTitleBaseline(bool inline = true) // Draw the title on the baseline row
Secondary series (bidirectional modes)
.WithSecondaryData(IEnumerable<double> dataPoints)
.WithSecondaryBarColor(Color color)
.WithSecondaryMaxValue(double maxValue)
.WithSecondaryGradient(ColorGradient gradient)
.WithSecondaryGradient(string gradientSpec)
.WithSecondaryGradient(params Color[] colors)
.WithBidirectionalData(IEnumerable<double> primaryData, IEnumerable<double> secondaryData)
X-axis
.WithXAxis(Func<SparklineAxisContext, IEnumerable<SparklineAxisTick>>? axisProvider,
double unitsPerPoint = 1.0)
The control supplies the geometry and your provider returns the ticks:
SparklineAxisContext(int PointCount, int GraphWidth, double UnitsPerPoint)SparklineAxisTick(int PointIndex, string Label, Color? Color = null)
Passing a null provider leaves the axis off, which is handy for enabling it conditionally.
Layout
.WithWidth(int width)
.WithAlignment(HorizontalAlignment alignment)
.WithVerticalAlignment(VerticalAlignment alignment)
.WithMargin(int left, int top, int right, int bottom)
.WithMargin(Margin margin)
.Visible(bool visible)
.WithName(string name)
Properties
| Property | Type | Default | Description |
|---|---|---|---|
DataPoints |
IReadOnlyList<double> |
empty | The primary series |
MaxDataPoints |
int |
50 |
Rolling buffer size |
AutoFitDataPoints |
bool |
false |
Size the buffer to the control's width |
MinValue |
double? |
null |
Scale floor; auto-scales when null |
MaxValue |
double? |
null |
Scale ceiling; auto-scales when null |
GraphHeight |
int |
8 |
Graph height in rows |
Mode |
SparklineMode |
Block |
Render mode |
BarColor |
Color |
theme | Primary series color |
Gradient |
ColorGradient? |
null |
Gradient applied across the primary series |
BackgroundColor |
Color? |
null |
Control background; inherits when null |
ForegroundColor |
Color? |
null |
Text color; inherits when null |
BorderStyle |
BorderStyle |
None |
Border around the graph |
BorderColor |
Color? |
null |
Border color |
ColorRole |
ColorRole |
Default |
Semantic color role |
ColorRoleMode |
ThemeMode? |
null |
Optional theme mode override |
Outline |
bool |
false |
Outline styling |
Title |
string? |
null |
Optional title |
TitleColor |
Color? |
null |
Title color |
TitlePosition |
TitlePosition |
Top |
Title above or below the graph |
ShowBaseline |
bool |
false |
Draw a baseline rule |
BaselineChar |
char |
'┈' |
Baseline character |
BaselineColor |
Color |
Grey50 |
Baseline color |
BaselinePosition |
TitlePosition |
Bottom |
Baseline above or below |
InlineTitleWithBaseline |
bool |
false |
Render the title on the baseline row |
ShowXAxis |
bool |
false |
Whether the X-axis is drawn |
UnitsPerPoint |
double |
1.0 |
Passed to the axis provider (e.g. seconds per sample) |
SecondaryDataPoints |
IReadOnlyList<double> |
empty | Secondary series |
SecondaryBarColor |
Color |
Green |
Secondary series color |
SecondaryMaxValue |
double? |
null |
Independent scale for the secondary series |
SecondaryGradient |
ColorGradient? |
null |
Gradient for the secondary series |
Methods
void AddDataPoint(double value); // Append; drops the oldest when full
void SetDataPoints(IEnumerable<double> dataPoints); // Replace the series
void ClearDataPoints();
void AddSecondaryDataPoint(double value);
void SetSecondaryDataPoints(IEnumerable<double> dataPoints);
void ClearSecondaryDataPoints();
void SetBidirectionalData(IEnumerable<double> primaryData, IEnumerable<double> secondaryData);
Events
None. SparklineControl implements INotifyPropertyChanged via BaseControl, so its properties can
be data-bound — see Data Binding.
Keyboard and mouse support
None — this is a display-only control. It takes no focus and processes no input.
Examples
Live CPU history
WithAutoFitDataPoints sizes the buffer to the control's width, so the graph always fills the
available space no matter how the window is resized.
var spark = Controls.Sparkline()
.WithName("cpuSparkline")
.WithHeight(4)
.WithAutoFitDataPoints()
.WithMode(SparklineMode.Block)
.WithBarColor(Color.Cyan1)
.WithGradient("cool")
.WithBaseline(true, '─', Color.Grey35, TitlePosition.Bottom)
.WithAlignment(HorizontalAlignment.Stretch)
.Build();
// From a window thread
var spark = window.FindControl<SparklineControl>("cpuSparkline");
spark?.AddDataPoint(ReadCpuPercent());
Braille mode for denser detail
var mem = Controls.Sparkline()
.WithHeight(4)
.WithAutoFitDataPoints()
.WithMode(SparklineMode.Braille)
.WithBarColor(Color.Green)
.WithGradient("warm")
.Build();
Bidirectional network graph
Upload rises from the centre line, download falls below it.
var net = Controls.Sparkline()
.WithHeight(6)
.WithAutoFitDataPoints()
.WithMode(SparklineMode.BidirectionalBraille)
.WithBarColor(Color.Green) // upload, drawn upward
.WithSecondaryBarColor(Color.Red) // download, drawn downward
.Build();
net.AddDataPoint(uploadKbps);
net.AddSecondaryDataPoint(downloadKbps);
Give the two series independent scales with WithSecondaryMaxValue when their magnitudes differ
— otherwise a large download flattens the upload trace into invisibility.
Fixed scale
Auto-scaling makes a flat series look dramatic, because the control stretches whatever range it has to fill the height. Pin the scale when absolute level matters.
var load = Controls.Sparkline()
.WithHeight(5)
.WithMinValue(0)
.WithMaxValue(100) // 0–100% always, however calm the data
.WithTitle("Load", Color.Grey70)
.Build();
Time axis
var spark = Controls.Sparkline()
.WithHeight(6)
.WithAutoFitDataPoints()
.WithXAxis(ctx =>
{
// One tick per 30 samples, labelled as seconds in the past
var ticks = new List<SparklineAxisTick>();
for (int i = 0; i < ctx.PointCount; i += 30)
{
double secondsAgo = (ctx.PointCount - 1 - i) * ctx.UnitsPerPoint;
ticks.Add(new SparklineAxisTick(i, $"-{secondsAgo:F0}s", Color.Grey50));
}
return ticks;
}, unitsPerPoint: 0.5) // a sample every 500 ms
.Build();
Seeding from existing history
var spark = Controls.Sparkline()
.WithData(recentSamples) // e.g. values loaded from a log
.WithMaxDataPoints(120)
.Build();
// Or replace the whole series later
spark.SetDataPoints(refreshedSamples);
Best Practices
- Prefer
WithAutoFitDataPoints()for live graphs. It keeps one sample per column, so the graph fills the width and stays honest when the window resizes. - Pin
MinValue/MaxValuewhen the absolute level matters. Auto-scaling exaggerates a flat series — steady 2 % CPU renders as a dramatic mountain range. - Match the mode to the height.
Braillepacks more detail into few rows;Blockreads more clearly when you have height to spare. - Give bidirectional series independent scales via
WithSecondaryMaxValueunless the two genuinely share a range. - Push with
AddDataPoint, don't rebuild the list. It maintains the rolling window and invalidates for you;SetDataPointsis for wholesale replacement. - Never call
Invalidateafter adding a point. The control already does it — see State Services and the reactive property contract. - Keep
MaxDataPointssane. Buffering thousands of points that render into 40 columns costs memory and buys nothing visible.
See Also
- BarGraphControl — a single value against a maximum
- LineGraphControl — multi-series plots with axes
- ProgressBarControl — task progress
- Gradients — gradient specification syntax
- Themes — color roles