Skip to content

Comments

feat: add charts to compare page#846

Merged
danielroe merged 32 commits intonpmx-dev:mainfrom
graphieros:main
Feb 4, 2026
Merged

feat: add charts to compare page#846
danielroe merged 32 commits intonpmx-dev:mainfrom
graphieros:main

Conversation

@graphieros
Copy link
Contributor

@graphieros graphieros commented Feb 3, 2026

  • add composable to extend listed packages with defined colours
  • support multiple series in large downloads chart
  • add comparative chart in the compare page
image

closes #803

  • bump vue-data-ui to 3.14.5 (fixing the overflow of the built-in annotator pixel value label)

@vercel
Copy link

vercel bot commented Feb 3, 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 4, 2026 11:18am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 4, 2026 11:18am
npmx-lunaria Ignored Ignored Feb 4, 2026 11:18am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Adds a new Vue component Compare/LineChart.vue that renders DownloadAnalytics for selected packages. Extends Package/DownloadAnalytics.vue to support multi-package mode (per-package datasets, legend, custom tooltips, coordinated loading, date-range handling and dynamic export filenames) while preserving single-package behaviour. Inserts the line chart into the compare page. Adds app/utils/frameworks.ts (SHOWCASED_FRAMEWORKS, getFrameworkColor, isListedFramework) and updates index.vue to use it. Adds tests for frameworks and accessibility tests for the new chart. Bumps vue-data-ui from 3.14.3 to 3.14.5.

Suggested reviewers

  • danielroe
  • serhalp
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset: adding a composable for colours, supporting multiple series in downloads chart, adding comparative chart to compare page, and bumping vue-data-ui version.
Linked Issues check ✅ Passed All core requirements from issue #803 are met: line charts added to package comparator, existing chart component reused and optimised for multi-package support, same filters as modal chart implemented, and one-year default history supported.
Out of Scope Changes check ✅ Passed All changes are scoped to the objectives: new LineChart component, DownloadAnalytics multi-package support, compare page integration, frameworks utility module, tests, and dependency bump are all directly related to the chart comparison feature.

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

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

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/Package/DownloadAnalytics.vue (1)

634-639: ⚠️ Potential issue | 🟡 Minor

X‑axis label is suppressed even for a single package passed via packageNames.
props.packageNames is truthy for empty arrays and single‑item arrays, so the x‑axis label is hidden even when it should show. Consider gating on multi‑package and length > 1.

🔧 Suggested fix
-            xLabel: props.packageNames ? '' : xAxisLabel.value, // for multiple series, names are displayed in the chart's legend
+            xLabel:
+              isMultiPackageMode.value && effectivePackageNames.value.length > 1
+                ? ''
+                : xAxisLabel.value, // for multiple series, names are displayed in the chart's legend
🧹 Nitpick comments (1)
app/composables/useFrameworks.ts (1)

3-20: Tighten framework typing and avoid the unsafe non‑null assertion.
FrameworkName currently widens to string, so isListedFramework doesn’t narrow and getFrameworkColor can still throw. Consider freezing the list with as const and returning a nullable colour (or a safe fallback) instead of !.

♻️ Suggested adjustment
-  const frameworks = ref([
+  const frameworks = ref([
     { name: 'nuxt', package: 'nuxt', color: 'oklch(0.7862 0.192 155.63)' },
     { name: 'vue', package: 'vue', color: 'oklch(0.7025 0.132 160.37)' },
     { name: 'nitro', package: 'nitro', color: 'oklch(70.4% 0.191 22.216)' },
     { name: 'react', package: 'react', color: 'oklch(0.832 0.1167 218.69)' },
     { name: 'svelte', package: 'svelte', color: 'oklch(0.6917 0.1865 35.04)' },
     { name: 'vite', package: 'vite', color: 'oklch(0.7484 0.1439 294.03)' },
     { name: 'next', package: 'next', color: 'oklch(71.7% .1648 250.794)' },
     { name: 'astro', package: 'astro', color: 'oklch(0.5295 0.2434 270.23)' },
     { name: 'typescript', package: 'typescript', color: 'oklch(0.5671 0.1399 253.3)' },
     { name: 'angular', package: '@angular/core', color: 'oklch(0.626 0.2663 310.4)' },
-  ])
+  ] as const)
 
-  function getFrameworkColor(framework: FrameworkName): string {
-    return frameworks.value.find(f => f.name === framework)!.color
-  }
+  function getFrameworkColor(framework: FrameworkName): string | undefined {
+    return frameworks.value.find(f => f.name === framework)?.color
+  }

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.

Comment on lines 157 to 165
<h2
id="comparison-heading"
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
>
downloads history
</h2>

<CompareLineChart :packages />
</div>
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid duplicate heading IDs and localise the new title.
Line 113 already uses comparison-heading, so reusing it here breaks aria-labelledby and can confuse assistive tech. The new heading text is also hard‑coded. Consider giving the downloads history block its own section + i18n key.

