Skip to content

Commit 7e9e27e

Browse files
Reshape downloads chart as a portrait stat card
In the 8:2 README row the old landscape 560x300 chart scaled to roughly 190x102 next to a 760x427 banner — wildly off in height and unreadable. Rebuild the SVG as a 400x900 portrait card so the README's 190-wide cell renders ~427 tall, matching the banner edge: - Hero stat at the top (big "10.3k" number with `+delta since baseline`). - Compact sparkline trend in the middle, axis labels at start/middle/end. - `through {date}` footer. Text scales up (32/22/96/24px) to stay legible after the 190/400 ≈ 47% downsample GitHub applies. Co-authored-by: Open Codex <hff582580@gmail.com>
1 parent 2abde9e commit 7e9e27e

2 files changed

Lines changed: 77 additions & 75 deletions

File tree

.github/npm-total-downloads.svg

Lines changed: 26 additions & 28 deletions
Loading

.github/scripts/update_npm_download_chart.py

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -189,32 +189,30 @@ def svg_text(x, y, text, extra=""):
189189

190190

191191
def generate_svg(package_name, cumulative_rows):
192-
# Scale width with bucket count so a 5-week chart isn't stretched across
193-
# the same canvas as a 26-week one. Clamp to a min so the header text
194-
# never wraps, and to the historical max (960) so long histories don't
195-
# blow past the README column width.
196-
per_bucket = 40
197-
min_width = 560
198-
max_width = 960
199-
left = 70
200-
right = 32
201-
bucket_count = max(1, len(cumulative_rows))
202-
width = max(min_width, min(max_width, left + right + per_bucket * bucket_count))
203-
height = 300
204-
top = 58
205-
bottom = 56
206-
chart_w = width - left - right
207-
chart_h = height - top - bottom
192+
# Portrait "stat card" sized to sit beside the unleashed banner in the
193+
# README 8:2 row. The banner is ~16:9 at 760 wide → ~427 tall on render;
194+
# this card's SVG is 400×900 so at the README's 190 display width it
195+
# comes out ~427 tall and lines up with the banner edge.
196+
width = 400
197+
height = 900
198+
pad = 32
199+
chart_top = 360
200+
chart_bottom = 760
201+
chart_w = width - 2 * pad
202+
chart_h = chart_bottom - chart_top
203+
208204
max_downloads = max([downloads for _, downloads in cumulative_rows] or [0])
209205
upper = nice_upper_bound(max_downloads)
210206

211207
def x_for(index):
212-
if len(cumulative_rows) == 1:
213-
return left + chart_w / 2.0
214-
return left + (chart_w * index / float(len(cumulative_rows) - 1))
208+
if len(cumulative_rows) <= 1:
209+
return pad + chart_w / 2.0
210+
return pad + (chart_w * index / float(len(cumulative_rows) - 1))
215211

216212
def y_for(value):
217-
return top + chart_h - (chart_h * value / float(upper))
213+
if upper <= 0:
214+
return chart_bottom
215+
return chart_bottom - (chart_h * value / float(upper))
218216

219217
points = []
220218
for index, (_, downloads) in enumerate(cumulative_rows):
@@ -225,46 +223,45 @@ def y_for(value):
225223
else:
226224
path = ""
227225

