Skip to content

feat: endpoint to embed downloads chart static svg#2833

Open
graphieros wants to merge 24 commits into
mainfrom
2811-endpoint-to-embed-downloads-chart
Open

feat: endpoint to embed downloads chart static svg#2833
graphieros wants to merge 24 commits into
mainfrom
2811-endpoint-to-embed-downloads-chart

Conversation

@graphieros
Copy link
Copy Markdown
Contributor

@graphieros graphieros commented Jun 1, 2026

Resolves #2811 for the embedding aspect of the issue.

This creates an endpoint that generates a static svg for download charts, from:

  • chart modal
  • compare page
page closed open
chart modal image image
compare image image
  • dark or light theme can be toggled
  • URL can be copied from the input
  • small preview image using the same URL

Important note regarding the data:

  • data corrections are ignored, because embedded static charts must show the raw data.
  • the local zoom state (using the chart's minimap) is ignored. Only the date filters are applied.
  • the embed only works for downloads for now.

Refactoring:

  • common logic used both in the embed & the client chart is refactored into shared utils

Rendering:

  • the rendered SVG will appear with the default font of the browser. Consumers can add styles to a wrapper, the chart will inherit the font-family.

Other:

  • vue-data-ui is updated to latest, with support for static svg generation for the VueUiXy component (release notes).
  • VueUiXy is the only component with this feature for now. Should embeds be created for other chart components used on npmx, the library would need to be updated. I'm planning on gradually adding this feature to all mainstream chart types.

@graphieros graphieros linked an issue Jun 1, 2026 that may be closed by this pull request
3 tasks
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jun 1, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Jun 1, 2026 2:11pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Jun 1, 2026 2:11pm
npmx-lunaria Ignored Ignored Jun 1, 2026 2:11pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

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

Adds a cached server SVG endpoint for downloads trends, extracts shared trends-chart builders and download-range utilities, refactors the client TrendsChart to use shared builders (adds embed UI), introduces date/i18n updates and a dependency bump, and adds comprehensive unit tests.

Changes

Embed downloads chart endpoint

Layer / File(s) Summary
Shared trends chart builders
shared/utils/trends-chart.ts
Runtime type guards, buildTrendsChartData, buildNormalisedTrendsDataset, getTrendsDatetimeFormatterOptions, buildTrendsChartConfig, and SVG helpers (drawTrendsEstimationLine, drawTrendsLastDatapointLabel, drawTrendsSvgPrintLegend, generateWatermarkLogo).
Download utilities & server evolution
shared/utils/download-ranges.ts, server/utils/download-evolution.ts
Inclusive UTC day-difference, inclusive ISO-range chunking, daily-point merging, and fetchDownloadsEvolution resolving effective ranges, chunking large ranges (<=540d), concurrent fetches, normalisation, and granularity dispatch.
Embed colour resolution
shared/utils/embed-chart-colors.ts
Adds EmbedTheme and resolveEmbedChartColors(theme) returning light/dark embed palettes.
Embed SVG endpoint
server/api/embed/downloads.svg/index.get.ts
Cached GET endpoint parsing/validating query params (packages, granularity, metric limited to downloads, theme, dimensions, locale, accent, y-label), fetching evolutions, building dataset/config via shared builders, dash-index adjustments for incomplete months, injecting value labels and watermark, and returning image/svg+xml with caching headers and SWR.
TrendsChart component refactor & embed UI
app/components/Package/TrendsChart.vue
Uses shared builders for datasets/config/formatters, removes local guards/formatters, adds defensive checks and maxDatapoints, simplifies CSV export, updates altCopy and tooltip customFormat, resets zoom on metric pending transitions, and adds an embed panel (embedQuery, embedUrl, copy and preview).
Supporting refactors
app/composables/useChartWatermark.ts, app/composables/useCharts.ts, app/utils/date.ts, app/utils/charts.ts, i18n/*, package.json
Imports generateWatermarkLogo from shared trends-chart; moves chunking/merge helpers to shared/utils/download-ranges; adds getEffectiveEndDateIso, isLastDayOfMonth, isLastDayOfYear; adds client-only guard for file downloads; adds embedding translations and schema; bumps vue-data-ui to 3.21.0.
Comprehensive tests
test/unit/shared/utils/*, test/unit/server/*, test/unit/app/utils/date.spec.ts
Adds unit tests for trends-chart utilities, download-range helpers, server download-evolution, embed endpoint parsing/edge cases, and new date helpers.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Embed as /embed/downloads.svg
    participant Evolution as fetchDownloadsEvolution
    participant ChartData as buildTrendsChartData
    participant ChartConfig as buildTrendsChartConfig
    participant Render as createStaticVueUiXy

    Client->>Embed: GET ?packages=tinyglobby&granularity=week&dark=true
    Embed->>Embed: Parse & validate query params
    Embed->>Evolution: fetchDownloadsEvolution(packages, options)
    Evolution->>Evolution: Resolve date range per granularity
    Evolution->>Evolution: Fetch NPM downloads (chunked if needed)
    Evolution-->>Embed: EvolutionData[]
    Embed->>ChartData: buildTrendsChartData(evolution, options)
    ChartData-->>Embed: VueUiXyDatasetItem[] + dates
    Embed->>ChartConfig: buildTrendsChartConfig(dataset, options)
    ChartConfig-->>Embed: VueUiXyConfig
    Embed->>Render: createStaticVueUiXy(config, data)
    Render-->>Embed: SVG string
    Embed->>Embed: Add value labels + watermark
    Embed-->>Client: SVG (image/svg+xml, cached)
Loading

Possibly related PRs

Suggested reviewers

  • gameroman
  • userquin
  • MatteoGabriele
  • serhalp
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: endpoint to embed downloads chart static svg' clearly and specifically describes the main change: implementing an endpoint that generates static SVG for embedding downloads charts.
Linked Issues check ✅ Passed The PR fully addresses issue #2811's objective: it implements an endpoint returning SVG for downloads charts with configurable URL parameters, supports dark/light themes, provides a preview mechanism, uses raw data, and ignores local zoom state whilst applying date filters.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the downloads chart embedding endpoint. Refactoring extracts shared chart-building logic used by both embed and client components, and test coverage is appropriately added for new functionality.
Description check ✅ Passed The pull request description clearly relates to the changeset: it describes implementing an endpoint for embedding downloads chart static SVGs with dark/light theme toggle and URL parameters, which directly corresponds to the new embed API endpoint, shared chart utilities, translation updates, and UI components visible in the raw_summary.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2811-endpoint-to-embed-downloads-chart

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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 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
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/fr-FR.json Localization changed, will be marked as complete.
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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

e18e dependency analysis

No dependency warnings found.

Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
server/utils/download-evolution.ts (1)

100-110: ⚡ Quick win

No timeout or error handling on the upstream npm fetch.

$fetch here has no timeout and no error handling. A slow or failing upstream (or a 404 for a non-existent package) will reject, and since the endpoint awaits these via Promise.all, one bad package fails the entire embed request as an unhandled 500. Consider a per-request timeout and graceful degradation (e.g. treat a failed package as empty series rather than failing the whole chart).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/download-evolution.ts` around lines 100 - 110,
fetchNpmDownloadsRangeServer currently calls $fetch without timeout or error
handling, so a slow/failed/404 upstream will reject and break callers; wrap the
$fetch call in a try/catch inside fetchNpmDownloadsRangeServer and apply a
per-request timeout (e.g., AbortController or $fetch timeout option) so the
request is aborted if slow, and on any error/404 return a safe fallback (an
empty NpmDownloadsRangeResponse or zeroed series) instead of throwing so callers
(e.g., Promise.all consumers) can continue gracefully. Ensure you update the
implementation referenced by fetchNpmDownloadsRangeServer and keep the returned
shape consistent with NpmDownloadsRangeResponse when returning the fallback.
test/unit/shared/utils/trends-chart.spec.ts (1)

261-261: ⚡ Quick win

Avoid as never casts in tests; they bypass the strict typing this repo asks for.

These casts silence meaningful type checks and can hide API regressions in shared utilities. Prefer satisfies, explicit union fixtures, or narrowly typed helpers instead.

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: 468-468, 542-542

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/shared/utils/trends-chart.spec.ts` at line 261, Replace the unsafe
"fgSubtle: undefined as never" cast on the test fixture object (the colors
object and any similar occurrences) with a strictly typed approach: either
declare the fixture as a Partial or exact narrowed type (e.g., const
colorsFixture: Partial<ColorPalette> = { fgSubtle: undefined }), use the
"satisfies" operator to ensure the object conforms to the expected type, or
provide an explicit union/optional type for fgSubtle; update all occurrences
(including the other noted lines) so no "as never" casts are used and TypeScript
will enforce correct types for fgSubtle and related properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/Package/TrendsChart.vue`:
- Around line 20-25: The component calls getTrendsDatetimeFormatterOptions but
it isn't imported; add an explicit import for getTrendsDatetimeFormatterOptions
from '`#shared/utils/trends-chart`' alongside buildNormalisedTrendsDataset,
buildTrendsChartConfig, buildTrendsChartData, and isWeeklyDataset so the SFC
compiles and the call to getTrendsDatetimeFormatterOptions (used around lines
~163-165) resolves correctly.
- Around line 1958-1978: The toggle button should be a proper disclosure
control: add aria-expanded bound to showEmbedFields and aria-controls pointing
to the region id "trends-embed-chart" on the button (the same element that
currently uses `@click` and showEmbedFields), and ensure the controlled region
(`#trends-embed-chart`) keeps aria-hidden bound to !showEmbedFields; this exposes
the expanded state and relationship to assistive tech while keeping the existing
showEmbedFields logic intact.
- Around line 1995-1998: The aria-label on the copy button uses the readme
translation key ($t('package.readme.copy_as_markdown')) which is misleading for
the embed URL action; update the aria-label logic around copiedEmbedUrl to use
embed-specific translation keys (e.g., a "copy embed URL" and "copied" key)
instead of the README copy key, and add corresponding i18n strings so the
accessible label correctly reflects the "copy embed URL" action while keeping
:aria-pressed="copiedEmbedUrl".

In `@test/unit/server/utils/download-evolution.spec.ts`:
- Around line 23-43: Add a teardown afterEach to restore fake timers and global
stubs to avoid leaking state from beforeEach; specifically, after the existing
beforeEach add an afterEach that calls vi.useRealTimers() and
vi.unstubAllGlobals() (and optionally vi.restoreAllMocks() or vi.clearAllMocks()
if you want to reset mocks), referencing the existing
beforeEach/vi.useFakeTimers and vi.stubGlobal('$fetch') so the tests revert
timers and the $fetch stub between tests.

---

Nitpick comments:
In `@server/utils/download-evolution.ts`:
- Around line 100-110: fetchNpmDownloadsRangeServer currently calls $fetch
without timeout or error handling, so a slow/failed/404 upstream will reject and
break callers; wrap the $fetch call in a try/catch inside
fetchNpmDownloadsRangeServer and apply a per-request timeout (e.g.,
AbortController or $fetch timeout option) so the request is aborted if slow, and
on any error/404 return a safe fallback (an empty NpmDownloadsRangeResponse or
zeroed series) instead of throwing so callers (e.g., Promise.all consumers) can
continue gracefully. Ensure you update the implementation referenced by
fetchNpmDownloadsRangeServer and keep the returned shape consistent with
NpmDownloadsRangeResponse when returning the fallback.

In `@test/unit/shared/utils/trends-chart.spec.ts`:
- Line 261: Replace the unsafe "fgSubtle: undefined as never" cast on the test
fixture object (the colors object and any similar occurrences) with a strictly
typed approach: either declare the fixture as a Partial or exact narrowed type
(e.g., const colorsFixture: Partial<ColorPalette> = { fgSubtle: undefined }),
use the "satisfies" operator to ensure the object conforms to the expected type,
or provide an explicit union/optional type for fgSubtle; update all occurrences
(including the other noted lines) so no "as never" casts are used and TypeScript
will enforce correct types for fgSubtle and related properties.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13b5a881-235a-4f7f-8c83-2aada6b422f0

📥 Commits

Reviewing files that changed from the base of the PR and between 4cab893 and 0102f47.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • app/components/Package/TrendsChart.vue
  • app/composables/useChartWatermark.ts
  • app/composables/useCharts.ts
  • app/utils/date.ts
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • i18n/schema.json
  • package.json
  • server/api/embed/downloads.svg/index.get.ts
  • server/utils/download-evolution.ts
  • shared/utils/download-ranges.ts
  • shared/utils/embed-chart-colors.ts
  • shared/utils/trends-chart.ts
  • test/unit/app/utils/date.spec.ts
  • test/unit/server/api/embed/downloads.svg/index.get.spec.ts
  • test/unit/server/utils/download-evolution.spec.ts
  • test/unit/shared/utils/download-ranges.spec.ts
  • test/unit/shared/utils/trends-chart.spec.ts

Comment thread app/components/Package/TrendsChart.vue
Comment thread app/components/Package/TrendsChart.vue
Comment thread app/components/Package/TrendsChart.vue Outdated
Comment thread test/unit/server/utils/download-evolution.spec.ts
@graphieros
Copy link
Copy Markdown
Contributor Author

graphieros commented Jun 1, 2026

TS fails are related to server/api/registry/timeline/[...pkg].get.ts which are not modified by this PR 🤨

@graphieros graphieros marked this pull request as ready for review June 1, 2026 06:34
@graphieros graphieros marked this pull request as draft June 1, 2026 07:12
Comment thread app/utils/charts.ts Outdated
Comment thread app/utils/charts.ts Outdated
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.

endpoint to embed downloads chart

2 participants