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/timeline.dart

228 lines
7.2 KiB

import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:github_dataviz/constants.dart';
import 'package:github_dataviz/data/week_label.dart';
import 'package:github_dataviz/mathutils.dart';
typedef MouseDownCallback = void Function(double xFraction);
typedef MouseMoveCallback = void Function(double xFraction);
typedef MouseUpCallback = void Function();
class Timeline extends StatefulWidget {
final int numWeeks;
final double animationValue;
final List<WeekLabel> weekLabels;
final MouseDownCallback? mouseDownCallback;
final MouseMoveCallback? mouseMoveCallback;
final MouseUpCallback? mouseUpCallback;
const Timeline(
{required this.numWeeks,
required this.animationValue,
required this.weekLabels,
this.mouseDownCallback,
this.mouseMoveCallback,
this.mouseUpCallback,
Key? key})
: super(key: key);
@override
State<StatefulWidget> createState() {
return TimelineState();
}
}
class TimelineState extends State<Timeline> {
HashMap<String, TextPainter> labelPainters = HashMap();
@override
void initState() {
super.initState();
for (int year = 2015; year < 2020; year++) {
String yearLabel = '$year';
labelPainters[yearLabel] =
_makeTextPainter(Constants.timelineLineColor, yearLabel);
}
for (var weekLabel in widget.weekLabels) {
labelPainters[weekLabel.label] =
_makeTextPainter(Constants.milestoneTimelineColor, weekLabel.label);
labelPainters[weekLabel.label + '_red'] =
_makeTextPainter(Colors.redAccent, weekLabel.label);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragDown: (DragDownDetails details) {
final mouseDownCallback = widget.mouseDownCallback;
if (mouseDownCallback != null) {
mouseDownCallback(
_getClampedXFractionLocalCoords(context, details.globalPosition));
}
},
onHorizontalDragEnd: (DragEndDetails details) {
final mouseUpCallback = widget.mouseUpCallback;
if (mouseUpCallback != null) {
mouseUpCallback();
}
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
final mouseMoveCallback = widget.mouseMoveCallback;
if (mouseMoveCallback != null) {
mouseMoveCallback(
_getClampedXFractionLocalCoords(context, details.globalPosition));
}
},
child: CustomPaint(
foregroundPainter: TimelinePainter(
this, widget.numWeeks, widget.animationValue, widget.weekLabels),
child: Container(
height: 200,
)),
);
}
TextPainter _makeTextPainter(Color color, String label) {
TextSpan span =
TextSpan(style: TextStyle(color: color, fontSize: 12), text: label);
TextPainter tp = TextPainter(
text: span,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr);
tp.layout();
return tp;
}
double _getClampedXFractionLocalCoords(
BuildContext context, Offset globalOffset) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localOffset = box.globalToLocal(globalOffset);
return MathUtils.clamp(localOffset.dx / context.size!.width, 0, 1);
}
}
class TimelinePainter extends CustomPainter {
TimelineState state;
late Paint mainLinePaint;
late Paint milestoneLinePaint;
Color lineColor = Colors.white;
int numWeeks;
double animationValue;
int weekYearOffset =
9; // Week 0 in our data is 9 weeks before the year boundary (i.e. week 43)
List<WeekLabel> weekLabels;
int yearNumber = 2015;
TimelinePainter(
this.state, this.numWeeks, this.animationValue, this.weekLabels) {
mainLinePaint = Paint();
mainLinePaint.style = PaintingStyle.stroke;
mainLinePaint.color = Constants.timelineLineColor;
milestoneLinePaint = Paint();
milestoneLinePaint.style = PaintingStyle.stroke;
milestoneLinePaint.color = Constants.milestoneTimelineColor;
}
@override
void paint(Canvas canvas, Size size) {
double labelHeight = 20;
double labelHeightDoubled = labelHeight * 2;
double mainLineY = size.height / 2;
canvas.drawLine(
Offset(0, mainLineY), Offset(size.width, mainLineY), mainLinePaint);
double currTimeX = size.width * animationValue;
canvas.drawLine(
Offset(currTimeX, labelHeightDoubled),
Offset(currTimeX, size.height - labelHeightDoubled),
milestoneLinePaint);
{
for (int week = 0; week < numWeeks; week++) {
double lineHeight = size.height / 32;
bool isYear = false;
if ((week - 9) % 52 == 0) {
// Year
isYear = true;
lineHeight = size.height / 2;
} else if ((week - 1) % 4 == 0) {
// Month
lineHeight = size.height / 8;
}
double currX = (week / numWeeks.toDouble()) * size.width;
if (lineHeight > 0) {
double margin = (size.height - lineHeight) / 2;
double currTimeXDiff = (currTimeX - currX) / size.width;
if (currTimeXDiff > 0) {
var mappedValue =
MathUtils.clampedMap(currTimeXDiff, 0, 0.025, 0, 1);
var lerpedColor = Color.lerp(Constants.milestoneTimelineColor,
Constants.timelineLineColor, mappedValue)!;
mainLinePaint.color = lerpedColor;
} else {
mainLinePaint.color = Constants.timelineLineColor;
}
canvas.drawLine(Offset(currX, margin),
Offset(currX, size.height - margin), mainLinePaint);
}
if (isYear) {
var yearLabel = '$yearNumber';
state.labelPainters[yearLabel]!
.paint(canvas, Offset(currX, size.height - labelHeight));
yearNumber++;
}
}
}
{
for (int i = 0; i < weekLabels.length; i++) {
WeekLabel weekLabel = weekLabels[i];
double currX = (weekLabel.weekNum! / numWeeks.toDouble()) * size.width;
var timelineXDiff = (currTimeX - currX) / size.width;
double maxTimelineDiff = 0.08;
TextPainter textPainter = state.labelPainters[weekLabel.label]!;
if (timelineXDiff > 0 &&
timelineXDiff < maxTimelineDiff &&
animationValue < 1) {
var mappedValue =
MathUtils.clampedMap(timelineXDiff, 0, maxTimelineDiff, 0, 1);
var lerpedColor = Color.lerp(
Colors.redAccent, Constants.milestoneTimelineColor, mappedValue)!;
milestoneLinePaint.strokeWidth =
MathUtils.clampedMap(timelineXDiff, 0, maxTimelineDiff, 6, 1);
milestoneLinePaint.color = lerpedColor;
} else {
milestoneLinePaint.strokeWidth = 1;
milestoneLinePaint.color = Constants.milestoneTimelineColor;
}
double lineHeight = size.height / 2;
double margin = (size.height - lineHeight) / 2;
canvas.drawLine(Offset(currX, margin),
Offset(currX, size.height - margin), milestoneLinePaint);
textPainter.paint(
canvas, Offset(currX, size.height - labelHeightDoubled));
}
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}