From e46ca0748082ad6b089f6af966ae25e0065cfd65 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 6 Jul 2026 15:38:16 +0200 Subject: [PATCH 1/4] add initial version of version bundle size chart --- components/Package/VersionSizeChart/index.tsx | 176 ++++++++++++++++++ scenes/PackageVersionsScene.tsx | 34 +++- 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 components/Package/VersionSizeChart/index.tsx diff --git a/components/Package/VersionSizeChart/index.tsx b/components/Package/VersionSizeChart/index.tsx new file mode 100644 index 000000000..20bf06fe5 --- /dev/null +++ b/components/Package/VersionSizeChart/index.tsx @@ -0,0 +1,176 @@ +import { useParentSize } from '@visx/responsive'; +import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; +import { Text, View } from 'react-native'; + +import { Label } from '~/common/styleguide'; +import { type PackageVersionsData } from '~/types'; +import { formatBytes } from '~/util/formatBytes'; +import tw from '~/util/tailwind'; + +const RECENT_RELEASE_COUNT = 20; + +type VersionSizeChartData = { + label: string; + size: number; + publishedAt: string; +}; + +type Props = { + registryData: PackageVersionsData; +}; + +export default function VersionSizeChart({ registryData }: Props) { + const isDark = tw.prefixMatch('dark'); + const { parentRef, width } = useParentSize({ debounceTime: 150 }); + + const series = buildRecentVersionSizeSeries( + registryData, + width > 480 ? RECENT_RELEASE_COUNT : RECENT_RELEASE_COUNT / 2 + ); + + if (!series.length) { + return ( + + + + ); + } + + const height = 280; + const maxSize = Math.max(...series.map(item => item.size), 0); + const yDomain = maxSize ? [0, maxSize + Math.max(1, maxSize * 0.08)] : undefined; + const latestVersion = series.at(-1)?.label; + + return ( + // @ts-expect-error Ref type miss-match + + + + ( + + + {formattedValue} + + + )} + /> + formatBytes(Number(value))} + tickComponent={({ formattedValue = 'unknown', x, y }) => ( + + + {formattedValue} + + + )} + /> + item.label} + yAccessor={(item: VersionSizeChartData) => item.size} + colorAccessor={(item: VersionSizeChartData) => + item.label === latestVersion + ? isDark + ? 'var(--primary)' + : 'var(--primary-dark)' + : 'var(--primary-darker)' + } + radius={4} + radiusAll + /> + + showVerticalCrosshair={false} + showSeriesGlyphs={false} + offsetLeft={8} + offsetTop={6} + detectBounds + unstyled + applyPositionStyle + renderTooltip={({ tooltipData }) => + renderTooltipContent(tooltipData?.nearestDatum?.datum) + } + /> + + + ); +} + +function buildRecentVersionSizeSeries( + data: PackageVersionsData, + count: number +): VersionSizeChartData[] { + return Object.entries(data.versions) + .filter(([version, versionData]) => { + const publishedAt = data.time[version]; + const size = versionData.dist?.unpackedSize; + + if (version.includes('-')) { + return false; + } + + return Boolean(publishedAt) && typeof size === 'number'; + }) + .sort( + (left, right) => + new Date(data.time[left[0]]).getTime() - new Date(data.time[right[0]]).getTime() + ) + .slice(-count) + .map(([version, versionData]) => ({ + label: version, + size: versionData.dist!.unpackedSize!, + publishedAt: data.time[version], + })); +} + +function renderTooltipContent(data?: VersionSizeChartData) { + if (!data) { + return null; + } + + return ( + + {data.label} + {formatBytes(data.size)} package size + + Published {new Date(data.publishedAt).toLocaleDateString('en-US')} + + + ); +} diff --git a/scenes/PackageVersionsScene.tsx b/scenes/PackageVersionsScene.tsx index cc5f2b849..4a254c596 100644 --- a/scenes/PackageVersionsScene.tsx +++ b/scenes/PackageVersionsScene.tsx @@ -19,13 +19,22 @@ const VersionDownloadsChartWithLoading = dynamic( { ssr: false, loading: () => ( - + ), } ); +const VersionSizeChartWithLoading = dynamic(() => import('~/components/Package/VersionSizeChart'), { + ssr: false, + loading: () => ( + + + + ), +}); + export default function PackageVersionsScene({ apiData, registryData, @@ -43,6 +52,12 @@ export default function PackageVersionsScene({ const hasVersionDownloads = Boolean( npmDownloads && Object.values(npmDownloads.downloads).some(downloads => downloads > 0) ); + const hasVersionSizes = Boolean( + registryData && + Object.values(registryData.versions).some( + versionData => typeof versionData.dist?.unpackedSize === 'number' + ) + ); if (!library || !registryData) { return ; @@ -83,6 +98,23 @@ export default function PackageVersionsScene({ ) : null} + {hasVersionSizes ? ( + <> + + + Package size by version + + + + + + + + ) : null} Tagged versions {taggedVersions.map(([label, versionData]) => ( From d05d009dc79a8df8423b9ffa374607520ee82870 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 6 Jul 2026 16:14:05 +0200 Subject: [PATCH 2/4] add gradients to chart bars, slight visual tweaks --- .../Package/VersionDownloadsChart/index.tsx | 42 ++++++++++++++++--- components/Package/VersionSizeChart/index.tsx | 30 ++++++++++--- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/components/Package/VersionDownloadsChart/index.tsx b/components/Package/VersionDownloadsChart/index.tsx index 5b1ac5bf2..6bb7aed99 100644 --- a/components/Package/VersionDownloadsChart/index.tsx +++ b/components/Package/VersionDownloadsChart/index.tsx @@ -1,3 +1,4 @@ +import { LinearGradient } from '@visx/gradient'; import { useParentSize } from '@visx/responsive'; import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; import { keyBy } from 'es-toolkit/array'; @@ -41,6 +42,9 @@ const DIST_TAG_LABEL_STYLE = { fontWeight: 400, fontSize: 10, }; +const TAGGED_BAR_GRADIENT_ID = 'version-downloads-chart-gradient-tagged'; +const BAR_GRADIENT_ID = 'version-downloads-chart-gradient'; +const OTHER_BAR_GRADIENT_ID = 'version-downloads-chart-other'; export default function VersionDownloadsChart({ npmDownloads, registryData }: Props) { const isDark = tw.prefixMatch('dark'); @@ -92,13 +96,41 @@ export default function VersionDownloadsChart({ npmDownloads, registryData }: Pr xScale={{ type: 'linear', domain: xDomain }} yScale={{ type: 'band', paddingInner: 0.23, paddingOuter: 0.15 }} margin={{ top: 2, right: 12, bottom: 20, left: leftMargin }}> + + + + + item.size} colorAccessor={(item: VersionSizeChartData) => item.label === latestVersion - ? isDark - ? 'var(--primary)' - : 'var(--primary-dark)' - : 'var(--primary-darker)' + ? `url(#${LATEST_BAR_GRADIENT_ID})` + : `url(#${BAR_GRADIENT_ID})` } radius={4} radiusAll From 357b9796b3dc8a12ee66926f2bb5da4d2ad3a8bb Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 6 Jul 2026 16:23:58 +0200 Subject: [PATCH 3/4] add chart tooltip shadow --- components/Package/VersionDownloadsChart/index.tsx | 11 +++++++---- components/Package/VersionSizeChart/index.tsx | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/components/Package/VersionDownloadsChart/index.tsx b/components/Package/VersionDownloadsChart/index.tsx index 6bb7aed99..0fa9c1b1f 100644 --- a/components/Package/VersionDownloadsChart/index.tsx +++ b/components/Package/VersionDownloadsChart/index.tsx @@ -4,7 +4,7 @@ import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; import { keyBy } from 'es-toolkit/array'; import { useRouter } from 'next/router'; import { useState } from 'react'; -import { Text, View } from 'react-native'; +import { View } from 'react-native'; import { Label } from '~/common/styleguide'; import { type NpmPerVersionDownloads, type PackageVersionsData } from '~/types'; @@ -233,8 +233,11 @@ function renderTooltipContent(data?: VersionsChartData) { } return ( - + {data.label} {data.distTags?.length && ( @@ -256,6 +259,6 @@ function renderTooltipContent(data?: VersionsChartData) { {new Date(data.publishedAt).toLocaleDateString('en-US')} ) : null} - + ); } diff --git a/components/Package/VersionSizeChart/index.tsx b/components/Package/VersionSizeChart/index.tsx index a1841ea3a..f7faf6cc4 100644 --- a/components/Package/VersionSizeChart/index.tsx +++ b/components/Package/VersionSizeChart/index.tsx @@ -1,7 +1,7 @@ import { LinearGradient } from '@visx/gradient'; import { useParentSize } from '@visx/responsive'; import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart'; -import { Text, View } from 'react-native'; +import { View } from 'react-native'; import { Label } from '~/common/styleguide'; import { type PackageVersionsData } from '~/types'; @@ -182,13 +182,16 @@ function renderTooltipContent(data?: VersionSizeChartData) { } return ( - + {data.label} {formatBytes(data.size)} package size Published {new Date(data.publishedAt).toLocaleDateString('en-US')} - + ); } From e7f17831e182913959f09cf46f4837cc2ce1e158 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 6 Jul 2026 16:46:28 +0200 Subject: [PATCH 4/4] prevent rendering too chunky bars with low versions count --- components/Package/VersionSizeChart/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/Package/VersionSizeChart/index.tsx b/components/Package/VersionSizeChart/index.tsx index f7faf6cc4..d94d5836a 100644 --- a/components/Package/VersionSizeChart/index.tsx +++ b/components/Package/VersionSizeChart/index.tsx @@ -9,6 +9,7 @@ import { formatBytes } from '~/util/formatBytes'; import tw from '~/util/tailwind'; const RECENT_RELEASE_COUNT = 20; +const HEIGHT = 280; const LATEST_BAR_GRADIENT_ID = 'version-size-chart-gradient-latest'; const BAR_GRADIENT_ID = 'version-size-chart-gradient'; @@ -39,7 +40,6 @@ export default function VersionSizeChart({ registryData }: Props) { ); } - const height = 280; const maxSize = Math.max(...series.map(item => item.size), 0); const yDomain = maxSize ? [0, maxSize + Math.max(1, maxSize * 0.08)] : undefined; const latestVersion = series.at(-1)?.label; @@ -49,10 +49,14 @@ export default function VersionSizeChart({ registryData }: Props) { + margin={{ top: 8, right: 12, bottom: 44, left: 58 }}>