300 lines
13 KiB
Dart
300 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
import 'package:tasktracker/AvgDay.dart';
|
|
import 'package:tasktracker/main.dart';
|
|
|
|
import 'Data.dart';
|
|
import 'DebugHelper.dart';
|
|
import 'User.dart' as User;
|
|
|
|
class AnalyticsPage extends StatefulWidget {
|
|
const AnalyticsPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<AnalyticsPage> createState() => _AnalyticsPageState();
|
|
}
|
|
|
|
class _AnalyticsPageState extends State<AnalyticsPage> {
|
|
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
List<Activity> avgDayActs = AnalyticTools.getAverageDayActs(DateTimeRange(start: DateTime.now().subtract(Duration(days:7)),end: DateTime.now()));
|
|
List<CatMapData> avgDayData =[];
|
|
Map<Category, int> avgTime = <Category,int>{};
|
|
|
|
for (var act in avgDayActs) {
|
|
if(avgTime.containsKey(act.taskType.cat!)){
|
|
int _val = avgTime[act.taskType.cat!]!;
|
|
avgTime.update(act.taskType.cat!, (value) => _val + act.endTime.difference(act.startTime).inMinutes);
|
|
}else{
|
|
avgTime.putIfAbsent(act.taskType.cat!, () => act.endTime.difference(act.startTime).inMinutes);
|
|
}
|
|
}
|
|
int trackedTime = 0;
|
|
avgTime.forEach((key, value) {
|
|
avgDayData.add(CatMapData(key.name, value, HexColor.fromHex(key.color)));
|
|
trackedTime+= value;
|
|
});
|
|
|
|
avgDayData.add(CatMapData('Untracked', 1440-trackedTime, Colors.black));
|
|
// for (var value in avgActs) {
|
|
// Debug.LogResponse('${value.taskType.name} : ${DateFormat('HH:mm').format(value.startTime)} - ${DateFormat('HH:mm').format(value.endTime)}');
|
|
// }
|
|
bool landscape=((MediaQuery.of(context).size.width / MediaQuery.of(context).size.height) > 1);
|
|
return Scaffold(
|
|
appBar: AppBar(title: Row(
|
|
children: [FaIcon(FontAwesomeIcons.chartLine),SizedBox(width: 15,), Text("Analytics")],
|
|
)),
|
|
drawer: landscape ? null : navDrawer(context, 1),
|
|
body: Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
landscape?navDrawer(context, 1) : Container(),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.topCenter ,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.vertical,
|
|
child: Container(
|
|
padding: EdgeInsets.all(8),
|
|
child:Column(
|
|
children: [
|
|
Card(
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Average Day',style: TextStyle(fontSize: 17)),
|
|
MaterialButton(
|
|
|
|
color: Colors.green,
|
|
onPressed: (){
|
|
Navigator.of(context).push(MaterialPageRoute(builder: (context)=> AvgDayPage()));
|
|
},
|
|
child: Text('More...'),
|
|
)
|
|
],
|
|
),
|
|
Divider(),
|
|
Container(
|
|
height: 400,
|
|
padding: EdgeInsets.all(10),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8),
|
|
child: (!days.isEmpty)
|
|
? Column(
|
|
children: [
|
|
|
|
Expanded(
|
|
child: SfCircularChart(legend: Legend(isVisible: true,position: LegendPosition.bottom,overflowMode: LegendItemOverflowMode.wrap), series: <CircularSeries>[
|
|
// Render pie chart
|
|
PieSeries<CatMapData, String>(
|
|
dataSource: avgDayData,
|
|
pointColorMapper: (CatMapData data, _) => data.color,
|
|
xValueMapper: (CatMapData data, _) => data.name,
|
|
yValueMapper: (CatMapData data, _) => data.time,
|
|
dataLabelMapper: (CatMapData sales, _) => MinutesToTimeString(sales.time),
|
|
dataLabelSettings: DataLabelSettings(isVisible: true, useSeriesColor: true, overflowMode: OverflowMode.shift, showZeroValue: false))
|
|
]))
|
|
],
|
|
)
|
|
: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()]))),
|
|
|
|
],
|
|
),
|
|
)
|
|
),
|
|
Card(
|
|
child: Container(
|
|
padding: EdgeInsets.all(8),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Text("Compare",style: TextStyle(fontSize: 17)),
|
|
],
|
|
),
|
|
Divider(),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
MaterialButton(color: Colors.blue,onPressed: (){}, child: Text("Task Types"),),
|
|
MaterialButton(color: Colors.blue,onPressed: (){}, child: Text("Categories"),),
|
|
MaterialButton(color: Colors.blue,onPressed: (){}, child: Text("Days"),),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
)
|
|
],
|
|
)
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class AnalyticTools{
|
|
|
|
static Map<DateTime, double> getProductivities(DateTimeRange _range){
|
|
DateTimeRange range = DateTimeRange(start: DateTime(_range.start.year, _range.start.month,_range.start.day, 0,0,0), end: DateTime(_range.end.year, _range.end.month,_range.end.day, 23,59,59));
|
|
Map<DateTime, double> prodData = <DateTime, double>{};
|
|
Map<DateTime, int> prodActs = <DateTime, int>{};
|
|
Debug.Log("Calculating productivity between ${range.start} - ${range.end}");
|
|
//PASS 1: Split Activities into prod and unprod
|
|
for (var activity in User.activities) {
|
|
if(activity.startTime.isAfter(range.start) && activity.endTime.isBefore(range.end)){
|
|
//Eligible for anal
|
|
DateTime day = DateTime(activity.startTime.year, activity.startTime.month, activity.startTime.day);
|
|
if(activity.taskType!.cat!.productive){
|
|
if(prodActs.containsKey(day)){
|
|
int _prod = prodActs[day] ?? 0;
|
|
prodActs.update(day, (value) => _prod +activity.endTime.difference(activity.startTime).inMinutes);
|
|
}else{
|
|
prodActs.putIfAbsent(day, () => activity.endTime.difference(activity.startTime).inMinutes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//PASS 2: Calculate productivity percentage and add them into a list
|
|
for(int i = 0; i <= range.end.difference(range.start).inDays; i++){
|
|
DateTime day = DateTime(range.start.add(Duration(days: i)).year,range.start.add(Duration(days: i)).month,range.start.add(Duration(days: i)).day);
|
|
|
|
double productivity = ((prodActs[day] ?? 0) / 1440) * 100;
|
|
Debug.LogTest('productivity @ $day = $productivity');
|
|
prodData.putIfAbsent(day, () => productivity);
|
|
|
|
}
|
|
|
|
return prodData;
|
|
}
|
|
|
|
static double dailyThreshold = 0.9;
|
|
static int activityGroupingThreshold = 5;
|
|
|
|
static List<Activity> getAverageDayActs(DateTimeRange range){
|
|
int totalDays = range.end.difference(range.start).inDays;
|
|
|
|
Map<TaskType, List<AvgActData>> avgActs = <TaskType, List<AvgActData>>{};
|
|
Map<TaskType, List<DateTimeRange>> acts = <TaskType, List<DateTimeRange>>{};
|
|
List<Activity> listToReturn = [];
|
|
//PASS 1: Split Activities and mark their time range
|
|
User.activities.forEach((element) {
|
|
if(element.startTime.isAfter(range.start) && element.endTime.isBefore(range.end)){
|
|
//Eligible activity to calculate
|
|
if(acts.containsKey(element.taskType)){
|
|
acts[element.taskType]!.add(DateTimeRange(start: element.startTime, end: element.endTime));
|
|
}else{
|
|
acts.putIfAbsent(element.taskType, () => [DateTimeRange(start: element.startTime, end: element.endTime)]);
|
|
}
|
|
}
|
|
});
|
|
Debug.Log('Analysing ${acts.length} activities');
|
|
//PASS 2: Calculate avg Time Range for each Activity
|
|
acts.forEach((key, value) {
|
|
if(value.length < totalDays * dailyThreshold){
|
|
//Not a frequent Activity,
|
|
}else{
|
|
//Iterate through the time ranges of this activity
|
|
for (var _actTime in value) {
|
|
DateTime _start = DateTime(0,0,0,_actTime.start.hour, _actTime.start.minute);
|
|
DateTime _end = DateTime(0,0,0,_actTime.end.hour, _actTime.end.minute);
|
|
if(_start.isAfter(_end)){Debug.LogError('${key.name} start after end? : $_start, $_end'); continue;}
|
|
DateTimeRange actTime = DateTimeRange(start: _start, end: _end);
|
|
if(!avgActs.containsKey(key)){
|
|
avgActs.putIfAbsent(key, () => []);
|
|
}
|
|
if(avgActs[key]!.isEmpty){
|
|
//No need to check groups. No groups exists.
|
|
avgActs[key]!.add(AvgActData(actTime, 1, 1));
|
|
}else{
|
|
//Check for the closest time group
|
|
bool foundGroup = false;
|
|
for(int i = 0; i < avgActs[key]!.length; i++){
|
|
if(foundGroup){continue;}
|
|
DateTimeRange avgTime = avgActs[key]![i].avgRange;
|
|
int count = avgActs[key]![i].count;
|
|
if(actTime.start.difference(avgTime.start).inHours.abs() < activityGroupingThreshold && actTime.end.difference(avgTime.end).inHours.abs() < activityGroupingThreshold){
|
|
foundGroup = true;
|
|
int curStartMin = (avgTime.start.hour * 60) + avgTime.start.minute;
|
|
int curEndMin = (avgTime.end.hour * 60) + avgTime.end.minute;
|
|
int newStartMin = (actTime.start.hour * 60) + actTime.start.minute;
|
|
int newEndMin = (actTime.end.hour * 60) + actTime.end.minute;
|
|
|
|
int avgStartMin = ((curStartMin + newStartMin)/2).toInt();
|
|
int avgEndMin = ((curEndMin + newEndMin)/2).toInt();
|
|
|
|
Debug.Log('Avged : ${key.name} [$i] : ${avgStartMin} - ${avgEndMin}');
|
|
if(avgStartMin > avgEndMin){
|
|
//Malfunctioned
|
|
Debug.LogError('Start is after End? Wut???\nAvged : ${key.name}[$i] : ${avgStartMin} - ${avgEndMin}');
|
|
}else {
|
|
DateTime baseline = DateTime(0, 0, 0, 0, 0);
|
|
// avgActs[key]!.keys.toList()[i] =
|
|
// DateTimeRange(start: baseline.add(Duration(minutes: avgStartMin)), end: baseline.add(Duration(minutes: avgEndMin)));
|
|
// avgActs[key]!.values.toList()[i]++;
|
|
//avgActs[key]!.removeWhere((key, value) => key==avgTime);
|
|
avgActs[key]![i].avgRange = DateTimeRange(start: baseline.add(Duration(minutes: avgStartMin)), end: baseline.add(Duration(minutes: avgEndMin)));
|
|
avgActs[key]![i].count++;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!foundGroup){
|
|
avgActs[key]!.add(AvgActData(actTime, 1, 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
//PASS 3: Prepare the Activity list to return
|
|
|
|
avgActs.forEach((key, value) {
|
|
value.sort((a,b) => b.count.compareTo(a.count));
|
|
value.forEach((element) {
|
|
Activity thisAct = Activity(key, element.avgRange.start, element.avgRange.end);
|
|
Debug.LogResponse(
|
|
'${thisAct.taskType.name} (${element.count}) -> (${element.dailyCountAvg}) : ${DateFormat('HH:mm').format(thisAct.startTime)} - ${DateFormat('HH:mm').format(
|
|
thisAct.endTime)}');
|
|
if(element.count < totalDays * dailyThreshold/2){
|
|
Debug.Log('Ignoring due to DailyThreshold, ${element.count} < ${totalDays * dailyThreshold/2}');
|
|
}else {
|
|
|
|
listToReturn.add(thisAct);
|
|
}
|
|
});
|
|
// value.forEach((act, count) {
|
|
//
|
|
// });
|
|
});
|
|
|
|
return listToReturn;
|
|
}
|
|
}
|
|
|
|
class AvgActData{
|
|
AvgActData(this.avgRange, this.count, this.dailyCountAvg);
|
|
|
|
DateTimeRange avgRange;
|
|
int count;
|
|
double dailyCountAvg;
|
|
}
|