♻️ Rebuild the activity heatmap to close #189
This commit is contained in:
@@ -1,11 +1,24 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:fl_heatmap/fl_heatmap.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/heatmap.dart';
|
import 'package:island/models/heatmap.dart';
|
||||||
import '../services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
|
||||||
|
/// Custom data class for selected heatmap item
|
||||||
|
class SelectedHeatmapItem {
|
||||||
|
final double value;
|
||||||
|
final String unit;
|
||||||
|
final String dateString;
|
||||||
|
final String dayLabel;
|
||||||
|
|
||||||
|
SelectedHeatmapItem({
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
required this.dateString,
|
||||||
|
required this.dayLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
|
||||||
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
|
||||||
@@ -21,7 +34,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedItem = useState<HeatmapItem?>(null);
|
final selectedItem = useState<SelectedHeatmapItem?>(null);
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
@@ -101,48 +114,18 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final heatmapData = HeatmapData(
|
// Find maximum value for color scaling
|
||||||
rows: [
|
final maxValue =
|
||||||
'Mon',
|
dataMap.values.isNotEmpty
|
||||||
'Tue',
|
? dataMap.values.reduce((a, b) => a > b ? a : b)
|
||||||
'Wed',
|
: 1.0;
|
||||||
'Thu',
|
|
||||||
'Fri',
|
// Helper function to get color based on activity level
|
||||||
'Sat',
|
Color getActivityColor(double value) {
|
||||||
'Sun',
|
if (value == 0) return Colors.grey.withOpacity(0.1);
|
||||||
], // Days of week vertically
|
final intensity = value / maxValue;
|
||||||
columns:
|
return Colors.green.withOpacity(0.2 + (intensity * 0.8));
|
||||||
weeks
|
}
|
||||||
.map(
|
|
||||||
(w) =>
|
|
||||||
'${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}',
|
|
||||||
)
|
|
||||||
.toList(), // Weeks horizontally
|
|
||||||
items: [
|
|
||||||
for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
|
|
||||||
for (final week in weeks) // For each week
|
|
||||||
HeatmapItem(
|
|
||||||
value: dataMap[week.add(Duration(days: day))] ?? 0.0,
|
|
||||||
unit: heatmap.unit,
|
|
||||||
xAxisLabel:
|
|
||||||
'${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}',
|
|
||||||
yAxisLabel:
|
|
||||||
day == 0
|
|
||||||
? 'Mon'
|
|
||||||
: day == 1
|
|
||||||
? 'Tue'
|
|
||||||
: day == 2
|
|
||||||
? 'Wed'
|
|
||||||
: day == 3
|
|
||||||
? 'Thu'
|
|
||||||
: day == 4
|
|
||||||
? 'Fri'
|
|
||||||
: day == 5
|
|
||||||
? 'Sat'
|
|
||||||
: 'Sun',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@@ -151,39 +134,103 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// Month labels row - aligned with month start positions
|
||||||
'activityHeatmap',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
).tr(),
|
|
||||||
const Gap(8),
|
|
||||||
// Month labels row
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 30), // Space for day labels
|
const SizedBox(width: 30), // Space for day labels
|
||||||
...monthLabels.asMap().entries.map((entry) {
|
...List.generate(weeks.length, (weekIndex) {
|
||||||
final month = entry.value;
|
// Check if this week is the start of a month
|
||||||
|
final monthIndex = monthPositions.indexOf(weekIndex);
|
||||||
|
final monthText =
|
||||||
|
monthIndex != -1 ? monthLabels[monthIndex] : null;
|
||||||
|
|
||||||
return Expanded(
|
return monthText != null
|
||||||
child: Container(
|
? Expanded(
|
||||||
alignment: Alignment.center,
|
child: Text(
|
||||||
child: Text(
|
monthText,
|
||||||
month,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
),
|
)
|
||||||
),
|
: SizedBox.shrink();
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Heatmap(
|
// Custom heatmap grid
|
||||||
heatmapData: heatmapData,
|
Column(
|
||||||
rowsVisible: 7,
|
children: List.generate(7, (dayIndex) {
|
||||||
showXAxisLabels: false,
|
final dayLabels = [
|
||||||
onItemSelectedListener: (item) {
|
'Mon',
|
||||||
selectedItem.value = item;
|
'Tue',
|
||||||
},
|
'Wed',
|
||||||
|
'Thu',
|
||||||
|
'Fri',
|
||||||
|
'Sat',
|
||||||
|
'Sun',
|
||||||
|
];
|
||||||
|
final dayLabel = dayLabels[dayIndex];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Day label
|
||||||
|
SizedBox(
|
||||||
|
width: 30,
|
||||||
|
child: Text(
|
||||||
|
dayLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Activity squares for each week - evenly distributed
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(weeks.length, (weekIndex) {
|
||||||
|
final week = weeks[weekIndex];
|
||||||
|
final date = week.add(Duration(days: dayIndex));
|
||||||
|
final value = dataMap[date] ?? 0.0;
|
||||||
|
final dateString =
|
||||||
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
selectedItem.value = SelectedHeatmapItem(
|
||||||
|
value: value,
|
||||||
|
unit: heatmap.unit,
|
||||||
|
dateString: dateString,
|
||||||
|
dayLabel: dayLabel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.all(0.5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: getActivityColor(value),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
border:
|
||||||
|
selectedItem.value != null &&
|
||||||
|
selectedItem.value!.dateString ==
|
||||||
|
dateString &&
|
||||||
|
selectedItem.value!.dayLabel ==
|
||||||
|
dayLabel
|
||||||
|
? Border.all(
|
||||||
|
color: Colors.blue,
|
||||||
|
width: 1,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
// Legend
|
// Legend
|
||||||
@@ -203,9 +250,7 @@ class ActivityHeatmapWidget extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _formatDate(
|
text: _formatDate(selectedItem.value!.dateString),
|
||||||
selectedItem.value!.xAxisLabel ?? '',
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class PageItem extends HookConsumerWidget {
|
|||||||
final String pubName;
|
final String pubName;
|
||||||
|
|
||||||
const PageItem({
|
const PageItem({
|
||||||
|
super.key,
|
||||||
required this.page,
|
required this.page,
|
||||||
required this.site,
|
required this.site,
|
||||||
required this.pubName,
|
required this.pubName,
|
||||||
|
|||||||
Reference in New Issue
Block a user