Skip to content

GuideDay-to-day use. Assumes you've run a first notebook.

A second notebook ​

The step after Getting started, before the MoMA notebook: Express's release history from the npm registry, fetched, reshaped, charted and summarized in six cells. See Example notebooks for the others.

A notebook run on the cdp-node kernel, shown with its saved output.Download second-notebook.node.ipynb · View on GitHub

A short notebook in the shape a lot of data work takes: fetch some JSON, reshape it, draw a chart, write a summary. The data is every release of Express, from the npm registry's public API, which needs no key. The only package it imports is vega-lite-api, for the chart.

If this is your first notebook, start with Getting started.

Fetch ​

ts
const pkg = await (await fetch("https://registry.npmjs.org/express")).json();
({ name: pkg.name, description: pkg.description, distTags: pkg["dist-tags"] })
{
  name: 'express',
  description: 'Fast, unopinionated, minimalist web framework',
  distTags: { latest: '5.3.0', 'latest-4': '4.22.3' }
}

The response is the package's whole registry record: metadata for every version under versions, and when each was published under time. It stays in pkg, so the cells below don't fetch it again.

Reshape ​

One row per published version. time also lists versions that were later unpublished, so the rows come from versions. A version is a prerelease if it has anything after major.minor.patch, as in 5.0.0-beta.1 or the older 1.0.0rc4.

ts
type Release = { version: string; line: string; prerelease: boolean; date: Date };

const releases: Release[] = Object.keys(pkg.versions)
  .map((version) => ({
    version,
    line: version.split(".")[0] + ".x",
    prerelease: !/^\d+\.\d+\.\d+$/.test(version),
    date: new Date(pkg.time[version]),
  }))
  // By date, then by version for releases published at the same moment.
  .toSorted((a, b) => a.date.getTime() - b.date.getTime()
    || a.version.localeCompare(b.version, "en", { numeric: true }));

console.log(releases.length, "releases,", releases.filter((r) => r.prerelease).length, "of them prereleases");
releases.slice(-5)
290 releases, 43 of them prereleases
[
  {
    version: '5.2.1',
    line: '5.x',
    prerelease: false,
    date: 2025-12-01T20:49:43.268Z
  },
  {
    version: '4.22.1',
    line: '4.x',
    prerelease: false,
    date: 2025-12-01T20:50:41.122Z
  },
  {
    version: '4.22.2',
    line: '4.x',
    prerelease: false,
    date: 2026-05-11T18:50:00.007Z
  },
  {
    version: '4.22.3',
    line: '4.x',
    prerelease: false,
    date: 2026-09-14T06:46:38.631Z
  },
  {
    version: '5.3.0',
    line: '5.x',
    prerelease: false,
    date: 2026-10-09T12:56:54.505Z
  }
]

Chart ​

Stable releases per year, colored by major version. Prereleases are left out: the old ones for 1.x to 3.x all share one timestamp from August 2013, so their dates say little.

vega-lite-api builds a Vega-Lite chart in code, and the kernel shows any object with a toSpec() method as a chart, so the cell's last expression is all it takes. Vega-Lite does the counting: year("date") groups the rows by year and count() counts each group.

ts
import * as vl from "npm:vega-lite-api@5";

vl.markBar()
  .data(releases.filter((r) => !r.prerelease))
  .encode(
    vl.x().year("date").title(null),
    vl.y().count().title("releases"),
    vl.color().fieldN("line").title("version"),
    vl.tooltip([vl.year("date"), vl.fieldN("line"), vl.count()]),
  )
  .width(460)
  .height(220)

Downloads ​

Which of those versions are people installing? A second endpoint returns last week's downloads per version. The cell shows a placeholder while it fetches, then replaces it with the result: displaying again with the same display_id and update: true updates the output in place instead of adding another.

ts
await jupyter.display(jupyter.md`Fetching last week's downloads...`, { display_id: "downloads" });

const { downloads }: { downloads: Record<string, number> } =
  await (await fetch("https://api.npmjs.org/versions/express/last-week")).json();
const total = Object.values(downloads).reduce((sum, n) => sum + n, 0);

await jupyter.display(
  jupyter.md`**${total.toLocaleString()}** downloads last week, across ${Object.keys(downloads).length} versions.`,
  { display_id: "downloads", update: true },
);

87,264,652 downloads last week, across 290 versions.

Summary ​

One row per major version, built as a Markdown table and rendered with jupyter.md. The downloads share counts every version in the line, prereleases included.

ts
const byLine = Object.groupBy(releases, (r) => r.line);

const rows = Object.entries(byLine).map(([line, list]) => {
  const stable = list!.filter((r) => !r.prerelease);
  const latest = stable.at(-1)!;
  const weekly = list!.reduce((sum, r) => sum + (downloads[r.version] ?? 0), 0);
  return `| ${line} | ${stable.length} | ${list!.length - stable.length} | ${stable[0].date.getUTCFullYear()} | `
    + `${latest.version} (${latest.date.toISOString().slice(0, 10)}) | ${(100 * weekly / total).toFixed(1)}% |`;
});

jupyter.md`| Line | Releases | Prereleases | First release | Latest release | Share of last week's downloads |
| --- | ---: | ---: | ---: | --- | ---: |
${rows.join("\n")}`
LineReleasesPrereleasesFirst releaseLatest releaseShare of last week's downloads
0.x2020100.14.1 (2010-12-29)0.0%
1.x9620101.0.8 (2011-03-02)0.0%
2.x39620112.5.11 (2012-07-04)0.0%
3.x961620123.21.2 (2015-07-31)0.0%
4.x95420144.22.3 (2026-09-14)41.8%
5.x61120245.3.0 (2026-10-09)58.1%

Next ​

  • Swap express for another package, or the registry for another JSON API.
  • The jupyter namespace covers jupyter.display, jupyter.md and the other ways to show output.
  • MoMA's collection is a longer notebook: a 160,000-row dataset with Polars, Observable Plot, a widget and React.

MIT license. Parts are ports of Deno's Jupyter kernel, © the Deno authors.