chartificial
Cartesian charts for Flutter, built on the framework's two-dimensional viewport. Designed for the charts that are awkward everywhere else: time series that scroll, history that loads as you reach it, and axis labels that are real widgets.

Why chartificial
- Lazy paged loading. Give it a
ChartLoaderand history is fetched before the user scrolls to the edge, with a configurable prefetch margin. Prepending a page never moves the pixels already on screen. - Axis labels are widgets.
AxisLabelConfig.builderreturns whatever you like — an icon, a chip, a two-line date. The viewport recycles them instead of rebuilding, so scrolling costs nothing extra. - A fixed spine, pluggable policies. The frame sequence is closed; every
decision inside it is an interface you can implement. Series, decorations,
range policies, tick strategies, scales and pointer snapping are all open
seams — the built-in bar series is itself just a client of the public
ChartSeries. - No dependencies.
flutterand nothing else. Android, iOS, web, macOS, Windows and Linux.
Gallery
Scrolling time axis — newest point pinned right, scroll into the past![]() |
Value-space color bands — stroke colored by value, not by position![]() |
Line styles — curves, dashes, area fill, gradient stroke, end badge![]() |
Floating bars — min–max pills over a background track![]() |
Stacked + grouped bars — cumulative segments, clustered per slot![]() |
Mixed series — bars and a line sharing a scrolling axis![]() |
Custom decorations — replace the grid, ring the viewport edge crossings![]() |
Custom series — a lollipop series defined outside the package![]() |
Install
$ flutter pub add chartificial
import 'package:chartificial/chartificial.dart';
Quick start
Everything but the axes and series has a working default, and it is all
const-constructible:
SizedBox(
height: 240,
child: Chartificial(
xAxis: const NumericAxis(),
yAxis: const NumericAxis(),
series: const [
LineSeries(
id: 'demo',
data: [
ChartPoint(0, 1),
ChartPoint(1, 3),
ChartPoint(2, 2),
ChartPoint(3, 5),
],
),
],
),
)
Like every viewport, the chart cannot size itself to its content — give it bounded constraints in both directions (
SizedBox,Expanded,AspectRatio).
Scrolling time series
The case the package is built for. AxisAnchor.end starts at the newest point
and scrolls left into history; AxisResolution.interval fixes how many pixels
an hour occupies, which is what makes the axis scrollable at all; and
ChartLoader fetches older pages before the user reaches them.
final controller = ChartificialController(
followLatest: true, // stay pinned to "now" until the user scrolls away
loader: ChartLoader(
onLoadOlder: (request) async {
final page = await api.fetchBefore(request.edgeDateTime);
controller.prependPoints('aqi', page.points);
return page.hasMore; // returning false latches exhaustion
},
),
);
Chartificial(
controller: controller,
xAxis: DateTimeAxis(
anchor: AxisAnchor.end,
resolution: AxisResolution.interval(const Duration(hours: 1), 24),
ticks: AxisTicks.timeInterval(const Duration(hours: 6)),
labels: AxisLabelConfig(
builder: (context, tick) => Text('${tick.dateTime.hour}:00'),
),
),
yAxis: const NumericAxis(range: AxisRange.fixed(0, 300)),
series: const [
LineSeries(
id: 'aqi',
colorScale: ChartColorScale.bands([
ColorBand(0, 50, Color(0xFF4CAF50)),
ColorBand(50, 100, Color(0xFFFFC107)),
ColorBand(100, 300, Color(0xFFF44336)),
]),
),
],
trackball: TrackballBehavior(
builder: (context, details) => MyTooltip(details),
),
)
ChartColorScale stops are defined in value space, so a band boundary sits
exactly at 50 however the axis is transformed — including a log axis.
Extension points
The spine being closed is the point: paint order, label recycling, scroll anchoring and load triggering are invariants you compose within.
| Seam | Use |
|---|---|
ChartSeries |
Define your own series type: painting, wide-mark overhang, marker color, pointer snapping. BarSeries is built on this same interface. |
ChartSeries.snap |
Per-series trackball/hit snapping — clustered bars shift by their offset; custom series redefine "nearest" or opt out. |
AxisRange |
Custom range policies. The auto/fixed/visible built-ins are exported so yours can delegate to them. |
AxisTicks |
Custom tick strategies via resolve(TickResolveContext) → ResolvedTicks. |
AxisScale |
Any strictly-increasing domain transform; LinearScale and LogScale ship. |
AxisLabelConfig.builder |
Any widget as an axis label, recycled while scrolling. |
ChartDecoration |
Paint layers behind or above the series — the default grid is one of these, and is removable. |
ChartColorScale |
Value-space gradient stops or hard bands for series strokes. |
ChartPoint |
Subclass it; yExtent lets range-like data auto-range correctly. BarPoint does exactly this. |
SeriesRenderSnapshot.geometry |
Per-frame channel for a series to publish paint products to decorations above it. |
ChartLoader |
Async paged loading triggered by scroll position, with a prefetch margin. |
Every open seam takes a single context object (ChartPaintContext,
AxisRangeContext, TickResolveContext), so new capabilities arrive as new
context fields rather than as signature breaks.
Example app
example/ has twelve runnable demos:
| Demo | Shows |
|---|---|
| Infinite past + lazy loading | Newest at the right, pages loaded ahead of the scroll |
| Live values + followLatest | A point per second, pinned to "now" until you scroll away |
| AQI gradient line | Value-band stroke coloring with band backgrounds |
| Line styles | Curves, dashes, area fill, gradient stroke, end badge, opposed axis |
| Bar chart: daily range | Floating min–max pill bars on a background track |
| Stacked + grouped bars | Pollutant mix stacked per bar, indoor and outdoor clustered |
| Scrolling time bars | Hourly bars plus an average line, stable at the edges |
| Custom grid + edge markers | A replaced grid, rings where the line crosses the edges |
| Custom lollipop series | A series defined by the app — the ChartSeries seam |
| Fixed axes + callbacks | Non-scrollable chart with gaps, markers, raw hit callbacks |
| Logarithmic axis | Four decades of particle counts with decade ticks |
| Visible-window y range | The y axis rescales to whatever is on screen |
$ cd example && flutter run
Requirements
Flutter >=3.44.0 / Dart ^3.12.0, for the TwoDimensionalViewport APIs the
chart is built on.
Contributing
Issues and pull requests are welcome — see CONTRIBUTING.md.
License
MIT — see LICENSE.
Libraries
- chartificial
- Cartesian charts built on Flutter's two-dimensional viewport: scrollable time series with lazy data loading, widget-based axis labels, value-space color scales, and pluggable rendering layers.







