diff --git a/components/Package/VersionDownloadsChart/index.tsx b/components/Package/VersionDownloadsChart/index.tsx
index 5b1ac5bf2..0fa9c1b1f 100644
--- a/components/Package/VersionDownloadsChart/index.tsx
+++ b/components/Package/VersionDownloadsChart/index.tsx
@@ -1,9 +1,10 @@
+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';
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';
@@ -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 }}>
+
+
+
+
{data.label}
{data.distTags?.length && (
@@ -224,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
new file mode 100644
index 000000000..d94d5836a
--- /dev/null
+++ b/components/Package/VersionSizeChart/index.tsx
@@ -0,0 +1,201 @@
+import { LinearGradient } from '@visx/gradient';
+import { useParentSize } from '@visx/responsive';
+import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart';
+import { 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;
+const HEIGHT = 280;
+const LATEST_BAR_GRADIENT_ID = 'version-size-chart-gradient-latest';
+const BAR_GRADIENT_ID = 'version-size-chart-gradient';
+
+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 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
+ ? `url(#${LATEST_BAR_GRADIENT_ID})`
+ : `url(#${BAR_GRADIENT_ID})`
+ }
+ 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]) => (