in packages/devtools_app/lib/src/charts/chart.dart [152:423]
void paint(Canvas canvas, Size size) {
// TODO(terry): Used to monitor total painting time. For large
// datasets e.g., offline files of 10,000s of data
// points time can be kind of slows. Consider only
// sampling 1 point per horizontal pixel.
final startTime = DateTime.now();
final axis = Paint()
..strokeWidth = axisWidth
..color = Colors.grey;
if (size != chartController.size) {
chartController.size = size;
chartController.computeChartArea();
}
drawTranslate(
canvas,
chartController.xCanvasChart,
chartController.yCanvasChart,
(canavas) {
drawAxes(
canvas,
size,
axis,
displayX: chartController.displayXAxis,
displayTopLine: chartController.displayTopLine,
);
},
);
final traces = chartController.traces;
final tracesDataIndex = List<int>.generate(
traces.length,
(int index) {
final length = traces[index].data.length;
return length > 0 ? length - 1 : -1;
},
);
/// Key is trace index and value is x,y point.
final previousTracesData = <int, PointAndBase>{};
/// Key is trace index and value is x,y point.
final currentTracesData = <int, PointAndBase>{};
// Visible Y max.
var visibleYMax = 0.0;
// TODO(terry): Need to compute x-axis left-most position for last timestamp.
// May need to do the other direction so it looks better.
final endVisibleIndex =
chartController.timestampsLength - chartController.visibleTicks;
final xTranslation = chartController.xCoordLeftMostVisibleTimestamp;
final yTranslation = chartController.zeroYPosition;
int xTickIndex = chartController.timestampsLength;
while (--xTickIndex >= 0) {
final currentTimestamp = chartController.timestamps[xTickIndex];
if (xTickIndex < endVisibleIndex) {
// Once outside of visible range of data skip the rest of the collected data.
break;
}
final tracesLength = traces.length;
// Short-circuit if no traceDataIndexes left (all are -1) then we're done.
if (!tracesDataIndex.any((element) => element >= 0)) continue;
// Remember old cliprect.
canvas.save();
// Clip to the just the area being plotted. This is important so symbols
// larger than the tick area doesn't spill out on the left-side.
clipChart(canvas);
// Y base (stackedY) used for stacked traces each Y base for stack traces
// starts at zero. Then Y base for the current data point is the previous Y
// base (previous Y's datapoint). Then current data point's Y is added to Y base.
double stackedY = 0.0;
for (var index = 0; index < tracesLength; index++) {
final traceDataIndex = tracesDataIndex[index];
if (traceDataIndex >= 0) {
final trace = traces[index];
final traceData = trace.data[traceDataIndex];
final yValue = (trace.stacked) ? stackedY + traceData.y : traceData.y;
final xTimestamp = traceData.timestamp;
final xCanvasCoord =
chartController.timestampToXCanvasCoord(xTimestamp);
if (currentTimestamp == xTimestamp) {
if (xCanvasCoord != null) {
// Get ready to render on canvas. Remember old canvas state
// and setup translations for x,y coordinates into the rendering
// area of the chart.
drawTranslate(
canvas,
xTranslation,
yTranslation,
(canvas) {
final xCoord = xCanvasCoord;
final yCoord =
chartController.yPositionToYCanvasCoord(yValue);
final hasMultipleExtensionEvents =
traceData is DataAggregate ? traceData.count > 1 : false;
// Is the visible Y-axis max larger.
if (yValue > visibleYMax) {
visibleYMax = yValue;
}
currentTracesData[index] = PointAndBase(
xCoord,
yCoord,
yBase: chartController.yPositionToYCanvasCoord(stackedY),
);
if (trace.chartType == ChartType.symbol) {
assert(!trace.stacked);
drawSymbol(
canvas,
trace.characteristics,
xCoord,
yCoord,
hasMultipleExtensionEvents,
trace.symbolPath,
);
} else if (trace.chartType == ChartType.line) {
if (trace.characteristics.symbol ==
ChartSymbol.dashedLine) {
// TODO(terry): Collect all points and draw a dashed line using
// path_drawing package.
drawDashed(
canvas,
trace.characteristics,
xCoord,
yCoord,
chartController.tickWidth - 4,
);
} else if (previousTracesData[index] != null) {
// Stacked lines.
// Drawline from previous plotted point to new point.
drawConnectedLine(
canvas,
trace.characteristics,
xCoord,
yCoord,
previousTracesData[index].x,
previousTracesData[index].y,
);
drawSymbol(
canvas,
trace.characteristics,
xCoord,
yCoord,
hasMultipleExtensionEvents,
trace.symbolPath,
);
// TODO(terry): Honor z-order and also maybe path just on the traces e.g.,
// fill from top of trace 0 to top of trace 1 don't origin
// from zero.
// Fill area between traces.
drawFillArea(
canvas,
trace.characteristics,
previousTracesData[index].x,
previousTracesData[index].y,
previousTracesData[index].base,
currentTracesData[index].x,
currentTracesData[index].y,
currentTracesData[index].base,
);
} else {
// Draw point
drawSymbol(
canvas,
trace.characteristics,
xCoord,
yCoord,
hasMultipleExtensionEvents,
trace.symbolPath,
);
}
}
tracesDataIndex[index]--;
},
);
}
final tapLocation = chartController.tapLocation?.value;
if (tapLocation?.index == xTickIndex ||
tapLocation?.timestamp == currentTimestamp) {
drawTranslate(
canvas,
xTranslation,
yTranslation,
(canavas) {
drawSelection(
canvas,
xCanvasCoord,
chartController.canvasChartHeight,
);
},
);
}
}
if (trace.stacked) {
stackedY += traceData.y;
}
}
previousTracesData.addAll(currentTracesData);
currentTracesData.clear();
}
// Undo the clipRect at beginning of for loop.
canvas.restore();
}
chartController.computeChartArea();
chartController.buildLabelTimestamps();
if (chartController.displayXAxis || chartController.displayXLabels) {
// Y translation is below X-axis line.
drawTranslate(
canvas,
xTranslation,
chartController.zeroYPosition + 1,
(canvas) {
// Draw the X-axis labels.
for (var timestamp in chartController.labelTimestamps) {
final xCoord = chartController.timestampToXCanvasCoord(timestamp);
drawXTick(canvas, timestamp, xCoord, axis, displayTime: true);
}
},
);
// X translation is left-most edge of chart widget.
drawTranslate(
canvas,
chartController.xCanvasChart,
yTranslation,
(canvas) {
// Rescale Y-axis to max visible Y range.
chartController.resetYMaxValue(visibleYMax);
// Draw Y-axis ticks and labels.
// TODO(terry): Optimization add a listener for Y-axis range changing
// only need to redraw Y-axis if the range changed.
if (chartController.displayYLabels) {
drawYTicks(canvas, chartController, axis);
}
},
);
}
drawTitle(canvas, size, chartController.title);
final elapsedTime = DateTime.now().difference(startTime).inMilliseconds;
if (debugTrackPaintTime && elapsedTime > 500) {
logger.log('${chartController.name} ${chartController.timestampsLength} '
'CustomPainter paint elapsed time $elapsedTime');
}
// Once painted we're not dirty anymore.
chartController.dirty = false;
}