Skip to content

Commit 684229c

Browse files
committed
npm stats
1 parent e0f9ce4 commit 684229c

File tree

7 files changed

+1605
-1
lines changed

7 files changed

+1605
-1
lines changed

app/components/NpmStatsChart.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as React from 'react'
2+
import * as Plot from '@observablehq/plot'
3+
import { ParentSize } from '@visx/responsive'
4+
5+
type NpmStats = {
6+
start: string
7+
end: string
8+
package: string
9+
downloads: Array<{
10+
downloads: number
11+
day: string
12+
}>
13+
}
14+
15+
export function NpmStatsChart({ stats }: { stats: NpmStats[] }) {
16+
const plotRef = React.useRef<HTMLDivElement>(null)
17+
18+
React.useEffect(() => {
19+
if (!stats.length || !plotRef.current) return
20+
21+
// Flatten the data for the plot
22+
const plotData = stats.flatMap((stat) =>
23+
stat.downloads.map((d) => ({
24+
...d,
25+
package: stat.package,
26+
date: new Date(d.day),
27+
}))
28+
)
29+
30+
const chart = Plot.plot({
31+
marginLeft: 50,
32+
marginRight: 0,
33+
marginBottom: 70,
34+
width: plotRef.current.clientWidth,
35+
height: plotRef.current.clientHeight,
36+
marks: [
37+
Plot.line(plotData, {
38+
x: 'date',
39+
y: 'downloads',
40+
stroke: 'package',
41+
strokeWidth: 2,
42+
}),
43+
],
44+
x: {
45+
type: 'time',
46+
label: 'Date',
47+
labelOffset: 35,
48+
tickFormat: (d: Date) => d.toLocaleDateString(),
49+
},
50+
y: {
51+
label: 'Downloads',
52+
labelOffset: 35,
53+
tickFormat: (d: number) => {
54+
if (d >= 1000000) {
55+
return `${(d / 1000000).toFixed(1)}M`
56+
}
57+
if (d >= 1000) {
58+
return `${(d / 1000).toFixed(1)}K`
59+
}
60+
return d.toString()
61+
},
62+
},
63+
grid: true,
64+
color: {
65+
legend: true,
66+
},
67+
})
68+
69+
plotRef.current.appendChild(chart)
70+
71+
return () => {
72+
if (plotRef.current) {
73+
plotRef.current.innerHTML = ''
74+
}
75+
}
76+
}, [stats])
77+
78+
return (
79+
<ParentSize>
80+
{({ width, height }) => (
81+
<div
82+
ref={plotRef}
83+
style={{
84+
width,
85+
height: Math.max(400, height * 0.6),
86+
}}
87+
/>
88+
)}
89+
</ParentSize>
90+
)
91+
}

app/components/Tooltip.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from 'react'
2+
import {
3+
useFloating,
4+
useHover,
5+
useInteractions,
6+
FloatingPortal,
7+
offset,
8+
shift,
9+
flip,
10+
autoUpdate,
11+
} from '@floating-ui/react'
12+
13+
interface TooltipProps {
14+
content: React.ReactNode
15+
children: React.ReactElement
16+
placement?: 'top' | 'right' | 'bottom' | 'left'
17+
className?: string
18+
}
19+
20+
export function Tooltip({
21+
content,
22+
children,
23+
placement = 'top',
24+
className = '',
25+
}: TooltipProps) {
26+
const [isOpen, setIsOpen] = React.useState(false)
27+
28+
const { refs, floatingStyles, context } = useFloating({
29+
open: isOpen,
30+
onOpenChange: setIsOpen,
31+
placement,
32+
middleware: [offset(5), flip(), shift()],
33+
whileElementsMounted: autoUpdate,
34+
})
35+
36+
const hover = useHover(context)
37+
38+
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
39+
40+
return (
41+
<>
42+
{React.cloneElement(children, {
43+
ref: refs.setReference,
44+
...getReferenceProps(),
45+
})}
46+
<FloatingPortal>
47+
{isOpen && (
48+
<div
49+
ref={refs.setFloating}
50+
style={floatingStyles}
51+
{...getFloatingProps()}
52+
className={`z-50 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white shadow-lg dark:bg-gray-800 ${className}`}
53+
>
54+
{content}
55+
</div>
56+
)}
57+
</FloatingPortal>
58+
</>
59+
)
60+
}

app/routeTree.gen.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Route as LibrariesEthosImport } from './routes/_libraries/ethos'
2727
import { Route as LibrariesDedicatedSupportImport } from './routes/_libraries/dedicated-support'
2828
import { Route as LibrariesBlogImport } from './routes/_libraries/blog'
2929
import { Route as LibraryIdVersionImport } from './routes/$libraryId/$version'
30+
import { Route as StatsNpmIndexImport } from './routes/stats/npm/index'
3031
import { Route as LibrariesBlogIndexImport } from './routes/_libraries/blog.index'
3132
import { Route as LibrariesBlogSplatImport } from './routes/_libraries/blog.$'
3233
import { Route as LibraryIdVersionDocsImport } from './routes/$libraryId/$version.docs'
@@ -142,6 +143,12 @@ const LibraryIdVersionRoute = LibraryIdVersionImport.update({
142143
getParentRoute: () => LibraryIdRouteRoute,
143144
} as any)
144145

