Skip to content

feat: fix known download anomalies with interpolation#1636

Merged
graphieros merged 19 commits intonpmx-dev:mainfrom
jycouet:feat/clean-up-graphs
Feb 27, 2026
Merged

feat: fix known download anomalies with interpolation#1636
graphieros merged 19 commits intonpmx-dev:mainfrom
jycouet:feat/clean-up-graphs

Conversation

@jycouet
Copy link
Contributor

@jycouet jycouet commented Feb 25, 2026

🔗 Linked issue

Closes #1241

🧭 Context

Having nicer charts in general without data going off!

Right now, this PR is the Option 4 with these defaults:

chartFilter: {
  averageWindow: 0,
  smoothingTau: 1,
  anomaliesFixed: true,
},

FYI, I split commits to be able to go back to other options, if you want to try on some packages :)

📚 Description

1/ Hampel

2026.02.24.-.Hampel.mp4

Was good, be it's a lot of variables... And it's hard to get all situations correct

2/ Outlier Sensitivity

2026.02.24.-.Outlier.Sensitivity.mp4

Same thing... I tried with some linear reduction & some tweaks to keep first an last point. Good for some... not the bast for others...

3/ 2 Passes Avg or Smoothing

2026.02.24.-.2.Passes.avg.or.Smoothing.mp4

Std Average / Smoothing are not so good because they change the start or end. But here in the 2 passes process we minimise this effect.

4/ 2 Passes Avg or Smoothing & Manual tuning

2026.02.24.-.Manu.mp4

This is actually the best! Manually declaring where it's wrong. In the vite example it's obvious, so we can just add it. And the community is soooo into it that we could encourage people to enter things there (we could even offer an API at some point).

Keeping the option for Avg & Smooth to have a good mix.


What do you think?!

@vercel
Copy link

vercel bot commented Feb 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 27, 2026 9:41am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 27, 2026 9:41am
npmx-lunaria Ignored Ignored Feb 27, 2026 9:41am

Request Review

@codecov
Copy link

codecov bot commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 40.66667% with 89 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/utils/download-anomalies.ts 6.89% 44 Missing and 10 partials ⚠️
app/utils/chart-data-correction.ts 57.14% 18 Missing ⚠️
app/components/Package/TrendsChart.vue 61.90% 9 Missing and 7 partials ⚠️
app/components/Package/WeeklyDownloadStats.vue 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request introduces infrastructure for cleaning npm download data displayed in charts. New utilities detect and correct known download anomalies through linear interpolation, and apply data smoothing via moving averages and exponential filtering. The TrendsChart and WeeklyDownloadStats components integrate blocklist-based and general corrections into their data pipelines. Settings are extended to include correction parameters (averageWindow, smoothingTau, anomaliesFixed), and corresponding UI controls are added to the downloads metric view. Translations support the new interface elements across localisation files.

Suggested reviewers

  • graphieros
  • serhalp
  • danielroe
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description relates to the changeset by explaining the data correction approach implemented, providing context about alternative methods tested, and aligning with the objective to improve chart rendering.
Linked Issues check ✅ Passed The PR implements the core objectives from issue #1241: detecting and correcting download anomalies through interpolation, supporting both automatic smoothing/averaging and manual anomaly declarations, with functionality for extrapolating across anomalies.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked objective: new chart filtering utilities, anomaly detection/correction logic, UI controls for data correction, translation strings for correction labels, and settings integration for filter configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
app/utils/chart-filters.ts (1)

9-10: No-op paths return the original array reference — document the contract.

Both movingAverage and smoothing return the input data array unchanged when the guard condition fires. This means any caller that mutates the returned array will also mutate the original. Current callers in the pipeline are safe, but this is a non-obvious contract that could cause subtle bugs if new callers are added without reading the source.

