[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add watermark on chart SVG and PNG prints (#1269)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

authored by

Alec Lloyd Probert
coderabbitai[bot]
and committed by
GitHub
ad0a6458 4e43b851

+162 -23
+156 -17
app/components/Package/DownloadAnalytics.vue
··· 55 55 }) 56 56 57 57 const { colors } = useCssVariables( 58 - ['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], 58 + [ 59 + '--bg', 60 + '--fg', 61 + '--bg-subtle', 62 + '--bg-elevated', 63 + '--fg-subtle', 64 + '--fg-muted', 65 + '--border', 66 + '--border-subtle', 67 + ], 59 68 { 60 69 element: rootEl, 61 70 watchHtmlAttributes: true, ··· 303 312 return (props.packageNames ?? []).map(n => String(n).trim()).filter(Boolean) 304 313 const single = String(props.packageName ?? '').trim() 305 314 return single ? [single] : [] 306 - }) 307 - 308 - const xAxisLabel = computed(() => { 309 - if (!isMultiPackageMode.value) return props.packageName ?? '' 310 - const names = effectivePackageNames.value 311 - if (names.length === 1) return names[0] 312 - return 'packages' 313 315 }) 314 316 315 317 const selectedGranularity = shallowRef<ChartTimeGranularity>('weekly') ··· 783 785 784 786 return { dataset, dates } 785 787 }) 788 + 789 + const maxDatapoints = computed(() => 790 + Math.max(0, ...(chartData.value.dataset ?? []).map(d => d.series.length)), 791 + ) 786 792 787 793 /** 788 794 * Maximum estimated value across all series when the chart is ··· 1310 1316 return dataLabels.join('\n') 1311 1317 } 1312 1318 1319 + /** 1320 + * Build and return a legend to be injected during the SVG export only, since the custom legend is 1321 + * displayed as an independant div, content has to be injected within the chart's viewBox. 1322 + * 1323 + * Legend items are displayed in a column, on the top left of the chart. 1324 + */ 1325 + function drawSvgPrintLegend(svg: Record<string, any>) { 1326 + const data = Array.isArray(svg?.data) ? svg.data : [] 1327 + if (!data.length) return '' 1328 + 1329 + const seriesNames: string[] = [] 1330 + 1331 + data.forEach((serie, index) => { 1332 + seriesNames.push(` 1333 + <rect 1334 + x="${svg.drawingArea.left + 12}" 1335 + y="${svg.drawingArea.top + 24 * index - 7}" 1336 + width="12" 1337 + height="12" 1338 + fill="${serie.color}" 1339 + rx="3" 1340 + /> 1341 + <text 1342 + text-anchor="start" 1343 + dominant-baseline="middle" 1344 + x="${svg.drawingArea.left + 32}" 1345 + y="${svg.drawingArea.top + 24 * index}" 1346 + font-size="16" 1347 + fill="${colors.value.fg}" 1348 + stroke="${colors.value.bg}" 1349 + stroke-width="1" 1350 + paint-order="stroke fill" 1351 + > 1352 + ${serie.name} 1353 + </text> 1354 + `) 1355 + }) 1356 + 1357 + // Inject the estimation legend item when necessary 1358 + if ( 1359 + ['monthly', 'yearly'].includes(displayedGranularity.value) && 1360 + !isEndDateOnPeriodEnd.value && 1361 + !isZoomed.value 1362 + ) { 1363 + seriesNames.push(` 1364 + <line 1365 + x1="${svg.drawingArea.left + 12}" 1366 + y1="${svg.drawingArea.top + 24 * data.length}" 1367 + x2="${svg.drawingArea.left + 24}" 1368 + y2="${svg.drawingArea.top + 24 * data.length}" 1369 + stroke="${colors.value.fg}" 1370 + stroke-dasharray="4" 1371 + stroke-linecap="round" 1372 + /> 1373 + <text 1374 + text-anchor="start" 1375 + dominant-baseline="middle" 1376 + x="${svg.drawingArea.left + 32}" 1377 + y="${svg.drawingArea.top + 24 * data.length}" 1378 + font-size="16" 1379 + fill="${colors.value.fg}" 1380 + stroke="${colors.value.bg}" 1381 + stroke-width="1" 1382 + paint-order="stroke fill" 1383 + > 1384 + ${$t('package.trends.legend_estimation')} 1385 + </text> 1386 + `) 1387 + } 1388 + 1389 + return seriesNames.join('\n') 1390 + } 1391 + 1392 + /** 1393 + * Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports 1394 + */ 1395 + function drawNpmxLogoAndTaglineWatermark(svg: Record<string, any>) { 1396 + if (!svg?.drawingArea) return '' 1397 + const npmxLogoWidthToHeight = 2.64 1398 + const npmxLogoWidth = 100 1399 + const npmxLogoHeight = npmxLogoWidth / npmxLogoWidthToHeight 1400 + 1401 + return ` 1402 + <svg x="${svg.drawingArea.left + svg.drawingArea.width / 2 - npmxLogoWidth / 2 - 3}" y="${svg.height - npmxLogoHeight}" width="${npmxLogoWidth}" height="${npmxLogoHeight}" viewBox="0 0 330 125" fill="none" xmlns="http://www.w3.org/2000/svg"> 1403 + <path d="M22.848 97V85.288H34.752V97H22.848ZM56.4105 107.56L85.5945 25H93.2745L64.0905 107.56H56.4105ZM121.269 97V46.12H128.661L128.949 59.08L127.989 58.216C128.629 55.208 129.781 52.744 131.445 50.824C133.173 48.84 135.221 47.368 137.589 46.408C139.957 45.448 142.453 44.968 145.077 44.968C148.981 44.968 152.213 45.832 154.773 47.56C157.397 49.288 159.381 51.624 160.725 54.568C162.069 57.448 162.741 60.68 162.741 64.264V97H154.677V66.568C154.677 61.832 153.749 58.248 151.893 55.816C150.037 53.32 147.189 52.072 143.349 52.072C140.725 52.072 138.357 52.648 136.245 53.8C134.133 54.888 132.437 56.52 131.157 58.696C129.941 60.808 129.333 63.432 129.333 66.568V97H121.269ZM173.647 111.4V46.12H181.135L181.327 57.64L180.175 57.064C181.455 53.096 183.568 50.088 186.512 48.04C189.519 45.992 192.976 44.968 196.88 44.968C201.936 44.968 206.064 46.216 209.264 48.712C212.528 51.208 214.928 54.472 216.464 58.504C218 62.536 218.767 66.888 218.767 71.56C218.767 76.232 218 80.584 216.464 84.616C214.928 88.648 212.528 91.912 209.264 94.408C206.064 96.904 201.936 98.152 196.88 98.152C194.256 98.152 191.792 97.704 189.487 96.808C187.247 95.912 185.327 94.664 183.727 93.064C182.191 91.464 181.135 89.576 180.559 87.4L181.711 86.056V111.4H173.647ZM196.111 90.472C200.528 90.472 203.984 88.808 206.48 85.48C209.04 82.152 210.319 77.512 210.319 71.56C210.319 65.608 209.04 60.968 206.48 57.64C203.984 54.312 200.528 52.648 196.111 52.648C193.167 52.648 190.607 53.352 188.431 54.76C186.319 56.168 184.655 58.28 183.439 61.096C182.287 63.912 181.711 67.4 181.711 71.56C181.711 75.72 182.287 79.208 183.439 82.024C184.591 84.84 186.255 86.952 188.431 88.36C190.607 89.768 193.167 90.472 196.111 90.472ZM222.57 97V46.12H229.962L230.25 57.448L229.29 57.256C229.866 53.48 231.082 50.504 232.938 48.328C234.858 46.088 237.29 44.968 240.234 44.968C243.242 44.968 245.546 46.056 247.146 48.232C248.81 50.408 249.834 53.608 250.218 57.832H249.258C249.834 53.864 251.114 50.728 253.098 48.424C255.146 46.12 257.706 44.968 260.778 44.968C264.874 44.968 267.85 46.376 269.706 49.192C271.562 52.008 272.49 56.68 272.49 63.208V97H264.426V64.36C264.426 59.816 263.946 56.648 262.986 54.856C262.026 53 260.522 52.072 258.474 52.072C257.13 52.072 255.946 52.52 254.922 53.416C253.898 54.248 253.066 55.592 252.426 57.448C251.85 59.304 251.562 61.672 251.562 64.552V97H243.498V64.36C243.498 60.008 243.018 56.872 242.058 54.952C241.162 53.032 239.658 52.072 237.546 52.072C236.202 52.072 235.018 52.52 233.994 53.416C232.97 54.248 232.138 55.592 231.498 57.448C230.922 59.304 230.634 61.672 230.634 64.552V97H222.57ZM276.676 97L295.396 70.888L277.636 46.12H287.044L300.388 65.32L313.444 46.12H323.044L305.38 71.08L323.908 97H314.5L300.388 76.456L286.276 97H276.676Z" fill="${colors.value.fg}"/> 1404 + </svg> 1405 + <text 1406 + fill="${colors.value.fgMuted}" 1407 + x="${svg.drawingArea.left + svg.drawingArea.width / 2}" 1408 + y="${svg.height - npmxLogoHeight - 6}" 1409 + font-size="12" 1410 + text-anchor="middle" 1411 + > 1412 + ${$t('tagline')} 1413 + </text> 1414 + ` 1415 + } 1416 + 1313 1417 // VueUiXy chart component configuration 1314 1418 const chartConfig = computed(() => { 1315 1419 return { ··· 1317 1421 chart: { 1318 1422 height: isMobile.value ? 950 : 600, 1319 1423 backgroundColor: colors.value.bg, 1320 - padding: { bottom: 36, right: 100 }, // padding right is set to leave space of last datapoint label(s) 1424 + padding: { bottom: displayedGranularity.value === 'yearly' ? 84 : 64, right: 100 }, // padding right is set to leave space of last datapoint label(s) 1321 1425 userOptions: { 1322 1426 buttons: { pdf: false, labels: false, fullscreen: false, table: false, tooltip: false }, 1323 1427 buttonTitles: { ··· 1356 1460 }, 1357 1461 grid: { 1358 1462 stroke: colors.value.border, 1463 + showHorizontalLines: true, 1359 1464 labels: { 1360 1465 fontSize: isMobile.value ? 24 : 16, 1361 1466 color: pending.value ? colors.value.border : colors.value.fgSubtle, ··· 1364 1469 granularity: getGranularityLabel(selectedGranularity.value), 1365 1470 facet: $t('package.trends.items.downloads'), 1366 1471 }), 1367 - xLabel: isMultiPackageMode.value ? '' : xAxisLabel.value, // for multiple series, names are displayed in the chart's legend 1368 1472 yLabelOffsetX: 12, 1369 1473 fontSize: isMobile.value ? 32 : 24, 1370 1474 }, 1371 1475 xAxisLabels: { 1372 - show: false, 1476 + show: true, 1477 + showOnlyAtModulo: true, 1478 + modulo: 12, 1373 1479 values: chartData.value?.dates, 1374 1480 datetimeFormatter: { 1375 1481 enable: true, ··· 1570 1676 1571 1677 <div role="region" aria-labelledby="download-analytics-title"> 1572 1678 <ClientOnly v-if="chartData.dataset"> 1573 - <div :data-pending="pending"> 1679 + <div :data-pending="pending" :data-minimap-visible="maxDatapoints > 6"> 1574 1680 <VueUiXy 1575 1681 :dataset="chartData.dataset" 1576 1682 :config="chartConfig" ··· 1603 1709 v-html="drawLastDatapointLabel(svg)" 1604 1710 /> 1605 1711 1712 + <!-- Inject legend during SVG print only --> 1713 + <g v-if="svg.isPrintingSvg" v-html="drawSvgPrintLegend(svg)" /> 1714 + 1715 + <!-- Inject npmx logo & tagline during SVG and PNG print --> 1716 + <g 1717 + v-if="svg.isPrintingSvg || svg.isPrintingImg" 1718 + v-html="drawNpmxLogoAndTaglineWatermark(svg)" 1719 + /> 1720 + 1606 1721 <!-- Overlay covering the chart area to hide line resizing when switching granularities recalculates VueUiXy scaleMax when estimation lines are necessary --> 1607 1722 <rect 1608 1723 v-if="pending" 1609 1724 :x="svg.drawingArea.left" 1610 1725 :y="svg.drawingArea.top - 12" 1611 1726 :width="svg.drawingArea.width + 12" 1612 - :height="svg.drawingArea.height + 24" 1727 + :height="svg.drawingArea.height + 48" 1613 1728 :fill="colors.bg" 1614 1729 /> 1615 1730 </template> ··· 1623 1738 </template> 1624 1739 1625 1740 <!-- Custom legend for multiple series --> 1626 - <template 1627 - v-if="isMultiPackageMode || ['monthly', 'yearly'].includes(displayedGranularity)" 1628 - #legend="{ legend }" 1629 - > 1741 + <template #legend="{ legend }"> 1630 1742 <div class="flex gap-4 flex-wrap justify-center"> 1631 1743 <template v-if="isMultiPackageMode"> 1632 1744 <button ··· 1651 1763 {{ datapoint.name }} 1652 1764 </span> 1653 1765 </button> 1766 + </template> 1767 + 1768 + <!-- Single series legend (no user interaction) --> 1769 + <template v-else-if="legend.length > 0"> 1770 + <div class="flex gap-1 place-items-center"> 1771 + <div class="h-3 w-3"> 1772 + <svg viewBox="0 0 2 2" class="w-full"> 1773 + <rect x="0" y="0" width="2" height="2" rx="0.3" :fill="legend[0]?.color" /> 1774 + </svg> 1775 + </div> 1776 + <span> 1777 + {{ legend[0]?.name }} 1778 + </span> 1779 + </div> 1654 1780 </template> 1655 1781 1656 1782 <!-- Estimation extra legend item --> ··· 1801 1927 1802 1928 [data-pending='true'] .vue-data-ui-zoom { 1803 1929 opacity: 0.1; 1930 + } 1931 + 1932 + [data-pending='true'] .vue-data-ui-time-label { 1933 + opacity: 0; 1934 + } 1935 + 1936 + /** Override print watermark position to have it below the chart */ 1937 + .vue-data-ui-watermark { 1938 + top: unset !important; 1939 + } 1940 + 1941 + [data-minimap-visible='false'] .vue-data-ui-watermark { 1942 + top: calc(100% - 2rem) !important; 1804 1943 } 1805 1944 </style>
+1 -1
package.json
··· 107 107 "vite-plugin-pwa": "1.2.0", 108 108 "vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", 109 109 "vue": "3.5.27", 110 - "vue-data-ui": "3.14.9" 110 + "vue-data-ui": "3.14.10" 111 111 }, 112 112 "devDependencies": { 113 113 "@e18e/eslint-plugin": "0.1.4",
+5 -5
pnpm-lock.yaml
··· 207 207 specifier: 3.5.27 208 208 version: 3.5.27(typescript@5.9.3) 209 209 vue-data-ui: 210 - specifier: 3.14.9 211 - version: 3.14.9(vue@3.5.27(typescript@5.9.3)) 210 + specifier: 3.14.10 211 + version: 3.14.10(vue@3.5.27(typescript@5.9.3)) 212 212 devDependencies: 213 213 '@e18e/eslint-plugin': 214 214 specifier: 0.1.4 ··· 9414 9414 vue-component-type-helpers@3.2.4: 9415 9415 resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} 9416 9416 9417 - vue-data-ui@3.14.9: 9418 - resolution: {integrity: sha512-ITq2xDK1LC2JrlDw0V17j/KsgVs/TXQEkdC3gPl6dkB4AvX88FsaNU1abGR1D5nXyCxaluPqIOiqSa/qDPDFSg==} 9417 + vue-data-ui@3.14.10: 9418 + resolution: {integrity: sha512-2mzt/5InMFWpE1458gm1h26ILpQxotQ9cOM1xcS8boWRZnjEw1ficfay+g/HNQZL0k4AzMSZKnWWBJ/PaKgclA==} 9419 9419 peerDependencies: 9420 9420 jspdf: '>=3.0.1' 9421 9421 vue: '>=3.3.0' ··· 21025 21025 21026 21026 vue-component-type-helpers@3.2.4: {} 21027 21027 21028 - vue-data-ui@3.14.9(vue@3.5.27(typescript@5.9.3)): 21028 + vue-data-ui@3.14.10(vue@3.5.27(typescript@5.9.3)): 21029 21029 dependencies: 21030 21030 vue: 3.5.27(typescript@5.9.3) 21031 21031