fix(ui): all-time graph as year rows with 52 weekly columns each
Restructure the all-time contribution graph from a single row of ~700 circles (sub-pixel when scaled) to one row per year with ~52 weekly columns, matching the width of the daily graph above. Year labels on the left. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,57 +169,59 @@ export function AllTimeGraph() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { weeklyData, yearMarkers, thresholds, totalCount } = useMemo(() => {
|
||||
const { yearRows, thresholds, totalCount } = useMemo(() => {
|
||||
const counts = dailyQ.data ?? [];
|
||||
if (counts.length === 0) return { weeklyData: [], yearMarkers: [], thresholds: [1, 2, 3], totalCount: 0 };
|
||||
if (counts.length === 0) return { yearRows: [], thresholds: [1, 2, 3], totalCount: 0 };
|
||||
|
||||
const countMap = new Map(counts.map((d) => [d.date, d.count]));
|
||||
|
||||
// Start from the Sunday before earliest
|
||||
const start = new Date(from);
|
||||
start.setDate(start.getDate() - start.getDay());
|
||||
// Group into year rows, each with ~52 weekly columns
|
||||
// Start each year from Jan 1, aligned to its preceding Sunday
|
||||
const startYear = from.getFullYear();
|
||||
const endYear = to.getFullYear();
|
||||
const yearRows: { year: number; weeks: { weekStart: string; weekEnd: string; count: number; col: number }[] }[] = [];
|
||||
|
||||
const weeklyData: { weekStart: string; weekEnd: string; count: number; col: number }[] = [];
|
||||
const yearMarkers: { col: number; label: string }[] = [];
|
||||
let col = 0;
|
||||
let prevYear = -1;
|
||||
const cursor = new Date(start);
|
||||
for (let yr = startYear; yr <= endYear; yr++) {
|
||||
const yearStart = new Date(yr, 0, 1);
|
||||
const yearEnd = yr === endYear ? to : new Date(yr, 11, 31);
|
||||
// Align to preceding Sunday
|
||||
const cursor = new Date(yearStart);
|
||||
cursor.setDate(cursor.getDate() - cursor.getDay());
|
||||
|
||||
while (cursor <= to) {
|
||||
const weekStart = fmt(cursor);
|
||||
let weekCount = 0;
|
||||
for (let d = 0; d < 7; d++) {
|
||||
weekCount += countMap.get(fmt(cursor)) ?? 0;
|
||||
if (d < 6) cursor.setDate(cursor.getDate() + 1);
|
||||
const weeks: typeof yearRows[0]['weeks'] = [];
|
||||
let col = 0;
|
||||
while (cursor <= yearEnd) {
|
||||
const weekStart = fmt(cursor);
|
||||
let weekCount = 0;
|
||||
for (let d = 0; d < 7; d++) {
|
||||
weekCount += countMap.get(fmt(cursor)) ?? 0;
|
||||
if (d < 6) cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
const weekEnd = fmt(cursor);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
weeks.push({ weekStart, weekEnd, count: weekCount, col });
|
||||
col++;
|
||||
}
|
||||
const weekEnd = fmt(cursor);
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
|
||||
const yr = new Date(weekStart).getFullYear();
|
||||
if (yr !== prevYear) {
|
||||
yearMarkers.push({ col, label: String(yr) });
|
||||
prevYear = yr;
|
||||
}
|
||||
|
||||
weeklyData.push({ weekStart, weekEnd, count: weekCount, col });
|
||||
col++;
|
||||
yearRows.push({ year: yr, weeks });
|
||||
}
|
||||
|
||||
const nonZero = weeklyData.map((w) => w.count).filter((c) => c > 0).sort((a, b) => a - b);
|
||||
const allWeekCounts = yearRows.flatMap((r) => r.weeks.map((w) => w.count));
|
||||
const nonZero = allWeekCounts.filter((c) => c > 0).sort((a, b) => a - b);
|
||||
const thresholds = computeThresholds(nonZero);
|
||||
const totalCount = counts.reduce((sum, d) => sum + d.count, 0);
|
||||
|
||||
return { weeklyData, yearMarkers, thresholds, totalCount };
|
||||
return { yearRows, thresholds, totalCount };
|
||||
}, [dailyQ.data]);
|
||||
|
||||
if (!earliest || dailyQ.isLoading) return null;
|
||||
if (dailyQ.isError) return null;
|
||||
if (weeklyData.length === 0) return null;
|
||||
if (yearRows.length === 0) return null;
|
||||
|
||||
const cols = weeklyData.length;
|
||||
const labelHeight = 14;
|
||||
const svgWidth = cols * (CELL_SIZE + GAP);
|
||||
const svgHeight = labelHeight + CELL_SIZE + GAP;
|
||||
const maxCols = 53; // max weeks in a year
|
||||
const yearLabelWidth = 32;
|
||||
const svgWidth = yearLabelWidth + maxCols * (CELL_SIZE + GAP);
|
||||
const rows = yearRows.length;
|
||||
const svgHeight = rows * (CELL_SIZE + GAP);
|
||||
|
||||
return (
|
||||
<div className="contribution-graph mb-4">
|
||||
@@ -228,29 +230,31 @@ export function AllTimeGraph() {
|
||||
</p>
|
||||
<div>
|
||||
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block">
|
||||
{yearMarkers.map(({ col, label }, i) => (
|
||||
<text
|
||||
key={i}
|
||||
x={col * (CELL_SIZE + GAP) + RADIUS}
|
||||
y={10}
|
||||
textAnchor="middle"
|
||||
className="graph-label"
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
))}
|
||||
{weeklyData.map(({ weekStart, weekEnd, count, col }) => (
|
||||
<circle
|
||||
key={weekStart}
|
||||
cx={col * (CELL_SIZE + GAP) + RADIUS}
|
||||
cy={labelHeight + RADIUS}
|
||||
r={RADIUS - 1}
|
||||
fill={colorFor(count, thresholds)}
|
||||
className="graph-cell"
|
||||
onClick={() => navigate(`/activity/${weekStart}..${weekEnd}`)}
|
||||
>
|
||||
<title>{`${weekStart} — ${weekEnd}: ${count} ${count === 1 ? 'contribution' : 'contributions'}`}</title>
|
||||
</circle>
|
||||
{yearRows.map(({ year, weeks }, rowIdx) => (
|
||||
<g key={year}>
|
||||
<text
|
||||
x={yearLabelWidth - 4}
|
||||
y={rowIdx * (CELL_SIZE + GAP) + CELL_SIZE / 2}
|
||||
textAnchor="end"
|
||||
dominantBaseline="central"
|
||||
className="graph-label"
|
||||
>
|
||||
{year}
|
||||
</text>
|
||||
{weeks.map(({ weekStart, weekEnd, count, col }) => (
|
||||
<circle
|
||||
key={weekStart}
|
||||
cx={yearLabelWidth + col * (CELL_SIZE + GAP) + RADIUS}
|
||||
cy={rowIdx * (CELL_SIZE + GAP) + RADIUS}
|
||||
r={RADIUS - 1}
|
||||
fill={colorFor(count, thresholds)}
|
||||
className="graph-cell"
|
||||
onClick={() => navigate(`/activity/${weekStart}..${weekEnd}`)}
|
||||
>
|
||||
<title>{`${weekStart} — ${weekEnd}: ${count} ${count === 1 ? 'contribution' : 'contributions'}`}</title>
|
||||
</circle>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user