🔧 Suggested fix
-          <h2
-            id="comparison-heading"
-            class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
-          >
-            downloads history
-          </h2>
-
-          <CompareLineChart :packages />
+          <section class="mt-10" aria-labelledby="downloads-history-heading">
+            <h2
+              id="downloads-history-heading"
+              class="text-xs text-fg-subtle uppercase tracking-wider mb-4"
+            >
+              {{ $t('compare.packages.section_downloads_history') }}
+            </h2>
+            <CompareLineChart :packages="packages" />
+          </section>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h2
id="comparison-heading"
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
>
downloads history
</h2>
<CompareLineChart :packages />
</div>
<section class="mt-10" aria-labelledby="downloads-history-heading">
<h2
id="downloads-history-heading"
class="text-xs text-fg-subtle uppercase tracking-wider mb-4"
>
{{ $t('compare.packages.section_downloads_history') }}
</h2>
<CompareLineChart :packages="packages" />
</section>
</div>

Copy link
Contributor

Choose a reason for hiding this comment

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

Good bot

Copy link
Contributor

Choose a reason for hiding this comment

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

@wojtekmaj Thank you! 🙏


🧠 Learnings used
Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 838
File: app/pages/package/[...package].vue:445-449
Timestamp: 2026-02-03T13:59:33.392Z
Learning: The copy button pattern in app/pages/package/[...package].vue may be made into a reusable component or pattern in the future, but currently it's acceptable to keep it inline with the CSS-only approach for smooth animations.

@graphieros graphieros marked this pull request as draft February 3, 2026 14:10
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 (2)
app/components/Package/DownloadAnalytics.vue (2)

27-27: Consider removing or documenting the always-true computed.

shouldFetch is a computed that always returns true. If this is a placeholder for future conditional logic, consider adding a brief comment explaining intent. Otherwise, the indirection adds no value and the usages could be simplified.


454-455: Avoid as any cast; define a proper type for options.

Casting options.value to any bypasses TypeScript's type checking for startDate and endDate properties. Consider extending the options type union to include these optional fields explicitly.

🔧 Suggested improvement

The options type already includes DateRangeFields via applyDateRange, but accessing those fields requires a type guard or a widened base type. One approach:

-    const o = options.value
-    const hasExplicitRange = Boolean((o as any).startDate || (o as any).endDate)
+    const o = options.value as typeof options.value & DateRangeFields
+    const hasExplicitRange = Boolean(o.startDate || o.endDate)

Alternatively, define a common base type that includes the optional date fields.

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 (1)
app/components/Package/DownloadAnalytics.vue (1)

454-456: Avoid any casts on options to keep type‑safety.
All union variants already share startDate/endDate, so the casts can be removed.

🔧 Suggested refactor
-    const o = options.value
-    const hasExplicitRange = Boolean((o as any).startDate || (o as any).endDate)
+    const o = options.value
+    const hasExplicitRange = Boolean(o.startDate || o.endDate)
-  const o = options.value as any
+  const o = options.value

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: 480-493

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: 3

Comment on lines +789 to 791
:disabled="pending"
class="w-full px-2.5 py-1.75 bg-bg-subtle font-mono text-sm text-fg outline-none appearance-none focus-visible:outline-accent/70"
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove inline focus-visible utilities on select/button.

Project guidance applies focus-visible styling globally for buttons and selects; these per‑element utilities should be removed for consistency.

Proposed diff
-              class="w-full px-2.5 py-1.75 bg-bg-subtle font-mono text-sm text-fg outline-none appearance-none focus-visible:outline-accent/70"
+              class="w-full px-2.5 py-1.75 bg-bg-subtle font-mono text-sm text-fg outline-none appearance-none"
-          class="self-end flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-accent/70 sm:mb-0"
+          class="self-end flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border sm:mb-0"

Based on learnings, focus-visible styling for buttons/selects is global via app/assets/main.css and inline utilities should not be added.

Also applies to: 844-849

