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]) => (