diff --git a/app/contributors/page.massive-scaling.test.tsx b/app/contributors/page.massive-scaling.test.tsx
index b55c32faf..7f3d3c4ab 100644
--- a/app/contributors/page.massive-scaling.test.tsx
+++ b/app/contributors/page.massive-scaling.test.tsx
@@ -237,7 +237,7 @@ describe('ContributorsPage - Massive Data Sets & High Bounds Scaling', () => {
const endTime = performance.now();
const renderTime = endTime - startTime;
- // Bounded higher to protect execution consistency against noisy neighbor/CPU constraints in the testing runner
- expect(renderTime).toBeLessThan(process.env.CI ? 20000 : 15000);
+ // Rendering 500 mock cards should take less than 1500ms under virtual DOM + Vitest
+ expect(renderTime).toBeLessThan(process.env.CI ? 12000 : 5000);
});
});
diff --git a/components/dashboard/DashboardClient.massive-scaling.test.tsx b/components/dashboard/DashboardClient.massive-scaling.test.tsx
index 1d5d574f3..af97a4865 100644
--- a/components/dashboard/DashboardClient.massive-scaling.test.tsx
+++ b/components/dashboard/DashboardClient.massive-scaling.test.tsx
@@ -124,7 +124,7 @@ describe('DashboardClient - Massive Data Sets and Extreme High Bounds Scaling',
const endTime = performance.now();
const executionTime = endTime - startTime;
- expect(executionTime).toBeLessThan(3000);
+ expect(executionTime).toBeLessThan(1000);
});
// Test Case 2: Extreme High Bounds Value Handling (No Layout Overflow)
diff --git a/lib/svg/generator.additional.test.ts b/lib/svg/generator.additional.test.ts
index 15ed6e892..2e003f3c7 100644
--- a/lib/svg/generator.additional.test.ts
+++ b/lib/svg/generator.additional.test.ts
@@ -1,4 +1,4 @@
-// lib/svg/generator.test.new.ts
+// lib/svg/generator.additional.test.ts
// New tests covering gaps in existing generator.test.ts coverage.
// Covers: generateVersusSVG, neon theme bg, accent override, border param, org/repo title entity.
@@ -762,3 +762,124 @@ describe('[Refactor] renderGhostTowers — shared helper consistency', () => {
expect(rateLimitSvg).toContain('');
});
});
+
+// ─── generateAutoThemeVersusSVG refactor — renderTowers consistency ──────────
+// Verifies that after replacing the manual tower loops with renderTowers(),
+// the auto-theme versus SVG produces the same CSS class output as other
+// auto-theme paths. Any regression here means the refactor broke something.
+
+describe('[Refactor] generateAutoThemeVersusSVG — uses renderTowers consistency', () => {
+ const stats1: StreakStats = {
+ currentStreak: 5,
+ longestStreak: 10,
+ totalContributions: 120,
+ todayDate: '2024-06-12',
+ };
+
+ const stats2: StreakStats = {
+ currentStreak: 3,
+ longestStreak: 8,
+ totalContributions: 80,
+ todayDate: '2024-06-12',
+ };
+
+ const calendar1: ContributionCalendar = {
+ totalContributions: 120,
+ weeks: [
+ {
+ contributionDays: [
+ { contributionCount: 0, date: '2024-06-09' },
+ { contributionCount: 5, date: '2024-06-10' },
+ { contributionCount: 11, date: '2024-06-11' },
+ { contributionCount: 6, date: '2024-06-12' },
+ ],
+ },
+ ],
+ };
+
+ const calendar2: ContributionCalendar = {
+ totalContributions: 80,
+ weeks: [
+ {
+ contributionDays: [
+ { contributionCount: 0, date: '2024-06-09' },
+ { contributionCount: 3, date: '2024-06-10' },
+ { contributionCount: 12, date: '2024-06-11' },
+ { contributionCount: 2, date: '2024-06-12' },
+ ],
+ },
+ ],
+ };
+
+ const autoVersusParams: BadgeParams = {
+ user: 'chetan',
+ versus: 'rival',
+ bg: hexColor('0d1117'),
+ text: hexColor('ffffff'),
+ accent: hexColor('58a6ff'),
+ speed: '8s',
+ scale: 'linear',
+ autoTheme: true,
+ };
+
+ it('auto-theme versus SVG contains cp-accent-fill class for active towers', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ expect(svg).toContain('class="cp-accent-fill"');
+ });
+
+ it('auto-theme versus SVG contains cp-text-fill class for ghost towers', () => {
+ const ghostCalendar: ContributionCalendar = {
+ totalContributions: 0,
+ weeks: [{ contributionDays: [{ contributionCount: 0, date: '2024-06-12' }] }],
+ };
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, ghostCalendar, ghostCalendar);
+ expect(svg).toContain('class="cp-text-fill"');
+ });
+
+ it('auto-theme versus SVG contains cp-bg CSS variable (from auto-theme style block)', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ expect(svg).toContain('--cp-bg');
+ expect(svg).toContain('--cp-accent');
+ expect(svg).toContain('prefers-color-scheme: dark');
+ });
+
+ it('auto-theme versus SVG has heat particles for high-contribution days', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ expect(svg).toContain('class="heat-particles"');
+ });
+
+ it('auto-theme versus SVG has today pulse animation', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ expect(svg).toContain('attributeName="opacity" values="1;0.4;1"');
+ });
+
+ it('auto-theme versus SVG has staggered tower animation delays', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ expect(svg).toMatch(/style="animation-delay: \d+\.\d+s;"/);
+ });
+
+ it('auto-theme versus and static versus produce same structural tower count', () => {
+ const staticParams: BadgeParams = {
+ ...autoVersusParams,
+ autoTheme: false,
+ };
+ const autoSvg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+ const staticSvg = generateVersusSVG(stats1, stats2, staticParams, calendar1, calendar2);
+
+ // Both should have the same number of tower groups
+ const autoTowers = [...autoSvg.matchAll(/class="cp-tower"/g)].length;
+ const staticTowers = [...staticSvg.matchAll(/class="cp-tower"/g)].length;
+ expect(autoTowers).toBe(staticTowers);
+ });
+
+ it('auto-theme versus does not contain inline hex fill colors on tower paths', () => {
+ const svg = generateVersusSVG(stats1, stats2, autoVersusParams, calendar1, calendar2);
+
+ // Auto-theme uses CSS classes — tower paths must NOT have fill="#hexvalue"
+ // (scan-line and other elements may have fill attrs, but not tower paths)
+ const towerSection = svg.match(/class="cp-tower"[\s\S]*?<\/g>/g) || [];
+ for (const tower of towerSection) {
+ expect(tower).not.toMatch(/fill="#[0-9a-fA-F]{6}"/);
+ }
+ });
+});
diff --git a/lib/svg/generator.deterministicRandom.test.ts b/lib/svg/generator.deterministicRandom.test.ts
index 218879661..d5ccbdaad 100644
--- a/lib/svg/generator.deterministicRandom.test.ts
+++ b/lib/svg/generator.deterministicRandom.test.ts
@@ -46,7 +46,7 @@ describe('Helper Function: deterministicRandom', () => {
const endTime = performance.now();
const duration = endTime - startTime;
- // Bounded higher to protect execution consistency against execution timing spikes in virtual test configurations
+ // 10,000 FNV-1a hashes should easily complete under 50ms on modern hardware
expect(duration).toBeLessThan(150);
});
});
diff --git a/lib/svg/generator.ts b/lib/svg/generator.ts
index 0672d2093..da37c782a 100644
--- a/lib/svg/generator.ts
+++ b/lib/svg/generator.ts
@@ -27,25 +27,16 @@ import { GRID_ORIGIN_X, GRID_ORIGIN_Y, TILE_HEIGHT_HALF, TILE_WIDTH_HALF } from
import { SVG_WIDTH, SVG_HEIGHT } from './generatorConstants';
const FONT_MAP = {
- // ── Pre-existing entries ────────────────────────────────────────────────
jetbrains: '"JetBrains Mono", monospace',
fira: '"Fira Code", monospace',
roboto: '"Roboto", sans-serif',
-
- // ── Previously missing — both fonts are in the unconditional @import ───
- // Without these entries, passing ?font=syncopate or ?font=spacegrotesk
- // incorrectly triggers a duplicate dynamic Google Fonts fetch.
syncopate: '"Syncopate", sans-serif',
spacegrotesk: '"Space Grotesk", sans-serif',
- 'space grotesk': '"Space Grotesk", sans-serif', // handles spaced user input
-
- // ── Aliases for common variations ───────────────────────────────────────
- firacode: '"Fira Code", monospace', // alias: fira is the canonical key
- 'jetbrains mono': '"JetBrains Mono", monospace', // handles spaced user input
-
- // ── Legacy keys for backward compatibility ──────────────────────────────
+ 'space grotesk': '"Space Grotesk", sans-serif',
+ firacode: '"Fira Code", monospace',
+ 'jetbrains mono': '"JetBrains Mono", monospace',
inter: '"Inter", sans-serif',
- space: '"Space Grotesk", sans-serif', // old key for spacegrotesk
+ space: '"Space Grotesk", sans-serif',
} as const;
export function resolveFont(sanitizedFont?: string | null): string | null {
@@ -64,7 +55,6 @@ function isBundledFont(sanitizedFont?: string | null): boolean {
return fontKey in FONT_MAP && fontKey !== 'inter';
}
-// helpers
export function getSizeScale(size?: 'small' | 'medium' | 'large') {
if (size === 'small') return 400 / SVG_WIDTH;
if (size === 'large') return 800 / SVG_WIDTH;
@@ -121,12 +111,6 @@ export interface TowerPaths {
top: string;
}
-/**
- * Builds the SVG path strings for the three faces of an isometric 3D tower.
- *
- * @param h - The height of the tower.
- * @param scale - Optional scale factor (defaults to 1, which represents the standard 16x10 grid).
- */
export function buildTowerPaths(h: number, scale: number = 1): TowerPaths {
const tileHalfWidth = 16 * scale;
const tileHalfHeight = 10 * scale;
@@ -183,8 +167,6 @@ export function getInteractiveTowerCSS(accentColorExpr: string): string {
`;
}
-// ── Section helpers for generateSVG ──────────────────────────────────────
-
function renderHeader(
safeUser: string,
stats: StreakStats,
@@ -203,24 +185,14 @@ function renderHeader(
${renderDefs(sf, params)}`;
}
-/**
- * Generates custom SVG gradient definitions from gradient_stops and gradient_dir parameters.
- * Returns an object with gradient SVG elements and the gradient ID (or empty string if invalid).
- * If custom stops are invalid or insufficient, returns { gradients: '', gradientId: '' }.
- * Also stores the gradient ID on the params object for tower rendering to use.
- */
function generateCustomGradients(params: BadgeParams): { gradients: string; gradientId: string } {
const stops = parseGradientStops(params.gradient_stops);
- // Require at least 2 valid colors for custom gradient
if (stops.length < 2) {
return { gradients: '', gradientId: '' };
}
const coords = getGradientCoordinates(params.gradient_dir);
-
- // Create a deterministic gradient ID based on the color stops and direction
- // This ensures consistent output and avoids random/duplicate IDs
const gradientSignature = `${stops.join('-')}-${params.gradient_dir || 'vertical'}`;
const gradientId = `custom-grad-${deterministicRandom(gradientSignature)
.toString()
@@ -228,19 +200,15 @@ function generateCustomGradients(params: BadgeParams): { gradients: string; grad
let gradients = '';
- // Generate 4 gradient definitions (one for each intensity level)
- // Each uses the same color stops but with different opacity progression
for (let i = 0; i < 4; i++) {
const level = i + 1;
const levelId = `${gradientId}-level-${level}`;
- // Build the stop elements
let stopElements = '';
const stopCount = stops.length;
stops.forEach((color, stopIdx) => {
const offset = (stopIdx / (stopCount - 1)) * 100;
- // Increase opacity with intensity level (0.4 to 0.8)
const baseOpacity = 0.4 + i * 0.2;
const stopOpacity = Math.min(1, baseOpacity + stopIdx * 0.1);
@@ -255,9 +223,7 @@ ${stopElements}
`;
}
- // Store the gradient ID on params for tower rendering to use
params.__customGradientId = gradientId;
-
return { gradients, gradientId };
}
@@ -266,13 +232,10 @@ function renderDefs(sf: number, params: BadgeParams): string {
let gradients = '';
if (params.gradient) {
- // Try to use custom gradient if gradient_stops is provided
const result = generateCustomGradients(params);
if (result.gradientId) {
- // Custom gradient stops were valid and used
gradients = result.gradients;
} else {
- // Fallback to default gradient behavior
const bgStr = params.bg || '0d1117';
const bgHex = bgStr.startsWith('#') ? bgStr : `#${bgStr}`;
@@ -476,7 +439,6 @@ function renderTowers(
topFillAttr = leftRightFillAttr;
}
- // opacity scalar: clamp 0.1–1.0, applied globally to all tower faces
let leftFaceOpacity = Math.round(t.faceOpacity.left * opacity * 100) / 100;
let rightFaceOpacity = Math.round(t.faceOpacity.right * opacity * 100) / 100;
let topFaceOpacity = Math.round(t.faceOpacity.top * opacity * 100) / 100;
@@ -493,7 +455,6 @@ function renderTowers(
let finalTopFillAttr = topFillAttr;
if (!isGhost && t.intensityLevel > 0 && params.gradient === true) {
- // Use custom gradient ID if available, otherwise use default gradient ID
const customGradId = params.__customGradientId;
const gradId = customGradId
? `${customGradId}-level-${t.intensityLevel}`
@@ -662,11 +623,10 @@ const MONTH_NAMES = [
'Dec',
];
-// Layout constants for 3D isometric label positioning
const ISOMETRIC_VERTICAL_OFFSET = 20;
-
const MONTH_LABEL_ROW_OFFSET = 7.2;
const WEEKDAY_LABEL_COL_OFFSET = -1.2;
+
function renderIsometricLabels(
calendar: ContributionCalendar,
params: BadgeParams,
@@ -768,8 +728,6 @@ function renderMilestoneBadges(stats: StreakStats, params: BadgeParams, sf: numb
return `${elements}`;
}
-// ── Main static-theme renderer ────────────────────────────────────────────
-
export function generateSVG(
stats: StreakStats,
params: BadgeParams,
@@ -946,15 +904,6 @@ ${renderMilestoneBadges(stats, params, sf)}
`;
}
-/**
- * Computes the formatted delta text string for the monthly stats badge.
- * Shared between generateMonthlySVG (static theme) and
- * generateAutoThemeMonthlySVG (auto theme) to prevent divergence.
- *
- * @param stats - Monthly contribution statistics
- * @param deltaUnit - 'commits' or 'lines' depending on mode
- * @param deltaFormat - 'percent' | 'absolute' | 'both' from URL params
- */
function computeDeltaText(
stats: MonthlyStats,
deltaUnit: string,
@@ -978,7 +927,6 @@ function computeDeltaText(
: `0% (${stats.deltaAbsolute > 0 ? '+' : ''}${stats.deltaAbsolute})`;
}
- // percent (default)
return stats.deltaPercentage === null
? 'N/A'
: stats.deltaPercentage > 0
@@ -1026,7 +974,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
const deltaUnit = params.mode === 'loc' ? 'LINES (EST.)' : 'commits';
const deltaText = computeDeltaText(stats, deltaUnit, params.delta_format);
- // Resolve negative color
let negativeColor = '#ff4444';
const cleanBg = sanitizeHexColor(params.bg, '0d1117');
const matchedTheme = Object.values(themes).find(
@@ -1036,7 +983,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
if (matchedTheme && matchedTheme.negative) {
negativeColor = `#${matchedTheme.negative}`;
} else {
- // Dynamic fallback based on background luminance
const luminance = getLuminance(cleanBg);
negativeColor = luminance > 0.5 ? '#cf222e' : '#f85149';
}
@@ -1085,10 +1031,6 @@ export function generateMonthlySVG(stats: MonthlyStats, params: BadgeParams): st
`;
}
-/**
- * Backwards-compatible alias used by some integrations/tests.
- * Keeps the public API explicit: `generateMonthlyBadge` -> `generateMonthlySVG`.
- */
export function generateMonthlyBadge(stats: MonthlyStats, params: BadgeParams): string {
return generateMonthlySVG(stats, params);
}
@@ -1126,7 +1068,6 @@ export function generateWrappedSVG(
? `@import url('https://fonts.googleapis.com/css2?family=${googleFontUrlPart}&display=swap');`
: '';
- // Format month name (e.g. "2025-11" -> "NOVEMBER")
const MONTH_NAMES: Record = {
'01': 'JANUARY',
'02': 'FEBRUARY',
@@ -1146,7 +1087,6 @@ export function generateWrappedSVG(
? MONTH_NAMES[monthPart] || stats.busiestMonth
: stats.busiestMonth || 'N/A';
- // Format peak day (e.g. "2025-11-20" -> "Nov 20")
function formatActiveDate(dateStr: string): string {
if (!dateStr) return 'N/A';
const parts = dateStr.split('-');
@@ -1172,29 +1112,20 @@ export function generateWrappedSVG(
}
const formattedPeakDate = formatActiveDate(stats.mostActiveDate);
- // Circular progress calculations for weekend grind
- // Radius = 14, circumference = 2 * PI * 14 = 87.96
const radiusCircle = 14;
- const circ = 2 * Math.PI * radiusCircle; // ~87.96
+ const circ = 2 * Math.PI * radiusCircle;
const clampedRatio = Math.max(0, Math.min(stats.weekendRatio || 0, 100));
const strokeDashoffset = circ - (clampedRatio / 100) * circ;
- // Background Mini-Monolith calculations
- // Get 14 weeks of towers and scale them down
- const sf = 0.45; // scale down
+ const sf = 0.45;
const rawTowers = computeTowers(calendar, params.scale, '', 'commits');
- // We want to translate them to align beautifully behind the total contributions count
- // Scale raw coordinates: tx * sf, ty * sf
let bgTowersMarkup = '';
const resolvedSolidColor = accent;
for (const t of rawTowers) {
- // Only draw towers that have contributions or represent ghost city landscape
- // We scale down the height and coordinates
const scaleHeight = t.h * sf;
- const scaleX = Math.round(t.x * sf) - 50; // offset left to shift it to the background
- const scaleY = Math.round(t.y * sf) + 80; // shift down slightly
+ const scaleX = Math.round(t.x * sf) - 50;
+ const scaleY = Math.round(t.y * sf) + 80;
- // Extreme low opacity for elegant backdrop watermark
const leftFaceOpacity = t.isGhost ? 0.01 : 0.015;
const rightFaceOpacity = t.isGhost ? 0.005 : 0.01;
const topFaceOpacity = t.isGhost ? 0.02 : 0.035;
@@ -1210,7 +1141,6 @@ export function generateWrappedSVG(
`;
}
- // Border override or default glow
const borderAttr = params.border
? `stroke="#${sanitizeHexColor(params.border, '58a6ff')}" stroke-width="1.5"`
: `stroke="${accent}" stroke-opacity="0.15" stroke-width="1.5"`;
@@ -1430,8 +1360,6 @@ function generateAutoThemeMonthlySVG(stats: MonthlyStats, params: BadgeParams):
`;
}
-// ── Heatmap View ──────────────────────────────────────────────────────────
-
const HEATMAP_CELL_SIZE = 16;
const HEATMAP_CELL_GAP = 3;
const HEATMAP_CELL_RADIUS = 2;
@@ -1482,7 +1410,6 @@ function renderHeatmapGrid(
const originY = Math.round(HEATMAP_GRID_ORIGIN_Y * sf);
const step = cellSize + cellGap;
- // Find max contribution count for intensity calculation
let maxCount = 0;
weeks.forEach((week) => {
week.contributionDays.forEach((day) => {
@@ -1492,14 +1419,12 @@ function renderHeatmapGrid(
});
});
- // Check if todayDate is in visible window
const todayInWindow = weeks.some((w) => w.contributionDays.some((d) => d.date === todayDate));
let cells = '';
let monthHeaders = '';
let prevMonth = '';
- // Render grid cells
weeks.forEach((week, col) => {
week.contributionDays.forEach((day, row) => {
const count =
@@ -1518,8 +1443,6 @@ function renderHeatmapGrid(
const tooltip = `${tooltipPrefix}${day.date}: ${count} ${unit}`;
const fillAttr = isAutoTheme ? 'fill="var(--cp-accent)"' : `fill="${accent}"`;
-
- // Glow on high-intensity cells
const filterAttr = intensity === 4 && glow !== false ? ' filter="url(#hm-glow)"' : '';
cells += `
@@ -1533,7 +1456,6 @@ function renderHeatmapGrid(
`;
});
- // Month header: detect month change from first day of each week
if (week.contributionDays.length > 0) {
const firstDay = week.contributionDays[0];
const monthNum = parseInt(firstDay.date.substring(5, 7), 10);
@@ -1549,7 +1471,6 @@ function renderHeatmapGrid(
}
});
- // Weekday labels
let weekdayLabels = '';
HEATMAP_WEEKDAY_LABELS.forEach((label, row) => {
if (!label) return;
@@ -1877,7 +1798,6 @@ function generateAutoThemeHeatmapSVG(
`;
}
-// Fixed isometric tower layout for the not-found ghost city.
const GHOST_LAYOUT: { col: number; row: number; h: number }[] = [
{ col: 0, row: 0, h: 8 },
{ col: 1, row: 0, h: 20 },
@@ -1929,16 +1849,6 @@ const GHOST_LAYOUT: { col: number; row: number; h: number }[] = [
{ col: 7, row: 5, h: 6 },
];
-/**
- * Renders a list of ghost tower entries as isometric wireframe SVG paths.
- * Shared by generateNotFoundSVG and generateRateLimitSVG to avoid duplicated
- * ghost-city rendering logic. Any visual change to ghost tower geometry
- * (stroke widths, fill-opacity, coordinate math) only needs to happen here.
- *
- * @param layout - Array of {col, row, h} tower descriptors
- * @param accent - Hex color string (with #) for tower stroke and fill tint
- * @returns SVG string: a wrapping all tower groups
- */
function renderGhostTowers(
layout: { col: number; row: number; h: number }[],
accent: string
@@ -2192,26 +2102,8 @@ function generateAutoThemeVersusSVG(
sf
);
- const towers1 = renderTowers(
- towerData1,
- params,
- '',
- '',
- sf,
- true,
- params.opacity ?? 1.0,
- params.animate ?? true
- );
- const towers2 = renderTowers(
- towerData2,
- params,
- '',
- '',
- sf,
- true,
- params.opacity ?? 1.0,
- params.animate ?? true
- );
+ const towers1 = renderTowers(towerData1, params, '', '', sf, true, params.opacity ?? 1.0);
+ const towers2 = renderTowers(towerData2, params, '', '', sf, true, params.opacity ?? 1.0);
const s = createScaler(sf);
const fs = (n: number): number => Math.round(n * sf * 10) / 10;
@@ -2317,7 +2209,6 @@ export function generatePulseSVG(
const width = params.width || 800;
const height = params.height || 170;
- // Extract the last 30 days of contributions
const days: number[] = [];
calendar.weeks.forEach((week) => {
week.contributionDays.forEach((day) => {
@@ -3210,21 +3101,18 @@ export function generateLanguagesSVG(
.sort((a, b) => b.percentage - a.percentage)
.slice(0, 5);
- // We use custom isometric pixel coordinates from a center origin
- // to spread the 5 towers across the canvas.
- const TOWER_SCALE = 2.5; // Make the 5 language towers much larger than daily contribution tiles
+ const TOWER_SCALE = 2.5;
const V_SHAPE_COORDS = [
- { x: 0, y: 0, zIndex: 3 }, // 1st (Center, Front)
- { x: -80, y: -45, zIndex: 2 }, // 2nd (Left, Mid)
- { x: 80, y: -45, zIndex: 2 }, // 3rd (Right, Mid)
- { x: -160, y: -90, zIndex: 1 }, // 4th (Far Left, Back)
- { x: 160, y: -90, zIndex: 1 }, // 5th (Far Right, Back)
+ { x: 0, y: 0, zIndex: 3 },
+ { x: -80, y: -45, zIndex: 2 },
+ { x: 80, y: -45, zIndex: 2 },
+ { x: -160, y: -90, zIndex: 1 },
+ { x: 160, y: -90, zIndex: 1 },
];
let towersHtml = '';
const maxPercent = languages[0]?.percentage || 100;
- // Sort languages by zIndex so we render back-to-front (painter's algorithm)
const sortedLanguages = languages
.map((lang, idx) => ({
...lang,
@@ -3233,13 +3121,13 @@ export function generateLanguagesSVG(
.sort((a, b) => a.coord.zIndex - b.coord.zIndex);
sortedLanguages.forEach((lang, idx) => {
- // W and H are already scaled by sf, so we only scale the coordinate offsets
const scaledX = W / 2 + lang.coord.x * sf;
const scaledY = H / 2 + (50 + lang.coord.y) * sf;
const h = Math.max(30, (lang.percentage / maxPercent) * 140) * sf;
const towerScale = TOWER_SCALE * sf;
const paths = buildTowerPaths(h, towerScale);
+ const th = 10 * towerScale;
const hexColor = lang.color.startsWith('#') ? lang.color : `#${lang.color}`;
const delay = (idx * 0.15).toFixed(3);
diff --git a/lib/svg/themes.test.ts b/lib/svg/themes.test.ts
index 00dbb5a9a..3ec08515a 100644
--- a/lib/svg/themes.test.ts
+++ b/lib/svg/themes.test.ts
@@ -45,7 +45,7 @@ describe('theme count', () => {
// If this fails, either a theme was added to themes.ts without updating
// THEMES.md, or a theme was removed without updating the docs.
// Update this count when intentionally adding/removing themes.
- expect(themeNames).toHaveLength(25);
+ expect(themeNames).toHaveLength(26);
});
it('contains all expected theme keys', () => {