Comment on lines +59 to +74
// VueUiXy is imported directly in <script setup>, so global stubs cannot override it.
// We mock the module itself to prevent vue-data-ui from mounting charts during tests
// (it relies on DOM measurements and causes runtime errors in Vitest / Playwright).
// This render-function stub avoids the Vue runtime-compiler warning and keeps slots working.
vi.mock('vue-data-ui/vue-ui-xy', () => {
return {
VueUiXy: defineComponent({
name: 'VueUiXy',
inheritAttrs: false,
setup(_, { attrs, slots }) {
return () =>
h('div', { ...attrs, 'data-test-id': 'vue-ui-xy-stub' }, slots.default?.() ?? [])
},
}),
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the current imports in the test file
cat -n test/nuxt/a11y.spec.ts | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1517


🏁 Script executed:

# Also check if there's a nuxt config file that might enable auto-imports
fd -e config.ts -e config.js | grep -E "(nuxt|vite)" | head -10

Repository: npmx-dev/npmx.dev

Length of output: 112


🏁 Script executed:

# Check for any auto-import configuration in the repository
rg -t ts -t js "auto.?import|unplugin" --max-count 5

Repository: npmx-dev/npmx.dev

Length of output: 290


🏁 Script executed:

# Check nuxt.config.ts for auto-import configuration
cat -n nuxt.config.ts | head -80

Repository: npmx-dev/npmx.dev

Length of output: 2617


🏁 Script executed:

# Check vitest.config.ts for test environment setup
cat vitest.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 2292


🏁 Script executed:

# Search for other uses of defineComponent or h in test files
rg "defineComponent|^import.*from\s+['\"]vue['\"]" test/nuxt/ --type ts -B 2 -A 2

Repository: npmx-dev/npmx.dev

Length of output: 3697


🏁 Script executed:

# Check if there are any runtime errors or issues mentioned in the test setup
cat -n test/nuxt/setup.ts

Repository: npmx-dev/npmx.dev

Length of output: 1101


Add explicit imports for defineComponent and h from Vue.

The Nuxt test environment provides auto-imports, but other test files in this codebase (e.g. use-colors.spec.ts) explicitly import Vue utilities. This makes the mock self-contained and improves type safety consistency.

Proposed diff
import { afterEach, describe, expect, it, vi } from 'vitest'
+import { defineComponent, h } from 'vue'

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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 (1)
app/utils/frameworks.ts (1)

58-60: Non-null assertion bypasses type safety.

The ! operator assumes .find() always succeeds. If FrameworkPackageName isn't properly constrained (see above), any string could be passed, causing a runtime error when accessing .color on undefined.

Consider adding a fallback or explicit error handling for robustness.

🛡️ Proposed defensive handling
 export function getFrameworkColor(framework: FrameworkPackageName): string {
-  return SHOWCASED_FRAMEWORKS.find(f => f.package === framework)!.color
+  const found = SHOWCASED_FRAMEWORKS.find(f => f.package === framework)
+  if (!found) {
+    throw new Error(`Unknown framework: ${framework}`)
+  }
+  return found.color
 }

Alternatively, with proper as const typing, the ! becomes acceptable since only valid frameworks can be passed.

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".

Comment on lines +7 to +56
export const SHOWCASED_FRAMEWORKS = [
{
name: 'nuxt',
package: 'nuxt',
color: 'oklch(0.7862 0.192 155.63)',
},
{ name: 'vue', package: 'vue', color: 'oklch(0.7025 0.132 160.37)' },
{
name: 'nitro',
package: 'nitro',
color: 'oklch(70.4% 0.191 22.216)',
},
{
name: 'react',
package: 'react',
color: 'oklch(0.832 0.1167 218.69)',
},
{
name: 'svelte',
package: 'svelte',
color: 'oklch(0.6917 0.1865 35.04)',
},
{
name: 'vite',
package: 'vite',
color: 'oklch(0.7484 0.1439 294.03)',
},
{
name: 'next',
package: 'next',
color: 'oklch(71.7% .1648 250.794)',
},
{
name: 'astro',
package: 'astro',
color: 'oklch(0.5295 0.2434 270.23)',
},
{
name: 'typescript',
package: 'typescript',
color: 'oklch(0.5671 0.1399 253.3)',
},
{
name: 'angular',
package: '@angular/core',
color: 'oklch(0.626 0.2663 310.4)',
},
]

export type FrameworkPackageName = (typeof SHOWCASED_FRAMEWORKS)[number]['package']
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add as const to derive literal union types for FrameworkPackageName.

Without as const, TypeScript widens the array element types, causing FrameworkPackageName to resolve to string rather than the intended union type ('nuxt' | 'vue' | 'react' | ...). This undermines the type safety of getFrameworkColor.

🔧 Proposed fix
-export const SHOWCASED_FRAMEWORKS = [
+export const SHOWCASED_FRAMEWORKS = [
   {
     name: 'nuxt',
     package: 'nuxt',
     color: 'oklch(0.7862 0.192 155.63)',
   },
   // ... rest of frameworks
   {
     name: 'angular',
     package: '@angular/core',
     color: 'oklch(0.626 0.2663 310.4)',
   },
-]
+] as const satisfies readonly ShowcasedFramework[]

As per coding guidelines: "Ensure you write strictly type-safe code".

} from '../../../../app/utils/frameworks'

describe('getFrameworkColor', () => {
it('returns the color a listed framework', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor typo in test description.

Missing "of" in the test description.

📝 Proposed fix
-  it('returns the color a listed framework', () => {
+  it('returns the color of a listed framework', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('returns the color a listed framework', () => {
it('returns the color of a listed framework', () => {

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.

Add line chart to package comparator

3 participants