♻️ Suggested: add a clarifying note to the JSDoc
 /**
  * Bidirectional moving average. …
  * First and last points are preserved.
  *
  * `@param` halfWindow - number of points on each side (0 = disabled)
+ * `@returns` A new array when filtering is applied; the original `data` reference when the guard fires (no-op).
  */
 export function movingAverage<T extends { value: number }>(data: T[], halfWindow: number): T[] {
 /**
  * Forward-backward exponential smoothing (zero-phase). …
  * First and last points are preserved.
  *
  * `@param` tau - time constant (0 = disabled, higher = smoother)
+ * `@returns` A new array when filtering is applied; the original `data` reference when the guard fires (no-op).
  */
 export function smoothing<T extends { value: number }>(data: T[], tau: number): T[] {
app/utils/download-anomalies.ts (2)

15-26: Record<string, any> silently hides missing-field mismatches.

getDateString accesses point.day, point.weekStart, point.month, and point.year without any guard. If a data point is missing the expected field, the function returns undefined coerced to "undefined", which silently produces a no-match rather than a loud error. A discriminated-union parameter type (or at minimum an intersection with { value: number }) would surface the issue at the call site.

♻️ Suggested: narrow the parameter type
-function getDateString(point: Record<string, any>, granularity: ChartTimeGranularity): string {
+function getDateString(
+  point: Record<string, unknown>,
+  granularity: ChartTimeGranularity,
+): string {
   switch (granularity) {
     case 'daily':
-      return point.day
+      return String(point.day ?? '')
     case 'weekly':
-      return point.weekStart
+      return String(point.weekStart ?? '')
     case 'monthly':
-      return `${point.month}-01`
+      return `${String(point.month ?? '')}-01`
     case 'yearly':
-      return `${point.year}-01-01`
+      return `${String(point.year ?? '')}-01-01`
   }
 }

55-66: Monthly/yearly scaling uses calendar approximations — worth documenting.

Math.round((weeklyValue / 7) * 30) treats every month as exactly 30 days and * 365 treats every year as 365 days (ignoring leap years). For anomaly correction purposes these approximations are reasonable, but a brief inline comment would make the intent clear to future contributors who might otherwise reach for a calendar-aware alternative.

♻️ Suggested: add clarifying comments
   case 'monthly':
-    return Math.round((weeklyValue / 7) * 30)
+    return Math.round((weeklyValue / 7) * 30) // approximate: treats all months as 30 days
   case 'yearly':
-    return Math.round((weeklyValue / 7) * 365)
+    return Math.round((weeklyValue / 7) * 365) // approximate: treats all years as 365 days
app/components/Package/TrendsChart.vue (2)

1614-1615: isDownloadsMetric is declared 670+ lines after its first use — consider hoisting.

isDownloadsMetric is referenced inside the effectiveDataSingle (line 946) and chartData (line 990) computed properties, but is not declared until line 1614. Vue's lazy computed evaluation means this works at runtime (no TDZ crash), but the declaration order makes the dependency chain very hard to follow and is contrary to the convention in the rest of the file where helpers are declared before their consumers.

♻️ Suggested: hoist the declaration above `effectiveDataSingle`
+const isDownloadsMetric = computed(() => selectedMetric.value === 'downloads')
+const showFilterControls = shallowRef(false)
+
 const effectiveDataSingle = computed<EvolutionData>(() => {

…and remove the duplicate declarations at lines 1614–1615.


946-955: Download-filter logic is duplicated between effectiveDataSingle and chartData — extract a helper.

The anomaliesFixedapplyBlocklistFilterapplyDownloadFilter chain appears verbatim in both computed properties. Any future change to the pipeline (e.g. adding a new filter step) must be applied in both places.

♻️ Suggested: extract a shared helper
+function applyDownloadsFilters(
+  data: EvolutionData,
+  pkg: string,
+  granularity: ChartTimeGranularity,
+): EvolutionData {
+  let result = data
+  if (settings.value.chartFilter.anomaliesFixed) {
+    result = applyBlocklistFilter(result, pkg, granularity)
+  }
+  return applyDownloadFilter(
+    result as Array<{ value: number }>,
+    settings.value.chartFilter,
+  ) as EvolutionData
+}

Then in effectiveDataSingle:

-  if (isDownloadsMetric.value && data.length) {
-    const pkg = effectivePackageNames.value[0] ?? props.packageName ?? ''
-    if (settings.value.chartFilter.anomaliesFixed) {
-      data = applyBlocklistFilter(data, pkg, displayedGranularity.value)
-    }
-    return applyDownloadFilter(
-      data as Array<{ value: number }>,
-      settings.value.chartFilter,
-    ) as EvolutionData
-  }
+  if (isDownloadsMetric.value && data.length) {
+    const pkg = effectivePackageNames.value[0] ?? props.packageName ?? ''
+    return applyDownloadsFilters(data, pkg, displayedGranularity.value)
+  }

And in chartData:

-    if (isDownloadsMetric.value && data.length) {
-      if (settings.value.chartFilter.anomaliesFixed) {
-        data = applyBlocklistFilter(data, pkg, granularity)
-      }
-      data = applyDownloadFilter(
-        data as Array<{ value: number }>,
-        settings.value.chartFilter,
-      ) as EvolutionData
-    }
+    if (isDownloadsMetric.value && data.length) {
+      data = applyDownloadsFilters(data, pkg, granularity)
+    }

Also applies to: 990-999


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7c2e9 and 88b2745.

📒 Files selected for processing (6)
  • app/components/Package/TrendsChart.vue
  • app/components/Package/WeeklyDownloadStats.vue
  • app/composables/useSettings.ts
  • app/utils/chart-filters.ts
  • app/utils/download-anomalies.data.ts
  • app/utils/download-anomalies.ts

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@graphieros
Copy link
Contributor

graphieros commented Feb 25, 2026

@jycouet

This looks awesome :)
I do like the last option a lot.

A few remarks before I take a look at the code, regarding the labels for the range inputs.
I think the labels should either be more explicit about what they do:

  • Summary label: 'Filters' is probably too broad. Maybe something more precise like "Data correction controls" could be more explicit ?
  • Same for the labels of the range inputs, "Average window" perhaps ? "Smoothing" is fine, maybe something like "Noise reduction" ?

Also, perhaps an info icon with a tooltip could explain what the checkbox does (and only showing the checkbox when the 'database' contains corrections ?)

Finally, I could not test on a newly created package that has only one download datapoint.
Edit: did test, works fine :)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/components/Package/TrendsChart.vue (2)

1720-1734: Add ARIA attributes for accessible collapsible section.

The toggle button should include aria-expanded and aria-controls attributes for screen reader users to understand the collapsible state and relationship.

♻️ Suggested accessibility improvement
       <button
         type="button"
         class="self-start flex items-center gap-1 text-2xs font-mono text-fg-subtle hover:text-fg transition-colors"
+        :aria-expanded="showFilterControls"
+        aria-controls="download-filter-controls"
         `@click`="showFilterControls = !showFilterControls"
       >
         <span
           class="w-3.5 h-3.5 transition-transform"
           :class="showFilterControls ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
           aria-hidden="true"
         />
         {{ $t('package.trends.filters') }}
       </button>
-      <div v-if="showFilterControls" class="flex items-end gap-3">
+      <div v-if="showFilterControls" id="download-filter-controls" class="flex items-end gap-3">

1763-1772: Consider UX feedback from PR discussion.

Per the PR comments, graphieros suggested adding an info icon with a tooltip explaining the "Anomalies fixed" checkbox behaviour, and potentially showing this checkbox only when the database contains corrections for the current package. This would improve clarity for users unfamiliar with the feature.


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5faf4c and 5fa129e.

📒 Files selected for processing (6)
  • app/components/Package/TrendsChart.vue
  • app/components/Package/WeeklyDownloadStats.vue
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/components/Package/WeeklyDownloadStats.vue
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
  • i18n/locales/en.json

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lunaria/files/en-GB.json (1)

381-384: Consider more descriptive labels per PR feedback.

The translations are functional, but the PR comments from graphieros suggest more explicit labels that could improve user understanding:

Current Suggested
"Filters" "Data correction controls"
"Average" "Average window"
"Smoothing" "Noise reduction"

These suggestions aim to make the controls' purpose clearer to users unfamiliar with the chart filtering feature. Consider whether adopting some or all of these would benefit the UX.


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa129e and f597318.

📒 Files selected for processing (4)
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • lunaria/files/en-US.json
  • i18n/locales/en.json

Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks amazing!

@graphieros I'll let you press the merge button when you're happy 🙏

@jycouet
Copy link
Contributor Author

jycouet commented Feb 26, 2026

I'll try to address comments tomorrow morning 🤞

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/utils/download-anomalies.ts (1)

15-26: Avoid any in the anomaly correction path.

Record<string, any> and broad casts here bypass type safety exactly where granularity-specific date fields are resolved.

Typed alternative (outline)
-import type { ChartTimeGranularity, EvolutionData } from '~/types/chart'
+import type {
+  ChartTimeGranularity,
+  DailyDataPoint,
+  EvolutionData,
+  MonthlyDataPoint,
+  WeeklyDataPoint,
+  YearlyDataPoint,
+} from '~/types/chart'
+
+type EvolutionPoint = DailyDataPoint | WeeklyDataPoint | MonthlyDataPoint | YearlyDataPoint

-function getDateString(point: Record<string, any>, granularity: ChartTimeGranularity): string {
+function getDateString(point: EvolutionPoint, granularity: ChartTimeGranularity): string {
   switch (granularity) {
     case 'daily':
-      return point.day
+      return 'day' in point ? point.day : ''
     case 'weekly':
-      return point.weekStart
+      return 'weekStart' in point ? point.weekStart : ''
     case 'monthly':
-      return `${point.month}-01`
+      return 'month' in point ? `${point.month}-01` : ''
     case 'yearly':
-      return `${point.year}-01-01`
+      return 'year' in point ? `${point.year}-01-01` : ''
   }
 }

As per coding guidelines, "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

Also applies to: 88-89

app/utils/chart-data-correction.ts (1)

9-53: Add finite-value guards to chart filter parameters.

The movingAverage and smoothing functions lack validation for non-finite inputs (NaN, Infinity). If halfWindow or tau become non-finite via corrupted localStorage or an upstream bug, they propagate NaN through the entire corrected series, silently corrupting chart output. The default values are safe, but there is no validation in the settings chain to prevent this edge case.

Suggested hardening
 export function movingAverage<T extends { value: number }>(data: T[], halfWindow: number): T[] {
-  if (halfWindow <= 0 || data.length < 3) return data
+  const safeHalfWindow = Number.isFinite(halfWindow) ? Math.max(0, Math.floor(halfWindow)) : 0
+  if (safeHalfWindow <= 0 || data.length < 3) return data

   const n = data.length
@@
   for (let i = 0; i < n; i++) {
-    const lo = Math.max(0, i - halfWindow)
+    const lo = Math.max(0, i - safeHalfWindow)
@@
   for (let i = 0; i < n; i++) {
-    const hi = Math.min(n - 1, i + halfWindow)
+    const hi = Math.min(n - 1, i + safeHalfWindow)
 export function smoothing<T extends { value: number }>(data: T[], tau: number): T[] {
-  if (tau <= 0 || data.length < 3) return data
+  const safeTau = Number.isFinite(tau) ? Math.max(0, tau) : 0
+  if (safeTau <= 0 || data.length < 3) return data

-  const alpha = 1 / (1 + tau)
+  const alpha = 1 / (1 + safeTau)

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f597318 and 9f5aa2e.

📒 Files selected for processing (8)
  • app/components/Package/TrendsChart.vue
  • app/components/Package/WeeklyDownloadStats.vue
  • app/utils/chart-data-correction.ts
  • app/utils/download-anomalies.ts
  • i18n/locales/en.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • lunaria/files/en-US.json

Copy link
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome to me !
You can probably address the coderabbit critical notification.
One last detail, regarding the tooltip content when the chart is used for multiple series in the compare page, the translation could use plural management.

@jycouet
Copy link
Contributor Author

jycouet commented Feb 27, 2026

Looks awesome to me ! You can probably address the coderabbit critical notification. One last detail, regarding the tooltip content when the chart is used for multiple series in the compare page, the translation could use plural management.

haha, I didn't know this chart!
Thx for letting me know, I'm on it now.

I'll look at the rabbit as well. (I have to say that this is a bit overwhelming)

Copy link
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect!

@graphieros graphieros added this pull request to the merge queue Feb 27, 2026
Merged via the queue into npmx-dev:main with commit 790caf9 Feb 27, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

clean up npm downloads data for graphs

3 participants