generateHeatmap function
String
generateHeatmap(
- List<
DailyActivity> dailyActivity, { - HeatmapOptions options = const HeatmapOptions(),
Generates a GitHub-style activity heatmap for the terminal.
Implementation
String generateHeatmap(
List<DailyActivity> dailyActivity, {
HeatmapOptions options = const HeatmapOptions(),
}) {
final terminalWidth = options.terminalWidth;
final showMonthLabels = options.showMonthLabels;
// Day labels take 4 characters, calculate weeks that fit.
// Cap at 52 weeks (1 year) to match GitHub style.
const dayLabelWidth = 4;
final availableWidth = terminalWidth - dayLabelWidth;
final width = min(52, max(10, availableWidth));
// Build activity map by date
final activityMap = <String, DailyActivity>{};
for (final activity in dailyActivity) {
activityMap[activity.date] = activity;
}
// Pre-calculate percentiles once
final percentiles = _calculatePercentiles(dailyActivity);
// Calculate date range -- end at today, go back N weeks
final today = DateTime.now();
final todayDate = DateTime(today.year, today.month, today.day);
// Find the Sunday of the current week
final currentWeekStart = todayDate.subtract(
Duration(days: todayDate.weekday % 7),
);
// Go back (width - 1) weeks from the current week start
final startDate = currentWeekStart.subtract(Duration(days: (width - 1) * 7));
// Generate grid (7 rows for days of week, width columns for weeks)
final grid = List.generate(7, (_) => List.filled(width, ''));
final monthStarts = <({int month, int week})>[];
int lastMonth = -1;
var currentDate = startDate;
for (int week = 0; week < width; week++) {
for (int day = 0; day < 7; day++) {
// Don't show future dates
if (currentDate.isAfter(todayDate)) {
grid[day][week] = ' ';
currentDate = currentDate.add(const Duration(days: 1));
continue;
}
final dateStr = _toDateString(currentDate);
final activity = activityMap[dateStr];
// Track month changes (on day 0 = Sunday of each week)
if (day == 0) {
final month = currentDate.month;
if (month != lastMonth) {
monthStarts.add((month: month, week: week));
lastMonth = month;
}
}
final intensity = _getIntensity(activity?.messageCount ?? 0, percentiles);
grid[day][week] = _getHeatmapChar(intensity);
currentDate = currentDate.add(const Duration(days: 1));
}
}
// Build output
final lines = <String>[];
// Month labels
if (showMonthLabels) {
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
final uniqueMonths = monthStarts.map((m) => m.month).toList();
final labelWidth = (width / max(uniqueMonths.length, 1)).floor();
final monthLabels = uniqueMonths
.map((month) => monthNames[month - 1].padRight(labelWidth))
.join('');
lines.add(' $monthLabels');
}
// Day labels
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Grid
for (int day = 0; day < 7; day++) {
final label = [1, 3, 5].contains(day) ? dayLabels[day].padRight(3) : ' ';
final row = '$label ${grid[day].join('')}';
lines.add(row);
}
// Legend
lines.add('');
lines.add(
' Less '
'${_neomageOrange('\u2591')} '
'${_neomageOrange('\u2592')} '
'${_neomageOrange('\u2593')} '
'${_neomageOrange('\u2588')} '
'More',
);
return lines.join('\n');
}