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 createState() => _AnalyticsPageState(); } class _AnalyticsPageState extends State { @override Widget build(BuildContext context) { List avgDayActs = AnalyticTools.getAverageDayActs(DateTimeRange(start: DateTime.now().subtract(Duration(days:7)),end: DateTime.now())); List avgDayData =[]; Map avgTime = {}; 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: [ // Render pie chart PieSeries( 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 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 prodData = {}; Map prodActs = {}; 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 getAverageDayActs(DateTimeRange range){ int totalDays = range.end.difference(range.start).inDays; Map> avgActs = >{}; Map> acts = >{}; List 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; }