stats: improved histogram axis
This commit is contained in:
parent
358cf901ed
commit
dbc8aa6227
3 changed files with 133 additions and 9 deletions
|
@ -14,6 +14,12 @@ extension ExtraDateTime on DateTime {
|
|||
bool get isThisMonth => isAtSameMonthAs(DateTime.now());
|
||||
|
||||
bool get isThisYear => isAtSameYearAs(DateTime.now());
|
||||
|
||||
DateTime get date => DateTime(year, month, day);
|
||||
|
||||
DateTime addMonths(int months) => DateTime(year, month + months, day, hour, minute, second, millisecond, microsecond);
|
||||
|
||||
DateTime addDays(int days) => DateTime(year, month, day + days, hour, minute, second, millisecond, microsecond);
|
||||
}
|
||||
|
||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
|
|
97
lib/widgets/stats/date/axis.dart
Normal file
97
lib/widgets/stats/date/axis.dart
Normal file
|
@ -0,0 +1,97 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/utils/time_utils.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// cf charts.DateTimeTickFormatter factory internals for default formats
|
||||
class TimeAxisSpec {
|
||||
final List<charts.TickSpec<DateTime>> tickSpecs;
|
||||
|
||||
TimeAxisSpec(this.tickSpecs);
|
||||
|
||||
factory TimeAxisSpec.forLevel({
|
||||
required String locale,
|
||||
required DateLevel level,
|
||||
required DateTime first,
|
||||
required DateTime last,
|
||||
}) {
|
||||
switch (level) {
|
||||
case DateLevel.ymd:
|
||||
return TimeAxisSpec.days(locale, first, last);
|
||||
case DateLevel.ym:
|
||||
return TimeAxisSpec.months(locale, first, last);
|
||||
case DateLevel.y:
|
||||
default:
|
||||
return TimeAxisSpec.years(locale, first, last);
|
||||
}
|
||||
}
|
||||
|
||||
factory TimeAxisSpec.days(String locale, DateTime first, DateTime last) {
|
||||
final daysTickLongFormat = DateFormat.MMMd(locale);
|
||||
final daysTickShortFormat = DateFormat.d(locale);
|
||||
|
||||
first = first.date;
|
||||
last = last.date;
|
||||
final rangeDays = last.difference(first).inDays;
|
||||
final delta = max(1, (rangeDays / 5).ceil());
|
||||
|
||||
List<charts.TickSpec<DateTime>> ticks = [];
|
||||
int lastContext = -1;
|
||||
DateFormat dateFormat;
|
||||
for (int i = 0; i < rangeDays; i += delta) {
|
||||
final tickDate = first.addDays(i);
|
||||
if (lastContext != tickDate.month) {
|
||||
lastContext = tickDate.month;
|
||||
dateFormat = daysTickLongFormat;
|
||||
} else {
|
||||
dateFormat = daysTickShortFormat;
|
||||
}
|
||||
ticks.add(charts.TickSpec<DateTime>(tickDate, label: dateFormat.format(tickDate)));
|
||||
}
|
||||
return TimeAxisSpec(ticks);
|
||||
}
|
||||
|
||||
factory TimeAxisSpec.months(String locale, DateTime first, DateTime last) {
|
||||
final monthsTickLongFormat = DateFormat.yMMM(locale);
|
||||
final monthsTickShortFormat = DateFormat.MMM(locale);
|
||||
|
||||
first = DateTime(first.year, first.month);
|
||||
last = DateTime(last.year, last.month);
|
||||
final rangeMonths = last.month - first.month + (last.month < first.month ? 12 : 0);
|
||||
if (rangeMonths < 12) {
|
||||
first = first.addMonths(-((12 - rangeMonths) / 2).floor());
|
||||
}
|
||||
|
||||
List<charts.TickSpec<DateTime>> ticks = [];
|
||||
int lastContext = -1;
|
||||
DateFormat dateFormat;
|
||||
for (int i = 0; i < DateTime.monthsPerYear; i += 3) {
|
||||
final tickDate = first.addMonths(2 + i);
|
||||
if (lastContext != tickDate.year) {
|
||||
lastContext = tickDate.year;
|
||||
dateFormat = monthsTickLongFormat;
|
||||
} else {
|
||||
dateFormat = monthsTickShortFormat;
|
||||
}
|
||||
ticks.add(charts.TickSpec<DateTime>(tickDate, label: dateFormat.format(tickDate)));
|
||||
}
|
||||
return TimeAxisSpec(ticks);
|
||||
}
|
||||
|
||||
factory TimeAxisSpec.years(String locale, DateTime first, DateTime last) {
|
||||
final dateFormat = DateFormat.y(locale);
|
||||
|
||||
final firstYear = first.year;
|
||||
final lastYear = last.year;
|
||||
final delta = max(1, ((lastYear - firstYear) / 5).ceil());
|
||||
|
||||
List<charts.TickSpec<DateTime>> ticks = [];
|
||||
for (int year = firstYear; year <= lastYear; year += delta) {
|
||||
final tickDate = DateTime(year);
|
||||
ticks.add(charts.TickSpec<DateTime>(tickDate, label: dateFormat.format(tickDate)));
|
||||
}
|
||||
return TimeAxisSpec(ticks);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/stats/date/axis.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class Histogram extends StatefulWidget {
|
||||
|
@ -24,6 +27,7 @@ class Histogram extends StatefulWidget {
|
|||
|
||||
class _HistogramState extends State<Histogram> {
|
||||
DateLevel _level = DateLevel.y;
|
||||
DateTime? _firstDate, _lastDate;
|
||||
final Map<DateTime, int> _entryCountPerDate = {};
|
||||
final ValueNotifier<EntryByDate?> _selection = ValueNotifier(null);
|
||||
|
||||
|
@ -33,20 +37,20 @@ class _HistogramState extends State<Histogram> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final entries = widget.entries;
|
||||
final firstDate = entries.firstWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||
final lastDate = entries.lastWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||
final entriesByDateDescending = List.of(widget.entries)..sort(AvesEntry.compareByDate);
|
||||
_lastDate = entriesByDateDescending.firstWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||
_firstDate = entriesByDateDescending.lastWhereOrNull((entry) => entry.bestDate != null)?.bestDate;
|
||||
|
||||
if (lastDate != null && firstDate != null) {
|
||||
final range = firstDate.difference(lastDate);
|
||||
if (range > const Duration(days: 1)) {
|
||||
if (range < const Duration(days: 30)) {
|
||||
if (_lastDate != null && _firstDate != null) {
|
||||
final rangeDays = _lastDate!.difference(_firstDate!).inDays;
|
||||
if (rangeDays > 1) {
|
||||
if (rangeDays <= 31) {
|
||||
_level = DateLevel.ymd;
|
||||
} else if (range < const Duration(days: 365)) {
|
||||
} else if (rangeDays <= 365) {
|
||||
_level = DateLevel.ym;
|
||||
}
|
||||
|
||||
final dates = entries.map((entry) => entry.bestDate).whereNotNull();
|
||||
final dates = entriesByDateDescending.map((entry) => entry.bestDate).whereNotNull();
|
||||
late DateTime Function(DateTime) groupByKey;
|
||||
switch (_level) {
|
||||
case DateLevel.ymd:
|
||||
|
@ -84,8 +88,18 @@ class _HistogramState extends State<Histogram> {
|
|||
),
|
||||
];
|
||||
|
||||
final locale = context.l10n.localeName;
|
||||
final timeAxisSpec = _firstDate != null && _lastDate != null
|
||||
? TimeAxisSpec.forLevel(
|
||||
locale: locale,
|
||||
level: _level,
|
||||
first: _firstDate!,
|
||||
last: _lastDate!,
|
||||
)
|
||||
: null;
|
||||
final axisColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.9));
|
||||
final measureLineColor = charts.ColorUtil.fromDartColor(theme.colorScheme.onPrimary.withOpacity(.1));
|
||||
final measureFormat = NumberFormat.decimalPattern(locale);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -100,12 +114,17 @@ class _HistogramState extends State<Histogram> {
|
|||
labelStyle: charts.TextStyleSpec(color: axisColor),
|
||||
lineStyle: charts.LineStyleSpec(color: axisColor),
|
||||
),
|
||||
tickProviderSpec: timeAxisSpec != null && timeAxisSpec.tickSpecs.isNotEmpty ? charts.StaticDateTimeTickProviderSpec(timeAxisSpec.tickSpecs) : null,
|
||||
),
|
||||
primaryMeasureAxis: charts.NumericAxisSpec(
|
||||
renderSpec: charts.GridlineRendererSpec(
|
||||
labelStyle: charts.TextStyleSpec(color: axisColor),
|
||||
lineStyle: charts.LineStyleSpec(color: measureLineColor),
|
||||
),
|
||||
tickFormatterSpec: charts.BasicNumericTickFormatterSpec((v) {
|
||||
// localize and hide 0
|
||||
return (v == null || v == 0) ? '' : measureFormat.format(v);
|
||||
}),
|
||||
),
|
||||
defaultRenderer: charts.BarRendererConfig<DateTime>(),
|
||||
defaultInteractions: false,
|
||||
|
@ -152,6 +171,8 @@ class _HistogramState extends State<Histogram> {
|
|||
|
||||
return AnimatedSwitcher(
|
||||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
|
|
Loading…
Reference in a new issue