import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:scrobbles/config/app_colors.dart'; import 'package:scrobbles/models/heatmap.dart'; import 'package:scrobbles/utils/color_extensions.dart'; class ChartHeatmap extends StatelessWidget { final HeatmapData chartData; const ChartHeatmap({super.key, required this.chartData}); final double chartHeight = 150.0; final double titleFontSize = 9; final Color baseColor = AppColors.contentColorPink; final double scale = 8.0; final double darkenAmount = 50; @override Widget build(BuildContext context) { if (this.chartData.data.keys.length == 0) { return SizedBox( height: this.chartHeight, ); } return AspectRatio( aspectRatio: 3, child: ScatterChart( ScatterChartData( scatterSpots: getSpots(), minX: 0, maxX: 24, minY: 0, maxY: 7, borderData: FlBorderData(show: false), gridData: FlGridData(show: false), titlesData: getTitlesData(), scatterTouchData: ScatterTouchData(enabled: false), ), ), ); } Color getColorFromNormalizedValue(double value) { return baseColor.darken(1 + (darkenAmount * (1 - value)).toInt()); } int getMaxCount() { int maxValue = 0; this.chartData.data.forEach((day, hours) { hours.forEach((hour, count) { if (count > maxValue) { maxValue = count; } }); }); return maxValue; } List<ScatterSpot> getSpots() { List<ScatterSpot> spots = []; final int maxCount = getMaxCount(); this.chartData.data.forEach((day, hours) { hours.forEach((hour, count) { double normalizedValue = count / maxCount; spots.add(ScatterSpot( hour.toDouble(), day.toDouble(), color: getColorFromNormalizedValue(normalizedValue), radius: scale * normalizedValue, )); }); }); return spots; } FlTitlesData getTitlesData() { const AxisTitles none = const AxisTitles( sideTitles: SideTitles(showTitles: false), ); final AxisTitles verticalTitles = AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: getVerticalTitlesWidget, interval: 1, ), ); final AxisTitles horizontalTitles = AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 20, getTitlesWidget: getHorizontalTitlesWidget, interval: 4, ), ); return FlTitlesData( show: true, bottomTitles: horizontalTitles, leftTitles: verticalTitles, topTitles: none, rightTitles: none, ); } Widget getVerticalTitlesWidget(double value, TitleMeta meta) { final int day = value.toInt(); String dayShortName = ''; switch (day) { case 1: dayShortName = 'MON'; break; case 2: dayShortName = 'TUE'; break; case 3: dayShortName = 'WED'; break; case 4: dayShortName = 'THU'; break; case 5: dayShortName = 'FRI'; break; case 6: dayShortName = 'SAT'; break; case 7: dayShortName = 'SUN'; break; default: } return SideTitleWidget( axisSide: meta.axisSide, space: 10, child: Text( tr(dayShortName), style: TextStyle( color: AppColors.mainTextColor1, fontSize: this.titleFontSize, ), ), ); } Widget getHorizontalTitlesWidget(double value, TitleMeta meta) { String text = ''; if (value % 4 == 0 || value == 23) { text = value.toInt().toString().padLeft(2, '0'); } return SideTitleWidget( axisSide: meta.axisSide, space: 2, child: Text( text, style: TextStyle( color: AppColors.mainTextColor1, fontSize: this.titleFontSize, ), ), ); } }