Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions lib/svg/radar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// lib/svg/radar.ts

import type { BadgeParams, ContributionCalendar, StreakStats } from '../../types';
import { deterministicRandom, truncateUsername, getSizeScale } from './generator';
import { truncateUsername, getSizeScale } from './generator';
import { sanitizeHexColor, escapeXML } from './sanitizer';
import { calculateWrappedStats, calculateMonthlyStats } from '../calculate';
import {
Expand All @@ -22,11 +22,7 @@ import {
RADAR_AXIS_OPACITY,
} from './radarConstants';

function calculateRadarMetrics(
stats: StreakStats,
calendar: ContributionCalendar,
seed: string
): number[] {
function calculateRadarMetrics(stats: StreakStats, calendar: ContributionCalendar): number[] {
// 1. Consistency: current streak vs longest
const consistency =
stats.longestStreak > 0 ? Math.min(1, stats.currentStreak / stats.longestStreak) : 0;
Expand All @@ -38,8 +34,21 @@ function calculateRadarMetrics(
const wrapped = calculateWrappedStats(calendar);
const weekend = Math.min(1, wrapped.weekendRatio / 100);

// 4. Night Owl: Since we don't have hour-level data, we use a deterministic random for visual representation
const nightOwl = 0.2 + 0.6 * deterministicRandom(`${seed}:nightowl`);
// 4. Night Owl proxy: ratio of weeknight (Mon–Thu) contributions to all weekday contributions.
// GitHub's calendar has no hour data, but weeknight commits (Mon–Thu) are a real behavioral
// signal for late-evening coding patterns versus weekend/Friday casual commits.
const weeks = calendar.weeks || [];
const allDays = weeks.flatMap((w) => w?.contributionDays || []).filter(Boolean);
let weeknightCommits = 0;
let weekdayCommits = 0;
for (const day of allDays) {
if (!day?.date) continue;
const dow = new Date(day.date).getUTCDay(); // 0=Sun … 6=Sat
const count = day.contributionCount || 0;
if (dow >= 1 && dow <= 5) weekdayCommits += count;
if (dow >= 1 && dow <= 4) weeknightCommits += count; // Mon–Thu as weeknight proxy
}
const nightOwl = weekdayCommits > 0 ? Math.min(1, weeknightCommits / weekdayCommits) : 0;

// 5. Growth: using monthly delta
const monthly = calculateMonthlyStats(calendar, 'UTC', new Date());
Expand All @@ -48,8 +57,12 @@ function calculateRadarMetrics(
growth = Math.min(1, Math.max(0, 0.5 + monthly.deltaPercentage / 200));
}

// 6. Diversity: deterministic random as placeholder for repo counts since we only have calendar
const diversity = 0.3 + 0.7 * deterministicRandom(`${seed}:diversity`);
// 6. Diversity: repo contribution count from calendar (populated by the GraphQL fetch).
// Normalized against 20 repos as a reasonable "high diversity" ceiling.
const diversity =
typeof calendar.repoContributions === 'number' && calendar.repoContributions > 0
? Math.min(1, calendar.repoContributions / 20)
: 0.3; // neutral fallback when repo data is unavailable

return [consistency, volume, weekend, nightOwl, growth, diversity];
}
Expand All @@ -65,12 +78,7 @@ export function generateRadarSVG(
const textColor = params.text || 'c9d1d9';
const accentColor = Array.isArray(params.accent) ? params.accent[0] : params.accent || '58a6ff';

// Seed for deterministic generation
// We use stats instead of the raw username string to prevent CodeQL
// from flagging deterministicRandom as a weak cryptographic algorithm
// processing sensitive user data.
const seed = `${stats.totalContributions}:${stats.longestStreak}:${stats.currentStreak}`;
const metrics = calculateRadarMetrics(stats, calendar, seed);
const metrics = calculateRadarMetrics(stats, calendar);

// Build SVG content

Expand Down
Loading