We gladly accept contributions via GitHub pull requests!
You must complete the Contributor License Agreement. You can do this online, and it only takes a minute. If you‘ve never submitted code before, you must add your (or your organization’s) name and contact info to the AUTHORS file.
See Development prep in CONTRIBUTING
DevTools has used a number of existing charting packages with various issues e.g.,
This charting subsystem plots timeseries data. Two basic charts are supported:
A chart consist of one or more traces. A trace is a collection of data to be plotted in the same chart on the same temporal X-axis (time). For example, a chart could have 4 traces:
In all cases, the above 4 traces share the same X-Axis time scale. However, the first two traces have numeric data a real value in total bytes, the third is number of objects and the last is when a GC occurred. There are a few ways to display these seemly disparate pieces of data.
First, create a StatefulWidget to contain the chart e.g.,
class MyChart extends StatefulWidget {
MyChart({super.key});
final controller = ChartController();
@override
State<StatefulWidget> createState() => MyChartState();
}
class MyChartState extends State<MyChart> {
MyChartState(this.controller);
ChartController get controller => widget.controller;
}
Then, override the State's initState and build methods in MyChartState e.g.,
@override
void initState() {
super.initState();
// If you're creating a line chart then fix the Y-range. For a scatter chart
// don't create a fixed range, then the chart will autmatically compute the
// min/max range and rescale the axis.
controller.setFixedYRange(0.4, 2.4); // Line chart fixed.
// Create the traces to hold each set of data points e.g., temperature
// collected every hour as well as barametric pressure collected every hour.
setupTraces();
}
@override
Widget build(BuildContext context) {
final ballChart = Chart(controller, 'Balls Chart');
return Padding(
padding: const EdgeInsets.all(0.0),
child: ballChart,
);
}
Create each trace, in this example we‘re creating a red circle and blue ball to appear at particular points in time. The Y coordinate of a datum (for a line chart) is not significant other than where to place the event in time. For a scatter chart, the datum’s Y coordinate would be significant e.g., temperature. For both the line and scatter charts the X coordinate is the time. Important note a trace contains the specific plotting characteristers e.g., color, shape, connected line, etc.
This implies that even though different datum could be contained in a single trace each datum would need to hold its rendering characteristics when a datum is rendered in the trace. Considering that a trace may contain many thousands (tens of thousands) of pieces of data that would create unnessary datum overhead as well as more complex painting inside the canvas code when rendering different datum in a trace. The internal chart rendering instead, blasts each trace‘s data with the same trace’s monolithic rendering characterstics.
void setupTraces() {
// Green Circle
greenTraceIndex = controller.createTrace(
ChartType.symbol,
PaintCharacteristics(
color: Colors.green,
strokeWidth: 4,
diameter: 6,
fixedMinY: 0.4,
fixedMaxY: 2.4,
),
name: 'Green Circle',
);
// Small Blue Ball
blueTraceIndex = controller.createTrace(
ChartType.symbol,
PaintCharacteristics(
color: Colors.blue,
symbol: ChartSymbol.disc,
diameter: 4,
fixedMinY: 0.4,
fixedMaxY: 2.4,
),
name: 'Blue Ball',
);
// Red Circle
redTraceIndex = controller.createTrace(
ChartType.symbol,
PaintCharacteristics(
color: Colors.red,
strokeWidth: 4,
diameter: 6,
fixedMinY: 0.4,
fixedMaxY: 2.4,
),
name: 'Red Circle',
);
}
Create a datum (see Data class) and add the data to a trace (adddatum). In the below example, a datum is created first and added to the green trace (green circle), every second a datum is added to the blue trace (blue ball) then after 30 seconds the last datum is added to the red trace (red circle).
static const greenPosition = 0.4; // Starting symbol position
static const bluePosition = 1.4; // Every second symbol position
static const redPosition = 2.4; // Stoping symbol position
var previousTime = DateTime.now();
final startDatum = Data(previousTime.millisecondsSinceEpoch, greenPosition);
// Start the heartbeat.
controller.addTimestamp(startDatum.timestamp);
// Add the first real datum.
controller.trace(greenTraceIndex).addDatum(startDatum);
Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (controller.trace(blueTraceIndex).data.length < 30) {
final currentTime = DateTime.now();
final = currentTime.millisecondsSinceEpoch;
// Once a second heartbeat.
controller.addTimestamps(timestamp);
// Add the blue ball.
final datum = Data(timestamp, bluePosition);
controller.trace(blueTraceIndex).addDatum(datum);
previousTime = currentTime;
} else {
controller.addTimestamps(previousTime.millisecondsSinceEpoch);
final stopDatum = Data(
previousTime.millisecondsSinceEpoch,
redPosition,
);
// Last datum is the red circle.
controller.trace(redTraceIndex).addDatum(stopDatum);
timer.cancel();
}
},
);
Each call to addDatum will
One important piece of information is adding a heartbeat. If a live chart is needed where the timeline (X axis) moves in a linear fashion requires adding a heart beat (at the granularity requested of the X axis) a tickstamp is added to the ChartController timestamps field on every tick e.g.,
controller.addTimestamps(currentTime.millisecondsSinceEpoch)
The heartbeat allows the data to be replayed as if the data collection is live, at the same rate of experiencing the collecting of the live data.