1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 22:09:06 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
import 'package:github_dataviz/mathutils.dart';
class ControlPointAndValue {
int point;
double value;
ControlPointAndValue() {
value = 0;
point = 2;
}
}
class CatmullInterpolator implements Interpolator {
List<Point2D> controlPoints;
CatmullInterpolator(this.controlPoints);
@override
double get(double v) {
for (int i = 2; i < controlPoints.length - 1; i++) {
if (controlPoints[i].x >= v) {
double t = (v - controlPoints[i - 1].x) /
(controlPoints[i].x - controlPoints[i - 1].x);
double p0 = controlPoints[i - 2].y;
double p1 = controlPoints[i - 1].y;
double p2 = controlPoints[i].y;
double p3 = controlPoints[i + 1].y;
return 0.5 *
((2 * p1) +
(p2 - p0) * t +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t +
(3 * p1 - p0 - 3 * p2 + p3) * t * t * t);
}
}
// Will be unreachable if the control points were set up right
return 0;
}
ControlPointAndValue progressiveGet(ControlPointAndValue cpv) {
double v = cpv.value;
for (int i = cpv.point; i < controlPoints.length - 1; i++) {
if (controlPoints[i].x >= v) {
double t = (v - controlPoints[i - 1].x) /
(controlPoints[i].x - controlPoints[i - 1].x);
double p0 = controlPoints[i - 2].y;
double p1 = controlPoints[i - 1].y;
double p2 = controlPoints[i].y;
double p3 = controlPoints[i + 1].y;
cpv.value = 0.5 *
((2 * p1) +
(p2 - p0) * t +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t +
(3 * p1 - p0 - 3 * p2 + p3) * t * t * t);
cpv.point = i;
return cpv;
}
}
// Will be unreachable if the control points were set up right
return cpv;
}
static void test() {
List<Point2D> controlPoints = List<Point2D>();
controlPoints.add(Point2D(-1, 1));
controlPoints.add(Point2D(0, 1));
controlPoints.add(Point2D(1, -1));
controlPoints.add(Point2D(3, 4));
controlPoints.add(Point2D(10, -2));
controlPoints.add(Point2D(11, -2));
CatmullInterpolator catmull = CatmullInterpolator(controlPoints);
print(catmull.get(0));
print(catmull.get(1));
print(catmull.get(2));
print(catmull.get(5));
print(catmull.get(7));
print(catmull.get(8));
print(catmull.get(10));
}
}

View File

@@ -0,0 +1,9 @@
import 'package:flutter_web/material.dart';
import 'package:flutter_web_ui/ui.dart';
class Constants {
static final Color backgroundColor = const Color(0xFF000020);
static final Color timelineLineColor = Color(0x60FFFFFF);
static final Color milestoneColor = Color(0x40FFFFFF);
static final Color milestoneTimelineColor = Colors.white;
}

View File

@@ -0,0 +1,14 @@
class ContributionData {
int weekTime;
int add;
int delete;
int change;
ContributionData(this.weekTime, this.add, this.delete, this.change);
static ContributionData fromJson(Map<String, dynamic> jsonMap) {
ContributionData data = ContributionData(
jsonMap["w"], jsonMap["a"], jsonMap["d"], jsonMap["c"]);
return data;
}
}

View File

@@ -0,0 +1,6 @@
class DataSeries {
String label;
List<int> series;
DataSeries(this.label, this.series);
}

View File

@@ -0,0 +1,6 @@
class StatForWeek {
int weekIndex;
int stat;
StatForWeek(this.weekIndex, this.stat);
}

View File

@@ -0,0 +1,12 @@
class User {
int id;
String username;
String avatarUrl;
User(this.id, this.username, this.avatarUrl);
static User fromJson(Map<String, dynamic> jsonMap) {
User user = User(jsonMap["id"], jsonMap["login"], jsonMap["avatar_url"]);
return user;
}
}

View File

@@ -0,0 +1,18 @@
import 'package:github_dataviz/data/contribution_data.dart';
import 'package:github_dataviz/data/user.dart';
class UserContribution {
User user;
List<ContributionData> contributions;
UserContribution(this.user, this.contributions);
static UserContribution fromJson(Map<String, dynamic> jsonMap) {
List<ContributionData> contributionList = (jsonMap["weeks"] as List)
.map((e) => ContributionData.fromJson(e))
.toList();
var userContribution =
UserContribution(User.fromJson(jsonMap["author"]), contributionList);
return userContribution;
}
}

