@@ -13,6 +13,12 @@ import { parseRepoUrl } from '#shared/utils/git-providers'
1313import type { PackageMetaResponse } from '#shared/types'
1414import { encodePackageName } from '#shared/utils/npm'
1515import { fetchNpmDownloadsRange } from '~/utils/npm/api'
16+ import {
17+ buildDailyEvolution ,
18+ buildWeeklyEvolution ,
19+ buildMonthlyEvolution ,
20+ buildYearlyEvolution ,
21+ } from '~/utils/chart-data-buckets'
1622
1723export type PackumentLikeForTime = {
1824 time ?: Record < string , string >
@@ -32,25 +38,6 @@ function startOfUtcMonth(date: Date): Date {
3238 return new Date ( Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , 1 ) )
3339}
3440
35- function daysInMonth ( year : number , month : number ) : number {
36- return new Date ( Date . UTC ( year , month + 1 , 0 ) ) . getUTCDate ( )
37- }
38-
39- function daysInYear ( year : number ) : number {
40- return year % 4 === 0 && ( year % 100 !== 0 || year % 400 === 0 ) ? 366 : 365
41- }
42-
43- /**
44- * Scale up a partial bucket value proportionally.
45- * @param value - the raw sum for the partial bucket
46- * @param actualDays - number of days with data in the bucket
47- * @param totalDays - expected full bucket size in days
48- */
49- export function fillPartialBucket ( value : number , actualDays : number , totalDays : number ) : number {
50- if ( actualDays <= 0 || actualDays >= totalDays ) return value
51- return Math . round ( ( value * totalDays ) / actualDays )
52- }
53-
5441function startOfUtcYear ( date : Date ) : Date {
5542 return new Date ( Date . UTC ( date . getUTCFullYear ( ) , 0 , 1 ) )
5643}
@@ -108,164 +95,6 @@ function mergeDailyPoints(points: DailyRawPoint[]): DailyRawPoint[] {
10895 . map ( ( [ day , value ] ) => ( { day, value } ) )
10996}
11097
111- export function buildDailyEvolutionFromDaily ( daily : DailyRawPoint [ ] ) : DailyDataPoint [ ] {
112- return daily
113- . slice ( )
114- . sort ( ( a , b ) => a . day . localeCompare ( b . day ) )
115- . map ( item => {
116- const dayDate = parseIsoDateOnly ( item . day )
117- const timestamp = dayDate . getTime ( )
118-
119- return { day : item . day , value : item . value , timestamp }
120- } )
121- }
122-
123- export function buildRollingWeeklyEvolutionFromDaily (
124- daily : DailyRawPoint [ ] ,
125- rangeStartIso : string ,
126- rangeEndIso : string ,
127- ) : WeeklyDataPoint [ ] {
128- const sorted = daily . slice ( ) . sort ( ( a , b ) => a . day . localeCompare ( b . day ) )
129- if ( sorted . length === 0 ) return [ ]
130-
131- const rangeStartDate = parseIsoDateOnly ( rangeStartIso )
132- // Align from last day with actual data (npm has 1-2 day delay, today is incomplete)
133- const lastNonZero = sorted . findLast ( d => d . value > 0 )
134- const effectiveEnd = lastNonZero
135- ? parseIsoDateOnly ( lastNonZero . day )
136- : parseIsoDateOnly ( rangeEndIso )
137- const pickerEnd = parseIsoDateOnly ( rangeEndIso )
138- const rangeEndDate = effectiveEnd . getTime ( ) < pickerEnd . getTime ( ) ? effectiveEnd : pickerEnd
139-
140- // Build 7-day buckets from END backwards
141- const groupedByIndex = new Map < number , number > ( )
142-
143- for ( const item of sorted ) {
144- const itemDate = parseIsoDateOnly ( item . day )
145- const dayOffsetFromEnd = Math . floor ( ( rangeEndDate . getTime ( ) - itemDate . getTime ( ) ) / 86400000 )
146- if ( dayOffsetFromEnd < 0 ) continue
147-
148- const weekIndex = Math . floor ( dayOffsetFromEnd / 7 )
149- groupedByIndex . set ( weekIndex , ( groupedByIndex . get ( weekIndex ) ?? 0 ) + item . value )
150- }
151-
152- return Array . from ( groupedByIndex . entries ( ) )
153- . sort ( ( [ a ] , [ b ] ) => b - a ) // reverse: highest index = oldest week
154- . map ( ( [ weekIndex , value ] ) => {
155- const weekEndDate = addDays ( rangeEndDate , - ( weekIndex * 7 ) )
156- let weekStartDate = addDays ( weekEndDate , - 6 )
157-
158- // First bucket may be partial — scale up proportionally
159- if ( weekStartDate . getTime ( ) < rangeStartDate . getTime ( ) ) {
160- weekStartDate = rangeStartDate
161- const actualDays =
162- Math . floor ( ( weekEndDate . getTime ( ) - rangeStartDate . getTime ( ) ) / 86400000 ) + 1
163- value = fillPartialBucket ( value , actualDays , 7 )
164- }
165-
166- const weekStartIso = toIsoDateString ( weekStartDate )
167- const weekEndIso = toIsoDateString ( weekEndDate )
168-
169- const timestampStart = weekStartDate . getTime ( )
170- const timestampEnd = weekEndDate . getTime ( )
171-
172- return {
173- value,
174- weekKey : `${ weekStartIso } _${ weekEndIso } ` ,
175- weekStart : weekStartIso ,
176- weekEnd : weekEndIso ,
177- timestampStart,
178- timestampEnd,
179- }
180- } )
181- }
182-
183- export function buildMonthlyEvolutionFromDaily (
184- daily : DailyRawPoint [ ] ,
185- rangeStartIso ?: string ,
186- rangeEndIso ?: string ,
187- ) : MonthlyDataPoint [ ] {
188- const sorted = daily . slice ( ) . sort ( ( a , b ) => a . day . localeCompare ( b . day ) )
189- const valuesByMonth = new Map < string , number > ( )
190-
191- for ( const item of sorted ) {
192- const month = item . day . slice ( 0 , 7 )
193- valuesByMonth . set ( month , ( valuesByMonth . get ( month ) ?? 0 ) + item . value )
194- }
195-
196- const entries = Array . from ( valuesByMonth . entries ( ) ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
197-
198- return entries . map ( ( [ month , value ] , index ) => {
199- const monthStartDate = parseIsoDateOnly ( `${ month } -01` )
200- const [ y , m ] = month . split ( '-' ) . map ( Number ) as [ number , number ]
201- const totalDays = daysInMonth ( y , m - 1 )
202-
203- // Scale up partial first bucket
204- if ( index === 0 && rangeStartIso ) {
205- const rangeStartDay = Number ( rangeStartIso . split ( '-' ) [ 2 ] )
206- if ( rangeStartDay > 1 ) {
207- value = fillPartialBucket ( value , totalDays - rangeStartDay + 1 , totalDays )
208- }
209- }
210-
211- // Scale up partial last bucket
212- if ( index === entries . length - 1 && rangeEndIso ) {
213- const rangeEndDay = Number ( rangeEndIso . split ( '-' ) [ 2 ] )
214- if ( rangeEndDay < totalDays ) {
215- value = fillPartialBucket ( value , rangeEndDay , totalDays )
216- }
217- }
218-
219- const timestamp = monthStartDate . getTime ( )
220- return { month, value, timestamp }
221- } )
222- }
223-
224- export function buildYearlyEvolutionFromDaily (
225- daily : DailyRawPoint [ ] ,
226- rangeStartIso ?: string ,
227- rangeEndIso ?: string ,
228- ) : YearlyDataPoint [ ] {
229- const sorted = daily . slice ( ) . sort ( ( a , b ) => a . day . localeCompare ( b . day ) )
230- const valuesByYear = new Map < string , number > ( )
231-
232- for ( const item of sorted ) {
233- const year = item . day . slice ( 0 , 4 )
234- valuesByYear . set ( year , ( valuesByYear . get ( year ) ?? 0 ) + item . value )
235- }
236-
237- const entries = Array . from ( valuesByYear . entries ( ) ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
238-
239- return entries . map ( ( [ year , value ] , index ) => {
240- const y = Number ( year )
241- const totalDays = daysInYear ( y )
242-
243- // Scale up partial first bucket
244- if ( index === 0 && rangeStartIso ) {
245- const rangeStart = parseIsoDateOnly ( rangeStartIso )
246- const yearStart = parseIsoDateOnly ( `${ year } -01-01` )
247- const dayOfYear = Math . floor ( ( rangeStart . getTime ( ) - yearStart . getTime ( ) ) / 86400000 )
248- if ( dayOfYear > 0 ) {
249- value = fillPartialBucket ( value , totalDays - dayOfYear , totalDays )
250- }
251- }
252-
253- // Scale up partial last bucket
254- if ( index === entries . length - 1 && rangeEndIso ) {
255- const rangeEnd = parseIsoDateOnly ( rangeEndIso )
256- const yearStart = parseIsoDateOnly ( `${ year } -01-01` )
257- const actualDays = Math . floor ( ( rangeEnd . getTime ( ) - yearStart . getTime ( ) ) / 86400000 ) + 1
258- if ( actualDays < totalDays ) {
259- value = fillPartialBucket ( value , actualDays , totalDays )
260- }
261- }
262-
263- const yearStartDate = parseIsoDateOnly ( `${ year } -01-01` )
264- const timestamp = yearStartDate . getTime ( )
265- return { year, value, timestamp }
266- } )
267- }
268-
26998const npmDailyRangeCache = import . meta. client ? new Map < string , Promise < DailyRawPoint [ ] > > ( ) : null
27099const likesEvolutionCache = import . meta. client ? new Map < string , Promise < DailyRawPoint [ ] > > ( ) : null
271100const contributorsEvolutionCache = import . meta. client
@@ -552,12 +381,12 @@ export function useCharts() {
552381
553382 const sortedDaily = await fetchDailyRangeChunked ( resolvedPackageName , startIso , endIso )
554383
555- if ( resolvedOptions . granularity === 'day' ) return buildDailyEvolutionFromDaily ( sortedDaily )
384+ if ( resolvedOptions . granularity === 'day' ) return buildDailyEvolution ( sortedDaily )
556385 if ( resolvedOptions . granularity === 'week' )
557- return buildRollingWeeklyEvolutionFromDaily ( sortedDaily , startIso , endIso )
386+ return buildWeeklyEvolution ( sortedDaily , startIso , endIso )
558387 if ( resolvedOptions . granularity === 'month' )
559- return buildMonthlyEvolutionFromDaily ( sortedDaily , startIso , endIso )
560- return buildYearlyEvolutionFromDaily ( sortedDaily , startIso , endIso )
388+ return buildMonthlyEvolution ( sortedDaily , startIso , endIso )
389+ return buildYearlyEvolution ( sortedDaily , startIso , endIso )
561390 }
562391
563392 async function fetchPackageLikesEvolution (
@@ -596,12 +425,12 @@ export function useCharts() {
596425
597426 const filteredDaily = sortedDaily . filter ( d => d . day >= startIso && d . day <= endIso )
598427
599- if ( resolvedOptions . granularity === 'day' ) return buildDailyEvolutionFromDaily ( filteredDaily )
428+ if ( resolvedOptions . granularity === 'day' ) return buildDailyEvolution ( filteredDaily )
600429 if ( resolvedOptions . granularity === 'week' )
601- return buildRollingWeeklyEvolutionFromDaily ( filteredDaily , startIso , endIso )
430+ return buildWeeklyEvolution ( filteredDaily , startIso , endIso )
602431 if ( resolvedOptions . granularity === 'month' )
603- return buildMonthlyEvolutionFromDaily ( filteredDaily , startIso , endIso )
604- return buildYearlyEvolutionFromDaily ( filteredDaily , startIso , endIso )
432+ return buildMonthlyEvolution ( filteredDaily , startIso , endIso )
433+ return buildYearlyEvolution ( filteredDaily , startIso , endIso )
605434 }
606435
607436 async function fetchRepoContributorsEvolution (
0 commit comments