diff --git a/android/gradle.properties b/android/gradle.properties
index 62205f40150696555e74bed7fbf2f63d6f99f49b..0664a0bd52bb87641404da26292a8bbba0c4153b 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.0.41
-app.versionCode=41
+app.versionName=0.0.42
+app.versionCode=42
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 3c3161ddd6fdce8e232a174f94291efbdcacd95f..f56407bb0e040ae843bd5078573116c48885a23e 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -17,6 +17,7 @@
   "timeline_title": "Recent scrobbles ({daysCount} days)",
   "counts_by_day": "Counts by day ({daysCount} days)",
   "counts_by_hour": "Counts by hour ({daysCount} days)",
+  "heatmap": "Heatmap ({daysCount} days)",
 
   "discoveries_title": "Discoveries ({daysCount} days)",
   "discoveries_artists_title": "Artists",
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 7870ef831dea5485266e868ecb1552f28bf71c89..f9d2d14591516b4a112a53284bf1864eb5345744 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -17,6 +17,7 @@
   "timeline_title": "Écoutes récentes ({daysCount} jours)",
   "counts_by_day": "Écoutes par jour ({daysCount} jours)",
   "counts_by_hour": "Écoutes par heure ({daysCount} jours)",
+  "heatmap": "Répartition ({daysCount} jours)",
 
   "discoveries_title": "Découvertes ({daysCount} jours)",
   "discoveries_artists_title": "Artistes",