226+
# Horizontal grid lines spanning the card width.
228227
grid_lines = []
229228
for step in range(5):
230229
value = int(upper * step / 4)
231230
y = y_for(value)
232231
grid_lines.append(
233-
'<line x1="{left}" y1="{y:.1f}" x2="{right_x}" y2="{y:.1f}" stroke="#243047" stroke-width="1" />'.format(
234-
left=left,
232+
'<line x1="{left}" y1="{y:.1f}" x2="{right}" y2="{y:.1f}" stroke="#243047" stroke-width="1" />'.format(
233+
left=pad,
235234
y=y,
236-
right_x=width - right,
237-
)
238-
)
239-
grid_lines.append(
240-
svg_text(
241-
18,
242-
y + 4,
243-
fmt_count(value),
244-
'font-size="12" fill="#8b9ab8"',
235+
right=width - pad,
245236
)
246237
)
247238

239+
# Date labels at start / middle / end of the trend.
248240
labels = []
249241
if cumulative_rows:
250242
label_indexes = sorted(
251243
set([0, len(cumulative_rows) // 2, len(cumulative_rows) - 1])
252244
)
253245
for index in label_indexes:
254246
start, _ = cumulative_rows[index]
247+
anchor = "middle"
248+
if index == 0:
249+
anchor = "start"
250+
elif index == len(cumulative_rows) - 1:
251+
anchor = "end"
255252
labels.append(
256253
svg_text(
257254
x_for(index),
258-
height - 22,
255+
chart_bottom + 36,
259256
"{} {}".format(start.strftime("%b"), start.day),
260-
'font-size="12" fill="#8b9ab8" text-anchor="middle"',
257+
'font-size="24" fill="#8b9ab8" text-anchor="{}"'.format(anchor),
261258
)
262259
)
263260

264261
dots = []
265262
for x, y, downloads in points:
266263
dots.append(
267-
'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="#8b5cf6" stroke="#dbe7ff" stroke-width="2"><title>{downloads} downloads</title></circle>'.format(
264+
'<circle cx="{x:.1f}" cy="{y:.1f}" r="8" fill="#8b5cf6" stroke="#dbe7ff" stroke-width="3"><title>{downloads} downloads</title></circle>'.format(
268265
x=x,
269266
y=y,
270267
downloads=downloads,
@@ -282,9 +279,9 @@ def y_for(value):
282279

283280
return """<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">
284281
<title id="title">{package_name} cumulative npm downloads</title>
285-
<desc id="desc">Cumulative npm downloads sampled at weekly boundaries across the last {weeks} complete 7-day periods. Latest cumulative total: {latest} downloads.</desc>
282+
<desc id="desc">Cumulative npm downloads sampled at weekly boundaries across {weeks} complete 7-day periods. Latest cumulative total: {latest} downloads.</desc>
286283
<defs>
287-
<linearGradient id="line" x1="0" x2="1" y1="0" y2="0">
284+
<linearGradient id="line" x1="0" x2="0" y1="1" y2="0">
288285
<stop offset="0%" stop-color="#22d3ee" />
289286
<stop offset="55%" stop-color="#8b5cf6" />
290287
<stop offset="100%" stop-color="#f472b6" />
@@ -294,17 +291,20 @@ def y_for(value):
294291
<stop offset="100%" stop-color="#8b5cf6" stop-opacity="0" />
295292
</linearGradient>
296293
</defs>
297-
<rect width="{width}" height="{height}" rx="16" fill="#0b1020" />
298-
<rect x="1" y="1" width="{inner_w}" height="{inner_h}" rx="15" fill="none" stroke="#26324a" />
299-
<text x="{left}" y="30" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="18" font-weight="700" fill="#f8fafc">npm total downloads</text>
300-
<text x="{left}" y="50" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="12" fill="#8b9ab8">{package_name} · cumulative {latest_fmt} · +{delta_fmt} over the last {weeks} periods · through {through}</text>
294+
<rect width="{width}" height="{height}" rx="22" fill="#0b1020" />
295+
<rect x="1" y="1" width="{inner_w}" height="{inner_h}" rx="21" fill="none" stroke="#26324a" />
301296
<g font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">
297+
<text x="{cx}" y="80" font-size="32" font-weight="700" fill="#f8fafc" text-anchor="middle">npm downloads</text>
298+
<text x="{cx}" y="118" font-size="22" fill="#8b9ab8" text-anchor="middle">cumulative · {weeks} weeks</text>
299+
<text x="{cx}" y="240" font-size="96" font-weight="800" fill="#f8fafc" text-anchor="middle">{latest_fmt}</text>
300+
<text x="{cx}" y="290" font-size="24" fill="#22d3ee" text-anchor="middle">+{delta_fmt} since {first_fmt}</text>
302301
{grid}
303-
<line x1="{left}" y1="{baseline:.1f}" x2="{right_x}" y2="{baseline:.1f}" stroke="#3a4867" stroke-width="1.2" />
304-
<path d="{area_path} L {last_x:.1f} {baseline:.1f} L {left:.1f} {baseline:.1f} Z" fill="url(#fill)" />
305-
<path d="{path}" fill="none" stroke="url(#line)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
302+
<line x1="{pad}" y1="{baseline:.1f}" x2="{right}" y2="{baseline:.1f}" stroke="#3a4867" stroke-width="1.4" />
303+
<path d="{area_path} L {last_x:.1f} {baseline:.1f} L {first_x:.1f} {baseline:.1f} Z" fill="url(#fill)" />
304+
<path d="{path}" fill="none" stroke="url(#line)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
306305
{dots}
307306
{labels}
307+
<text x="{cx}" y="{footer_y}" font-size="22" fill="#5d6b88" text-anchor="middle">through {through}</text>
308308
</g>
309309
</svg>
310310
""".format(
@@ -317,14 +317,18 @@ def y_for(value):
317317
latest=latest,
318318
latest_fmt=fmt_count(latest),
319319
delta_fmt=fmt_count(window_delta),
320+
first_fmt=fmt_count(first),
320321
through=through,
321-
left=left,
322-
baseline=top + chart_h,
323-
right_x=width - right,
322+
pad=pad,
323+
cx=width / 2.0,
324+
baseline=chart_bottom,
325+
right=width - pad,
326+
footer_y=height - 32,
324327
grid="\n ".join(grid_lines),
325328
path=path,
326329
area_path=path,
327-
last_x=points[-1][0] if points else left,
330+
first_x=points[0][0] if points else pad,
331+
last_x=points[-1][0] if points else pad,
328332
dots="\n ".join(dots),
329333
labels="\n ".join(labels),
330334
)

0 commit comments

Comments
 (0)