You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
12 KiB
Dart
406 lines
12 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:fluffychat/config/themes.dart';
|
|
import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart';
|
|
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
import '../../enum/time_span.dart';
|
|
import '../../enum/use_type.dart';
|
|
import '../../models/chart_analytics_model.dart';
|
|
import 'bar_chart_card.dart';
|
|
import 'messages_legend_widget.dart';
|
|
|
|
class MessagesBarChart extends StatefulWidget {
|
|
final ChartAnalyticsModel? chartAnalytics;
|
|
final String barChartTitle;
|
|
|
|
const MessagesBarChart({
|
|
super.key,
|
|
required this.chartAnalytics,
|
|
required this.barChartTitle,
|
|
});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => MessagesBarChartState();
|
|
}
|
|
|
|
class MessagesBarChartState extends State<MessagesBarChart> {
|
|
final double barSpace = 16;
|
|
final List<List<TimeSeriesInterval>> intervalGroupings = [];
|
|
|
|
@override
|
|
initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final flLine = FlLine(
|
|
color: Theme.of(context).dividerColor,
|
|
strokeWidth: 1,
|
|
);
|
|
|
|
final flTitlesData = FlTitlesData(
|
|
show: true,
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 28,
|
|
getTitlesWidget: bottomTitles,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 40,
|
|
getTitlesWidget: leftTitles,
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
);
|
|
final barChartData = BarChartData(
|
|
alignment: BarChartAlignment.spaceEvenly,
|
|
barTouchData: BarTouchData(
|
|
enabled: false,
|
|
),
|
|
// barTouchData: barTouchData,
|
|
titlesData: flTitlesData,
|
|
gridData: FlGridData(
|
|
show: true,
|
|
// checkToShowHorizontalLine: (value) => value % 10 == 0,
|
|
checkToShowHorizontalLine: (value) => true,
|
|
getDrawingHorizontalLine: (value) => flLine,
|
|
checkToShowVerticalLine: (value) => false,
|
|
getDrawingVerticalLine: (value) => flLine,
|
|
),
|
|
borderData: FlBorderData(
|
|
show: false,
|
|
),
|
|
groupsSpace: barSpace,
|
|
barGroups: barChartGroupData,
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
);
|
|
final barChart = BarChart(
|
|
barChartData,
|
|
swapAnimationDuration: const Duration(milliseconds: 250),
|
|
);
|
|
|
|
return BarChartCard(
|
|
barChartTitle: widget.barChartTitle,
|
|
barChart: barChart,
|
|
loadingData: widget.chartAnalytics == null,
|
|
legend: const MessagesLegendsListWidget(),
|
|
);
|
|
}
|
|
|
|
bool showLabelBasedOnTimeSpan(
|
|
TimeSpan timeSpan,
|
|
TimeSeriesInterval current,
|
|
TimeSeriesInterval? last,
|
|
int labelIndex,
|
|
) {
|
|
switch (timeSpan) {
|
|
case TimeSpan.day:
|
|
return current.end.hour % 3 == 0;
|
|
case TimeSpan.month:
|
|
if (current.end.month != last?.end.month) {
|
|
return true;
|
|
}
|
|
double width = MediaQuery.of(context).size.width;
|
|
if (FluffyThemes.isColumnMode(context)) {
|
|
width = width - FluffyThemes.navRailWidth - FluffyThemes.columnWidth;
|
|
}
|
|
const int numDays = 28;
|
|
const int minSpacePerDay = 20;
|
|
final int availableSpaces = width ~/ minSpacePerDay;
|
|
final int showAtInterval = (numDays / availableSpaces).floor() + 1;
|
|
|
|
final int lastDayOfCurrentMonth =
|
|
DateTime(current.end.year, current.end.month + 1, 0).day;
|
|
final bool isNextToMonth = labelIndex == 1 ||
|
|
current.end.day == 2 ||
|
|
current.end.day == lastDayOfCurrentMonth;
|
|
final bool shouldShowNextToMonth = showAtInterval <= 1;
|
|
return (current.end.day % showAtInterval == 0) &&
|
|
(!isNextToMonth || shouldShowNextToMonth);
|
|
case TimeSpan.week:
|
|
case TimeSpan.sixmonths:
|
|
case TimeSpan.year:
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
String getLabelBasedOnTimeSpan(
|
|
TimeSpan timeSpan,
|
|
TimeSeriesInterval current,
|
|
TimeSeriesInterval? last,
|
|
int labelIndex,
|
|
) {
|
|
final bool showLabel = showLabelBasedOnTimeSpan(
|
|
timeSpan,
|
|
current,
|
|
last,
|
|
labelIndex,
|
|
);
|
|
|
|
if (widget.chartAnalytics == null || !showLabel) {
|
|
return "";
|
|
}
|
|
if (isInSameGroup(last, current, timeSpan)) {
|
|
return "-";
|
|
}
|
|
|
|
switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) {
|
|
case TimeSpan.day:
|
|
return DateFormat(DateFormat.HOUR).format(current.end);
|
|
case TimeSpan.week:
|
|
return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end);
|
|
case TimeSpan.month:
|
|
return current.end.month != last?.end.month
|
|
? DateFormat(DateFormat.ABBR_MONTH).format(current.end)
|
|
: DateFormat(DateFormat.DAY).format(current.end);
|
|
case TimeSpan.sixmonths:
|
|
case TimeSpan.year:
|
|
return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end);
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
Widget bottomTitles(double value, TitleMeta meta) {
|
|
if (widget.chartAnalytics == null) {
|
|
return Container();
|
|
}
|
|
String text;
|
|
final index = value.toInt();
|
|
final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month;
|
|
final TimeSeriesInterval? last =
|
|
index != 0 ? intervalGroupings[index - 1].last : null;
|
|
final TimeSeriesInterval current = intervalGroupings[index].last;
|
|
|
|
text = getLabelBasedOnTimeSpan(timeSpan, current, last, index);
|
|
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: Text(
|
|
text,
|
|
style: titleTextStyle(context),
|
|
),
|
|
);
|
|
}
|
|
|
|
TextStyle titleTextStyle(context) => TextStyle(
|
|
color: Theme.of(context).textTheme.bodyLarge!.color,
|
|
fontSize: 10,
|
|
);
|
|
|
|
Widget leftTitles(double value, TitleMeta meta) {
|
|
Widget textWidget;
|
|
if (value != meta.max) {
|
|
textWidget = Text(meta.formattedValue, style: titleTextStyle(context));
|
|
} else {
|
|
textWidget = const Icon(Icons.chat_bubble, size: 14);
|
|
}
|
|
return SideTitleWidget(
|
|
axisSide: meta.axisSide,
|
|
child: textWidget,
|
|
);
|
|
}
|
|
|
|
bool isInSameGroup(
|
|
TimeSeriesInterval? t1,
|
|
TimeSeriesInterval t2,
|
|
TimeSpan timeSpan,
|
|
) {
|
|
final DateTime? date1 = t1?.end;
|
|
final DateTime date2 = t2.end;
|
|
if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) {
|
|
return date1?.month == date2.month;
|
|
} else if (timeSpan == TimeSpan.week) {
|
|
return date1?.day == date2.day;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void makeIntervalGroupings() {
|
|
intervalGroupings.clear();
|
|
try {
|
|
for (final timeSeriesInterval
|
|
in widget.chartAnalytics?.timeSeries ?? []) {
|
|
//Note: if we decide we'd like to do some sort of grouping in the future,
|
|
// this is where that could happen. Currently, we're just putting one
|
|
// BarChartRod in each BarChartGroup
|
|
final TimeSeriesInterval? last =
|
|
intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null;
|
|
|
|
if (widget.chartAnalytics != null &&
|
|
isInSameGroup(
|
|
last,
|
|
timeSeriesInterval,
|
|
widget.chartAnalytics!.timeSpan,
|
|
)) {
|
|
intervalGroupings.last.add(timeSeriesInterval);
|
|
} else {
|
|
intervalGroupings.add([timeSeriesInterval]);
|
|
}
|
|
}
|
|
} catch (err, stack) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(e: err, s: stack);
|
|
}
|
|
}
|
|
|
|
List<BarChartGroupData> get barChartGroupData {
|
|
if (widget.chartAnalytics == null) {
|
|
return BarChartPlaceHolderData.getRandomData(context);
|
|
}
|
|
|
|
makeIntervalGroupings();
|
|
|
|
final List<BarChartGroupData> chartData = [];
|
|
|
|
intervalGroupings.asMap().forEach((index, intervalGroup) {
|
|
chartData.add(
|
|
BarChartGroupData(
|
|
x: index,
|
|
barsSpace: barSpace,
|
|
// barRods: intervalGroup.map(constructBarChartRodData).toList(),
|
|
barRods: constructBarChartRodData(intervalGroup),
|
|
),
|
|
);
|
|
});
|
|
return chartData;
|
|
}
|
|
|
|
// BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) {
|
|
// final double y1 = timeSeriesInterval.spanIT.toDouble();
|
|
// final double y2 =
|
|
// (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble();
|
|
// final double y3 = timeSeriesInterval.spanTotal.toDouble();
|
|
// return BarChartRodData(
|
|
// toY: y3,
|
|
// width: 10.toDouble(),
|
|
// rodStackItems: [
|
|
// BarChartRodStackItem(0, y1, UseType.ta.color(context)),
|
|
// BarChartRodStackItem(y1, y2, UseType.ga.color(context)),
|
|
// BarChartRodStackItem(y2, y3, UseType.wa.color(context)),
|
|
// ],
|
|
// borderRadius: BorderRadius.zero,
|
|
// );
|
|
// }
|
|
|
|
List<BarChartRodData> constructBarChartRodData(
|
|
List<TimeSeriesInterval> timeSeriesIntervalGroup,
|
|
) {
|
|
int y1 = 0;
|
|
int y2 = 0;
|
|
int y3 = 0;
|
|
int y4 = 0;
|
|
for (final e in timeSeriesIntervalGroup) {
|
|
y1 += e.totals.ta;
|
|
y2 += y1 + e.totals.ga;
|
|
y3 += y2 + e.totals.wa;
|
|
y4 += y3 + e.totals.un;
|
|
}
|
|
return [
|
|
BarChartRodData(
|
|
toY: y4.toDouble(),
|
|
width: 10.toDouble(),
|
|
rodStackItems: [
|
|
BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)),
|
|
BarChartRodStackItem(
|
|
y1.toDouble(),
|
|
y2.toDouble(),
|
|
UseType.ga.color(context),
|
|
),
|
|
BarChartRodStackItem(
|
|
y2.toDouble(),
|
|
y3.toDouble(),
|
|
UseType.wa.color(context),
|
|
),
|
|
BarChartRodStackItem(
|
|
y3.toDouble(),
|
|
y4.toDouble(),
|
|
UseType.un.color(context),
|
|
),
|
|
],
|
|
borderRadius: BorderRadius.zero,
|
|
),
|
|
];
|
|
}
|
|
|
|
// BarTouchData get barTouchData => BarTouchData(
|
|
// touchTooltipData: BarTouchTooltipData(
|
|
// fitInsideVertically: true,
|
|
// tooltipBgColor: Colors.blueGrey,
|
|
// getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// return BarTooltipItem(
|
|
// "groupindex $groupIndex rodIndex $rodIndex",
|
|
// const TextStyle(
|
|
// color: Colors.white,
|
|
// fontWeight: FontWeight.bold,
|
|
// fontSize: 18,
|
|
// ),
|
|
// children: <TextSpan>[
|
|
// toolTipText(rod),
|
|
// ],
|
|
// );
|
|
// },
|
|
// ),
|
|
// // touchCallback: (FlTouchEvent event, barTouchResponse) {
|
|
// // setState(() {
|
|
// // if (!event.isInterestedForInteractions ||
|
|
// // barTouchResponse == null ||
|
|
// // barTouchResponse.spot == null) {
|
|
// // touchedIndex = -1;
|
|
// // return;
|
|
// // }
|
|
// // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
|
|
// // });
|
|
// // },
|
|
// );
|
|
|
|
// TextSpan toolTipText(BarChartRodData rodData) {
|
|
// double rodPercentage(int index) {
|
|
// return (rodData.rodStackItems[index].toY -
|
|
// rodData.rodStackItems[index].fromY) /
|
|
// rodData.toY *
|
|
// 100;
|
|
// }
|
|
|
|
// return TextSpan(
|
|
// children: [
|
|
// const WidgetSpan(
|
|
// child: Icon(Icons.chat_bubble, size: 14),
|
|
// ),
|
|
// TextSpan(
|
|
// text: " ${rodData.toY}",
|
|
// ),
|
|
// TextSpan(
|
|
// text: "/nIT ${rodPercentage(0)}%",
|
|
// style: TextStyle(color: UseType.ta.color(context)),
|
|
// ),
|
|
// TextSpan(
|
|
// text: " IGC ${rodPercentage(1)}%",
|
|
// style: TextStyle(color: UseType.ga.color(context)),
|
|
// ),
|
|
// TextSpan(
|
|
// text: " Direct ${rodPercentage(2)}%",
|
|
// style: TextStyle(color: UseType.wa.color(context)),
|
|
// ),
|
|
// ],
|
|
// );
|
|
// }
|
|
}
|