You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
samples/web/github_dataviz/lib/layered_chart.dart

337 lines
11 KiB

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:github_dataviz/catmull.dart';
import 'package:github_dataviz/constants.dart';
import 'package:github_dataviz/data/data_series.dart';
import 'package:github_dataviz/data/week_label.dart';
import 'package:github_dataviz/mathutils.dart';
class LayeredChart extends StatefulWidget {
final List<DataSeries> dataToPlot;
final List<WeekLabel> milestones;
final double animationValue;
const LayeredChart(this.dataToPlot, this.milestones, this.animationValue,
{Key? key})
: super(key: key);
@override
State<StatefulWidget> createState() {
return LayeredChartState();
}
}
class LayeredChartState extends State<LayeredChart> {
late List<Path> paths;
late List<Path> capPaths;
late List<double> maxValues;
late double theta;
late double graphHeight;
late List<TextPainter> labelPainter;
late List<TextPainter> milestonePainter;
Size? lastSize;
void buildPaths(
Size size,
List<DataSeries> dataToPlot,
List<WeekLabel> milestones,
int numPoints,
double graphGap,
double margin,
double capTheta,
double capSize) {
double screenRatio = size.width / size.height;
double degrees = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 5);
theta = pi * degrees / 180;
graphHeight = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 150);
int m = dataToPlot.length;
paths = [];
capPaths = [];
maxValues = [];
for (int i = 0; i < m; i++) {
int n = dataToPlot[i].series.length;
maxValues.add(0);
for (int j = 0; j < n; j++) {
double v = dataToPlot[i].series[j].toDouble();
if (v > maxValues[i]) {
maxValues[i] = v;
}
}
}
double totalGap = m * graphGap;
double xIndent = totalGap / tan(capTheta);
double startX = margin + xIndent;
double endX = size.width - margin;
double startY = size.height;
double endY = startY - (endX - startX) * tan(theta);
double xWidth = (endX - startX) / numPoints;
double capRangeX = capSize * cos(capTheta);
double tanCapTheta = tan(capTheta);
final curvePoints = <double>[];
for (int i = 0; i < m; i++) {
List<int> series = dataToPlot[i].series;
int n = series.length;
final controlPoints = <Point2D>[];
controlPoints.add(Point2D(-1, 0));
double last = 0;
for (int j = 0; j < n; j++) {
double v = series[j].toDouble();
controlPoints.add(Point2D(j.toDouble(), v));
last = v;
}
controlPoints.add(Point2D(n.toDouble(), last));
CatmullInterpolator curve = CatmullInterpolator(controlPoints);
ControlPointAndValue cpv = ControlPointAndValue();
for (int j = 0; j < numPoints; j++) {
cpv.value = MathUtils.map(
j.toDouble(), 0, (numPoints - 1).toDouble(), 0, (n - 1).toDouble());
curve.progressiveGet(cpv);
curvePoints.add(MathUtils.map(
max(0, cpv.value!), 0, maxValues[i].toDouble(), 0, graphHeight));
}
paths.add(Path());
capPaths.add(Path());
paths[i].moveTo(startX, startY);
capPaths[i].moveTo(startX, startY);
for (int j = 0; j < numPoints; j++) {
double v = curvePoints[j];
int k = j + 1;
double xDist = xWidth;
double capV = v;
while (k < numPoints && xDist <= capRangeX) {
double cy = curvePoints[k] + xDist * tanCapTheta;
capV = max(capV, cy);
k++;
xDist += xWidth;
}
double x = MathUtils.map(
j.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX);
double baseY = MathUtils.map(
j.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY);
double y = baseY - v;
double cY = baseY - capV;
paths[i].lineTo(x, y);
if (j == 0) {
int k = capRangeX ~/ xWidth;
double mx = MathUtils.map(
-k.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX);
double my = MathUtils.map(
-k.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY) -
capV;
capPaths[i].lineTo(mx, my);
}
capPaths[i].lineTo(x, cY);
}
paths[i].lineTo(endX, endY);
paths[i].lineTo(endX, endY + 1);
paths[i].lineTo(startX, startY + 1);
paths[i].close();
capPaths[i].lineTo(endX, endY);
capPaths[i].lineTo(endX, endY + 1);
capPaths[i].lineTo(startX, startY + 1);
capPaths[i].close();
}
labelPainter = [];
for (int i = 0; i < dataToPlot.length; i++) {
TextSpan span = TextSpan(
style: const TextStyle(
color: Color.fromARGB(255, 255, 255, 255), fontSize: 12),
text: dataToPlot[i].label.toUpperCase());
TextPainter tp = TextPainter(
text: span,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr);
tp.layout();
labelPainter.add(tp);
}
milestonePainter = [];
for (int i = 0; i < milestones.length; i++) {
TextSpan span = TextSpan(
style: const TextStyle(
color: Color.fromARGB(255, 255, 255, 255), fontSize: 10),
text: milestones[i].label.toUpperCase());
TextPainter tp = TextPainter(
text: span,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr);
tp.layout();
milestonePainter.add(tp);
}
lastSize = Size(size.width, size.height);
}
@override
Widget build(BuildContext context) {
return Container(
color: Constants.backgroundColor,
child: CustomPaint(
foregroundPainter: ChartPainter(this, widget.dataToPlot,
widget.milestones, 80, 50, 50, 12, 500, widget.animationValue),
child: Container()));
}
}
class ChartPainter extends CustomPainter {
static List<Color?> colors = [
Colors.red[900],
const Color(0xffc4721a),
Colors.lime[900],
Colors.green[900],
Colors.blue[900],
Colors.purple[900],
];
static List<Color?> capColors = [
Colors.red[500],
Colors.amber[500],
Colors.lime[500],
Colors.green[500],
Colors.blue[500],
Colors.purple[500],
];
List<DataSeries> dataToPlot;
List<WeekLabel> milestones;
double margin;
double graphGap;
late double capTheta;
double capSize;
int numPoints;
double amount = 1.0;
late Paint pathPaint;
late Paint capPaint;
late Paint textPaint;
late Paint milestonePaint;
late Paint linePaint;
late Paint fillPaint;
LayeredChartState state;
ChartPainter(
this.state,
this.dataToPlot,
this.milestones,
this.margin,
this.graphGap,
double capDegrees,
this.capSize,
this.numPoints,
this.amount) {
capTheta = pi * capDegrees / 180;
pathPaint = Paint();
pathPaint.style = PaintingStyle.fill;
capPaint = Paint();
capPaint.style = PaintingStyle.fill;
textPaint = Paint();
textPaint.color = const Color(0xFFFFFFFF);
milestonePaint = Paint();
milestonePaint.color = Constants.milestoneColor;
milestonePaint.style = PaintingStyle.stroke;
milestonePaint.strokeWidth = 2;
linePaint = Paint();
linePaint.style = PaintingStyle.stroke;
linePaint.strokeWidth = 0.5;
fillPaint = Paint();
fillPaint.style = PaintingStyle.fill;
fillPaint.color = const Color(0xFF000000);
}
@override
void paint(Canvas canvas, Size size) {
if (dataToPlot.isEmpty) {
return;
}
if (state.lastSize == null ||
size.width != state.lastSize!.width ||
size.height != state.lastSize!.height) {
print('Building paths, lastsize = ${state.lastSize}');
state.buildPaths(size, dataToPlot, milestones, numPoints, graphGap,
margin, capTheta, capSize);
}
int m = dataToPlot.length;
int numWeeks = dataToPlot[0].series.length;
// How far along to draw
double totalGap = m * graphGap;
double xIndent = totalGap / tan(capTheta);
double dx = xIndent / (m - 1);
double startX = margin + xIndent;
double endX = size.width - margin;
double startY = size.height;
double endY = startY - (endX - startX) * tan(state.theta);
// MILESTONES
{
for (int i = 0; i < milestones.length; i++) {
WeekLabel milestone = milestones[i];
double p = (milestone.weekNum!.toDouble() / numWeeks) + (1 - amount);
if (p < 1) {
double x1 = MathUtils.map(p, 0, 1, startX, endX);
double y1 = MathUtils.map(p, 0, 1, startY, endY);
double x2 = x1 - xIndent;
double y2 = y1 - graphGap * (m - 1);
x1 += dx * 0.5;
y1 += graphGap * 0.5;
double textY = y1 + 5;
double textX = x1 + 5 * tan(capTheta);
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), milestonePaint);
canvas.save();
TextPainter tp = state.milestonePainter[i];
canvas.translate(textX, textY);
canvas.skew(tan(capTheta * 1.0), -tan(state.theta));
canvas.translate(-tp.width / 2, 0);
tp.paint(canvas, const Offset(0, 0));
canvas.restore();
}
}
}
for (int i = m - 1; i >= 0; i--) {
canvas.save();
canvas.translate(-dx * i, -graphGap * i);
{
// TEXT LABELS
canvas.save();
double textPosition = 0.2;
double textX = MathUtils.map(textPosition, 0, 1, startX, endX);
double textY = MathUtils.map(textPosition, 0, 1, startY, endY) + 5;
canvas.translate(textX, textY);
TextPainter tp = state.labelPainter[i];
canvas.skew(0, -tan(state.theta));
canvas.drawRect(
Rect.fromLTWH(-1, -1, tp.width + 2, tp.height + 2), fillPaint);
tp.paint(canvas, const Offset(0, 0));
canvas.restore();
}
linePaint.color = capColors[i]!;
canvas.drawLine(Offset(startX, startY), Offset(endX, endY), linePaint);
Path clipPath = Path();
clipPath.moveTo(startX - capSize, startY + 11);
clipPath.lineTo(endX, endY + 1);
clipPath.lineTo(endX, endY - state.graphHeight - capSize);
clipPath.lineTo(startX - capSize, startY - state.graphHeight - capSize);
clipPath.close();
canvas.clipPath(clipPath);
pathPaint.color = colors[i]!;
capPaint.color = capColors[i]!;
double offsetX = MathUtils.map(1 - amount, 0, 1, startX, endX);
double offsetY = MathUtils.map(1 - amount, 0, 1, startY, endY);
canvas.translate(offsetX - startX, offsetY - startY);
canvas.drawPath(state.capPaths[i], capPaint);
canvas.drawPath(state.paths[i], pathPaint);
canvas.restore();
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}