Skip to content

Commit 15ff370

Browse files
committed
feat: add size options for graph rendering and update related components
1 parent c130a32 commit 15ff370

5 files changed

Lines changed: 54 additions & 37 deletions

File tree

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ These themes are tuned for the `/graph` heatmap card — vivid `iconColor` cells
143143

144144
| Theme | Key | Preview |
145145
|---|---|---|
146-
| 🌌 Aurora | `aurora` | ![aurora](https://stats.pphat.top/graph?username=pphatdev&theme=aurora) |
147-
| 💚 Matrix | `matrix` | ![matrix](https://stats.pphat.top/graph?username=pphatdev&theme=matrix) |
148-
| 🔥 Inferno | `inferno` | ![inferno](https://stats.pphat.top/graph?username=pphatdev&theme=inferno) |
149-
| 🌊 Ocean | `ocean` | ![ocean](https://stats.pphat.top/graph?username=pphatdev&theme=ocean) |
150-
| 💜 Neon | `neon` | ![neon](https://stats.pphat.top/graph?username=pphatdev&theme=neon) |
151-
| ☀️ Solar | `solar` | ![solar](https://stats.pphat.top/graph?username=pphatdev&theme=solar) |
152-
| 🌠 Galaxy | `galaxy` | ![galaxy](https://stats.pphat.top/graph?username=pphatdev&theme=galaxy) |
153-
| 🐙 GitHub Dark | `github-dark` | ![github-dark](https://stats.pphat.top/graph?username=pphatdev&theme=github-dark) |
146+
| 🌌 Aurora | `aurora` | ![aurora](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=aurora) |
147+
| 💚 Matrix | `matrix` | ![matrix](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=matrix) |
148+
| 🔥 Inferno | `inferno` | ![inferno](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=inferno) |
149+
| 🌊 Ocean | `ocean` | ![ocean](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=ocean) |
150+
| 💜 Neon | `neon` | ![neon](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=neon) |
151+
| ☀️ Solar | `solar` | ![solar](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=solar) |
152+
| 🌠 Galaxy | `galaxy` | ![galaxy](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=galaxy) |
153+
| 🐙 GitHub Dark | `github-dark` | ![github-dark](https://stats.pphat.top/graph?username=mrdoob&size=small&show_title=false&show_total_contribution=false&theme=github-dark) |
154154

155155
### Animate Modes
156156

ecosystem.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
apps: [
33
{
4-
name: 'studio.pphat.top:3102',
4+
name: 'stats.pphat.top:3102',
55
port: 3102,
66
exec_mode: 'cluster',
77
instances: 'max',

src/components/graph-renderer.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,16 @@ export class GraphRenderer {
6060
titleColor: options.titleColor,
6161
});
6262

63-
const { WIDTH: width, HEIGHT: height } = this.DIMENSIONS;
63+
const SIZE_PRESETS: Record<string, { WIDTH: number; HEIGHT: number }> = {
64+
small: { WIDTH: 800, HEIGHT: 400 },
65+
medium: { WIDTH: 1000, HEIGHT: 500 },
66+
default: { WIDTH: 1200, HEIGHT: 600 },
67+
large: { WIDTH: 1400, HEIGHT: 700 },
68+
};
69+
const { WIDTH: width, HEIGHT: height } = SIZE_PRESETS[options.size ?? 'default'] ?? SIZE_PRESETS.default;
70+
const scale = width / 1200;
71+
const titleFontSize = Math.round(42 * scale);
72+
const contribFontSize = Math.round(18 * scale);
6473
const fontName = theme.fontName || 'Orbitron';
6574
const fontFamily = theme.fontFamily || `'${fontName}', 'Ubuntu', 'sans-serif'`;
6675
const fontUrl = theme.fontUrl || '/fonts/orbitron.woff2';
@@ -76,8 +85,8 @@ export class GraphRenderer {
7685
this.adjustColor(theme.iconColor, 25),
7786
];
7887

79-
const cellSize = 14;
80-
const gap = 4;
88+
const cellSize = Math.round(14 * scale);
89+
const gap = Math.max(2, Math.round(4 * scale));
8190
const step = cellSize + gap;
8291
const gridWidth = data.weeks.length * step;
8392
const gridHeight = 7 * step;
@@ -90,27 +99,27 @@ export class GraphRenderer {
9099
const svgWidth = showBackground ? width : gridWidth + bgMargin * 2;
91100
const startX = showBackground ? (width - gridWidth) / 2 : bgMargin;
92101

93-
const titleY = 90;
94-
const titleFrameY1 = 52;
95-
const titleFrameY2 = 100;
102+
const titleY = Math.round(90 * scale);
103+
const titleFrameY1 = Math.round(52 * scale);
104+
const titleFrameY2 = Math.round(100 * scale);
96105

97106
const svgRenderHeight = showContrib
98107
? height
99108
: showTitle
100-
? Math.max(300, 158 + gridHeight + 90)
101-
: Math.max(240, 48 + gridHeight + 90);
109+
? Math.max(Math.round(300 * scale), Math.round(158 * scale) + gridHeight + Math.round(90 * scale))
110+
: Math.max(Math.round(240 * scale), Math.round(48 * scale) + gridHeight + Math.round(90 * scale));
102111

103112
const startY = (showTitle && showContrib)
104-
? Math.round((height - gridHeight) / 2 + 60)
113+
? Math.round((height - gridHeight) / 2 + Math.round(60 * scale))
105114
: (!showTitle && showContrib)
106115
? Math.round((height - gridHeight) / 2)
107116
: showTitle
108-
? Math.round(svgRenderHeight * 0.15) + titleFrameY2 + 28
109-
: Math.round(svgRenderHeight * 0.15) + 28;
117+
? Math.round(svgRenderHeight * 0.15) + titleFrameY2 + Math.round(28 * scale)
118+
: Math.round(svgRenderHeight * 0.15) + Math.round(28 * scale);
110119

111-
const contribBaseY = showTitle ? 125 : Math.round(startY / 2);
112-
const contribFrameTop = contribBaseY - 28;
113-
const contribFrameBot = contribBaseY + 14;
120+
const contribBaseY = showTitle ? Math.round(125 * scale) : Math.round(startY / 2);
121+
const contribFrameTop = contribBaseY - Math.round(28 * scale);
122+
const contribFrameBot = contribBaseY + Math.round(14 * scale);
114123

115124
// Stars only needed when background is visible; cache uses full canvas dims
116125
const stars = showBackground ? this.getStarfield(width, height, theme.textColor) : '';
@@ -164,7 +173,7 @@ export class GraphRenderer {
164173
const month = new Date(day0.date).getMonth();
165174
if (month !== currentMonth) {
166175
currentMonth = month;
167-
monthParts.push(`<text x="${(startX + i * step).toFixed(1)}" y="${monthY}" fill="${theme.textColor}" font-size="10" opacity="0.5" font-family="${fontFamily}">${MONTHS[month]}</text>`);
176+
monthParts.push(`<text x="${(startX + i * step).toFixed(1)}" y="${monthY}" fill="${theme.textColor}" font-size="${Math.round(10 * scale)}" opacity="0.5" font-family="${fontFamily}">${MONTHS[month]}</text>`);
168177
}
169178
}
170179

@@ -181,25 +190,25 @@ export class GraphRenderer {
181190
if (!showTitle) return '';
182191
const cx = svgWidth / 2;
183192
const titleText = data.username + "'s Activity " + data.year;
184-
const tfw = (titleText.length * 24) / 2;
193+
const tfw = (titleText.length * Math.round(24 * scale)) / 2;
185194
return [
186-
`<text x="50%" y="${titleY}" text-anchor="middle" fill="${theme.titleColor}" font-size="42" font-family="${fontFamily}" font-weight="700" style="filter:drop-shadow(0 0 12px ${theme.titleColor}66)">${titleText}</text>`,
187-
`<g fill="none" stroke="${theme.titleColor}" stroke-width="1.5" opacity="0.25">${buildCornerPaths(cx - tfw - 12, titleFrameY1, cx + tfw + 12, titleFrameY2, 16)}</g>`,
195+
`<text x="50%" y="${titleY}" text-anchor="middle" fill="${theme.titleColor}" font-size="${titleFontSize}" font-family="${fontFamily}" font-weight="700" style="filter:drop-shadow(0 0 12px ${theme.titleColor}66)">${titleText}</text>`,
196+
`<g fill="none" stroke="${theme.titleColor}" stroke-width="1.5" opacity="0.25">${buildCornerPaths(cx - tfw - Math.round(12 * scale), titleFrameY1, cx + tfw + Math.round(12 * scale), titleFrameY2, Math.round(16 * scale))}</g>`,
188197
].join('\n ');
189198
})();
190199

191200
const contribSection = (() => {
192201
if (!showContrib) return '';
193202
const cx = svgWidth / 2;
194203
const contribText = data.totalContributions.toLocaleString() + ' total contributions';
195-
const tfw = (contribText.length * 11) / 2;
204+
const tfw = (contribText.length * Math.round(11 * scale)) / 2;
196205
return [
197-
`<text x="50%" y="${contribBaseY}" text-anchor="middle" fill="${theme.textColor}" font-size="18" opacity="0.8" font-family="${fontFamily}" filter="url(#textGlow)">${contribText}</text>`,
198-
`<g fill="none" stroke="${theme.iconColor}" stroke-width="1.5" opacity="0.35">${buildCornerPaths(cx - tfw - 9, contribFrameTop, cx + tfw + 9, contribFrameBot, 12)}</g>`,
206+
`<text x="50%" y="${contribBaseY}" text-anchor="middle" fill="${theme.textColor}" font-size="${contribFontSize}" opacity="0.8" font-family="${fontFamily}" filter="url(#textGlow)">${contribText}</text>`,
207+
`<g fill="none" stroke="${theme.iconColor}" stroke-width="1.5" opacity="0.35">${buildCornerPaths(cx - tfw - Math.round(9 * scale), contribFrameTop, cx + tfw + Math.round(9 * scale), contribFrameBot, Math.round(12 * scale))}</g>`,
199208
].join('\n ');
200209
})();
201210

202-
const gridCorners = `<g fill="none" stroke="${theme.iconColor}" stroke-width="1.5" opacity="0.35">${buildCornerPaths(startX - 24, startY - 28, startX + gridWidth + 24, startY + gridHeight + 24, 12)}</g>`;
211+
const gridCorners = `<g fill="none" stroke="${theme.iconColor}" stroke-width="1.5" opacity="0.35">${buildCornerPaths(startX - Math.round(24 * scale), startY - Math.round(28 * scale), startX + gridWidth + Math.round(24 * scale), startY + gridHeight + Math.round(24 * scale), Math.round(12 * scale))}</g>`;
203212

204213
const gridLines = showBackground ? (() => {
205214
const vLines = Array.from({ length: 24 }, (_, i) => {
@@ -214,10 +223,14 @@ export class GraphRenderer {
214223
})() : '';
215224

216225
const legend = (() => {
217-
const lx = (startX + gridWidth - 100).toFixed(1);
218-
const ly = (startY + gridHeight + 25).toFixed(1);
219-
const rects = levelColors.map((c, i) => `<rect x="${i * 15}" y="0" width="12" height="12" fill="${c}" rx="2" opacity="${i === 0 ? 0.3 : 0.9}"/>`).join('');
220-
return `<g transform="translate(${lx},${ly})"><text x="-35" y="10" fill="${theme.textColor}" font-size="10" opacity="0.5" font-family="${fontFamily}">Less</text>${rects}<text x="${levelColors.length * 15 + 5}" y="10" fill="${theme.textColor}" font-size="10" opacity="0.5" font-family="${fontFamily}">More</text></g>`;
226+
const lgCell = Math.round(12 * scale);
227+
const lgStep = Math.round(15 * scale);
228+
const lgFont = Math.round(10 * scale);
229+
const lgTextY = Math.round(10 * scale);
230+
const lx = (startX + gridWidth - Math.round(100 * scale)).toFixed(1);
231+
const ly = (startY + gridHeight + Math.round(25 * scale)).toFixed(1);
232+
const rects = levelColors.map((c, i) => `<rect x="${i * lgStep}" y="0" width="${lgCell}" height="${lgCell}" fill="${c}" rx="2" opacity="${i === 0 ? 0.3 : 0.9}"/>`).join('');
233+
return `<g transform="translate(${lx},${ly})"><text x="${-Math.round(35 * scale)}" y="${lgTextY}" fill="${theme.textColor}" font-size="${lgFont}" opacity="0.5" font-family="${fontFamily}">Less</text>${rects}<text x="${levelColors.length * lgStep + Math.round(5 * scale)}" y="${lgTextY}" fill="${theme.textColor}" font-size="${lgFont}" opacity="0.5" font-family="${fontFamily}">More</text></g>`;
221234
})();
222235

223236
const divClipX = (svgWidth / 2 - (width - 160) / 4).toFixed(1);

src/controllers/graph.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class GraphController {
1313
'theme',
1414
'year',
1515
'animate',
16+
'size',
1617
'show_title',
1718
'show_total_contribution',
1819
'show_background',
@@ -33,7 +34,7 @@ export class GraphController {
3334

3435
static async getSvg(req: Request, res: Response) {
3536
try {
36-
const { username, theme = 'default', year, animate, show_title, show_total_contribution, show_background, bgColor, borderColor, textColor, titleColor } = req.query;
37+
const { username, theme = 'default', year, animate, size, show_title, show_total_contribution, show_background, bgColor, borderColor, textColor, titleColor } = req.query;
3738

3839
if (!username || typeof username !== 'string') {
3940
return res.status(400).send('Username is required');
@@ -61,7 +62,7 @@ export class GraphController {
6162
displayYear = `${oneYearAgo.getFullYear()}-${now.getFullYear()}`;
6263
}
6364

64-
const cacheKey = `graph-${username}-${theme}-${cacheKeyExtra}-${animate || ''}-${show_title ?? ''}-${show_total_contribution ?? ''}-${show_background ?? ''}-${bgColor || ''}-${borderColor || ''}-${textColor || ''}-${titleColor || ''}`;
65+
const cacheKey = `graph-${username}-${theme}-${cacheKeyExtra}-${animate || ''}-${size || ''}-${show_title ?? ''}-${show_total_contribution ?? ''}-${show_background ?? ''}-${bgColor || ''}-${borderColor || ''}-${textColor || ''}-${titleColor || ''}`;
6566
const cached = GraphController.cache.get(cacheKey);
6667
if (cached && Date.now() - cached.timestamp < GraphController.CACHE_DURATION) {
6768
res.setHeader('Content-Type', 'image/svg+xml');
@@ -74,6 +75,7 @@ export class GraphController {
7475
const svg = GraphRenderer.generateGraphCard({ ...contributions, year: displayYear }, {
7576
theme: theme as string,
7677
animate: animate as 'none' | 'glow' | 'wave' | 'pulse' | undefined,
78+
size: size as 'small' | 'medium' | 'large' | 'default' | undefined,
7779
show_title: show_title === 'false' ? false : true,
7880
show_total_contribution: show_total_contribution === 'false' ? false : true,
7981
show_background: show_background === 'false' ? false : true,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export interface ContributionGraphData {
8383
export interface GraphCardOptions extends ThemeOverrides {
8484
year?: string | number;
8585
animate?: 'none' | 'glow' | 'wave' | 'pulse';
86+
/** Canvas size preset. 'default' = 1200×600, 'small' = 800×400, 'medium' = 1000×500, 'large' = 1400×700 */
87+
size?: 'small' | 'medium' | 'large' | 'default';
8688
/** Show/hide the title (username + year). When false, content is centered. Default: true */
8789
show_title?: boolean;
8890
/** Show/hide the total contributions subtitle. When false, SVG height shrinks to fit content. Default: true */

0 commit comments

Comments
 (0)