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,23 +169,28 @@ export function AllTimeGraph() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { weeklyData, yearMarkers, thresholds, totalCount } = useMemo(() => {
|
const { yearRows, thresholds, totalCount } = useMemo(() => {
|
||||||
const counts = dailyQ.data ?? [];
|
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]));
|
const countMap = new Map(counts.map((d) => [d.date, d.count]));
|
||||||
|
|
||||||
// Start from the Sunday before earliest
|
// Group into year rows, each with ~52 weekly columns
|
||||||
const start = new Date(from);
|
// Start each year from Jan 1, aligned to its preceding Sunday
|
||||||
start.setDate(start.getDate() - start.getDay());
|
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 }[] = [];
|
for (let yr = startYear; yr <= endYear; yr++) {
|
||||||
const yearMarkers: { col: number; label: string }[] = [];
|
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());
|
||||||
|
|
||||||
|
const weeks: typeof yearRows[0]['weeks'] = [];
|
||||||
let col = 0;
|
let col = 0;
|
||||||
let prevYear = -1;
|
while (cursor <= yearEnd) {
|
||||||
const cursor = new Date(start);
|
|
||||||
|
|
||||||
while (cursor <= to) {
|
|
||||||
const weekStart = fmt(cursor);
|
const weekStart = fmt(cursor);
|
||||||
let weekCount = 0;
|
let weekCount = 0;
|
||||||
for (let d = 0; d < 7; d++) {
|
for (let d = 0; d < 7; d++) {
|
||||||
@@ -194,32 +199,29 @@ export function AllTimeGraph() {
|
|||||||
}
|
}
|
||||||
const weekEnd = fmt(cursor);
|
const weekEnd = fmt(cursor);
|
||||||
cursor.setDate(cursor.getDate() + 1);
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
weeks.push({ weekStart, weekEnd, count: weekCount, col });
|
||||||
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++;
|
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 thresholds = computeThresholds(nonZero);
|
||||||
const totalCount = counts.reduce((sum, d) => sum + d.count, 0);
|
const totalCount = counts.reduce((sum, d) => sum + d.count, 0);
|
||||||
|
|
||||||
return { weeklyData, yearMarkers, thresholds, totalCount };
|
return { yearRows, thresholds, totalCount };
|
||||||
}, [dailyQ.data]);
|
}, [dailyQ.data]);
|
||||||
|
|
||||||
if (!earliest || dailyQ.isLoading) return null;
|
if (!earliest || dailyQ.isLoading) return null;
|
||||||
if (dailyQ.isError) return null;
|
if (dailyQ.isError) return null;
|
||||||
if (weeklyData.length === 0) return null;
|
if (yearRows.length === 0) return null;
|
||||||
|
|
||||||
const cols = weeklyData.length;
|
const maxCols = 53; // max weeks in a year
|
||||||
const labelHeight = 14;
|
const yearLabelWidth = 32;
|
||||||
const svgWidth = cols * (CELL_SIZE + GAP);
|
const svgWidth = yearLabelWidth + maxCols * (CELL_SIZE + GAP);
|
||||||
const svgHeight = labelHeight + CELL_SIZE + GAP;
|
const rows = yearRows.length;
|
||||||
|
const svgHeight = rows * (CELL_SIZE + GAP);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="contribution-graph mb-4">
|
<div className="contribution-graph mb-4">
|
||||||
@@ -228,22 +230,22 @@ export function AllTimeGraph() {
|
|||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block">
|
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} width="100%" className="d-block">
|
||||||
{yearMarkers.map(({ col, label }, i) => (
|
{yearRows.map(({ year, weeks }, rowIdx) => (
|
||||||
|
<g key={year}>
|
||||||
<text
|
<text
|
||||||
key={i}
|
x={yearLabelWidth - 4}
|
||||||
x={col * (CELL_SIZE + GAP) + RADIUS}
|
y={rowIdx * (CELL_SIZE + GAP) + CELL_SIZE / 2}
|
||||||
y={10}
|
textAnchor="end"
|
||||||
textAnchor="middle"
|
dominantBaseline="central"
|
||||||
className="graph-label"
|
className="graph-label"
|
||||||
>
|
>
|
||||||
{label}
|
{year}
|
||||||
</text>
|
</text>
|
||||||
))}
|
{weeks.map(({ weekStart, weekEnd, count, col }) => (
|
||||||
{weeklyData.map(({ weekStart, weekEnd, count, col }) => (
|
|
||||||
<circle
|
<circle
|
||||||
key={weekStart}
|
key={weekStart}
|
||||||
cx={col * (CELL_SIZE + GAP) + RADIUS}
|
cx={yearLabelWidth + col * (CELL_SIZE + GAP) + RADIUS}
|
||||||
cy={labelHeight + RADIUS}
|
cy={rowIdx * (CELL_SIZE + GAP) + RADIUS}
|
||||||
r={RADIUS - 1}
|
r={RADIUS - 1}
|
||||||
fill={colorFor(count, thresholds)}
|
fill={colorFor(count, thresholds)}
|
||||||
className="graph-cell"
|
className="graph-cell"
|
||||||
@@ -252,6 +254,8 @@ export function AllTimeGraph() {
|
|||||||
<title>{`${weekStart} — ${weekEnd}: ${count} ${count === 1 ? 'contribution' : 'contributions'}`}</title>
|
<title>{`${weekStart} — ${weekEnd}: ${count} ${count === 1 ? 'contribution' : 'contributions'}`}</title>
|
||||||
</circle>
|
</circle>
|
||||||
))}
|
))}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user