@@ -189,32 +189,30 @@ def svg_text(x, y, text, extra=""):
189189
190190
191191def 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