146+
const StatsNpmIndexRoute = StatsNpmIndexImport.update({
147+
id: '/stats/npm/',
148+
path: '/stats/npm/',
149+
getParentRoute: () => rootRoute,
150+
} as any)
151+
145152
const LibrariesBlogIndexRoute = LibrariesBlogIndexImport.update({
146153
id: '/',
147154
path: '/',
@@ -397,6 +404,13 @@ declare module '@tanstack/react-router' {
397404
preLoaderRoute: typeof LibrariesBlogIndexImport
398405
parentRoute: typeof LibrariesBlogImport
399406
}
407+
'/stats/npm/': {
408+
id: '/stats/npm/'
409+
path: '/stats/npm'
410+
fullPath: '/stats/npm'
411+
preLoaderRoute: typeof StatsNpmIndexImport
412+
parentRoute: typeof rootRoute
413+
}
400414
'/$libraryId/$version/docs/$': {
401415
id: '/$libraryId/$version/docs/$'
402416
path: '/$'
@@ -624,6 +638,7 @@ export interface FileRoutesByFullPath {
624638
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsRouteWithChildren
625639
'/blog/$': typeof LibrariesBlogSplatRoute
626640
'/blog/': typeof LibrariesBlogIndexRoute
641+
'/stats/npm': typeof StatsNpmIndexRoute
627642
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
628643
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
629644
'/config/$version': typeof LibrariesConfigVersionIndexRoute
@@ -656,6 +671,7 @@ export interface FileRoutesByTo {
656671
'/': typeof LibrariesIndexRoute
657672
'/blog/$': typeof LibrariesBlogSplatRoute
658673
'/blog': typeof LibrariesBlogIndexRoute
674+
'/stats/npm': typeof StatsNpmIndexRoute
659675
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
660676
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsIndexRoute
661677
'/config/$version': typeof LibrariesConfigVersionIndexRoute
@@ -693,6 +709,7 @@ export interface FileRoutesById {
693709
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsRouteWithChildren
694710
'/_libraries/blog/$': typeof LibrariesBlogSplatRoute
695711
'/_libraries/blog/': typeof LibrariesBlogIndexRoute
712+
'/stats/npm/': typeof StatsNpmIndexRoute
696713
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
697714
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
698715
'/_libraries/config/$version/': typeof LibrariesConfigVersionIndexRoute
@@ -731,6 +748,7 @@ export interface FileRouteTypes {
731748
| '/$libraryId/$version/docs'
732749
| '/blog/$'
733750
| '/blog/'
751+
| '/stats/npm'
734752
| '/$libraryId/$version/docs/$'
735753
| '/$libraryId/$version/docs/'
736754
| '/config/$version'
@@ -762,6 +780,7 @@ export interface FileRouteTypes {
762780
| '/'
763781
| '/blog/$'
764782
| '/blog'
783+
| '/stats/npm'
765784
| '/$libraryId/$version/docs/$'
766785
| '/$libraryId/$version/docs'
767786
| '/config/$version'
@@ -797,6 +816,7 @@ export interface FileRouteTypes {
797816
| '/$libraryId/$version/docs'
798817
| '/_libraries/blog/$'
799818
| '/_libraries/blog/'
819+
| '/stats/npm/'
800820
| '/$libraryId/$version/docs/$'
801821
| '/$libraryId/$version/docs/'
802822
| '/_libraries/config/$version/'
@@ -821,6 +841,7 @@ export interface RootRouteChildren {
821841
LoginRoute: typeof LoginRoute
822842
MerchRoute: typeof MerchRoute
823843
SponsorsEmbedRoute: typeof SponsorsEmbedRoute
844+
StatsNpmIndexRoute: typeof StatsNpmIndexRoute
824845
}
825846

826847
const rootRouteChildren: RootRouteChildren = {
@@ -830,6 +851,7 @@ const rootRouteChildren: RootRouteChildren = {
830851
LoginRoute: LoginRoute,
831852
MerchRoute: MerchRoute,
832853
SponsorsEmbedRoute: SponsorsEmbedRoute,
854+
StatsNpmIndexRoute: StatsNpmIndexRoute,
833855
}
834856

835857
export const routeTree = rootRoute
@@ -847,7 +869,8 @@ export const routeTree = rootRoute
847869
"/dashboard",
848870
"/login",
849871
"/merch",
850-
"/sponsors-embed"
872+
"/sponsors-embed",
873+
"/stats/npm/"
851874
]
852875
},
853876
"/$libraryId": {
@@ -957,6 +980,9 @@ export const routeTree = rootRoute
957980
"filePath": "_libraries/blog.index.tsx",
958981
"parent": "/_libraries/blog"
959982
},
983+
"/stats/npm/": {
984+
"filePath": "stats/npm/index.tsx"
985+
},
960986
"/$libraryId/$version/docs/$": {
961987
"filePath": "$libraryId/$version.docs.$.tsx",
962988
"parent": "/$libraryId/$version/docs"

0 commit comments

Comments
 (0)