View File

@@ -0,0 +1,24 @@
import 'package:intl/intl.dart';
class WeekLabel {
int weekNum;
String label;
WeekLabel(this.weekNum, this.label);
WeekLabel.forDate(DateTime date, String label) {
this.label = label;
int year = getYear(date);
int weekOfYearNum = getWeekNumber(date);
this.weekNum = 9 + ((year - 2015) * 52) + weekOfYearNum;
}
int getYear(DateTime date) {
return int.parse(DateFormat("y").format(date));
}
int getWeekNumber(DateTime date) {
int dayOfYear = int.parse(DateFormat("D").format(date));
return ((dayOfYear - date.weekday + 10) / 7).floor();
}
}

View File

@@ -0,0 +1,336 @@
import 'dart:math';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/painting.dart';
import 'package:flutter_web/widgets.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;
LayeredChart(this.dataToPlot, this.milestones, this.animationValue);
@override
State<StatefulWidget> createState() {
return LayeredChartState();
}
}
class LayeredChartState extends State<LayeredChart> {
List<Path> paths;
List<Path> capPaths;
List<double> maxValues;
double theta;
double graphHeight;
List<TextPainter> labelPainter;
List<TextPainter> milestonePainter;
Size lastSize = null;
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 = List<Path>(m);
capPaths = List<Path>(m);
maxValues = List<double>(m);
for (int i = 0; i < m; i++) {
int n = dataToPlot[i].series.length;
maxValues[i] = 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);
List<double> curvePoints = List<double>(numPoints);
for (int i = 0; i < m; i++) {
List<int> series = dataToPlot[i].series;
int n = series.length;
List<Point2D> controlPoints = List<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[j] = MathUtils.map(
max(0, cpv.value), 0, maxValues[i].toDouble(), 0, graphHeight);
}
paths[i] = Path();
capPaths[i] = 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 = List<TextPainter>();
for (int i = 0; i < dataToPlot.length; i++) {
TextSpan span = TextSpan(
style: 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 = List<TextPainter>();
for (int i = 0; i < milestones.length; i++) {
TextSpan span = TextSpan(
style: 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],
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;
double capTheta;
double capSize;
int numPoints;
double amount = 1.0;
Paint pathPaint;
Paint capPaint;
Paint textPaint;
Paint milestonePaint;
Paint linePaint;
Paint fillPaint;
LayeredChartState state;
ChartPainter(
this.state,
this.dataToPlot,
this.milestones,
this.margin,
this.graphGap,
double capDegrees,
this.capSize,
this.numPoints,
this.amount) {
this.capTheta = pi * capDegrees / 180;
pathPaint = Paint();
pathPaint.style = PaintingStyle.fill;
capPaint = Paint();
capPaint.style = PaintingStyle.fill;
textPaint = Paint();
textPaint.color = 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 = Color(0xFF000000);
}
@override
void paint(Canvas canvas, Size size) {
if (dataToPlot.length == 0) {
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, 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, 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;
}
}

View File

@@ -0,0 +1,255 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'dart:convert';
import 'dart:html';
import 'package:flutter_web/material.dart';
import 'package:github_dataviz/constants.dart';
import 'package:github_dataviz/data/contribution_data.dart';
import 'package:github_dataviz/data/data_series.dart';
import 'package:github_dataviz/data/stat_for_week.dart';
import 'package:github_dataviz/data/user_contribution.dart';
import 'package:github_dataviz/data/week_label.dart';
import 'package:github_dataviz/layered_chart.dart';
import 'package:github_dataviz/mathutils.dart';
import 'package:github_dataviz/timeline.dart';
class MainLayout extends StatefulWidget {
@override
_MainLayoutState createState() => _MainLayoutState();
}
class _MainLayoutState extends State<MainLayout> with TickerProviderStateMixin {
AnimationController _animation;
List<UserContribution> contributions;
List<StatForWeek> starsByWeek;
List<StatForWeek> forksByWeek;
List<StatForWeek> pushesByWeek;
List<StatForWeek> issueCommentsByWeek;
List<StatForWeek> pullRequestActivityByWeek;
List<WeekLabel> weekLabels;
static final double earlyInterpolatorFraction = 0.8;
static final EarlyInterpolator interpolator =
EarlyInterpolator(earlyInterpolatorFraction);
double animationValue = 1.0;
double interpolatedAnimationValue = 1.0;
bool timelineOverride = false;
@override
void initState() {
super.initState();
createAnimation(0);
weekLabels = List();
weekLabels.add(WeekLabel.forDate(DateTime(2019, 2, 26), "v1.2"));
weekLabels.add(WeekLabel.forDate(DateTime(2018, 12, 4), "v1.0"));
// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 9, 19), "Preview 2"));
weekLabels.add(WeekLabel.forDate(DateTime(2018, 6, 21), "Preview 1"));
// weekLabels.add(WeekLabel.forDate(new DateTime(2018, 5, 7), "Beta 3"));
weekLabels.add(WeekLabel.forDate(DateTime(2018, 2, 27), "Beta 1"));
weekLabels.add(WeekLabel.forDate(DateTime(2017, 5, 1), "Alpha"));
weekLabels.add(WeekLabel(48, "Repo Made Public"));
loadGitHubData();
}
void createAnimation(double startValue) {
_animation?.dispose();
_animation = AnimationController(
value: startValue,
duration: const Duration(milliseconds: 14400),
vsync: this,
)..repeat();
_animation.addListener(() {
setState(() {
if (!timelineOverride) {
animationValue = _animation.value;
interpolatedAnimationValue = interpolator.get(animationValue);
}
});
});
}
@override
Widget build(BuildContext context) {
// Combined contributions data
List<DataSeries> dataToPlot = List();
if (contributions != null) {
List<int> series = List();
for (UserContribution userContrib in contributions) {
for (int i = 0; i < userContrib.contributions.length; i++) {
ContributionData data = userContrib.contributions[i];
if (series.length > i) {
series[i] = series[i] + data.add;
} else {
series.add(data.add);
}
}
}
dataToPlot.add(DataSeries("Added Lines", series));
}
if (starsByWeek != null) {
dataToPlot
.add(DataSeries("Stars", starsByWeek.map((e) => e.stat).toList()));
}
if (forksByWeek != null) {
dataToPlot
.add(DataSeries("Forks", forksByWeek.map((e) => e.stat).toList()));
}
if (pushesByWeek != null) {
dataToPlot
.add(DataSeries("Pushes", pushesByWeek.map((e) => e.stat).toList()));
}
if (issueCommentsByWeek != null) {
dataToPlot.add(DataSeries(
"Issue Comments", issueCommentsByWeek.map((e) => e.stat).toList()));
}
if (pullRequestActivityByWeek != null) {
dataToPlot.add(DataSeries("Pull Request Activity",
pullRequestActivityByWeek.map((e) => e.stat).toList()));
}
LayeredChart layeredChart =
LayeredChart(dataToPlot, weekLabels, interpolatedAnimationValue);
const double timelinePadding = 60.0;
var timeline = Timeline(
numWeeks: dataToPlot != null && dataToPlot.length > 0
? dataToPlot.last.series.length
: 0,
animationValue: interpolatedAnimationValue,
weekLabels: weekLabels,
mouseDownCallback: (double xFraction) {
setState(() {
timelineOverride = true;
_animation.stop();
interpolatedAnimationValue = xFraction;
});
},
mouseMoveCallback: (double xFraction) {
setState(() {
interpolatedAnimationValue = xFraction;
});
},
mouseUpCallback: () {
setState(() {
timelineOverride = false;
createAnimation(
interpolatedAnimationValue * earlyInterpolatorFraction);
});
},
);
Column mainColumn = Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(child: layeredChart),
Padding(
padding: const EdgeInsets.only(
left: timelinePadding,
right: timelinePadding,
bottom: timelinePadding),
child: timeline,
),
],
);
return Container(
color: Constants.backgroundColor,
child:
Directionality(textDirection: TextDirection.ltr, child: mainColumn),
);
}
@override
void dispose() {
_animation.dispose();
super.dispose();
}
Future loadGitHubData() async {
String contributorsJsonStr =
await HttpRequest.getString("github_data/contributors.json");
List jsonObjs = jsonDecode(contributorsJsonStr) as List;
List<UserContribution> contributionList =
jsonObjs.map((e) => UserContribution.fromJson(e)).toList();
print(
"Loaded ${contributionList.length} code contributions to /flutter/flutter repo.");
int numWeeksTotal = contributionList[0].contributions.length;
String starsByWeekStr =
await HttpRequest.getString("github_data/stars.tsv");
List<StatForWeek> starsByWeekLoaded =
summarizeWeeksFromTSV(starsByWeekStr, numWeeksTotal);
String forksByWeekStr =
await HttpRequest.getString("github_data/forks.tsv");
List<StatForWeek> forksByWeekLoaded =
summarizeWeeksFromTSV(forksByWeekStr, numWeeksTotal);
String commitsByWeekStr =
await HttpRequest.getString("github_data/commits.tsv");
List<StatForWeek> commitsByWeekLoaded =
summarizeWeeksFromTSV(commitsByWeekStr, numWeeksTotal);
String commentsByWeekStr =
await HttpRequest.getString("github_data/comments.tsv");
List<StatForWeek> commentsByWeekLoaded =
summarizeWeeksFromTSV(commentsByWeekStr, numWeeksTotal);
String pullRequestActivityByWeekStr =
await HttpRequest.getString("github_data/pull_requests.tsv");
List<StatForWeek> pullRequestActivityByWeekLoaded =
summarizeWeeksFromTSV(pullRequestActivityByWeekStr, numWeeksTotal);
setState(() {
this.contributions = contributionList;
this.starsByWeek = starsByWeekLoaded;
this.forksByWeek = forksByWeekLoaded;
this.pushesByWeek = commitsByWeekLoaded;
this.issueCommentsByWeek = commentsByWeekLoaded;
this.pullRequestActivityByWeek = pullRequestActivityByWeekLoaded;
});
}
List<StatForWeek> summarizeWeeksFromTSV(
String statByWeekStr, int numWeeksTotal) {
List<StatForWeek> loadedStats = List();
HashMap<int, StatForWeek> statMap = HashMap();
statByWeekStr.split("\n").forEach((s) {
List<String> split = s.split("\t");
if (split.length == 2) {
int weekNum = int.parse(split[0]);
statMap[weekNum] = StatForWeek(weekNum, int.parse(split[1]));
}
});
print("Laoded ${statMap.length} weeks.");
// Convert into a list by week, but fill in empty weeks with 0
for (int i = 0; i < numWeeksTotal; i++) {
StatForWeek starsForWeek = statMap[i];
if (starsForWeek == null) {
loadedStats.add(StatForWeek(i, 0));
} else {
loadedStats.add(starsForWeek);
}
}
return loadedStats;
}
}
void main() {
runApp(Center(child: MainLayout()));
}

View File

@@ -0,0 +1,48 @@
abstract class Interpolator {
double get(double x);
}
class EarlyInterpolator implements Interpolator {
double amount;
EarlyInterpolator(this.amount);
@override
double get(double x) {
if (x >= amount) {
return 1;
} else {
return MathUtils.map(x, 0, amount, 0, 1);
}
}
}
class Point2D {
double x, y;
Point2D(this.x, this.y);
}
class MathUtils {
static double map(double x, double a, double b, double u, double v) {
double p = (x - a) / (b - a);
return u + p * (v - u);
}
static double clampedMap(double x, double a, double b, double u, double v) {
if (x <= a) {
return u;
} else if (x >= b) {
return v;
} else {
double p = (x - a) / (b - a);
return u + p * (v - u);
}
}
static double clamp(double x, double a, double b) {
if (x < a) return a;
if (x > b) return b;
return x;
}
}

View File

@@ -0,0 +1,222 @@
import 'dart:collection';
import 'package:flutter_web/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;
Timeline(
{@required this.numWeeks,
@required this.animationValue,
@required this.weekLabels,
this.mouseDownCallback,
this.mouseMoveCallback,
this.mouseUpCallback});
@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);
}
widget.weekLabels.forEach((WeekLabel weekLabel) {
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) {
if (widget.mouseDownCallback != null) {
widget.mouseDownCallback(
_getClampedXFractionLocalCoords(context, details.globalPosition));
}
},
onHorizontalDragEnd: (DragEndDetails details) {
if (widget.mouseUpCallback != null) {
widget.mouseUpCallback();
}
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (widget.mouseMoveCallback != null) {
widget.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();
final Offset localOffset = box.globalToLocal(globalOffset);
return MathUtils.clamp(localOffset.dx / context.size.width, 0, 1);
}
}
class TimelinePainter extends CustomPainter {
TimelineState state;
Paint mainLinePaint;
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;
}
}