|
|
|
// 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 'package:http/http.dart' as http;
|
|
|
|
|
|
|
|
import 'package:flutter/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 {
|
|
|
|
const MainLayout({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@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;
|
|
|
|
late List<WeekLabel> weekLabels;
|
|
|
|
|
|
|
|
static const 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 = [];
|
|
|
|
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 = [];
|
|
|
|
if (contributions != null) {
|
|
|
|
List<int> series = [];
|
|
|
|
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.isNotEmpty ? 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: [
|
|
|
|
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 http.get(Uri.parse('assets/github_data/contributors.json')))
|
|
|
|
.body;
|
|
|
|
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 http.get(Uri.parse('assets/github_data/stars.tsv'))).body;
|
|
|
|
List<StatForWeek> starsByWeekLoaded =
|
|
|
|
summarizeWeeksFromTSV(starsByWeekStr, numWeeksTotal);
|
|
|
|
|
|
|
|
String forksByWeekStr =
|
|
|
|
(await http.get(Uri.parse('assets/github_data/forks.tsv'))).body;
|
|
|
|
List<StatForWeek> forksByWeekLoaded =
|
|
|
|
summarizeWeeksFromTSV(forksByWeekStr, numWeeksTotal);
|
|
|
|
|
|
|
|
String commitsByWeekStr =
|
|
|
|
(await http.get(Uri.parse('assets/github_data/commits.tsv'))).body;
|
|
|
|
List<StatForWeek> commitsByWeekLoaded =
|
|
|
|
summarizeWeeksFromTSV(commitsByWeekStr, numWeeksTotal);
|
|
|
|
|
|
|
|
String commentsByWeekStr =
|
|
|
|
(await http.get(Uri.parse('assets/github_data/comments.tsv'))).body;
|
|
|
|
List<StatForWeek> commentsByWeekLoaded =
|
|
|
|
summarizeWeeksFromTSV(commentsByWeekStr, numWeeksTotal);
|
|
|
|
|
|
|
|
String pullRequestActivityByWeekStr =
|
|
|
|
(await http.get(Uri.parse('assets/github_data/pull_requests.tsv')))
|
|
|
|
.body;
|
|
|
|
List<StatForWeek> pullRequestActivityByWeekLoaded =
|
|
|
|
summarizeWeeksFromTSV(pullRequestActivityByWeekStr, numWeeksTotal);
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
contributions = contributionList;
|
|
|
|
starsByWeek = starsByWeekLoaded;
|
|
|
|
forksByWeek = forksByWeekLoaded;
|
|
|
|
pushesByWeek = commitsByWeekLoaded;
|
|
|
|
issueCommentsByWeek = commentsByWeekLoaded;
|
|
|
|
pullRequestActivityByWeek = pullRequestActivityByWeekLoaded;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
List<StatForWeek> summarizeWeeksFromTSV(
|
|
|
|
String statByWeekStr, int numWeeksTotal) {
|
|
|
|
List<StatForWeek> loadedStats = [];
|
|
|
|
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('Loaded ${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(const Center(child: MainLayout()));
|
|
|
|
}
|