UI Charts

Overview

UIChart is eepp’s interactive, hardware-accelerated line chart widget. It supports multiple series and Y axes, live data, pan and zoom, hover tooltips, configurable grids, smooth curves, and rotated X-axis labels. Charts can be created in C++ or placed in a UI XML layout as <Chart>.

The public API is available through <eepp/ui.hpp>. The individual headers are under <eepp/ui/charts/>, and the types live in EE::UI::Charts. The complete runnable example is ` <https://github.com/SpartanJ/eepp/blob/develop/src/examples/ui_charts/ui_charts.cpp>`__.

The module currently draws line series on linear axes. X and Y values are numeric double s; formatters decide how ticks appear, including dates or units. A chart does not convert date strings or other application data into numeric coordinates for you.

A first chart

This example assumes an initialized UISceneNode, such as app.getUI() in a UIApplication :

#include <eepp/ui.hpp>

using namespace EE::UI;
using namespace EE::UI::Charts;

auto* chart = UIChart::New();
chart->setParent( ui->getRoot() );
chart->setSize( 500, 300 );
chart->xAxis().setLabel( "Second" );
chart->yAxis().setLabel( "Temperature (C)" );

auto& temperature = chart->addLineSeries( "Temperature" );
temperature.setColor( Color( 255, 113, 132 ) );
temperature.setWidth( 2.f ); // Device-independent pixels.
temperature.setPoints( std::vector<ChartPoint>{
    { 0.0, 20.0 }, { 1.0, 21.5 }, { 2.0, 21.0 }, { 3.0, 23.0 }
} );
chart->fit();

ChartPoint contains double x and double y. setPoints(std::vector<ChartPoint>) creates an owned data source and moves the vector into it when possible. There are also convenience overloads:

series.setPoints( xs, ys );              // Copies paired spans of double values.
series.setValues( ys, 10.0, 0.5 );        // Copies Y values; X is 10 + index * 0.5.

The paired overload copies up to the length of the shorter span. Supply equal-length spans when every sample matters. If the data already lives in another representation, use one of the data sources described below to avoid an unnecessary point copy.

Chart in an XML layout

Chart is registered with the standard widget creator. The layout controls the widget’s size; configure its model and chart-specific style from C++ after loading it:

<vbox lw="mp" lh="mp" padding="8dp">
    <TextView text="Temperature history" lw="mp" lh="wc" />
    <Chart id="temperature-chart" lw="mp" lh="0" lw8="1" />
</vbox>
auto* layout = ui->loadLayoutFromString( xml );
auto* chart = layout->find<UIChart>( "temperature-chart" );
chart->addLineSeries( "Temperature" ).setPoints( std::move( points ) );
chart->fit();

The lh="0" lh8="1" combination gives the chart the remaining height in a vbox, so it grows with its parent. Standard widget CSS can set its background, padding, and other widget properties. Chart-specific drawing options are set through ChartStyle.

Model, series, and axes

Every UIChart starts with a ChartModel, a bottom X axis, and a left Y axis. The model owns its axes and line series. addLineSeries(), addAxis(), xAxis(), and yAxis() return borrowed references to those model-owned objects. Added axes and series remain at stable addresses until the model is destroyed. Keep a std::shared_ptr<ChartModel> when their lifetime must extend beyond a particular widget.

auto& load = chart->addLineSeries( "Load" );
load.setPoints( std::move( loadPoints ) );

auto& rightAxis = chart->model().addAxis( AxisPosition::Right );
rightAxis.setLabel( "Temperature (C)" );
auto& ambient = chart->addLineSeries( "Ambient" );
ambient.setYAxis( rightAxis );
ambient.setPoints( std::move( ambientPoints ) );
chart->fit();

New series use the model’s default X and Y axes. setXAxis() and setYAxis() accept references to axes owned by the same model. ChartAxis::setLabel() stores a String, and setFormatter() customizes tick text:

chart->yAxis().setFormatter( []( double value, double ) {
    return String::format( "%.0f%%", value );
} );

The formatter receives the tick value and selected step. The default formatter selects readable decimal or scientific notation. Tick values are chosen automatically to approach the requested visual spacing; a formatter changes their text, not their positions.

To display the same data in separate viewports, share the model:

auto shared = chart->sharedModel();
otherChart->setModel( shared );
otherChart->setAxisRange( otherChart->xAxis(), { 50.0, 100.0 } );

Each widget retains its own visible ranges, style, layout, and hover state. setModel(nullptr) creates a fresh empty model. References obtained from the old model may then expire unless another shared_ptr keeps it alive.

Ranges and constraints

fit() fits every axis to the visible series data and turns off X follow mode. Without a manual range, axes fit their data automatically as sources change. You can set a range for a particular chart view and read it back:

chart->setAxisRange( chart->xAxis(), { 0.0, 60.0 } );
DataRange visibleX = chart->axisRange( chart->xAxis() );

