Skip to content
Snippets Groups Projects
heatmap.dart 3.24 KiB
Newer Older
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/ui/widgets/abstracts/custom_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';

class ChartHeatmap extends CustomChart {
  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(),
          8 - 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: getHorizontalTitlesWidgetWithHour,
        interval: 4,
      ),
    );

    return FlTitlesData(
      show: true,
      bottomTitles: horizontalTitles,
      leftTitles: verticalTitles,
      topTitles: none,
      rightTitles: none,
    );
  }

  Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
    final String dayShortName = getDayShortName(8 - value.toInt());

    return SideTitleWidget(
      axisSide: meta.axisSide,
      space: 10,
      child: Text(
        tr(dayShortName),
        style: TextStyle(
          color: AppColors.mainTextColor1,
          fontSize: this.titleFontSize,
        ),
      ),
    );
  }
}