Skip to content

Commit c3d9777

Browse files
authored
refactor(layout): extract ghost-city detection into a named isGhostCi… (JhaSourav07#2089)
## Description Fixes JhaSourav07#293 ## Changes made - Extracted logic into a pure predicate function. - Removed inline loop and accumulator. - Added dedicated unit tests for `isGhostCity`. ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support. (closes JhaSourav07#293)
2 parents d5abb08 + 572b49d commit c3d9777

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

lib/svg/layout.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { computeTowers, computeFaceOpacity, computeTowerHeight } from './layout';
2+
import { isGhostCity, computeTowers, computeFaceOpacity, computeTowerHeight } from './layout';
33
import type { ContributionCalendar } from '../../types';
44
import {
55
GHOST_HEIGHT_PX,
@@ -313,3 +313,41 @@ describe('computeTowerHeight', () => {
313313
expect(computeTowerHeight(999999, 'log', false)).toBe(MAX_LOG_HEIGHT);
314314
});
315315
});
316+
317+
describe('isGhostCity', () => {
318+
it('should return true if there are zero contributions across all weeks', () => {
319+
const emptyCalendarWeeks = [
320+
{
321+
contributionDays: [
322+
{ contributionCount: 0, locAdditions: 0, locDeletions: 0 },
323+
{ contributionCount: 0, locAdditions: 0, locDeletions: 0 },
324+
],
325+
},
326+
];
327+
expect(isGhostCity(emptyCalendarWeeks)).toBe(true);
328+
});
329+
330+
it('should return false if at least one day has standard contributions', () => {
331+
const activeCalendarWeeks = [
332+
{
333+
contributionDays: [
334+
{ contributionCount: 0, locAdditions: 0, locDeletions: 0 },
335+
{ contributionCount: 5, locAdditions: 0, locDeletions: 0 },
336+
],
337+
},
338+
];
339+
expect(isGhostCity(activeCalendarWeeks)).toBe(false);
340+
});
341+
342+
it('should return false if at least one day has lines of code (LoC) modifications', () => {
343+
const locOnlyCalendarWeeks = [
344+
{
345+
contributionDays: [
346+
{ contributionCount: 0, locAdditions: 120, locDeletions: 0 },
347+
{ contributionCount: 0, locAdditions: 0, locDeletions: 0 },
348+
],
349+
},
350+
];
351+
expect(isGhostCity(locOnlyCalendarWeeks)).toBe(false);
352+
});
353+
});

lib/svg/layout.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ export interface TowerData {
3636
intensityLevel: number; // Quartile level (0 for no commits, 1 to 4 based on contribution intensity)
3737
}
3838

39+
interface MinimalDay {
40+
contributionCount?: number;
41+
locAdditions?: number;
42+
locDeletions?: number;
43+
}
44+
45+
interface MinimalWeek {
46+
contributionDays: MinimalDay[];
47+
}
48+
49+
/**
50+
* Determines if the entire visible calendar monolith is empty (a "ghost city").
51+
* It returns true only if there are absolutely zero contributions (commits or LoC)
52+
* across all visible weeks.
53+
*/
54+
export function isGhostCity(weeks: MinimalWeek[]): boolean {
55+
return !weeks.some((week) =>
56+
week.contributionDays.some((day) => {
57+
const commits = day.contributionCount || 0;
58+
const loc = (day.locAdditions || 0) + (day.locDeletions || 0);
59+
return commits > 0 || loc > 0;
60+
})
61+
);
62+
}
63+
3964
export function computeTowerHeight(
4065
count: number,
4166
scale: 'linear' | 'log',
@@ -86,22 +111,22 @@ export function computeTowers(
86111
const weeks = calendar.weeks.slice(-14);
87112
const towers: TowerData[] = [];
88113

114+
const shouldShowGhostCity = isGhostCity(weeks);
115+
89116
// Calculate if the entire monolith is empty and retrieve the maximum count (commits or LoC)
90-
let totalVisibleContributions = 0;
117+
91118
let maxCommits = 0;
92119
weeks.forEach((week) => {
93120
week.contributionDays.forEach((day) => {
94121
const count =
95122
mode === 'loc' ? (day.locAdditions || 0) + (day.locDeletions || 0) : day.contributionCount;
96-
totalVisibleContributions += count;
123+
97124
if (count > maxCommits) {
98125
maxCommits = count;
99126
}
100127
});
101128
});
102129

103-
const shouldShowGhostCity = totalVisibleContributions === 0;
104-
105130
// Pre-check: is todayDate present in the visible 14-week window?
106131
const todayInWindow = weeks.some((w) => w.contributionDays.some((d) => d.date === todayDate));
107132

0 commit comments

Comments
 (0)