generateHeatmap function

String generateHeatmap(
  1. List<DailyActivity> dailyActivity, {
  2. 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');
}