Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions components/Package/VersionDownloadsChart/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 }}>
<LinearGradient
id={BAR_GRADIENT_ID}
from={isDark ? 'var(--primary-darker)' : 'var(--primary-darker)'}
to={isDark ? 'var(--primary-darker)' : 'var(--primary-darker)'}
toOpacity={1}
toOffset={0.75}
fromOpacity={isDark ? 0.25 : 0.5}
rotate={-90}
/>
<LinearGradient
id={TAGGED_BAR_GRADIENT_ID}
from={isDark ? 'var(--primary)' : 'var(--primary-dark)'}
to={isDark ? 'var(--primary)' : 'var(--primary-dark)'}
toOpacity={1}
toOffset={0.75}
fromOpacity={isDark ? 0.25 : 0.5}
rotate={-90}
/>
<LinearGradient
id={OTHER_BAR_GRADIENT_ID}
from="var(--pewter)"
to="var(--pewter)"
toOpacity={1}
toOffset={0.75}
fromOpacity={isDark ? 0.25 : 0.5}
rotate={-90}
/>
<Grid
columns
rows={false}
strokeDasharray="4 4"
lineStyle={{
stroke: isDark ? '#374151' : '#e5e7eb',
stroke: isDark ? '#374151' : '#cecfd3',
strokeOpacity: isDark ? 0.66 : 1,
strokeWidth: isDark ? 0.5 : 0.66,
strokeWidth: 0.5,
}}
/>
<Axis
Expand Down Expand Up @@ -162,14 +194,14 @@ export default function VersionDownloadsChart({ npmDownloads, registryData }: Pr
const { kind, distTags } = item;

if (kind === 'other') {
return 'var(--pewter)';
return `url(#${OTHER_BAR_GRADIENT_ID})`;
}

if (distTags?.length) {
return isDark ? 'var(--primary)' : 'var(--primary-dark)';
return `url(#${TAGGED_BAR_GRADIENT_ID})`;
}

return 'var(--primary-darker)';
return `url(#${BAR_GRADIENT_ID})`;
}}
radius={3}
radiusAll
Expand Down Expand Up @@ -201,8 +233,11 @@ function renderTooltipContent(data?: VersionsChartData) {
}