diff --git a/fastlane/metadata/android/en-US/changelogs/42.txt b/fastlane/metadata/android/en-US/changelogs/42.txt
new file mode 100644
index 0000000000000000000000000000000000000000..711bbbfd94446e694b48160adff8323320d165a9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/42.txt
@@ -0,0 +1 @@
+Add day/time distribution heatmap.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/42.txt b/fastlane/metadata/android/fr-FR/changelogs/42.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2dd7878b7e337e233947ba2ae309f74f6fa7ea8f
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/42.txt
@@ -0,0 +1 @@
+Ajout du graphique de répartition par jour/heure.
diff --git a/lib/cubit/data_heatmap_cubit.dart b/lib/cubit/data_heatmap_cubit.dart
new file mode 100644
index 0000000000000000000000000000000000000000..646ba623036421f8a97eccda0c9f642e0bf10d2a
--- /dev/null
+++ b/lib/cubit/data_heatmap_cubit.dart
@@ -0,0 +1,45 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:scrobbles/models/heatmap.dart';
+
+part 'data_heatmap_state.dart';
+
+class DataHeatmapCubit extends HydratedCubit<DataHeatmapState> {
+  DataHeatmapCubit() : super(const DataHeatmapState());
+
+  void getData(DataHeatmapState state) {
+    emit(state);
+  }
+
+  HeatmapData? getValue() {
+    return state.heatmap;
+  }
+
+  void update(HeatmapData? heatmapData) {
+    if ((heatmapData != null) && (state.heatmap.toString() != heatmapData.toString())) {
+      setValue(heatmapData);
+    }
+  }
+
+  void setValue(HeatmapData? heatmapData) {
+    emit(DataHeatmapState(
+      heatmap: heatmapData,
+    ));
+  }
+
+  @override
+  DataHeatmapState? fromJson(Map<String, dynamic> json) {
+    return DataHeatmapState(
+      heatmap: HeatmapData.fromJson(json['heatmap']),
+    );
+  }
+
+  @override
+  Map<String, Object?>? toJson(DataHeatmapState state) {
+    return <String, Object?>{
+      'heatmap': state.heatmap?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/data_heatmap_state.dart b/lib/cubit/data_heatmap_state.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0814a3bd507987f5a7bd0c5a3178a9c767ed4bc2
--- /dev/null
+++ b/lib/cubit/data_heatmap_state.dart
@@ -0,0 +1,15 @@
+part of 'data_heatmap_cubit.dart';
+
+@immutable
+class DataHeatmapState extends Equatable {
+  const DataHeatmapState({
+    this.heatmap,
+  });
+
+  final HeatmapData? heatmap;
+
+  @override
+  List<Object?> get props => <Object?>[
+        heatmap,
+      ];
+}
diff --git a/lib/main.dart b/lib/main.dart
index d4ca87cf3c8e94138b5fa45e851b5c143fa6ce1f..d5123a18c168b1c89afbbea128aa1a11e5924935 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -12,6 +12,7 @@ import 'package:scrobbles/cubit/bottom_nav_cubit.dart';
 import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
 import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
 import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
+import 'package:scrobbles/cubit/data_heatmap_cubit.dart';
 import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
 import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
 import 'package:scrobbles/cubit/data_timeline_cubit.dart';
@@ -55,6 +56,7 @@ class MyApp extends StatelessWidget {
         BlocProvider<DataCountsByDayCubit>(create: (context) => DataCountsByDayCubit()),
         BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()),
         BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()),
+        BlocProvider<DataHeatmapCubit>(create: (context) => DataHeatmapCubit()),
         BlocProvider<DataStatisticsGlobalCubit>(
             create: (context) => DataStatisticsGlobalCubit()),
         BlocProvider<DataStatisticsRecentCubit>(
diff --git a/lib/models/heatmap.dart b/lib/models/heatmap.dart
new file mode 100644
index 0000000000000000000000000000000000000000..a29537b8127ed0205e7476dd45b648ac19af599b
--- /dev/null
+++ b/lib/models/heatmap.dart
@@ -0,0 +1,47 @@
+import 'dart:convert';
+
+class HeatmapData {
+  final Map<int, Map<int, int>> data;
+
+  const HeatmapData({
+    required this.data,
+  });
+
+  factory HeatmapData.fromJson(Map<String, dynamic>? json) {
+    Map<int, Map<int, int>> data = {};
+
+    if (json?['heatmap'] != null) {
+      json?['heatmap'].keys.forEach((day) {
+        Map<String, dynamic> rawDataForThisDay = json['heatmap'][day];
+
+        Map<int, int> dataForThisDay = {};
+        rawDataForThisDay.keys.forEach((hour) {
+          dataForThisDay[int.parse(hour)] = int.parse(rawDataForThisDay[hour].toString());
+        });
+
+        data[int.parse(day)] = dataForThisDay;
+      });
+    }
+
+    return HeatmapData(data: data);
+  }
+
+  Map<String, dynamic> toJson() {
+    Map<String, Map<String, int>> map = {};
+
+    this.data.keys.forEach((day) {
+      Map<String, int> dayMap = {};
+      this.data.keys.forEach((hour) {
+        int? value = this.data[day]?[hour];
+        dayMap[hour.toString()] = value != null ? value.toInt() : 0;
+      });
+      map[day.toString()] = dayMap;
+    });
+
+    return {'heatmap': map};
+  }
+
+  String toString() {
+    return jsonEncode(this.toJson());
+  }
+}
diff --git a/lib/network/scrobbles.dart b/lib/network/scrobbles.dart
index 7da32f94bc30b36644f40d1dc94b3565b0877e98..7f5c6f96f0276d7386147cf6197b34fd29ad1f27 100644
--- a/lib/network/scrobbles.dart
+++ b/lib/network/scrobbles.dart
@@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
 import 'package:scrobbles/models/counts_by_day.dart';
 import 'package:scrobbles/models/counts_by_hour.dart';
 import 'package:scrobbles/models/discoveries.dart';
+import 'package:scrobbles/models/heatmap.dart';
 import 'package:scrobbles/models/statistics_global.dart';
 import 'package:scrobbles/models/statistics_recent.dart';
 import 'package:scrobbles/models/timeline.dart';
@@ -88,4 +89,15 @@ class ScrobblesApi {
       throw Exception('Failed to get data from API.');
     }
   }
+
+  static Future<HeatmapData> fetchHeatmap(int daysCount) async {
+    final String url = baseUrl + '/data/' + daysCount.toString() + '/heatmap';
+    final response = await http.get(Uri.parse(url));
+
+    if (response.statusCode == 200) {
+      return HeatmapData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
+    } else {
+      throw Exception('Failed to get data from API.');
+    }
+  }
 }
diff --git a/lib/ui/screens/statistics.dart b/lib/ui/screens/statistics.dart
index 1dfffd1cb85cdf5680169cca761a5195e1779cb3..55e3fe9c5e9953e4da424240398b2125b0f94874 100644
--- a/lib/ui/screens/statistics.dart
+++ b/lib/ui/screens/statistics.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 
 import 'package:scrobbles/ui/widgets/cards/counts_by_day.dart';
 import 'package:scrobbles/ui/widgets/cards/counts_by_hour.dart';
+import 'package:scrobbles/ui/widgets/cards/heatmap.dart';
 
 class ScreenStatistics extends StatelessWidget {
   final Function() notifyParent;
@@ -24,6 +25,8 @@ class ScreenStatistics extends StatelessWidget {
             const CardCountsByDay(),
             const SizedBox(height: 6),
             const CardCountsByHour(),
+            const SizedBox(height: 6),
+            const CardHeatmap(),
             const SizedBox(height: 36),
           ],
         ),
diff --git a/lib/ui/widgets/cards/heatmap.dart b/lib/ui/widgets/cards/heatmap.dart
new file mode 100644
index 0000000000000000000000000000000000000000..06b5a0ac9ba6a68a8a65c9bb39ac6ff4f046ac43
--- /dev/null
+++ b/lib/ui/widgets/cards/heatmap.dart
@@ -0,0 +1,65 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'package:scrobbles/cubit/data_heatmap_cubit.dart';
+import 'package:scrobbles/cubit/settings_cubit.dart';
+import 'package:scrobbles/models/heatmap.dart';
+import 'package:scrobbles/network/scrobbles.dart';
+import 'package:scrobbles/ui/widgets/card_content.dart';
+import 'package:scrobbles/ui/widgets/charts/heatmap.dart';
+import 'package:scrobbles/ui/widgets/error.dart';
+
+class CardHeatmap extends StatelessWidget {
+  const CardHeatmap({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
+
+    final int daysCount = settings.getDistributionDaysCount();
+
+    return BlocBuilder<DataHeatmapCubit, DataHeatmapState>(
+      builder: (BuildContext context, DataHeatmapState state) {
+        HeatmapData heatmap = state.heatmap ?? HeatmapData.fromJson({});
+
+        return CardContent(
+          color: Theme.of(context).colorScheme.surface,
+          title: 'heatmap'.tr(
+            namedArgs: {
+              'daysCount': daysCount.toString(),
+            },
+          ),
+          loader: updateCountsByHour(daysCount),
+          content: ChartHeatmap(
+            chartData: heatmap,
+          ),
+        );
+      },
+    );
+  }
+
+  Widget updateCountsByHour(int daysCount) {
+    final Widget loading = const Text('⏳');
+    final Widget done = const Text('');
+
+    late Future<HeatmapData> futureHeatmap = ScrobblesApi.fetchHeatmap(daysCount);
+
+    return BlocBuilder<DataHeatmapCubit, DataHeatmapState>(
+      builder: (BuildContext context, DataHeatmapState state) {
+        return FutureBuilder<HeatmapData>(
+          future: futureHeatmap,
+          builder: (context, snapshot) {
+            if (snapshot.hasError) {
+              return ShowErrorWidget(message: '${snapshot.error}');
+            }
+
+            BlocProvider.of<DataHeatmapCubit>(context).update(snapshot.data);
+
+            return !snapshot.hasData ? loading : done;
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/charts/heatmap.dart b/lib/ui/widgets/charts/heatmap.dart
new file mode 100644
index 0000000000000000000000000000000000000000..1f4874ee6673fe3110a47c7ab69feb0cc65c5ff8
--- /dev/null
+++ b/lib/ui/widgets/charts/heatmap.dart
@@ -0,0 +1,179 @@
+import 'dart:math';
+
+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,
+        ),
+      ),
+    );
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 8af3ab653348dc325ed678895f8b7d517e76f93a..9879f9ac4bc0b6d03348bdafdbb402302bbd9ffe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
 
 publish_to: 'none'
 
-version: 0.0.41+41
+version: 0.0.42+42
 
 environment:
   sdk: '^3.0.0'