|
1 | 1 | import * as React from 'react' |
2 | | -import * as v from 'valibot' |
3 | 2 | import * as Plot from '@observablehq/plot' |
4 | 3 | import * as d3 from 'd3' |
5 | 4 | import { ParentSize } from '~/components/ParentSize' |
6 | | -import { packageGroupSchema } from '~/routes/stats/npm/-comparisons' |
7 | | -import { defaultColors } from '~/utils/npm-packages' |
8 | | - |
9 | | -// Types |
10 | | -export type PackageGroup = v.InferOutput<typeof packageGroupSchema> |
11 | | - |
12 | | -export const binTypeSchema = v.picklist([ |
13 | | - 'yearly', |
14 | | - 'monthly', |
15 | | - 'weekly', |
16 | | - 'daily', |
17 | | -]) |
18 | | -export type BinType = v.InferOutput<typeof binTypeSchema> |
19 | | - |
20 | | -export const transformModeSchema = v.picklist(['none', 'normalize-y']) |
21 | | -export type TransformMode = v.InferOutput<typeof transformModeSchema> |
22 | | - |
23 | | -export const showDataModeSchema = v.picklist(['all', 'complete']) |
24 | | -export type ShowDataMode = v.InferOutput<typeof showDataModeSchema> |
25 | | - |
26 | | -export type TimeRange = |
27 | | - | '7-days' |
28 | | - | '30-days' |
29 | | - | '90-days' |
30 | | - | '180-days' |
31 | | - | '365-days' |
32 | | - | '730-days' |
33 | | - | '1825-days' |
34 | | - | 'all-time' |
35 | | - |
36 | | -export type FacetValue = 'name' |
37 | | - |
38 | | -// Type for query data returned from npm stats API |
39 | | -export type NpmQueryData = Array<{ |
40 | | - packages: Array<{ |
41 | | - name?: string |
42 | | - hidden?: boolean |
43 | | - downloads: Array<{ day: string; downloads: number }> |
44 | | - }> |
45 | | - start: string |
46 | | - end: string |
47 | | - error?: string |
48 | | - actualStartDate?: Date |
49 | | -}> |
50 | | - |
51 | | -// Binning options configuration |
52 | | -export const binningOptions = [ |
53 | | - { |
54 | | - label: 'Yearly', |
55 | | - value: 'yearly', |
56 | | - single: 'year', |
57 | | - bin: d3.utcYear, |
58 | | - }, |
59 | | - { |
60 | | - label: 'Monthly', |
61 | | - value: 'monthly', |
62 | | - single: 'month', |
63 | | - bin: d3.utcMonth, |
64 | | - }, |
65 | | - { |
66 | | - label: 'Weekly', |
67 | | - value: 'weekly', |
68 | | - single: 'week', |
69 | | - bin: d3.utcWeek, |
70 | | - }, |
71 | | - { |
72 | | - label: 'Daily', |
73 | | - value: 'daily', |
74 | | - single: 'day', |
75 | | - bin: d3.utcDay, |
76 | | - }, |
77 | | -] as const |
78 | | - |
79 | | -export const binningOptionsByType = binningOptions.reduce( |
80 | | - (acc, option) => { |
81 | | - acc[option.value] = option |
82 | | - return acc |
83 | | - }, |
84 | | - {} as Record<BinType, (typeof binningOptions)[number]>, |
85 | | -) |
86 | | - |
87 | | -export const timeRanges = [ |
88 | | - { value: '7-days', label: '7 Days' }, |
89 | | - { value: '30-days', label: '30 Days' }, |
90 | | - { value: '90-days', label: '90 Days' }, |
91 | | - { value: '180-days', label: '6 Months' }, |
92 | | - { value: '365-days', label: '1 Year' }, |
93 | | - { value: '730-days', label: '2 Years' }, |
94 | | - { value: '1825-days', label: '5 Years' }, |
95 | | - { value: 'all-time', label: 'All Time' }, |
96 | | -] as const |
97 | | - |
98 | | -export const defaultRangeBinTypes: Record<TimeRange, BinType> = { |
99 | | - '7-days': 'daily', |
100 | | - '30-days': 'daily', |
101 | | - '90-days': 'weekly', |
102 | | - '180-days': 'weekly', |
103 | | - '365-days': 'weekly', |
104 | | - '730-days': 'monthly', |
105 | | - '1825-days': 'monthly', |
106 | | - 'all-time': 'monthly', |
107 | | -} |
108 | | - |
109 | | -// Get or assign colors for packages |
110 | | -export function getPackageColor( |
111 | | - packageName: string, |
112 | | - packages: PackageGroup[], |
113 | | -): string { |
114 | | - // Find the package group that contains this package |
115 | | - const packageInfo = packages.find((pkg) => |
116 | | - pkg.packages.some((p) => p.name === packageName), |
117 | | - ) |
118 | | - if (packageInfo?.color) { |
119 | | - return packageInfo.color |
120 | | - } |
121 | | - |
122 | | - // Otherwise, assign a default color based on the package's position |
123 | | - const packageIndex = packages.findIndex((pkg) => |
124 | | - pkg.packages.some((p) => p.name === packageName), |
125 | | - ) |
126 | | - return defaultColors[packageIndex % defaultColors.length] |
127 | | -} |
128 | | - |
129 | | -// Custom number formatter for more precise control |
130 | | -export const formatNumber = (num: number) => { |
131 | | - if (num >= 1_000_000) { |
132 | | - return `${(num / 1_000_000).toFixed(1)}M` |
133 | | - } |
134 | | - if (num >= 1_000) { |
135 | | - return `${(num / 1_000).toFixed(1)}k` |
136 | | - } |
137 | | - return num.toString() |
138 | | -} |
139 | | - |
140 | | -// Check if a binning option is valid for a time range |
141 | | -export function isBinningOptionValidForRange( |
142 | | - range: TimeRange, |
143 | | - binType: BinType, |
144 | | -): boolean { |
145 | | - switch (range) { |
146 | | - case '7-days': |
147 | | - case '30-days': |
148 | | - return binType === 'daily' |
149 | | - case '90-days': |
150 | | - case '180-days': |
151 | | - return ( |
152 | | - binType === 'daily' || binType === 'weekly' || binType === 'monthly' |
153 | | - ) |
154 | | - case '365-days': |
155 | | - return ( |
156 | | - binType === 'daily' || binType === 'weekly' || binType === 'monthly' |
157 | | - ) |
158 | | - case '730-days': |
159 | | - case '1825-days': |
160 | | - case 'all-time': |
161 | | - return true |
162 | | - } |
163 | | -} |
| 5 | +import { binningOptionsByType } from './binning' |
| 6 | +import type { |
| 7 | + BinType, |
| 8 | + FacetValue, |
| 9 | + NpmQueryData, |
| 10 | + PackageGroup, |
| 11 | + ShowDataMode, |
| 12 | + TimeRange, |
| 13 | + TransformMode, |
| 14 | +} from './shared' |
| 15 | +import { getPackageColor } from './shared' |
164 | 16 |
|
165 | 17 | // Plot figure component |
166 | 18 | function PlotFigure({ options }: { options: Parameters<typeof Plot.plot>[0] }) { |
|
0 commit comments