Setting a range makes that axis manual. fit() returns it to automatic fitting. To limit pan, zoom, and fitted ranges, configure the axis itself:

RangeConstraints limits;
limits.absoluteMinimum = 0.0;
limits.absoluteMaximum = 100.0;
limits.minimumViewSpan = 5.0;
limits.maximumViewSpan = 100.0; // Zero means unbounded.
chart->yAxis().setConstraints( limits );

Constraints belong to the model’s axis and therefore apply to every view sharing that model. Visible ranges belong to each UIChart.

Data sources

LineSeries::setDataSource(std::shared_ptr<XYDataSource>) accepts a reusable source and retains a shared reference. Several series can share one source, including across charts. Choose a source according to how data is stored and updated:

Source

Use

Storage and updates

For a live chart, retain the source and append on a UI timer:

auto ring = std::make_shared<RingXYDataSource>( 600 );
auto& live = chart->addLineSeries( "CPU" );
live.setDataSource( ring );
chart->setFollowX( 60.0 ); // Keep a 60-unit X window at the newest sample.

ui->setInterval( [ring, nextX = 0.0]() mutable {
    const double value = readCpuLoad();
    ring->append( { nextX++, value } );
}, Milliseconds( 100 ), String::hash( "cpu-chart-update" ) );

Here readCpuLoad() stands for an application-provided sampler. Cancel the timer when the associated screen is removed if the application keeps its UI scene alive.

Keep X values increasing for the optimized visible-range lookup and pixel-bucket reduction. In a ring source, a wrapped read exposes two logical chunks in the right order; callers do not need to rearrange samples. setFollowX(span) uses the newest X value on the primary X axis and refits Y axes to the visible window as data changes. Pass zero to stop following. Manual range changes, mouse pan, mouse zoom, and fit() also stop follow mode. Call setFollowX() again to resume it.

For immutable X and Y arrays, keep them in shared vectors and create a zero-copy source:

auto xs = std::make_shared<const std::vector<double>>( std::move( times ) );
auto ys = std::make_shared<const std::vector<Float>>( std::move( values ) );
series.setDataSource( makeArrayXYDataSource( xs, ys ) );

makeMemberXYDataSource() similarly reads numeric members of a shared vector of standard-layout records. Its X and Y members can be float or double. Treat storage behind an immutable source as immutable for the source’s whole lifetime. ModelXYDataSource accepts a shared Models::Model, X and Y column indexes, and optional model roles; nonnumeric values become gaps. Its cache is rebuilt when the model reports an update, so it is most useful when model integration matters more than avoiding a copy on each update.

Custom sources implement XYDataSource::acquireRead() and publish a coherent XYDataRead with a generation, data order, and one or more XYDataChunk s. A read’s spans and backing token must stay valid until that read is destroyed. For mutable storage, writers must be synchronized with active readers. DataDelta can describe an append or reset for consumers that can update incrementally. The built-in mutable sources use locks and release them before notifying listeners. Keep read transactions short so producers are not blocked by a long-held read lock.

Nonfinite X or Y values (NaN or infinity) form gaps rather than connected line segments. An ascending X order is required for the fast reduction path; out-of-order X data does not receive that reduction. If plotting becomes unexpectedly slow or empty, inspect the source’s XYDataRead::order() and the X values.

Appearance

Copy the current ChartStyle, change the desired fields, and pass the full style back. The chart keeps its own copy. Lengths such as margins, font size, tick spacing, line width, hover distance, and grid screen spacing are device-independent pixels (dp), scaled by eepp’s PixelDensity.

ChartStyle style = chart->chartStyle();
style.fontSize = 13.f;
style.xTickSpacing = 96.f;
style.tickLabelColor = Color( 210, 214, 220 );
style.axisColor = Color( 125, 130, 138 );
chart->setChartStyle( style );

series.setColor( Color( 83, 179, 255 ) );
series.setWidth( 3.f );
series.setJoin( LineJoin::Bevel );
series.setCap( LineCap::Square );
series.setMiterLimit( 4.f );
series.setVisible( true );

Text

Margins and text gaps

Ticks

Strokes and hover

Grid

An unset optional color follows the current UI theme. The chart uses the theme’s --tab-line for axes, --font-hint for tick text, --font for axis labels, and --primary for its default series and hover colors, with built-in fallback colors. Tick color defaults to the resolved axis color. Grid color defaults to the axis color with reduced opacity. ChartStyle::font == nullptr uses the UI theme’s default font. The optional font pointer is borrowed; its font must outlive the chart’s use of it. A series’s explicit setColor() overrides the style’s default series color.

Grids

Vertical and horizontal grids are configured independently. Both start disabled.

Input Result =============================== ===================================================== Hover near a series Show the nearest sample’s tooltip and an X guide line Drag with the left mouse button Pan the visible ranges; stop X follow mode Mouse wheel over the plot Zoom around the cursor; stop X follow mode

Fit visible series and stop X follow mode