Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Charts

teksilo-charts provides bar, line, and pie/donut widgets backed by ChartModel<T> or a ChartWindow<T> tail projection. Add teksilo-charts at the same version as the framework.

Minimal example

use teksilo::prelude::*;
use teksilo_charts::{BarChart, ChartModel, ChartSeries};

fn main() {
    TeksiloAppBuilder::new()
        .theme(intui::light())
        .initial_window(
            WindowConfig::new()
                .title("Revenue")
                .size(640, 400)
                .root(|tree, _| {
                    let mut revenue = ChartSeries::<String>::new("Revenue");
                    revenue.push("Q1".into(), 12.5);
                    revenue.push("Q2".into(), 18.3);
                    let model = ChartModel::from_series_vec(vec![revenue]);
                    tree.add(BarChart::new(model).grid(true).value_labels(true))
                }),
        )
        .run();
}

Common operations

TaskAPI
Show bars, lines, or slicesBarChart, LineChart, PieChart
Set labels and number formattingAxisConfig::label and formatter
Show a legendlegend(true) and legend_position(...)
Update dataMutate the shared ChartModel
Share a selected datumPass a ChartSelection to .selection(...)
Change chart appearanceInstall a ChartStyle
Show the last N samplesPass ChartWindow::new(model.clone(), n) to a chart
Aggregate samplesCompute buckets with ChartAggregate in application code

Use patterns as well as color when series must remain distinguishable without color perception. Give the chart and its series meaningful labels.

Streaming history and scrolling

LineChart fits all supplied points into its current plot width. It does not increase its width as samples arrive and has no built-in history scrollbar, panning, or time viewport. Crowded category labels tilt and then use a stride; this changes labels, not the number of points drawn.

The x-axis is categorical: samples are spaced by index, even when their labels are timestamps. Unequal polling intervals are not represented as unequal gaps.

For a readable live tail, pass a ChartWindow directly. This constructor support is available on dev; published 0.14.3 only accepts ChartModel.

#![allow(unused)]
fn main() {
use teksilo_charts::{AxisConfig, ChartModel, ChartWindow, LineChart};

let history = ChartModel::<String>::new();
let gpu = history.add_series("GPU");
let tail = ChartWindow::new(history.clone(), 120);
let chart = LineChart::new(tail.clone())
    .axis_y(AxisConfig::new().range(0.0, 100.0))
    .legend(true);
history.push_point(gpu, "00:01".into(), 42.0);
tail.set_window_size(60);
}

BarChart, PieChart and ChartLegend accept the same ChartSource inputs. Window slices borrow the source points. Appends, changes to the window size, colors, patterns and legend visibility remain reactive. Hover and selection indices are relative to the visible window, so a point at index zero changes identity when the tail slides. Do not use those indices as persistent sample IDs.

A window bounds rendering, not storage. The source above retains every sample. For a long-running dashboard, choose a retention limit and remove old source points, archive them, or aggregate them. Keep the model in app state so rebuilding the widget tree does not reset history.

A ScrollArea can scroll a deliberately wider child, for example a chart wrapped in a FixedSize with an explicit width. That scrolls widget geometry; it does not provide a time-domain viewport or bound rendering work. A chart has no fixed 360 by 280 minimum: its intrinsic minimum depends on labels and legend layout.

BarChart

Vertical or horizontal bars, single or grouped series. Value labels, grid lines, axis titles, and an embedded legend are all opt-in flags on the builder.

#![allow(unused)]
fn main() {
use teksilo_charts::{AxisConfig, BarChart, BarGrouping, ChartModel, ChartSeries, LegendPosition};

let mut revenue = ChartSeries::<String>::new("Revenue");
revenue.push("Q1".into(), 12.5);
revenue.push("Q2".into(), 18.3);
revenue.push("Q3".into(), 9.8);
revenue.push("Q4".into(), 22.1);

let model = ChartModel::from_series_vec(vec![revenue]);

BarChart::new(model)
    .grid(true)
    .value_labels(true)
    .legend(true)
    .legend_position(LegendPosition::Bottom)
    .axis_y(
        AxisConfig::new()
            .label("USD (k)")
            .formatter(|v| format!("${:.0}", v)),
    )
    .axis_x(AxisConfig::new().label("Quarter"))
    .bar_corner_radius(2.0)
}

The y-domain auto-includes zero, bars without a zero baseline aren't legible, and the bar of a 100→102 series on a [100, 102] axis looks identical to a 0→2 series on a [0, 100] axis. Override with AxisConfig::range(min, max) if you really mean it.

LineChart

Polyline per series with optional area fill, the shared datum readout and an embedded legend.

#![allow(unused)]
fn main() {
use teksilo_charts::{AxisConfig, ChartModel, ChartSeries, LineChart};

let mut series = ChartSeries::<String>::new("Latency p99");
series.push("Mon".into(), 142.0);
series.push("Tue".into(), 138.5);
// ...

let model = ChartModel::from_series_vec(vec![series]);

LineChart::new(model)
    .grid(true)
    .points(true)
    .area_fill(true)
    .area_fill_opacity(0.15)
    .hover_tooltip(true)
    .axis_y(AxisConfig::new().label("ms"))
}

The y-domain pads ±5% so points at the data extremes don't sit on the axis edge. nice_ticks then snaps ticks outward, which can extend the range slightly past the padding, that's the standard data-viz behavior and matches matplotlib / d3.

PieChart (and donut)

One widget for both shapes. Set inner_radius_ratio == 0.0 (the default) for a pie; any positive value is a donut. The optional center widget slot is silently ignored when the ratio is 0.0, so swapping pie ↔ donut at runtime is safe.

#![allow(unused)]
fn main() {
use teksilo_charts::{ChartDatum, ChartModel, LegendPosition, PieChart, PieLabelMode};
use teksilo::widgets::{TextWidget, VStack};

let data: Vec<ChartDatum<String>> = /* … */;
let total = format!("${:.0}", data.iter().map(|d| d.value).sum::<f32>());
let model = ChartModel::from_points(data);

PieChart::new(model)
    .donut(0.55)
    .label_mode(PieLabelMode::Outside)
    .show_percentages(true)
    .legend(true)
    .legend_position(LegendPosition::Trailing)
    .center(
        VStack::new()
            .child(TextWidget::new(lit!("Total")).style(TextStyleRole::Tiny))
            .child(TextWidget::new(lit!(total))),
    )
}

The center slot follows the existing Option<PendingChild> pattern used by Card, DialogContent, and GroupBox: one builder, .center(impl IntoTeksiChild), taking either a widget or a WidgetId, resolved in build() via ctx.add_boxed.

The placement is the largest square inscribed in the donut hole (side = inner_radius * √2). A TextWidget for the total / a VStack of label + value / a small IconWidget all fit comfortably; larger compositions need to be self-clipping.

Constraints

  • Bars support single and grouped series; stacked bars are not implemented.
  • Axes use linear tick generation. There is no logarithmic axis or calendar-aware tick spacing. A custom label formatter changes labels, not tick placement.
  • Data changes update layout and paint without animated data transitions.
  • ChartWindow and ChartAggregate are application-side building blocks; chart widgets do not install them automatically.
  • Check legend target sizes at Compact density and test keyboard and assistive-technology navigation in the application.

Reference

Engineering reference

Implementation details and review history are retained in the repository.