return (
<Text
style={tw`font-sans flex flex-col gap-px rounded bg-black px-2.5 py-1.5 text-xs font-light text-white dark:border dark:border-default`}>
<View
style={[
tw`font-sans flex flex-col gap-px rounded bg-black px-2.5 py-1.5 text-xs font-light text-white dark:border dark:border-default`,
{ boxShadow: '0 8px 24px #00000088' },
]}>
<span style={tw`text-[14px] font-medium tabular-nums`}>
{data.label}
{data.distTags?.length && (
Expand All @@ -224,6 +259,6 @@ function renderTooltipContent(data?: VersionsChartData) {
{new Date(data.publishedAt).toLocaleDateString('en-US')}
</span>
) : null}
</Text>
</View>
);
}
201 changes: 201 additions & 0 deletions components/Package/VersionSizeChart/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={tw`min-h-[220px] items-center justify-center`}>
<Label style={tw`text-secondary`}>Size data unavailable</Label>
</View>
);
}

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
<View style={tw`w-full gap-3`} ref={parentRef}>
<XYChart
width={width}
height={HEIGHT}
xScale={{
type: 'band',
paddingInner: series.length < RECENT_RELEASE_COUNT / 2 ? 0.55 : 0.24,
paddingOuter: series.length < RECENT_RELEASE_COUNT / 2 ? 0.4 : 0.15,
}}
yScale={{ type: 'linear', domain: yDomain }}
margin={{ top: 8, right: 12, bottom: 44, left: 58 }}>
<LinearGradient
id={BAR_GRADIENT_ID}
from={isDark ? 'var(--primary-darker)' : 'var(--primary-darker)'}
to={isDark ? 'var(--primary-darker)' : 'var(--primary-darker)'}
fromOpacity={1}
fromOffset={0.25}
toOpacity={isDark ? 0.25 : 0.5}
/>
<LinearGradient
id={LATEST_BAR_GRADIENT_ID}
from={isDark ? 'var(--primary)' : 'var(--primary-dark)'}
to={isDark ? 'var(--primary)' : 'var(--primary-dark)'}
fromOpacity={1}
fromOffset={0.25}
toOpacity={isDark ? 0.25 : 0.5}
/>
<Grid
rows
columns={false}
strokeDasharray="4 4"
lineStyle={{
stroke: isDark ? '#374151' : '#cecfd3',
strokeOpacity: isDark ? 0.66 : 1,
strokeWidth: 0.5,
}}
/>
<Axis
orientation="bottom"
hideAxisLine
hideTicks
numTicks={series.length}
tickComponent={({ formattedValue = 'unknown', x, y }) => (
<text
x={x}
y={y}
textAnchor="end"
dominantBaseline="middle"
transform={`rotate(-45 ${x ?? 0} ${y ?? 0})`}
style={tw`select-none tabular-nums`}
fill="var(--secondary)">
<tspan x={x} style={tw`text-[11px] font-light`}>
{formattedValue}
</tspan>
</text>
)}
/>
<Axis
orientation="left"
hideAxisLine
hideTicks
numTicks={4}
tickFormat={value => formatBytes(Number(value))}
tickComponent={({ formattedValue = 'unknown', x, y }) => (
<text
x={(x ?? 0) - 8}
y={y}
textAnchor="end"
dominantBaseline="middle"
style={tw`select-none tabular-nums`}
fill={isDark ? 'var(--white)' : 'var(--black)'}>
<tspan x={(x ?? 0) - 8} style={tw`text-[12px] font-light`}>
{formattedValue}
</tspan>
</text>
)}
/>
<BarSeries
dataKey="size"
data={series}
xAccessor={(item: VersionSizeChartData) => 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
/>
<Tooltip<VersionSizeChartData>
showVerticalCrosshair={false}
showSeriesGlyphs={false}
offsetLeft={8}
offsetTop={6}
detectBounds
unstyled
applyPositionStyle
renderTooltip={({ tooltipData }) =>
renderTooltipContent(tooltipData?.nearestDatum?.datum)
}
/>
</XYChart>
</View>
);
}

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 (
<View
style={[
tw`font-sans flex flex-col gap-px rounded bg-black px-2.5 py-1.5 text-xs font-light text-white dark:border dark:border-default`,
{ boxShadow: '0 8px 24px #00000088' },
]}>
<span style={tw`text-[14px] font-medium tabular-nums`}>{data.label}</span>
<span>{formatBytes(data.size)} package size</span>
<span style={tw`text-palette-gray3 dark:text-secondary`}>
Published {new Date(data.publishedAt).toLocaleDateString('en-US')}
</span>
</View>
);
}
34 changes: 33 additions & 1 deletion scenes/PackageVersionsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@ const VersionDownloadsChartWithLoading = dynamic(
{
ssr: false,
loading: () => (
<View style={tw`min-h-12 items-center justify-center`}>
<View style={tw`min-h-[406px] items-center justify-center`}>
<ThreeDotsLoader />
</View>
),
}
);

const VersionSizeChartWithLoading = dynamic(() => import('~/components/Package/VersionSizeChart'), {
ssr: false,
loading: () => (
<View style={tw`min-h-[280px] items-center justify-center`}>
<ThreeDotsLoader />
</View>
),
});

export default function PackageVersionsScene({
apiData,
registryData,
Expand All @@ -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 <NotFound />;
Expand Down Expand Up @@ -83,6 +98,23 @@ export default function PackageVersionsScene({
</View>
</>
) : null}
{hasVersionSizes ? (
<>
<View style={tw`mt-3 gap-1`}>
<H6Section
style={[
tw`flex items-end justify-between text-secondary`,
isSmallScreen && tw`flex-col items-start gap-y-0.5`,
]}>
Package size by version
<Label style={tw`font-light text-secondary`}>Last published</Label>
</H6Section>
</View>
<View style={tw`overflow-hidden rounded-lg border border-default p-3`}>
<VersionSizeChartWithLoading registryData={registryData} />
</View>
</>
) : null}
<H6Section style={tw`mt-3 text-secondary`}>Tagged versions</H6Section>
<View style={tw`gap-2`}>
{taggedVersions.map(([label, versionData]) => (
Expand Down