<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Jupyter Blog - JavaScript</title><link href="https://blog.jupyter.org/" rel="alternate"/><link href="https://blog.jupyter.org/feeds/tag-javascript.atom.xml" rel="self"/><id>https://blog.jupyter.org/</id><updated>2026-06-11T15:13:00+00:00</updated><subtitle>JavaScript and TypeScript in Jupyter.</subtitle><entry><title>Catching Jupyter-specific bugs before CI does: announcing jupyter eslint plugin</title><link href="https://blog.jupyter.org/posts/2026/catching-jupyter-specific-bugs-before-ci-does/" rel="alternate"/><published>2026-06-11T15:13:00+00:00</published><updated>2026-06-11T15:13:00+00:00</updated><author><name>Darshan Kr. Paudyal</name></author><id>tag:blog.jupyter.org,2026-06-11:/posts/2026/catching-jupyter-specific-bugs-before-ci-does/</id><summary type="html">&lt;p&gt;We’re excited to announce the release of the first version of the Jupyter ESLint plugin @jupyter/eslint-plugin, now available on npm. It’s a custom ESLint plugin designed specifically for the official Jupyter frontends (JupyterLab, Notebook, JupyterLite) and TypeScript extensions built around them.&lt;/p&gt;</summary><content type="html">&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2026/catching-jupyter-specific-bugs-before-ci-does/images/001-1_CXmGuOa5O6Z60l4KZSEo3g.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;We’re excited to announce the release of the first version of the Jupyter ESLint plugin &lt;a href="https://www.npmjs.com/package/@jupyter/eslint-plugin"&gt;&lt;code&gt;@jupyter/eslint-plugin&lt;/code&gt;&lt;/a&gt;, now available on npm. It’s a custom ESLint plugin designed specifically for the official Jupyter frontends (JupyterLab, Notebook, JupyterLite) and TypeScript extensions built around them.&lt;/p&gt;
&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;
&lt;p&gt;If you’ve ever written a JupyterLab extension, you’ve probably hit something like this:&lt;/p&gt;
&lt;p&gt;You wire up a new plugin, push a PR, walk away to make coffee, and 30 minutes later CI fails on a test you didn’t expect to be related. You scroll through logs, eventually trace it back, and realize the problem was a single misaligned argument in your &lt;code&gt;activate&lt;/code&gt; function. TypeScript was happy. The editor showed no red squiggles. Nothing in your local toolchain said a word.&lt;/p&gt;
&lt;p&gt;And the slow CI feedback loop is only half the story. Some of these mistakes don’t fail CI at all, a hardcoded English &lt;code&gt;label&lt;/code&gt;, an &lt;code&gt;enum&lt;/code&gt; in a settings schema, a malformed token id. They pass tests, ship to users, and only surface at runtime. They’re the kind of bugs you hear about from users months later, if at all.&lt;/p&gt;
&lt;p&gt;JupyterLab, Jupyter Notebook, and the wider extension ecosystem follow a lot of project-specific patterns around plugins, commands, tokens, settings schemas, translations, and visual tests. These patterns aren’t enforced by TypeScript, because they live one level above the type system. They’re contracts about &lt;em&gt;how&lt;/em&gt; the framework expects code to be written, not about whether the code type-checks.&lt;/p&gt;
&lt;p&gt;A few examples of what generic linters and TypeScript can’t catch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;JupyterFrontEndPlugin&lt;/code&gt; that requires &lt;code&gt;[TokenA, TokenB]&lt;/code&gt; but whose &lt;code&gt;activate&lt;/code&gt; function takes those arguments in the wrong order. Both are tokens, both type-check but the plugin fails at activation.&lt;/li&gt;
&lt;li&gt;A command added with &lt;code&gt;commands.addCommand(...)&lt;/code&gt; whose &lt;code&gt;label&lt;/code&gt; is a hardcoded English string. The extension ships, gets translated by the community, and that one button silently stays in English.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are real, recurring issues. They cost contributor time, they frustrate first-time contributors, and they pile onto reviewer load. They’re exactly the kind of knowledge that lives in the heads of long-time maintainers and exactly the kind of knowledge that a linter is great at encoding. So we built one.&lt;/p&gt;
&lt;h2 id="what-v1-catches"&gt;What v1 catches&lt;/h2&gt;
&lt;p&gt;The plugin currently ships eight rules, grouped by what they protect:&lt;/p&gt;
&lt;h3 id="plugin-structure"&gt;Plugin structure&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-activation-args/"&gt;&lt;code&gt;plugin-activation-args&lt;/code&gt;&lt;/a&gt; — Keeps &lt;code&gt;activate&lt;/code&gt; function arguments aligned with &lt;code&gt;requires&lt;/code&gt; and &lt;code&gt;optional&lt;/code&gt; tokens.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/plugin-description/"&gt;&lt;code&gt;plugin-description&lt;/code&gt;&lt;/a&gt; — Requires a non-empty &lt;code&gt;description&lt;/code&gt; on every &lt;code&gt;JupyterFrontEndPlugin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/token-format/"&gt;&lt;code&gt;token-format&lt;/code&gt;&lt;/a&gt; — Enforces the &lt;code&gt;&amp;lt;package&amp;gt;:&amp;lt;TokenSymbol&amp;gt;&lt;/code&gt; convention for &lt;code&gt;new Token(...)&lt;/code&gt; ids.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="commands-and-settings"&gt;Commands and settings&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/command-described-by/"&gt;&lt;code&gt;command-described-by&lt;/code&gt;&lt;/a&gt; — Requires a &lt;code&gt;describedBy&lt;/code&gt; schema on commands that accept &lt;code&gt;args&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/no-schema-enum/"&gt;&lt;code&gt;no-schema-enum&lt;/code&gt;&lt;/a&gt; — Disallows &lt;code&gt;enum&lt;/code&gt; in settings JSON schemas in favor of translatable &lt;code&gt;oneOf&lt;/code&gt; with &lt;code&gt;const&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="internationalization"&gt;Internationalization&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/no-untranslated-string/"&gt;&lt;code&gt;no-untranslated-string&lt;/code&gt;&lt;/a&gt; — Flags hardcoded user-facing strings in command labels, dialogs, accessibility attributes, JSX, and more.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/no-translation-concatenation/"&gt;&lt;code&gt;no-translation-concatenation&lt;/code&gt;&lt;/a&gt; — Forbids dynamic concatenation inside &lt;code&gt;trans.__()&lt;/code&gt; calls that translation extractors can’t pick up.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="testing"&gt;Testing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eslint-plugin.readthedocs.io/en/latest/rules/require-soft-assertions-before-snapshots/"&gt;&lt;code&gt;require-soft-assertions-before-snapshots&lt;/code&gt;&lt;/a&gt; — Requires &lt;code&gt;expect.soft(...)&lt;/code&gt; for all but the last snapshot in a Playwright test, so a single failure doesn’t block the rest from updating.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each rule has incorrect/correct examples, options, and rationale in the &lt;a href="https://eslint-plugin.readthedocs.io/en/latest/category/rules/"&gt;rules reference&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="getting-started-in-60-seconds"&gt;Getting started in 60 seconds&lt;/h2&gt;
&lt;p&gt;Install the plugin:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;npm&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--save-dev&lt;span class="w"&gt; &lt;/span&gt;@jupyter/eslint-plugin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add it to your ESLint config and turn on the recommended ruleset:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jupyterPlugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;@jupyter/eslint-plugin&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;jupyter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jupyterPlugin&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;jupyterPlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommended&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Run &lt;code&gt;jlpm eslint&lt;/code&gt; and you’ll start seeing Jupyter-specific issues alongside your existing lint output. If you’re still on legacy &lt;code&gt;.eslintrc&lt;/code&gt; configs, there’s a &lt;a href="https://eslint-plugin.readthedocs.io/en/latest/user-guide/#for-legacy-eslintrc-configs"&gt;&lt;code&gt;recommended-legacy&lt;/code&gt;&lt;/a&gt; config too. However, we strongly recommend upgrading to &lt;a href="https://eslint.org/docs/latest/use/migrate-to-9.0.0"&gt;ESLint v9&lt;/a&gt; and using the new &lt;a href="https://eslint.org/blog/2022/08/new-config-system-part-2/"&gt;flat config&lt;/a&gt; format.&lt;/p&gt;
&lt;p&gt;Full setup details, including type-aware configuration for &lt;code&gt;plugin-activation-args&lt;/code&gt; and the JSON parser setup for &lt;code&gt;no-schema-enum&lt;/code&gt;, are in the &lt;a href="https://eslint-plugin.readthedocs.io/en/latest/user-guide/"&gt;user guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you maintain an extension, &lt;strong&gt;the most useful thing you can do today is install it and run it once on your codebase.&lt;/strong&gt; Even if you don’t adopt it permanently, the first run is often informative most extensions surface a handful of issues they didn’t know they had.&lt;/p&gt;
&lt;h2 id="help-us-encode-more-knowledge"&gt;Help us encode more knowledge&lt;/h2&gt;
&lt;p&gt;Eight rules is a starting point. The plugin is most valuable when it captures the recurring mistakes that maintainers across the ecosystem keep running into and a lot of that knowledge isn’t written down anywhere.&lt;/p&gt;
&lt;p&gt;If you’ve ever been bitten by a Jupyter-specific issue that &lt;em&gt;could&lt;/em&gt; have been a lint rule, something where you found yourself thinking “I wish my editor had warned me about this” please &lt;a href="https://github.com/jupyterlab/eslint-plugin/issues"&gt;open an issue on the repository&lt;/a&gt;. Even a short description of the bug and a minimal example helps. We’d much rather encode that knowledge into an automated check once than have every new contributor learn it the hard way.&lt;/p&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
&lt;h2 id="acknowledgement"&gt;Acknowledgement&lt;/h2&gt;
&lt;p&gt;This work was funded under the &lt;a href="https://blog.jupyter.org/posts/2025/announcing-our-first-jupyter-community-funded-proposals/"&gt;first round of Jupyter Community Funded Proposals&lt;/a&gt;. We want to thank the &lt;a href="https://jupyterfoundation.org/"&gt;Jupyter Foundation&lt;/a&gt; and everyone involved in setting up this funding mechanism.&lt;/p&gt;
&lt;p&gt;We’re also grateful to &lt;a href="https://github.com/jtpio"&gt;Jeremy Tuloup&lt;/a&gt; and &lt;a href="https://github.com/brichet"&gt;Nicolas Brichet&lt;/a&gt; for their help in adopting the plugin.&lt;/p&gt;
&lt;h2 id="about-the-developers"&gt;About the developers&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/darshan-poudel8/"&gt;&lt;strong&gt;Darshan Paudyal&lt;/strong&gt;&lt;/a&gt; is an intern at &lt;a href="https://openteams.com/"&gt;OpenTeams&lt;/a&gt; and a member of the &lt;a href="https://jupyterlab-team-compass.readthedocs.io/en/latest/team.html"&gt;Jupyter Frontend Council&lt;/a&gt;. Darshan led the development of v1 of &lt;code&gt;@jupyter/eslint-plugin&lt;/code&gt;, designing and implementing the rule set, the documentation, and the release.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/michal-krassowski/"&gt;&lt;strong&gt;Michał Krassowski&lt;/strong&gt;&lt;/a&gt; is a Senior Software Engineer at &lt;a href="https://openteams.com/"&gt;OpenTeams&lt;/a&gt;. Mike provided guidance and direction for the development of the plugin.&lt;/p&gt;
&lt;h2 id="useful-links"&gt;Useful links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;📦 &lt;a href="https://www.npmjs.com/package/@jupyter/eslint-plugin"&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://eslint-plugin.readthedocs.io/en/latest/"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/jupyterlab/eslint-plugin"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://github.com/jupyterlab/eslint-plugin/issues"&gt;Open an issue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="extensions"/><category term="JavaScript"/><category term="JupyterLab"/></entry><entry><title>Exploring Open Access Art Using the Deno Jupyter Kernel</title><link href="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/" rel="alternate"/><published>2025-04-17T15:27:00+00:00</published><updated>2025-04-17T15:27:00+00:00</updated><author><name>Trevor Manz</name></author><id>tag:blog.jupyter.org,2025-04-17:/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/</id><summary type="html">&lt;p&gt;Jupyter notebooks are the de facto standard for interactive computing and data analysis, combining code, prose, and visualizations in a single document.&lt;/p&gt;</summary><content type="html">&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/001-1_MJgcofpQm8Pl6hSZDvSdAw.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Jupyter notebooks are the &lt;em&gt;de facto&lt;/em&gt; standard for interactive computing and data analysis, combining code, prose, and visualizations in a single document.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/manzt/deno-jupyter-blog/blob/main/post.ipynb"&gt;In fact, this post was written in a notebook!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.deno.com/"&gt;Deno&lt;/a&gt; is a simplified and secure runtime for JavaScript and TypeScript, with built-in tooling and modern defaults. Since &lt;a href="https://deno.com/blog/v1.37"&gt;v1.37&lt;/a&gt;, Deno ships with a &lt;a href="https://docs.deno.com/runtime/reference/cli/jupyter/"&gt;built-in Jupyter kernel&lt;/a&gt;, making it easy to use TypeScript for data science and interactive computing.&lt;/p&gt;
&lt;p&gt;Having worked extensively with computational notebooks (mainly in Python), I find this exciting for these reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple setup&lt;/strong&gt; — The kernel is built into the Deno CLI, so there’s no need for additional installation. Just install Deno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved dependency management&lt;/strong&gt; — Notebooks run like standalone scripts, and Deno’s support for importing dependencies in the code itself improves dependency management and reproducibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A unified ecosystem for interactive data analysis&lt;/strong&gt; — Jupyter supports rich HTML/CSS/JS outputs, and the JavaScript ecosystem is ideal for interactive UIs. Deno connects the kernel and frontend, making notebooks more powerful and flexible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this post, we’ll explore the National Gallery of Art’s Open Access dataset in Jupyter with Deno. We’ll clean and analyze the data, looking at public domain works, artists, origins, and patterns. Along the way, we’ll introduce Deno-compatible tools like &lt;a href="https://github.com/pola-rs/nodejs-polars"&gt;npm:nodejs-polars&lt;/a&gt; and &lt;a href="https://observablehq.com/plot/"&gt;npm:@observablehq/plot&lt;/a&gt;, and wrap up by adding widgets and custom displays for a more interactive experience.&lt;/p&gt;
&lt;h2 id="the-dataset"&gt;The dataset&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;National Gallery of Art (NGA)&lt;/strong&gt; &lt;a href="https://www.nga.gov/open-access-images/open-data.html"&gt;Open Data Program&lt;/a&gt; provides access to over 130,000 artworks and their creators, available &lt;a href="https://github.com/NationalGalleryOfArt/opendata/tree/main/data"&gt;on GitHub&lt;/a&gt;. This dataset includes valuable metadata such as titles, dates, artists, and classifications, all under a &lt;a href="https://creativecommons.org/public-domain/cc0/"&gt;Creative Commons 0 (CC0) license&lt;/a&gt;, meaning it’s free to use and share.&lt;/p&gt;
&lt;p&gt;The collection spans a wide variety of artworks, from sculptures to paintings, by famous artists like Mary Cassatt, M.C. Escher, Vincent van Gogh, Pablo Picasso, and Georgia O’Keeffe.&lt;/p&gt;
&lt;p&gt;However, navigating this resource is challenging. The National Gallery’s website is not very user-friendly, making it difficult to make sense of what is actually in the dataset. It’s nearly impossible to get any high-level insights, like the number of paintings, which time periods are most represented, or which artists are most prevalent–let alone make specific queries.&lt;/p&gt;
&lt;p&gt;Fortunately, the &lt;a href="https://github.com/NationalGalleryOfArt/opendata/tree/main/data"&gt;dataset is available on GitHub&lt;/a&gt; as a collection of related tables, exported as CSV files. For our analysis, we’ll focus on three key tables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;objects.csv&lt;/strong&gt; — Metadata about artworks, including titles, dates, materials, and classifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;constituents.csv&lt;/strong&gt; — Artist details such as names, nationalities, and lifespans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;published_images.csv&lt;/strong&gt; — Links to artwork images via the NGA’s IIIF API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ll &lt;strong&gt;clean&lt;/strong&gt; and &lt;strong&gt;join&lt;/strong&gt; these tables to create a &lt;strong&gt;unified dataset&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="but-wait"&gt;But wait!&lt;/h3&gt;
&lt;p&gt;While these NGA tables are free to use, they only include &lt;em&gt;metadata&lt;/em&gt;. The actual artwork images have separate licensing, with only about half being CC0-licensed. We’ll gather this info separately to identify freely available images.&lt;/p&gt;
&lt;p&gt;Our goal is to unify the data to identify public domain images, explore subsets, and view available artworks. Maybe you’ll even find an upgrade for that default OS wallpaper.&lt;/p&gt;
&lt;h2 id="loading-and-cleaning-the-data"&gt;Loading and cleaning the data&lt;/h2&gt;
&lt;h3 id="open-access-dataset"&gt;Open Access dataset&lt;/h3&gt;
&lt;p&gt;To get started, we first need to download the data. If you’ve worked with CSVs in JavaScript before, this typically looks like a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"&gt;“fetch”&lt;/a&gt; request followed by some parsing. Here, we’re using &lt;a href="https://jsr.io/@std/csv"&gt;jsr:@std/csv&lt;/a&gt; to stream the data, which gives us an array of objects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jsr:@std/csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://github.com/NationalGalleryOfArt/opendata/raw/refs/heads/main/data/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objects.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TextDecoderStream&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CsvParseStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;skipFirstRow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;objectid&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;row.title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beginyear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;medium&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;row.medium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kr"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visualbrowserclassification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Saint James Major&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1310&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;medium&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tempera on panel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;painting&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Saint Paul and a Group of Worshippers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1333&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;medium&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tempera on panel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;painting&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Saint Bernard and Saint Catherine of Alexandria with the Virgin of the Annunciation [right panel]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1387&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;medium&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tempera on poplar panel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;painting&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we were just plotting the data, this would work fine. But an array of objects isn’t ideal for building our dataset. The NGA data is relational and needs merging, cleaning, and restructuring. A &lt;strong&gt;DataFrame&lt;/strong&gt; is better suited for our use case, offering higher-level, efficient APIs that eliminate the need to write manipulation functions by hand.&lt;/p&gt;
&lt;p&gt;Let’s see how we can load the same dataset using a DataFrame from &lt;a href="https://github.com/pola-rs/nodejs-polars"&gt;npm:nodejs-polars&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;npm:nodejs-polars&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objects.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;pl.DataFrame&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;quoteChar&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;beginyear&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;medium&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;visualbrowserclassification&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/002-1_DbMTGoODjquyHq4C2F18Zw.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Here, we fetch the dataset as before, but instead of parsing the CSV ourselves, we read the text response directly with Polars to create a &lt;strong&gt;pl.DataFrame&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We then chain a &lt;strong&gt;.select&lt;/strong&gt; expression to choose and rename the columns we want. Notice how, rather than operating on each row in a loop, with Polars we work directly with &lt;em&gt;columns&lt;/em&gt;. This API allows us to express complex, efficient operations (in Rust) without materializing JavaScript objects.&lt;/p&gt;
&lt;p&gt;Let’s load our other two datasets as pl.DataFrames as well:&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;constituents.csv&lt;/strong&gt; table contains information about any person or entity associated with a piece of art, such as artists, curators, or collectors.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;constituents.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;constituents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quoteChar&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;constituentid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;forwarddisplayname&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;visualbrowsernationality&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nationality&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;constituents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/003-1_RkX0rBRpRv89jfCrgv8rZw.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;published_images.csv&lt;/strong&gt; table contains additional information about the artwork’s image, including a URL for the thumbnail and mapping the objectid to the &lt;a href="https://iiif.io/"&gt;IIIF (International Image Interoperability Framework)&lt;/a&gt; for the image.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;published_images.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publishedImages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quoteChar&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;depictstmsobjectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;uuid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iiifthumburl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;thumburl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;publishedImages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/004-1_Gk0o33-bs2AR7Ys4ajyypQ.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The final table, &lt;strong&gt;objects_constituents.csv&lt;/strong&gt;, represents a many-to-many relationship between artworks and the people or entities associated with them. Each artwork may have multiple people linked to it.&lt;/p&gt;
&lt;p&gt;For our analysis, we’re interested in selecting the “primary” constituent (i.e., the artist). We define the primary artist as the one with the highest &lt;strong&gt;displayorder&lt;/strong&gt; for each object.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objects_constituents.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objectToArtist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readCSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quoteChar&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;artist&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;displayorder&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;constituentid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;objectToArtist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/005-1_kr-dapKFd1hmgpYEbG8rJA.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;This is a much more advanced usage of Polars, so let’s break down what’s going on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Filter&lt;/strong&gt; the table to select only rows where the role is “artist”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sort&lt;/strong&gt; the filtered data by displayorder to ensure all entries are at the top.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; the rows &lt;strong&gt;by&lt;/strong&gt; objectid, ensuring one row per artwork.&lt;/li&gt;
&lt;li&gt;Get the &lt;strong&gt;first&lt;/strong&gt; in each group (i.e., artist with highest display order).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select&lt;/strong&gt; just the “objectid” and “constituentid” for joining our tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing this logic by hand in JavaScript would be tedius, but with Polars it’s declarative and reads like a sentence. This type of chaining is common when working with relational data.&lt;/p&gt;
&lt;h2 id="determining-whether-art-is-in-public-domain"&gt;Determining whether art is in public domain&lt;/h2&gt;
&lt;p&gt;We’ve loaded the relevant tables from the Open Access dataset, but they do not indicate whether images associated with an artwork are in the public domain. While CC0 images can be freely downloaded, shared, and repurposed, not all art with the CC0 license are &lt;em&gt;legally&lt;/em&gt; in the public domain everywhere due to local copyright laws and edge cases.&lt;/p&gt;
&lt;p&gt;Let’s find a way to add public domain information to our data set.&lt;/p&gt;
&lt;p&gt;That information is available on the NGA website, but only through the search user interface. Extracting it manually would be impractical, so I reverse-engineered an API call to retrieve the IDs of artworks with public domain images.&lt;/p&gt;
&lt;p&gt;We only need the IDs, not the full data, but the query is slow. Since this isn’t an official API, I’ve saved the results separately. There are about 50k IDs identifying public domain images in our dataset.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is not an official API, so I’ve cached the results to avoid &lt;/span&gt;
&lt;span class="c1"&gt;// repeatedly querying the server.&lt;/span&gt;
&lt;span class="c1"&gt;// &lt;/span&gt;
&lt;span class="c1"&gt;// let response = await fetch(&amp;quot;https://www.nga.gov/bin/ngaweb/collection-search-result/search.pageSize__100000.pageNumber__1.lastFacet__artobj_downloadable.json?artobj_downloadable=Image_download_available&amp;quot;); &lt;/span&gt;
&lt;span class="c1"&gt;// let data = await response.json();&lt;/span&gt;
&lt;span class="c1"&gt;// Deno.writeTextFileSync(&amp;quot;public-domain-ids.txt&amp;quot;, data.results.map((object) =&amp;gt; object.id).join(&amp;quot;\n&amp;quot;));&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publicDomainIds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readTextFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public-domain-ids.txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\n&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="join-to-single-unified-table"&gt;“Join” to single unified table&lt;/h2&gt;
&lt;p&gt;Now that we have all our data cleaned and loaded, we’ll perform a large join to combine all these tables into a &lt;strong&gt;single unified table&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Again, this challenging to do manually, but with Polars, we can nicely express these complex join operations. Finally, we use a &lt;strong&gt;.isIn&lt;/strong&gt; expression to derive a new column that indicates whether a piece of art is public domain.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectToArtist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constituents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;constituentid&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publishedImages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;constituentid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;withColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;isIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicDomainIds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;descending&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nullsLast&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/006-1_pfamudvyutL613ZC_nuGJg.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;With our unified dataset finally loaded, let’s move on to some plotting.&lt;/p&gt;
&lt;h2 id="exploring-art-who-created-it-when-and-more-with-plots"&gt;Exploring art, who created it, when, and more with plots&lt;/h2&gt;
&lt;p&gt;The NGA portal allows some exploration of these data but focuses on specific artworks. We’re more interested in broader insights — where most art comes from, who created it, when it was made, and whether it’s public domain.&lt;/p&gt;
&lt;p&gt;As a JavaScript/TypeScript runtime, Deno gives us access to many visualization libraries to ask these kinds of questions. We’ll use &lt;a href="https://observablehq.com/plot/"&gt;npm:@observablehq/plot&lt;/a&gt; (Observable Plot) to create plots, spot patterns, and better understand the collection.&lt;/p&gt;
&lt;p&gt;Note this post is not a tutorial on Observable Plot, so don’t worry if some of the code below is not immediately clear. The goal is to demonstrate how to use Observable Plot in Jupyter with Deno to extract insights from our data. I’ll highlight any Deno-specific details or useful tips along the way.&lt;/p&gt;
&lt;p&gt;Let’s start by examining the distribution of artworks across different “types” in the NGA collection.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;npm:@observablehq/plot&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jsr:@manzt/jupyter-helper&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Convert our DataFrame to Array&amp;lt;Object&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toRecords&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marginLeft&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;barY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupX&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-y&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Public domain&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Copyrighted&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Provide a custom `document`&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For the code above, the key things to keep in mind are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom document&lt;/strong&gt; — Observable Plot relies on the browser DOM, but since we’re in Deno, we provide a custom document from &lt;a href="https://jsr.io/@manzt/jupyter-helper"&gt;jsr:@manzt/jupyter-helper&lt;/a&gt; to enable rendering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Converting our Polars DataFrame to records&lt;/strong&gt; — Observable Plot works best with arrays of objects, so we use &lt;strong&gt;df.toRecords()&lt;/strong&gt; to make the data easier to work with.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;For specifics on the &lt;strong&gt;Plot&lt;/strong&gt; API, see the &lt;a href="https://observablehq.com/plot/"&gt;documentation&lt;/a&gt; and &lt;a href="https://observablehq.com/@observablehq/plot-gallery"&gt;examples&lt;/a&gt; available.&lt;/em&gt;&lt;/p&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/007-0_iO8EF2CUzpVxl6em.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Right away, we can see that prints, drawings, and photographs make up most of the collection. Each category varies in how many images are public domain — images of prints, drawings, sculptures, and paintings are mostly public domain, while images of photographs and portfolios are largely copyrighted.&lt;/p&gt;
&lt;p&gt;We can get more granular in our plot very easily with Observable Plot, which is &lt;strong&gt;composable and expressive&lt;/strong&gt;. With just a few tweaks, you can completely change how the data is represented. For example, starting from the plot above, we can modify marks and some encoding fields and produce an entirely different chart, allowing for quick exploration and iteration — like this “waffle” bar chart.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marginLeft&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;waffleY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="cm"&gt;/* changed, Plot.barY */&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="cm"&gt;/* changed, Plot.groupX */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Public Domain&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Copyrighted&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-y&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* changed, sort: { x: &amp;quot;-y&amp;quot;  } */&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* new */&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruleY&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/008-0_QtnqOHV1z4f0n1Kx.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;With both Polars and Plot, we can ask more specific questions about the data, such as &lt;strong&gt;“Which artists have the most artwork in the collection?”&lt;/strong&gt; Here, we first use Polars to group the data by artist (“name”) and whether the artwork is public domain (“public”), counting the number of works for each. Then, we use Plot to visualize the top 25 artists.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;artworkTotals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name_count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toRecords&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marginLeft&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;barX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;artworkTotals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name_count&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-x&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Public domain&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Copyrighted&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/009-0_uT6qwM4VdbZE5jjU.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Again, we can draw some high-level takeaways from this plot: The photographer Robert Frank has nearly twice as many works as the next highest artist. Some entries, like &lt;strong&gt;“American 20th Century”&lt;/strong&gt; and &lt;strong&gt;“German 15th Century,”&lt;/strong&gt; represent groups rather than individuals. Additionally, an artist’s work is typically either &lt;strong&gt;entirely public domain or not at all&lt;/strong&gt; — likely because public domain status is determined at the &lt;strong&gt;collection level&lt;/strong&gt;, with institutions clearing entire groups of works at once rather than evaluating individual pieces separately.&lt;/p&gt;
&lt;p&gt;With our tidy and clean dataset, we can quickly iterate in Deno to ask both broad questions (“What even is &lt;em&gt;in&lt;/em&gt; this collection?”) and very specific ones (“Who created this artwork in a given year?”).&lt;/p&gt;
&lt;h2 id="deeper-exploration-with-interactivity"&gt;Deeper exploration with interactivity&lt;/h2&gt;
&lt;p&gt;So far, we’ve used &lt;strong&gt;static plots&lt;/strong&gt; to explore our data, but Jupyter notebooks support rich, interactive outputs using &lt;strong&gt;HTML, CSS, and JavaScript&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With Deno, we can add more interactivity to our data exploration by either using &lt;strong&gt;off-the-shelf interactive &lt;a href="https://github.com/manzt/anywidget"&gt;anywidgets&lt;/a&gt;&lt;/strong&gt; by quickly building &lt;strong&gt;custom components&lt;/strong&gt; for visualizing subsets in ways prebuilt tools don’t support. Deno provides direct access to the full web ecosystem, making custom visualizations seamless.&lt;/p&gt;
&lt;p&gt;In this section, we’ll look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prebuilt interactive table &lt;strong&gt;anywidgets&lt;/strong&gt; for exploring data&lt;/li&gt;
&lt;li&gt;Create custom &lt;code&gt;&amp;lt;Gallery /&amp;gt;&lt;/code&gt; component to visualize subsets of the dataset&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="exploring-data-with-dataframe-viewer-anywidgets"&gt;Exploring data with DataFrame viewer anywidgets&lt;/h2&gt;
&lt;p&gt;In Jupyter, “widgets” extend notebook outputs with interactive views and controls for objects in the kernel (i.e., backend). Unlike standard outputs, they consist of both kernel-side and front-end code, communicating directly via custom messages. While the Jupyter Widget ecosystem is largely Python-centric, the &lt;a href="https://github.com/manzt/anywidget"&gt;&lt;strong&gt;anywidget&lt;/strong&gt; project&lt;/a&gt; provides a kernel-agnostic way to create and share these components.&lt;/p&gt;
&lt;p&gt;With anywidget, widgets can be published to &lt;a href="https://jsr.io/"&gt;JSR&lt;/a&gt; and used in the Deno Jupyter kernel. For example, &lt;a href="https://jsr.io/@manzt/jupyter-helper"&gt;jsr:@manzt/jupyter-helper&lt;/a&gt; provides some off-the-shelf anywidgets for interactively viewing Polars data frames.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;agGrid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jsr:@manzt/jupyter-helper&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;agGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;200&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// just look at the first 200 items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/010-1_62m9scX8q8zpeMppdsV5hg.mp4" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;agGrid&lt;/strong&gt; export renders the &lt;strong&gt;pl.DataFrame&lt;/strong&gt; using the popular &lt;a href="https://www.ag-grid.com/"&gt;AG Grid&lt;/a&gt; library, allowing for interactive sorting, filtering, and pagination. This default view is minimal but can be easily extended with AG Grid’s many features.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jsr:@manzt/jupyter-helper&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;paintings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;painting&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;quak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paintings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/011-1_7gY1tFtpCC_4oi5SN8lAEA.mp4" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;quak&lt;/strong&gt; export renders the &lt;strong&gt;pl.DataFrame&lt;/strong&gt; with the &lt;a href="https://github.com/manzt/quak"&gt;&lt;strong&gt;quak&lt;/strong&gt;&lt;/a&gt; data table viewer. Like &lt;strong&gt;agGrid&lt;/strong&gt;, it also includes summary visualizations above each column, showing 1D distributions, and can scale to much more data (e.g., the entire collection). These 1D summary visualizations are &lt;em&gt;interactive&lt;/em&gt;, enabling &lt;em&gt;cross-filtering&lt;/em&gt; across columns.&lt;/p&gt;
&lt;p&gt;Together, the programmatic and interactive layers enable unique hybrid workflows working with data. Some queries are easier to express in code, while others are more intuitive through direct manipulation. In notebooks, the two approaches come together naturally.&lt;/p&gt;
&lt;p&gt;For example, Polars can be used to subset the data to just public domain paintings (~2k out of ~115k artworks), and then a date range can be selected interactively by &lt;em&gt;brushing&lt;/em&gt; in the UI to explore variation across other columns.&lt;/p&gt;
&lt;h2 id="creating-a-custom-gallery-component"&gt;Creating a custom &lt;code&gt;&amp;lt;Gallery/&amp;gt;&lt;/code&gt; component&lt;/h2&gt;
&lt;p&gt;Deno gives us direct access to the web ecosystem, making it easy to build small, useful UIs for exploring our data in ways that off-the-shelf tools don’t support.&lt;/p&gt;
&lt;p&gt;For example, it would be helpful to actually see a set of artwork in a “gallery view” to provide intuition about what’s in that subset. We can do that since the dataset contains image URLs.&lt;/p&gt;
&lt;p&gt;Let’s create a custom &lt;a href="https://react.dev/learn/writing-markup-with-jsx"&gt;&lt;strong&gt;JSX component&lt;/strong&gt;&lt;/a&gt; that server-side renders (SSR) a gallery view for any dataset subset using the &lt;strong&gt;render&lt;/strong&gt; export from &lt;a href="https://jsr.io/@manzt/jupyter-helper"&gt;jsr:@manzt/jupyter-helper&lt;/a&gt;. This helper simply converts JSX to an HTML string, which we display using the &lt;a href="https://docs.deno.com/api/deno/~/Deno.jupyter.Displayable"&gt;&lt;strong&gt;Deno.jupyter.Displayable&lt;/strong&gt;&lt;/a&gt; interface.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;npm:react&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jsr:@manzt/jupyter-helper&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Gallery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;grid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;gridTemplateColumns&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`repeat(auto-fill, minmax(&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;px, 1fr))`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;4px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;objectid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;thumburl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;thumburl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publicDomain&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;relative&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;center&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sb"&gt;`https://www.nga.gov/collection/art-object-page.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;objectid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;.html`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;block&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;relative&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;thumburl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;100%&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;100%&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;objectFit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cover&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;borderRadius&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;publicDomain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://mirrors.creativecommons.org/presskit/icons/zero.svg&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Public Domain&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;absolute&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;3px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;3px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;20px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;20px&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/a&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/div&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Gallery&lt;/strong&gt; is a custom component that displays a grid of artwork thumbnails from a given dataset. Each image &lt;strong&gt;links to its NGA collection page&lt;/strong&gt;, and public domain works are marked with a CC0 icon.&lt;/p&gt;
&lt;p&gt;We can SSR &lt;strong&gt;Gallery&lt;/strong&gt; with our data to better understand a smaller subset, making it easier to explore specific artists, time periods, or categories visually. For example, &lt;strong&gt;a random sample&lt;/strong&gt; from all the collection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Gallery&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/012-0_-_2C_srE0EGp-0HE.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Or sample of just paintings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Gallery&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;paintings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/013-0_RFL3Dapu3QIwpN2P.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;It’s worth taking a moment to appreciate that in just a few lines of code, we’ve built a &lt;strong&gt;domain-specific utility&lt;/strong&gt; that makes our data more intuitive to explore. This kind of flexibility is powerful and unique to Deno, letting us quickly craft custom utilities that provide deeper insights.&lt;/p&gt;
&lt;p&gt;With our toolkit, let’s take a closer look at the NGA dataset:&lt;/p&gt;
&lt;h2 id="drilling-down-on-when-the-art-was-created"&gt;Drilling down on &lt;em&gt;when&lt;/em&gt; the art was created&lt;/h2&gt;
&lt;p&gt;So far, our data exploration has focused on &lt;strong&gt;high-level summaries&lt;/strong&gt; of the NGA collection by artist and artwork type using static plots. Now, let’s drill down into something potentially more interesting using both our complete toolkit: &lt;em&gt;when&lt;/em&gt; these works were created.&lt;/p&gt;
&lt;p&gt;We’ll start with a stacked histogram to see the overall distribution of artwork types over time, then facet by public domain status to uncover any patterns.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rectY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1401&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 15th century or later&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binX&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;fy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Public Domain&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Copyrighted&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruleY&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marginLeft&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marginRight&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/014-0_WU1i-imsUsRnWiTe.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The distribution of artworks with &lt;strong&gt;copyrighted&lt;/strong&gt; and &lt;strong&gt;public domain&lt;/strong&gt; images are noticeably different. Nearly all &lt;strong&gt;copyrighted works appear after 1850&lt;/strong&gt;, while &lt;strong&gt;public domain artworks are more evenly spread&lt;/strong&gt; over time — &lt;em&gt;except&lt;/em&gt; for a &lt;strong&gt;sharp spike in public domain drawings from the 1940s&lt;/strong&gt;, with over 15k artworks added.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a striking anomaly&lt;/strong&gt;. Are they all from the same artist? Why only drawings? Let’s take a closer look.&lt;/p&gt;
&lt;p&gt;We’ll filter down the public domain data to this span.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;notablePeriod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// just public domain&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1925&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// between 1925 - 1955&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1955&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;marks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rectY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;notablePeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toRecords&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Plot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binX&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/015-0_wa5EYkPYIidClyna.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Zooming into the &lt;strong&gt;1925–1955 range&lt;/strong&gt;, we see that almost all the data falls within a much narrower window. Interestingly, the spike is highly concentrated between &lt;strong&gt;1935 and 1942&lt;/strong&gt;, with very little artwork outside this period.&lt;/p&gt;
&lt;p&gt;Narrowing in closer, we can see that in this window there are ~18k individual drawings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;totalNumberOfDrawings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;notablePeriod&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1934&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1943&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;totalNumberOfDrawings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 18096&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;made by over 1000 individuals:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;numberOfArtists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;notablePeriod&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1934&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1943&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;numberOfArtists&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1034&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This gets even more interesting! The artwork is not from just one specific source — there was a &lt;strong&gt;surge of public works between 1935 and 1942&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Let’s use our custom &lt;code&gt;&amp;lt;Gallery /&amp;gt;&lt;/code&gt; view to explore a subset of these works — maybe seeing them will give us some insight.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Gallery&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;notablePeriod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2025/exploring-open-access-art-using-the-deno-jupyter-kernel/images/016-0_u40CvXdHxGXBjEOp.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;So all these artworks, despite being from different individuals, have a very similar style and medium. That’s something worth digging into — what might explain this pattern?&lt;/p&gt;
&lt;p&gt;Thanks to our custom &lt;code&gt;&amp;lt;Gallery /&amp;gt;&lt;/code&gt; component, we can click on artworks and view their metadata. Sampling a few, we see that they all belong to the &lt;a href="https://www.nga.gov/features/exhibitions/outliers-and-american-vanguard-artist-biographies/index-of-american-design.html"&gt;“Index of American Design”&lt;/a&gt; collection.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Conceived as an effort to document and preserve American folk and decorative arts, the Index of American Design consists of &lt;strong&gt;18,257&lt;/strong&gt; watercolor renderings created between &lt;strong&gt;1935 and 1942&lt;/strong&gt; as part of a Federal Art Project (FAP) work-relief program. Around 400 artists meticulously recreated textiles, woodcarvings, weathervanes, and other objects from across the U.S., aiming to establish a distinct American visual lineage. The project was ultimately housed at the National Gallery of Art, becoming a widely exhibited visual archive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s it! We found a &lt;strong&gt;large&lt;/strong&gt; &lt;strong&gt;collection of public works funded by a federal program during the Great Depression&lt;/strong&gt; — clear in both the data and historical context.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Using Deno’s Jupyter kernel, we explored the NGA Open Access dataset, combining static plots, interactive widgets, and custom JSX components to uncover patterns in the data. This led us to a historical insight — the Index of American Design, a public art project from the 1930s.&lt;/p&gt;
&lt;p&gt;With Deno, JSX, and anywidget, we combine data analysis with interactive web-based visualization, making it easy to build domain-specific tools for deeper exploration.&lt;/p&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
</content><category term="anywidget"/><category term="JavaScript"/><category term="kernels"/><category term="visualization"/></entry><entry><title>Bringing Modern JavaScript to the Jupyter Notebook</title><link href="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/" rel="alternate"/><published>2023-09-24T03:34:00+00:00</published><updated>2024-02-13T16:02:00+00:00</updated><author><name>Kyle Kelley</name></author><id>tag:blog.jupyter.org,2023-09-24:/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/</id><summary type="html">&lt;p&gt;The Deno kernel is the freshest runtime for Jupyter&lt;/p&gt;
</summary><content type="html">&lt;figure&gt;
&lt;img alt="Visualizing Data via Deno, TypeScript, and VegaLite in JupyterLab" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/001-1_dioQ00ymlsIb1PPH_SBdjg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Visualizing Data via Deno, TypeScript, and VegaLite in JupyterLab&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I never expected &lt;a href="https://deno.com/blog/v1.37"&gt;this&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;using modern JS, TS, and npm in jupyter notebooks&lt;/p&gt;
&lt;p&gt;👇&lt;/p&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/009-F6k0IzXbQAA1afQ.jpg" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;— &lt;a href="https://x.com/deno_land"&gt;Deno (@deno_land)&lt;/a&gt;, &lt;a href="https://x.com/deno_land/status/1704978299615957200"&gt;September 21, 2023&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://deno.land"&gt;Deno&lt;/a&gt; brings TypeScript, JavaScript, npm, and ES Modules to Jupyter with an easy to install kernel.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Deno Kernel is the first language runtime with a builtin jupyter kernel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There’s no better time to &lt;a href="https://docs.deno.com/runtime/manual/getting_started/installation"&gt;get started with Deno&lt;/a&gt; than now. Once deno is installed, run the &lt;a href="https://docs.deno.com/runtime/manual/tools/jupyter"&gt;deno jupyter kernel installation&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;deno&lt;span class="w"&gt; &lt;/span&gt;jupyter&lt;span class="w"&gt; &lt;/span&gt;--unstable&lt;span class="w"&gt; &lt;/span&gt;--install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;img alt="Screen recording of clicking to select a kernel in VS Code. The users clicks “Select Kernel” in their notebook and a “Select Kernel” dialog comes up in the top middle. They click “Jupyter Kernel” and a listing of several Python kernels comes up. They select Deno and it drops them into the notebook. They type in console.log(“HOORAY!”) and run it. Out pops HOORAY!" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/002-1__KUkFmq1lO3H-tx4XJPNew.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Enabling Deno in VS Code&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Since Deno supports web standards like &lt;code&gt;fetch&lt;/code&gt; out of the box, loading data is simple and easy.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="That’s so fetch" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/003-1_ndB49_FE9Mq8-9YSNFLuWw.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;That’s so fetch&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not only is this a fast &amp;amp; secure runtime, it’s fun. The timing is great too — &lt;a href="https://platform.noteable.io/blog/dataframes-answers-for-solutions"&gt;DataFrames&lt;/a&gt; aren’t just for &lt;a href="https://dataframes.juliadata.org/stable/"&gt;Julia&lt;/a&gt;, &lt;a href="https://pandas.pydata.org/"&gt;Python&lt;/a&gt;, &lt;a href="https://cran.r-project.org/doc/manuals/r-release/R-intro.html#Data-frames"&gt;R&lt;/a&gt;, and &lt;a href="https://www.pola.rs/"&gt;Rust&lt;/a&gt;. &lt;a href="https://github.com/pola-rs/nodejs-polars"&gt;Polars provides DataFrames for JavaScript&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Manipulating DataFrames in JavaScript" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/004-1_m6aEUA3UifW-qNnPu8etPw.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Manipulating DataFrames in JavaScript&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;What’s surprising me is how easily I can use &lt;a href="https://github.com/rgbkrk/denotebooks/blob/main/10.2_Polar%20DataFrames.ipynb"&gt;D3 with DataFrames&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="D3 and Polars playing well together in a notebook" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/005-1_BBPR6Od9aEwZanntimfkTA.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;D3 and Polars playing well together in a notebook&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We can draw plots directly to a canvas.&lt;/p&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/006-1_ZF4Q_S9RVCcf_fLe191kQw.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;as well as use any library that emits SVG&lt;/p&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/007-1_i8svfV32WtGxOxKCQv0-UA.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;This enables a broad range of JavaScript developers and data professionals to iterate in a notebook &lt;strong&gt;writing documentation that runs&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you want to explore and visualize data with JavaScript/TypeScript, check out &lt;a href="https://deno.com"&gt;Deno&lt;/a&gt; and their &lt;a href="https://docs.deno.com/runtime/manual/tools/jupyter"&gt;docs on getting started with Jupyter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you know Rust, come &lt;a href="https://github.com/denoland/deno"&gt;hack on Deno&lt;/a&gt;. If you don’t know rust, like me, come hack on Deno anyway. You can learn. The Deno team is a friendly group of people who believe deeply in the power of TypeScript, Jupyter, and standards.&lt;/p&gt;
&lt;p&gt;You can also contribute by &lt;a href="https://github.com/denoland/deno/issues"&gt;filing bugs&lt;/a&gt;, writing notebooks, and sharing the Deno kernel with others.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Jupyter Denos" src="https://blog.jupyter.org/posts/2023/bringing-modern-javascript-to-the-jupyter-notebook/images/008-1_Lq2UGOudjDsPyzNFtHXr2Q.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Jupyter Denos&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Happy Notebooking!&lt;/p&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
</content><category term="JavaScript"/><category term="kernels"/></entry><entry><title>anywidget: Jupyter Widgets Made Easy</title><link href="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/" rel="alternate"/><published>2023-07-11T23:09:00+00:00</published><updated>2023-07-11T23:09:00+00:00</updated><author><name>Trevor Manz</name></author><id>tag:blog.jupyter.org,2023-07-11:/posts/2023/anywidget-jupyter-widgets-made-easy/</id><summary type="html">&lt;p&gt;anywidget is a Python library that makes it simple and enjoyable to create custom Jupyter Widgets that run in classic Jupyter notebooks, JupyterLite, JupyterLab, Google Colab, VS Code, and more.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/manzt/anywidget"&gt;&lt;strong&gt;anywidget&lt;/strong&gt;&lt;/a&gt; is a Python library that makes it simple and enjoyable to create custom Jupyter Widgets that run in classic Jupyter notebooks, JupyterLite, JupyterLab, Google Colab, VS Code, and more. It focuses on:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Prototyping:&lt;/strong&gt; Create custom widgets right within your notebook.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Portability:&lt;/strong&gt; Share widgets as Python scripts or pip-installable packages.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Productivity:&lt;/strong&gt; Enjoy a modern front-end developer experience that supports real-time updates and removes manual project setup and other boilerplate.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;img alt="Real-time widget development in JupyterLab with anywidget. Changes to JavaScript source code are immediately reflected in the front end without requiring a page reload or clearing model state." src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/001-1_VCG-57YX8IrfTyeby3Ybjg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;&lt;strong&gt;anywidget&lt;/strong&gt; enables real-time widget development entirely from within JupyterLab&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;Skip ahead to see an &lt;a href="#a-realistic-example"&gt;example&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="why-making-widgets-is-hard"&gt;Why making widgets is hard&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Basics.html"&gt;Jupyter Widgets&lt;/a&gt; enrich notebooks with interactive JavaScript-based views and controls for Python objects in the Jupyter kernel. They enable a wide range of users, from students to professionals, to tailor their programming environment with custom or ready-made tools to interact with their programs and explore data. Consider a machine learning researcher adjusting model parameters with a slider or a computational biologist navigating a genome browser programmatically.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="A Jupyter Widget is comprised of two components: a JavaScript front-end and a Python object in the Jupyter kernel." src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/002-0_k0vqkYqq7F0MaF7q.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Jupyter Widgets link JavaScript-based views and controls with objects in the Jupyter kernel&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A useful feature of widgets is that they may be packaged and distributed as pip-installable modules, offering reusable components to address both general and specialized needs. For example, the &lt;a href="https://ipywidgets.readthedocs.io/en/stable/"&gt;ipywidgets&lt;/a&gt; project provides basic elements like form controls and layout containers, while numerous community projects offer custom widgets for domain-specific tasks (e.g., &lt;a href="https://github.com/bqplot/bqplot"&gt;bqplot&lt;/a&gt;, &lt;a href="https://ipyvolume.readthedocs.io/en/latest/"&gt;ipyvolume&lt;/a&gt;, &lt;a href="https://ipyleaflet.readthedocs.io/en/latest/"&gt;ipyleaflet&lt;/a&gt;).&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Side-by-Side comparison of two large genomic interaction matrices in Jupyter notebook using the interactive HiGlass browser." src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/003-1_vSfRgAQN2UnfDlYmhjruSg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Comparing two massive genomic contract matrices within Jupyter using a custom widget from &lt;a href="https://github.com/higlass/higlass-python"&gt;higlass-python&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;However, the growing number of Jupyter environments supporting &lt;code&gt;.ipynb&lt;/code&gt; files complicates custom widget creation and sharing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers must individually adapt front-end code to meet subtle, often undocumented, requirements of each notebook platform.&lt;/li&gt;
&lt;li&gt;Basic prototyping involves setting up a local Python package and manually installing extensions.&lt;/li&gt;
&lt;li&gt;Proper widget distribution requires deep understanding of both Python and JavaScript packaging.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve expanded on these challenges &lt;a href="https://anywidget.dev/blog/introducing-anywidget/#the-multi-platform-problem"&gt;previously&lt;/a&gt;, but to put it simply: traditional widget development has a steep learning curve and maintenance is both error-prone and tedious.&lt;/p&gt;
&lt;h2 id="a-universal-widget-adapter"&gt;A universal widget adapter&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;anywidget&lt;/strong&gt; introduces a fresh approach for creating and sharing custom widgets. It lets you avoid traditional development complexities, simplifying the process and making it easier than ever to start building widgets.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Development without anywidget" src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/004-0_W8GArznb6ZRmcxQ9.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Development &lt;strong&gt;without&lt;/strong&gt; anywidget&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;anywidget&lt;/strong&gt; is &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; a new framework, but rather a compatibility layer around traditional Jupyter Widgets. It utilizes the standard module system now found in web browsers to let widget developers write front-end code that executes universally. Think of &lt;strong&gt;anywidget&lt;/strong&gt; as an adapter that runs your widget’s JavaScript across various notebook environments.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Development with anywidget" src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/005-0_be82R4Wuyn-zpc_7.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Development &lt;strong&gt;with&lt;/strong&gt; anywidget&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;With &lt;strong&gt;anywidget,&lt;/strong&gt; a custom widget consists of two components: a Python class and an &lt;a href="https://nodejs.org/api/esm.html#modules-ecmascript-modules"&gt;ECMAScript module&lt;/a&gt; (ESM) — or web-standard JavaScript. You just write ESM and &lt;strong&gt;anywidget&lt;/strong&gt; handles the platform-specifics quirks.&lt;/p&gt;
&lt;p&gt;It takes less than 20 lines of code to &lt;a href="https://github.com/jupyter-widgets/widget-cookiecutter/tree/master/%7B%7Bcookiecutter.github_project_name%7D%7D"&gt;recreate&lt;/a&gt; a simple “Hello World” widget.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;anywidget&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;traitlets&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExampleWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anywidget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnyWidget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_esm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    export function render({ model, el }) {&lt;/span&gt;
&lt;span class="s2"&gt;        el.classList.add(&amp;quot;custom-widget&amp;quot;);&lt;/span&gt;
&lt;span class="s2"&gt;        function valueChanged() {&lt;/span&gt;
&lt;span class="s2"&gt;            el.textContent = model.get(&amp;quot;value&amp;quot;);&lt;/span&gt;
&lt;span class="s2"&gt;        }&lt;/span&gt;
&lt;span class="s2"&gt;        valueChanged();&lt;/span&gt;
&lt;span class="s2"&gt;        model.on(&amp;quot;change:value&amp;quot;, valueChanged);&lt;/span&gt;
&lt;span class="s2"&gt;    }&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;_css&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    .custom-widget {&lt;/span&gt;
&lt;span class="s2"&gt;        background-color: lightseagreen;&lt;/span&gt;
&lt;span class="s2"&gt;        padding: 0px 2px;&lt;/span&gt;
&lt;span class="s2"&gt;    }&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traitlets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello World&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ExampleWidget&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can &lt;strong&gt;copy and paste&lt;/strong&gt; this code directly into Jupyter notebooks, JupyterLite, JupyterLab, Google Colab, or VS Code and it just works.&lt;/p&gt;
&lt;p&gt;No installation, build configuration, or bundlers.&lt;/p&gt;
&lt;p&gt;By comparison, creating an identical &lt;code&gt;ExampleWidget&lt;/code&gt; the traditional way involves forking a Python repo template (including ~50 files), building JavaScript source code with Node.js and webpack, and manually installing the local extensions in classic notebooks or JupyterLab (not compatible with Google Colab or VS Code).&lt;/p&gt;
&lt;h2 id="a-realistic-example"&gt;A realistic example&lt;/h2&gt;
&lt;p&gt;This tutorial demonstrates using &lt;strong&gt;anywidget&lt;/strong&gt; to address a &lt;a href="https://github.com/altair-viz/altair/issues/1153"&gt;long-standing issue&lt;/a&gt; in the &lt;a href="https://github.com/altair-viz/altair"&gt;Altair&lt;/a&gt;: retrieving data from a brush selection back into Python. You can either follow along here or execute the notebook yourself &lt;a href="https://colab.research.google.com/gist/manzt/2b83d3461c40ac7b43fde29515cb8077/extending_altair_with_anywidget.ipynb"&gt;in Colab&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We’ll begin with an interactive scatterplot example from the Altair &lt;a href="https://github.com/altair-viz/altair#example"&gt;docs&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;altair&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;alt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;vega_datasets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;

&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;brush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selection_interval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark_point&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Horsepower&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Miles_per_Gallon&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Origin&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lightgray&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;brush&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark_bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Origin&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Origin&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;count(Origin)&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;brush&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/006-1_KIB7LU5YpN90zEh0-Czilg.mp4" alt="An interactive scatterplot displaying cars. Miles per gallon by horsepower. Individual points (cars) are colored by their country of origin." loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Notice how the brush selection on the scatter plot filters the data in the linked bar chart. Neat, but unfortunately we are unable inspect the selected points because the selection is processed in JavaScript, and Altair lacks a mechanism to communicate back to Python.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sounds like a job for a widget!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Behind the scenes, Altair produces JSON that follows the &lt;a href="https://vega.github.io/vega-lite/"&gt;Vega-Lite&lt;/a&gt; visualization grammar. With &lt;strong&gt;anywidget&lt;/strong&gt;, we can create a custom widget to render this validated JSON independently and additionally relay the JavaScript-based selections back to Python.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;anywidget&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;traitlets&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ChartWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anywidget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnyWidget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_esm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    import embed from &amp;quot;https://cdn.jsdelivr.net/npm/vega-embed@6/+esm&amp;quot;;&lt;/span&gt;
&lt;span class="s2"&gt;    &lt;/span&gt;
&lt;span class="s2"&gt;    export async function render({ model, el }) {&lt;/span&gt;
&lt;span class="s2"&gt;        let spec = JSON.parse(model.get(&amp;quot;spec&amp;quot;));&lt;/span&gt;
&lt;span class="s2"&gt;        let api = await embed(el, spec);&lt;/span&gt;
&lt;span class="s2"&gt;        api.view.addSignalListener(spec.params[0].name, (_, update) =&amp;gt; {&lt;/span&gt;
&lt;span class="s2"&gt;            console.log(update);&lt;/span&gt;
&lt;span class="s2"&gt;            model.set(&amp;quot;selection&amp;quot;, update);&lt;/span&gt;
&lt;span class="s2"&gt;            model.save_changes();&lt;/span&gt;
&lt;span class="s2"&gt;        });&lt;/span&gt;
&lt;span class="s2"&gt;    }&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traitlets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traitlets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What’s happening here? Our custom &lt;code&gt;ChartWidget&lt;/code&gt; is defined by subclassing &lt;code&gt;anywidget.AnyWidget&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_esm&lt;/code&gt; (line 6) defines the ECMAScript module for the widget. It imports necessary rendering dependencies (&lt;a href="https://github.com/vega/vega-embed"&gt;vega-embed&lt;/a&gt;) and exports &lt;code&gt;render&lt;/code&gt;: a function to displays the Vega-Lite chart and (importantly) links the JavaScript-based selection to Python.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spec&lt;/code&gt; and &lt;code&gt;selection&lt;/code&gt; (lines 19–20) define stateful properties via &lt;a href="https://github.com/ipython/traitlets"&gt;traitlets&lt;/a&gt; that are accessible by both client JavaScript and Python. These represent the Vega-Lite JSON specification and the JavaScript-based brush selection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now our ESM takes care of rendering instead of Altair.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;chart_widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChartWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# Prints updates log console (JupyterLab: View &amp;gt; Show Log Console)&lt;/span&gt;
&lt;span class="n"&gt;chart_widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;selection&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;chart_widget&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/007-1_OsSPlHcgfeF8ehIPGtFmpw.mp4" alt="The same scatterplot as before except the JavaScript-based brush selection is printed to the (Python) Jupyter console any time it changes." loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The original cross-filtering behavior stays the same, but now we have access to the JavaScript selection &lt;em&gt;in Python&lt;/em&gt; via &lt;code&gt;chart_widget.selection&lt;/code&gt;. The Python callback (line 4) prints the synchronized selection in the JupyterLab log console any time it changes.&lt;/p&gt;
&lt;p&gt;Finally, we can present this data more effectively using a second widget that displays our selection as &lt;code&gt;pd.DataFrame&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ipywidgets&lt;/span&gt;

&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipywidgets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clear_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;on_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;
    &lt;span class="n"&gt;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;chart_widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_change&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;selection&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;ipywidgets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VBox&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;chart_widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p class="standalone-image"&gt;&lt;img src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/008-1_0vn1t-SlN8LvAKLrVAiHoA.mp4" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;on_change&lt;/code&gt; callback (line 6) is invoked whenever the &lt;code&gt;chart_widget.selection&lt;/code&gt; changes (line 13). It filters the original data based on the selection bounds and displays the given subset as a table within &lt;code&gt;output&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With just a few lines of code, we enhanced Altair with new functionality using &lt;strong&gt;anywidget&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ChartWidget&lt;/code&gt; can be shared in its current state. However, if we add more features, we could transition the JavaScript code from inline strings to separate files, gradually evolving the widget into a fully-fledged Python package. This incremental development is a feature of &lt;strong&gt;anywidget&lt;/strong&gt;, allowing prototypes to grow into robust tools over time.&lt;/p&gt;
&lt;h2 id="modern-web-development-meets-jupyter"&gt;Modern web development meets Jupyter&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;anywidget&lt;/strong&gt; further embraces modern JavaScript to reduce friction and make developing widgets more accessible and &lt;em&gt;fun.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Because &lt;strong&gt;anywidget&lt;/strong&gt; takes care of all the necessary plumbing, you no longer need to setup up a local Python package or manually install extensions to start building. Instead, you can prototype and share widget ideas directly from notebooks — just like regular Python scripts.&lt;/p&gt;
&lt;p&gt;Since v0.2, &lt;strong&gt;anywidget&lt;/strong&gt; allows you to use a file path to define your widget’s front-end code (i.e., the &lt;code&gt;_esm&lt;/code&gt; and &lt;code&gt;_css&lt;/code&gt; attributes). During development, &lt;strong&gt;anywidget&lt;/strong&gt; will monitor for changes and immediately refresh the UI without requiring a full page reload or resetting widget model state.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;anywidget&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;traitlets&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExampleWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anywidget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnyWidget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_esm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;index.js&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;_css&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;styles.css&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traitlets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello World&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This &lt;a href="https://anywidget.dev/blog/anywidget-02/#native-hot-module-replacement-hmr"&gt;feature&lt;/a&gt; has been popularized by modern web frameworks, but &lt;strong&gt;anywidget&lt;/strong&gt; introduces it for the first time to Jupyter. See this real-time development workflow it &lt;a href="https://www.youtube.com/watch?v=600PU6E4Srw&amp;amp;t=305s"&gt;in action&lt;/a&gt; or try it out yourself!&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Real-time widget development with anywidget entirely from within JupyterLab. Changes to widget front-end source code are displayed instantly in the UI as changes are made." src="https://blog.jupyter.org/posts/2023/anywidget-jupyter-widgets-made-easy/images/001-1_VCG-57YX8IrfTyeby3Ybjg.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Real-time widget development entirely from within JupyterLab&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="try-it-out"&gt;Try it out!&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;anywidget&lt;/strong&gt; is available on &lt;a href="https://github.com/manzt/anywidget"&gt;GitHub&lt;/a&gt; and &lt;a href="https://pypi.org/project/anywidget/"&gt;PyPI&lt;/a&gt; and may be installed via &lt;code&gt;pip&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;anywidget[dev]&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I hope using &lt;strong&gt;anywidget&lt;/strong&gt; is simple and enjoyable. I have found it valuable in my work as a biomedical visualization researcher, and it’s been exciting to see the positive reception from the wider Jupyter community.&lt;/p&gt;
&lt;p&gt;Since its release a few months ago, &lt;strong&gt;anywidget&lt;/strong&gt; already been adopted by several notable projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uwdata/mosaic"&gt;Mosaic&lt;/a&gt;: an extensible framework for linking interactive views to databases for scalable data processing.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/flekschas/jupyter-scatter"&gt;jupyter-scatter&lt;/a&gt;: interactive 2D scatter plots that scale to millions of points and support view linking.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/opengeos/mapwidget"&gt;Mapwidget&lt;/a&gt;: interactive 2D/3D maps using popular JavaScript libraries with bidirectional communication, such as Cesium, Mapbox, MapLibre, Leaflet, and OpenLayers&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/juba/pyobsplot"&gt;pyobsplot&lt;/a&gt;: a Python interface for &lt;a href="https://observablehq.com/plot/"&gt;Observable Plot&lt;/a&gt; that supports Pandas and Polars dataframes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/widgetti/ipyreact"&gt;ipyreact&lt;/a&gt;: a Python library for authoring Jupyter Widgets with React components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;… demonstrating its current viability as an alternative to traditional widget development. I am committed to maintaining the simplicity that defines &lt;strong&gt;anywidget&lt;/strong&gt; while exploring ideas to further modernize widgets like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A simple decorator-based API&lt;/li&gt;
&lt;li&gt;Using builtin dataclasses or libraries like &lt;a href="https://pydantic.dev/"&gt;Pydantic&lt;/a&gt; or &lt;a href="https://jcristharif.com/msgspec/"&gt;msgspec&lt;/a&gt; to define widget models and serialization logic&lt;/li&gt;
&lt;li&gt;Handling TypeScript and JSX for front-end code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re curious about custom widgets or facing difficulties in widget development, please give &lt;strong&gt;anywidget&lt;/strong&gt; a try and &lt;a href="https://github.com/manzt/anywidget/issues"&gt;share your experience&lt;/a&gt;. Happy coding!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/manzt/anywidget"&gt;GitHub - anywidget: custom jupyter widgets made easy&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’d like to extend my gratitude to the numerous contributors to our documentation and specifically &lt;a href="https://github.com/tlambert03"&gt;Talley Lambert&lt;/a&gt; for his work towards modernizing widgets from a Python perspective. He’s played a key role in developing our &lt;a href="https://anywidget.dev/en/experimental/"&gt;&lt;code&gt;experimental&lt;/code&gt;&lt;/a&gt; API, which allows more flexible communication between JavaScript and Python without requiring ipywidgets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
</content><category term="anywidget"/><category term="JavaScript"/><category term="widgets"/></entry><entry><title>Build a Jupyter Widget with React and TypeScript</title><link href="https://blog.jupyter.org/posts/2021/build-a-jupyter-widget-with-react-and-typescript/" rel="alternate"/><published>2021-07-30T19:02:00+00:00</published><updated>2021-08-05T18:00:00+00:00</updated><author><name>John Waidhofer</name></author><id>tag:blog.jupyter.org,2021-07-30:/posts/2021/build-a-jupyter-widget-with-react-and-typescript/</id><summary type="html">&lt;p&gt;When using a Jupyter Notebook to work on data science projects, I’ve found small user interface abstractions to be very useful. Scrubbing a slider or uploading a file can be more intuitive than editing a script and running it manually.&lt;/p&gt;</summary><content type="html">&lt;figure&gt;
&lt;img alt="Photo by Adi Goldstein on Unsplash" src="https://blog.jupyter.org/posts/2021/build-a-jupyter-widget-with-react-and-typescript/images/001-0_lnZS93JgKwZAZD1m.jpg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Photo by &lt;a href="https://unsplash.com/@adigold1?utm_source=medium&amp;amp;utm_medium=referral"&gt;Adi Goldstein&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&gt;Unsplash&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;When using a Jupyter Notebook to work on data science projects, I’ve found small user interface abstractions to be very useful. Scrubbing a slider or uploading a file can be more intuitive than editing a script and running it manually. This is especially true when a notebook is shared with others, since most people already have a basic knowledge about user interface elements.&lt;/p&gt;
&lt;p&gt;Jupyter Widgets are fantastic tools for simplifying Jupyter Notebook workflows with custom user interfaces. While widgets are versatile and composable, sometimes the default implementations provided by Jupyter don’t have the exact functionality we are looking for. Luckily, we are able to create Custom Widgets to suit our needs using web technologies. To get an idea of how Custom Widgets work, we are going to build a sleek color picker for JupyterLab using React.&lt;/p&gt;
&lt;h2 id="setup"&gt;Setup&lt;/h2&gt;
&lt;p&gt;Before we can start coding, there is some basic boilerplate to download. Start by installing &lt;code&gt;cookiecutter&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip install cookiecutter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then download the React widget boilerplate code.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cookiecutter https://github.com/Waidhoferj/jupyter-widget-react-cookiecutter.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;cookiecutter&lt;/code&gt; template will guide us through some setup questions. The &lt;code&gt;author_name&lt;/code&gt;, &lt;code&gt;author_email&lt;/code&gt;, and &lt;code&gt;github_project_name&lt;/code&gt; fields are the most important. We can leave the others blank or go with the default values.&lt;/p&gt;
&lt;p&gt;Next, create a development environment using &lt;a href="https://conda.io/projects/conda/en/latest/user-guide/install/index.html"&gt;Anaconda&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;conda create -n jupyter-react-widget -c conda-forge nodejs yarn python jupyterlab

conda activate jupyter-react-widget
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Every package we install for this project will now be contained in a neat bundle.&lt;/p&gt;
&lt;p&gt;Inside the project directory created by &lt;code&gt;cookiecutter&lt;/code&gt;, run the following script to install the dependencies that we’ll use to build the widget. Then connect our widget environment to JupyterLab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pip install -e &amp;quot;.[test, examples]&amp;quot;

jupyter labextension develop --overwrite .

yarn run build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thats it for the setup! Let’s see what the default widget can do.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="interacting-with-the-widget"&gt;Interacting with the Widget&lt;/h2&gt;
&lt;p&gt;To test out the widget, run &lt;code&gt;jlpm watch&lt;/code&gt; in the project directory and open up JupyterLab with &lt;code&gt;jupyter lab&lt;/code&gt; in another terminal window. In JuypterLab, open the notebook file located at &lt;code&gt;&amp;lt;project&amp;gt;/examples/introduction.ipynb&lt;/code&gt;. Run all of the notebook cells to examine the state of the widget. In its current form, the widget displays a greeting alongside an input text field. Typing into the input field will update the target of the greeting. The value of the text box is automatically synced with &lt;code&gt;w.value&lt;/code&gt;, allowing us to access the input’s contents as a Python string. With the default setup working, we can start designing our own custom widget.&lt;/p&gt;
&lt;h2 id="building-a-color-picker"&gt;Building a Color Picker&lt;/h2&gt;
&lt;p&gt;Let’s take a look at the architecture of a Custom Widget. The Jupyter Widget system allows developers to send data between Python and TypeScript using a Model-View-Controller Architecture. The Python Widget Model represents the state of the widget. The TypeScript Controller edits and responds to changes in the Python model based on user input. These updates are rendered in the notebook using React.&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="Data is passed from the Python model to the TypeScript controller to the React view and back" src="https://blog.jupyter.org/posts/2021/build-a-jupyter-widget-with-react-and-typescript/images/002-1_cEy6OgSDz854q4Z0VJ59aQ.jpeg" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Data is passed from the Python model to the TypeScript controller to the React view and back&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We can start building our color picker in &lt;em&gt;example.py&lt;/em&gt; by adding a color property to the model.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExampleWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DOMWidget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# Your widget state goes here. Make sure to update the corresponding&lt;/span&gt;
    &lt;span class="c1"&gt;# JavaScript widget state (defaultModelProperties) in widget.ts&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Jupyter&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Jupyter&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The color property is set equal to the &lt;code&gt;Unicode&lt;/code&gt; traitlet, which represents a string of characters. The &lt;code&gt;.tag(sync=True)&lt;/code&gt; method syncs the value of the color property with the TypeScript widget state.&lt;/p&gt;
&lt;p&gt;In &lt;em&gt;widget.ts&lt;/em&gt;, we define the corresponding TypeScript representation of color in the &lt;code&gt;defaultModelProperties&lt;/code&gt; object. This object mirrors the structure of the Python model, allowing us to access the model state in our frontend code.&lt;/p&gt;
&lt;figure&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;defaultModelProperties&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Hello World&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;
&lt;p&gt;Add color to the default model properties object&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now that we have defined our model, the rest of the work can be done in React. If you’d like to flex your web development skills and design a custom color picker, go for it! Alternatively, we can get a great plug-and-play interface by installing &lt;code&gt;react-colorful&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;jlpm add react-colorful
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Create a &lt;em&gt;ColorPicker.tsx&lt;/em&gt; file in the &lt;em&gt;src&lt;/em&gt; folder. We can build out the component in a few lines of code:&lt;/p&gt;
&lt;figure&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;react&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HexColorPicker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;react-colorful&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;useModelState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;./hooks/widget-model&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ColorPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;useModelState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;HexColorPicker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;
&lt;p&gt;The ColorPicker React component&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Let’s walk through what’s happening here. At the top of the file, we import React, the react-colorful component, and a hook called &lt;code&gt;useModelState&lt;/code&gt;. The &lt;code&gt;useModelState&lt;/code&gt; hook operates like the &lt;a href="https://reactjs.org/docs/hooks-state.html"&gt;&lt;code&gt;useState&lt;/code&gt; hook&lt;/a&gt; but instead of accepting a default value as a parameter, it takes the name of a property from our Python model. The hook will automatically update the React view when the referenced property updates in Python. Also, the React widget can directly update the Python model with the &lt;code&gt;setColor&lt;/code&gt; function. By passing &lt;code&gt;color&lt;/code&gt; and &lt;code&gt;setColor&lt;/code&gt; to the &lt;code&gt;HexColorPicker&lt;/code&gt;, the Python widget model will reflect the chosen color whenever the user makes a selection in the interface.&lt;/p&gt;
&lt;p&gt;To display the color picker in our notebook, add the component to &lt;em&gt;ReactWidget.tsx&lt;/em&gt;, which is the top level React file in the widget system. Import the &lt;code&gt;ColorPicker&lt;/code&gt; component at the top of the file and make it the return value of &lt;code&gt;ReactWidget&lt;/code&gt;. We can delete all of the boilerplate within the &lt;code&gt;ReactWidget&lt;/code&gt; component in the process. Additionally, delete the &lt;code&gt;useModelState&lt;/code&gt; import at the top of the file, since it is no longer needed.&lt;/p&gt;
&lt;figure&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ColorPicker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;./ColorPicker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ReactWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WidgetProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ColorPicker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;
&lt;p&gt;Add ColorPicker to ReactWidget&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To test this out, &lt;a href="https://support.labs.cognitiveclass.ai/knowledgebase/articles/857388-how-to-restart-the-jupyter-kernel"&gt;restart the python kernel&lt;/a&gt; in JupyterLab and refresh the browser window. We will now see a color picker where the input field used to be. Pick a color and print out &lt;code&gt;w.color&lt;/code&gt; to get the hex code in Python. Congrats! You now know how to create Jupyter Widgets using React!&lt;/p&gt;
&lt;p&gt;The principles that we covered here can be applied to create more complex applications. To add additional properties, define them in both &lt;em&gt;example.py&lt;/em&gt; and &lt;em&gt;widget.ts&lt;/em&gt;, then interact with the data via the &lt;code&gt;useModelState&lt;/code&gt; hook in a React component. Go build some amazing widgets!&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Waidhoferj/jupyter-widget-react-cookiecutter#hooks"&gt;Additional React Jupyter Widget Hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Custom.html#Other-traitlet-types"&gt;Data types for the Python Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Custom.html"&gt;The official IPythonWidget tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
</content><category term="JavaScript"/><category term="widgets"/></entry><entry><title>Authoring Custom Jupyter Widgets</title><link href="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/" rel="alternate"/><published>2018-03-05T12:12:00+00:00</published><updated>2018-03-05T12:12:00+00:00</updated><author><name>Olivier Borderies</name></author><id>tag:blog.jupyter.org,2018-03-05:/posts/2018/authoring-custom-jupyter-widgets/</id><summary type="html">&lt;p&gt;A Hands-On Guide&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a href="https://jupyter.org/widgets"&gt;Jupyter interactive widgets&lt;/a&gt; enhance the notebook experience by allowing users to create graphical user interfaces. They enable richer interaction with the data and computing resources.&lt;/p&gt;
&lt;p&gt;While the base &lt;a href="https://github.com/jupyter-widgets/ipywidgets"&gt;ipywidgets&lt;/a&gt; library comes with a &lt;a href="https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html"&gt;number of controls&lt;/a&gt; such as sliders, buttons, and dropdowns, it is in fact much more than a collection of basic controls: it is the foundation of a framework upon which one can build arbitrarily complex interactions.&lt;/p&gt;
&lt;p&gt;Examples of custom widget libraries built upon the foundational package are&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bloomberg/bqplot"&gt;bqplot&lt;/a&gt;, a d3-Jupyter bridge, and a 2-D plotting library following the constructs of the Grammar of Graphics,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ellisonbg/ipyleaflet"&gt;ipyleaflet&lt;/a&gt;, a leaflet-Jupyter bridge enabling maps visualization in the Jupyter notebook,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jovyan/pythreejs"&gt;pythreejs&lt;/a&gt;, a 3-D visualization library bringing the functionalities of Three.js into the Jupyter notebook,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/maartenbreddels/ipyvolume"&gt;ipyvolume&lt;/a&gt;, a 3-D plotting library also based on Three.js enabling volume rendering, quiver plots and much more.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="jupyter-widgets-a-bridge-between-two-continents"&gt;Jupyter Widgets, a Bridge Between Two Continents&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Jupyter widgets provide a means to bridge the kernel and the rich ecosystem of JavaScript visualization libraries for the web browser. It is an amazing opportunity for scientific developers to use all these resources in their language of choice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This article is meant to serve as a guide for developers interested in authoring a custom widget library and bridge the gap between the basic examples of the official documentation and fully-fledged visualization libraries like the ones listed above.&lt;/p&gt;
&lt;p&gt;A number of resources are provided alongside the article, including the complete source code of the examples, notebooks, and Binder links.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In this post, we focus on the &lt;strong&gt;Python&lt;/strong&gt; back-end, even though &lt;a href="https://github.com/jupyter/jupyter/wiki/Jupyter-kernels"&gt;dozens of Jupyter kernels&lt;/a&gt; exist. The Python kernel is the reference implementation of the Jupyter protocol and remains the most featureful. We should also mention QuantStack’s &lt;a href="https://blog.jupyter.org/posts/2017/interactive-workflows-for-c-with-jupyter/"&gt;Xeus&lt;/a&gt;, a native C++ implementation of Jupyter kernel protocol, which supports interactive widgets. Xeus is used as the foundation for the support of Jupyter widgets for the R and C++ kernels.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;
&lt;img alt="Xeus: C++ implementation of Jupyter kernel protocol" src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/001-1_7cPVSk9PpumGA6vM79kHwQ.webp" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;Xeus: C++ implementation of Jupyter kernel protocol&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="a-hands-on-guide"&gt;A Hands-On Guide&lt;/h2&gt;
&lt;p&gt;While the Jupyter community is thriving, and we start seeing a growth in the number of custom widget libraries as well. Although the learning curve from the examples of the &lt;a href="https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html"&gt;official Jupyter documentation&lt;/a&gt; to authoring state-of-the-art libraries like the ones listed above is steep and may be intimidating.&lt;/p&gt;
&lt;p&gt;We started on this path a few months ago and benefited from guidance from core Jupyter developers along the way. Now, we would like to share the lessons learned. We hope this will help turn this mountain trail into a new silk road!&lt;/p&gt;
&lt;p&gt;Let us start with the ‘Hello World’ example widget from the ipywidgets documentation, before moving on to more advanced use cases.&lt;/p&gt;
&lt;h2 id="1-improving-on-the-hello-world-example-from-the-documentation"&gt;1 — Improving on the Hello-World Example from the Documentation&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;This section is derived from the &lt;a href="http://ipywidgets.readthedocs.io"&gt;ipywidgets documentation&lt;/a&gt;. The code snippets are CC-0 licensed.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The idea behind Jupyter widgets is to enable a bi-directional communication channel between the kernel (back-end) and the JavaScript front-end. In Python, widgets are special objects which are automatically synchronized with a counterpart object in the JavaScript front-end. In the back-end, the change events are handled by the &lt;a href="http://traitlets.readthedocs.io/en/stable/"&gt;traitlets&lt;/a&gt; package, which implements the observer pattern, while on the JavaScript side, this is done with the &lt;a href="http://backbonejs.org/"&gt;Backbone.js&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;The front-end implementation follows the &lt;a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller"&gt;MVC (Model View Controller) pattern&lt;/a&gt;. This allows the rendering of the same widget in multiple cell outputs, where all views share the same model, analogous to printing out a string variable multiple times.&lt;/p&gt;
&lt;p&gt;The official documentation includes a &lt;a href="https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html"&gt;Hello-World example widget&lt;/a&gt;, which offers an example of synchronization from Python to JavaScript, but not the other way around. Our &lt;a href="https://github.com/PierreMarion23/jupyter-widget-hello-world-binder"&gt;jupyter-widget-hello-world-binder&lt;/a&gt; example provides a slightly more advanced version of it which demonstrates the bi-directional communication between the Python kernel and the JavaScript front-end. You can experiment with this widget on Binder or simply check out the example notebook with nbviewer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mybinder.org/v2/gh/PierreMarion23/jupyter-widget-hello-world-binder/master?filepath=hello_world.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/002-1_x6oPwz8nMiy4YIgEpczQHg.webp" alt="Launch Binder" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://nbviewer.jupyter.org/github/PierreMarion23/jupyter-widget-hello-world-binder/blob/master/hello_world.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/003-1_lK1Zi8_qorxl5ZrL_ZMc7w.webp" alt="Render with nbviewer" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let us briefly present the main aspects of the implementation.&lt;/p&gt;
&lt;p&gt;The JavaScript front-end defines a custom view by extending the base &lt;code&gt;DOMWidgetView&lt;/code&gt; class from the base package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HelloView&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;widgets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOMWidgetView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value_changed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;change:value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value_changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;value_changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Python back-end extends the corresponding base class from the &lt;code&gt;ipywidgets&lt;/code&gt; package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;HelloWidget&lt;/span&gt;(&lt;span class="n"&gt;widgets&lt;/span&gt;.&lt;span class="n"&gt;DOMWidget&lt;/span&gt;):
    &lt;span class="n"&gt;_view_name&lt;/span&gt; = &lt;span class="n"&gt;Unicode&lt;/span&gt;(&lt;span class="s"&gt;&amp;#39;HelloView&amp;#39;&lt;/span&gt;).&lt;span class="n"&gt;tag&lt;/span&gt;(&lt;span class="n"&gt;sync&lt;/span&gt;=&lt;span class="nb"&gt;True&lt;/span&gt;)
    &lt;span class="n"&gt;_view_module&lt;/span&gt; = &lt;span class="n"&gt;Unicode&lt;/span&gt;(&lt;span class="s"&gt;&amp;#39;hello&amp;#39;&lt;/span&gt;).&lt;span class="n"&gt;tag&lt;/span&gt;(&lt;span class="n"&gt;sync&lt;/span&gt;=&lt;span class="nb"&gt;True&lt;/span&gt;)
    &lt;span class="n"&gt;_view_module_version&lt;/span&gt; = &lt;span class="n"&gt;Unicode&lt;/span&gt;(&lt;span class="s"&gt;&amp;#39;0.1.0&amp;#39;&lt;/span&gt;).&lt;span class="n"&gt;tag&lt;/span&gt;(&lt;span class="n"&gt;sync&lt;/span&gt;=&lt;span class="nb"&gt;True&lt;/span&gt;)

    &lt;span class="nb"&gt;value&lt;/span&gt; = &lt;span class="n"&gt;Unicode&lt;/span&gt;(&lt;span class="s"&gt;&amp;#39;Hello World!&amp;#39;&lt;/span&gt;).&lt;span class="n"&gt;tag&lt;/span&gt;(&lt;span class="n"&gt;sync&lt;/span&gt;=&lt;span class="nb"&gt;True&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;value&lt;/code&gt; attribute get synchronized between the back-end and the front-end.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;value_changed&lt;/code&gt; callback, which changes the HTML representation of the widget is attached to changes of the value property with the line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;change:value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value_changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the &lt;a href="https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Custom.html"&gt;Hello-World&lt;/a&gt; example from the documentation, there is no way to change the JavaScript &lt;code&gt;value&lt;/code&gt; from the notebook front-end. We have added this feature in order to illustrate the bi-directional synchronization between JavaScript and Python. The idea is to trigger an event in the browser which will update the JavaScript model and then automatically - that’s the magic of ipywidgets - the Python back-end. These additional lines trigger the model update:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;// update the JavasScript model&lt;/span&gt;
&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;formElement&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// sync with Python&lt;/span&gt;
&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Conversely, when changing the value from the Python kernel the corresponding JavaScript value gets updated, as explained above.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href="https://github.com/PierreMarion23/jupyter-widget-hello-world-binder"&gt;jupyter-widget-hello-world-binder&lt;/a&gt; repo for more information. The notebook includes additional details and comments.&lt;/p&gt;
&lt;h2 id="2-first-example-involving-bi-directional-communication"&gt;2 — First Example Involving Bi-Directional Communication&lt;/h2&gt;
&lt;p&gt;In the previous section, widgets were entirely defined in the notebook. We now show how to move the implementation outside of the notebook document and produce a proper installable package.&lt;/p&gt;
&lt;h3 id="21-first-widget"&gt;2.1 — First Widget&lt;/h3&gt;
&lt;p&gt;To help you create your own custom widget, the Jupyter team provides a &lt;a href="https://github.com/jupyter-widgets/widget-cookiecutter"&gt;cookiecutter&lt;/a&gt; template, producing a custom Jupyter widget library containing all the boilerplate for packaging. The cookiecutter is initialized with the hello-world widget from the documentation.&lt;/p&gt;
&lt;p&gt;We used the widget cookiecutter to put the hello-world widget presented in the previous section into a well-organized GitHub repository. It contains&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the Python part in the &lt;code&gt;first_widget&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;the JavaScript part in the &lt;code&gt;js&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;README&lt;/code&gt; provides detailed instructions to install the package, together with information about how to enable the Jupyter extension, and tips for custom widget authors. It delves into the technical details a bit more than this overview blog post.&lt;/p&gt;
&lt;p&gt;Now that the basics of two-way synchronization are covered, you can &lt;strong&gt;create arbitrarily complex widgets!&lt;/strong&gt; The key is to identify the data you would like to synchronize between the front-end and the back-end. Then you can set up the events that will update this data when a change is detected using the building blocks above.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Now you can &lt;strong&gt;‘widget-ify’ any JavaScript library!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="22-barebones-setuppy"&gt;2.2 — Barebones &lt;code&gt;setup.py&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://github.com/jupyter-widgets/widget-cookiecutter/blob/master/%7B%7Bcookiecutter.github_project_name%7D%7D/setup.py"&gt;setup.py&lt;/a&gt; described in the official documentation tries to automate many of the build steps, at the cost of readability. Fortunately, packaging Jupyter widgets will work with the bare-bones &lt;code&gt;setup.py&lt;/code&gt; we provide.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The gain is a clearer, lighter (~100 less lines) &lt;code&gt;setup.py&lt;/code&gt;, giving you a better understanding of what is happening.&lt;/li&gt;
&lt;li&gt;The drawback is that you need one extra step to install the widget from source.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;#&lt;/span&gt; Example: d3-slider
$ git clone https://gitlab.com/oscar6echo/jupyter-widget-d3-slider.git
$ cd js
$ npm install
$ cd ..
$ pip install -e .

&lt;span class="gh"&gt;#&lt;/span&gt; Extra line `jupyter nbextension install`
$ jupyter nbextension install --py --symlink --sys-prefix jupyter_widget_d3_slider
$ jupyter nbextension enable --py --sys-prefix jupyter_widget_d3_slider
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;More importantly we thought that it was clearer to keep the build steps of the Python and JavaScript packages separated. Both build processes are well documented, making the steps easier to follow. Finally, the inclusion of compiled JavaScript bundles in the Python package, is more explicit.&lt;/p&gt;
&lt;h3 id="23-side-note-naming-conventions"&gt;2.3 — Side Note: Naming Conventions&lt;/h3&gt;
&lt;p&gt;Naming conventions for Python packages are covered by &lt;a href="https://www.python.org/dev/peps/pep-0008/#package-and-module-names"&gt;PEP8&lt;/a&gt;. They can be a bit tricky in the case of 2-words names like “first-widget”. Where to use underscore (_) or hyphen (-) ? Typically “-” is used in GitHub repositories, and URLs and JavaScript while any folder or file in a Python module can only contain “_”. In the case of a Jupyter widget there is an extra attention point: in the setup.py file, the &lt;code&gt;data_files&lt;/code&gt; argument in the &lt;code&gt;setup&lt;/code&gt; function is a list of tuples. For each, the first element (representing a path in the filesystem) contains “-” as it relates to JavaScript code while the paths in the second contain “_” as they represent paths in the Python package. For a full example see the &lt;a href="https://github.com/ocoudray/FirstWidget"&gt;first-widget&lt;/a&gt; repo and the detailed README.&lt;/p&gt;
&lt;h2 id="3-increasingly-complex-widgets"&gt;3 — Increasingly Complex Widgets&lt;/h2&gt;
&lt;p&gt;We made the following three widgets, gradually adding complexity. The first two examples are meant as educational examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/oscar6echo/jupyter-widget-d3-slider/"&gt;d3-slider&lt;/a&gt;, a d3.js-based slider,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ocoudray/jupyter-drawing-pad"&gt;drawing-pad&lt;/a&gt;, a 2-D drawing pad,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We hope that the last example will become more than a demonstration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/PierreMarion23/ipypivot"&gt;ipypivot&lt;/a&gt;, a visual Pivot Table UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case, the GitHub repository includes a demo notebook and the required boilerplate for &lt;a href="https://mybinder.org/"&gt;Binder&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="31-d3-slider"&gt;3.1 — d3-slider&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://gitlab.com/oscar6echo/jupyter-widget-d3-slider/"&gt;custom d3-slider widget&lt;/a&gt; wraps a &lt;a href="https://bl.ocks.org/mbostock/6452972"&gt;simple custom slider&lt;/a&gt; based on the fantastic &lt;a href="https://d3js.org/"&gt;d3.js library&lt;/a&gt;. You can run and try it on the &lt;a href="https://github.com/ocoudray/jupyter-d3-slider-binder"&gt;Binder repo&lt;/a&gt; or watch it on nbviewer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mybinder.org/v2/gh/ocoudray/jupyter-d3-slider-binder/master?filepath=Slider_d3_demo.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/002-1_x6oPwz8nMiy4YIgEpczQHg.webp" alt="Launch Binder" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://nbviewer.jupyter.org/urls/gitlab.com/oscar6echo/jupyter-widget-d3-slider/raw/master/notebooks/demo_d3_slider.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/003-1_lK1Zi8_qorxl5ZrL_ZMc7w.webp" alt="Render with nbviewer" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="d3-slider widget" src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/004-1_W8oXVTdQ4na6qbkOSpo88w.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;d3-slider widget&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;jupyter_widget_d3_slider
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="32-drawing-pad"&gt;3.2 — drawing-pad&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://github.com/ocoudray/jupyter-drawing-pad"&gt;small drawing pad app&lt;/a&gt;, is inspired from &lt;a href="https://codepen.io/anon/pen/aLYeNB"&gt;this codepen&lt;/a&gt;. You can run and try it on the &lt;a href="https://github.com/PierreMarion23/jupyter-widget-drawing-pad-binder"&gt;Binder repo&lt;/a&gt; or watch it on nbviewer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mybinder.org/v2/gh/PierreMarion23/jupyter-widget-drawing-pad-binder/master?filepath=Demo_drawing_pad.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/002-1_x6oPwz8nMiy4YIgEpczQHg.webp" alt="Launch Binder" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://nbviewer.jupyter.org/github/ocoudray/jupyter-drawing-pad/blob/master/Example/Demo_drawing_pad.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/003-1_lK1Zi8_qorxl5ZrL_ZMc7w.webp" alt="Render with nbviewer" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="drawing pad widget" src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/005-1_32qRaKxSbihtMaetq6jFog.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;drawing pad widget&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;jupyter-drawing-pad
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="33-ipypivot"&gt;3.3 — ipypivot&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://github.com/PierreMarion23/pivot-table-widget"&gt;ipypivot&lt;/a&gt; widget, wraps the convenient &lt;a href="https://github.com/nicolaskruchten/pivottable"&gt;PivotTable.js library&lt;/a&gt;. You can run and try it on the &lt;a href="https://github.com/PierreMarion23/ipypivot-binder"&gt;binder repo&lt;/a&gt; or watch it on nbviewer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mybinder.org/v2/gh/PierreMarion23/ipypivot-binder/master?filepath=demo_pivot_table.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/002-1_x6oPwz8nMiy4YIgEpczQHg.webp" alt="Launch Binder" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://nbviewer.jupyter.org/github/PierreMarion23/ipypivot/blob/master/notebooks/demo_ipypivot.ipynb"&gt;&lt;img src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/003-1_lK1Zi8_qorxl5ZrL_ZMc7w.webp" alt="Render with nbviewer" loading="lazy" data-body-image=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure&gt;
&lt;img alt="ipypivot widget" src="https://blog.jupyter.org/posts/2018/authoring-custom-jupyter-widgets/images/006-1_FnkBH8yA-PfCNCxXA2PNIw.mp4" loading="lazy" data-body-image=""&gt;
&lt;figcaption&gt;ipypivot widget&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;ipypivot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;conda&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;conda-forge&lt;span class="w"&gt; &lt;/span&gt;ipypivot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;NOTE: The PivotUI widget is a &lt;a href="https://github.com/PierreMarion23/ipypivot/blob/master/js/lib/widget_pivotui_box.js"&gt;combo of a custom widget and core widgets&lt;/a&gt;. This modular approach is more flexible (and looks nicer) but the same features can be made in a &lt;a href="https://github.com/PierreMarion23/ipypivot/blob/alt/js/lib/widget_pivotui.js"&gt;single custom widget&lt;/a&gt; containing extra buttons and display fields. The &lt;a href="https://github.com/PierreMarion23/ipypivot/tree/alt"&gt;&lt;code&gt;alt&lt;/code&gt; branch&lt;/a&gt; of the repo contains this version.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="34-including-javascript-callbacks"&gt;3.4 — Including JavaScript Callbacks&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://github.com/PierreMarion23/pivot-table-widget"&gt;ipypivot&lt;/a&gt; widget is an example of a widget that transparently wraps a JavaScript library. Transparent in the sense that &lt;strong&gt;all parameters&lt;/strong&gt; of the JavaScript API are exposed, &lt;strong&gt;including functions&lt;/strong&gt;, which are exposed in the form of strings on the Python side. In Python, JavaScript functions can only be strings, so there is an &lt;code&gt;eval()&lt;/code&gt; to convert them to actual JavaScript functions.&lt;/p&gt;
&lt;p&gt;The benefit is that all the functionalities of the JS libraries are exposed to the Python users with a very thin API. Developers can ‘widget-ify’ a large array of interesting libraries, thereby boosting the productivity of a Jupyter notebook user.&lt;/p&gt;
&lt;p&gt;The downside is naturally the security concerns of enabling arbitrary JavaScript code to be injected by the notebook users. It is less a concern in the context of notebooks being shared within a small team of coworkers.&lt;/p&gt;
&lt;p&gt;We are currently exploring means to execute the user-provided arbitrary JavaScript function in a sandboxed fashion, for example using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"&gt;iframe &lt;code&gt;srcdoc&lt;/code&gt; field&lt;/a&gt; (Cf. this &lt;a href="https://github.com/oscar6echo/notebook-image-tabs"&gt;repo&lt;/a&gt; for an example, though not in a Jupyter widget context) and messages can be sent back and forth between the main page and an iframe with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage"&gt;Window.postMessage&lt;/a&gt; function (see this &lt;a href="https://gist.github.com/pbojinov/8965299"&gt;gist&lt;/a&gt; for a bare bones example).&lt;/p&gt;
&lt;h3 id="35-enabling-jupyter-widgets-by-default"&gt;3.5 — Enabling Jupyter Widgets By Default&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;jupyter nbextension enable&lt;/code&gt; command, arguably cumbersome, has become unnecessary as Jupyter widgets can be enabled by default from notebook version 5.3 (included). See this &lt;a href="https://github.com/jupyter/notebook/pull/3116"&gt;PR&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In order to be future-proof, all the widgets in this article include the file which triggers this ‘automatic enable’, and require notebook &amp;gt;= 5.3. Thus the pip-installation of our widgets is a one-line command. However, in dev mode, you still need to install and enable the notebook example (see section 3.1 for an example).&lt;/p&gt;
&lt;p&gt;If you are working with an older version of notebook (run &lt;code&gt;jupyter notebook --version&lt;/code&gt; to check), you will have to run the following command after pip-installing a widget:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;jupyter&lt;span class="w"&gt; &lt;/span&gt;nbextension&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--py&lt;span class="w"&gt; &lt;/span&gt;--sys-prefix&lt;span class="w"&gt; &lt;/span&gt;name_of_the_widget
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="4-packaging-and-publishing"&gt;4 — Packaging and Publishing&lt;/h2&gt;
&lt;h3 id="41-pypi-and-npm"&gt;4.1 — PyPI and npm&lt;/h3&gt;
&lt;p&gt;If you have followed the “best practices” so far, packaging shouldn’t be an issue. Indeed, the &lt;a href="https://github.com/jupyter-widgets/widget-cookiecutter"&gt;cookiecutter&lt;/a&gt; provides a template of an easily-packageable widget.&lt;/p&gt;
&lt;p&gt;Once your package is ready, you can publish it on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.python.org/pypi"&gt;PyPI&lt;/a&gt; for the package to be pip-installable&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/"&gt;npmjs&lt;/a&gt; for the JavaScript extension, which is necessary to use the widget as a standalone application (outside of the notebook), render it with nbviewer, and also in the JupyterLab context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To do so, you can follow &lt;a href="https://github.com/ocoudray/first-widget#4---publish-on-pypi-and-npm"&gt;these instructions&lt;/a&gt; in the documentation of &lt;a href="https://github.com/ocoudray/first-widget"&gt;first-widget&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="42-conda-forge"&gt;4.2 — conda-forge&lt;/h3&gt;
&lt;p&gt;Conda has several advantages over pip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is a general-purpose package manager, which allows for non-python dependencies.&lt;/li&gt;
&lt;li&gt;Unlike pip, it also has a real dependency solver, which prevents breaking your environments when updating a single package.&lt;/li&gt;
&lt;li&gt;It allows creating virtual environments to isolate your projects.&lt;/li&gt;
&lt;li&gt;It allows for one-line installation of Jupyter extension, including the enabling of the extension as a “post-link” script (temporary advantage: see the previous section).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Conda packages are available on different channels. The default channel is administrated by Anaconda Inc. The usually recommended channel to upload open source projects is conda-forge, as &lt;a href="https://www.anaconda.com/blog/developer-blog/anaconda-build-migration-conda-forge/"&gt;this article&lt;/a&gt; from Anaconda announces. To add this channel to your conda configuration, run the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;conda config --add channels conda-forge
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Several steps are needed to publish a package on conda forge, using a so-called ‘recipe’:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;writing the recipe, which describes how to build the package along with the dependencies required for building and running it&lt;/li&gt;
&lt;li&gt;testing the recipe&lt;/li&gt;
&lt;li&gt;publishing the package on the conda-forge GitHub by forking their &lt;a href="https://github.com/conda-forge/staged-recipes"&gt;staged-recipes repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;maintaining the package&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href="https://conda.io/docs/user-guide/tasks/build-packages/index.html"&gt;conda doc&lt;/a&gt; and the &lt;a href="https://conda-forge.org/docs/"&gt;conda-forge doc&lt;/a&gt; are very clear and give you much more detailed information about this subject. If you do not want to read the full doc, and jump straight to the necessary information for publishing a new package, you may want to have a look at our &lt;a href="https://github.com/ocoudray/first-widget"&gt;first-widget repo&lt;/a&gt;. The README contains a &lt;a href="https://github.com/ocoudray/first-widget#5---publish-on-conda-forge"&gt;section&lt;/a&gt; describing the publishing process for conda-forge.&lt;/p&gt;
&lt;h3 id="43-automatic-push-script"&gt;4.3 — Automatic Push Script&lt;/h3&gt;
&lt;p&gt;The sequence of steps to update the version of a Jupyter widget and do all the pushing to the various repositories is quite long.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run npm prepare to build the js in folder &lt;code&gt;static/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update version in &lt;code&gt;__meta__.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;push package to &lt;a href="https://pypi.python.org/pypi"&gt;pypi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;compute package sha256&lt;/li&gt;
&lt;li&gt;tag repo with version&lt;/li&gt;
&lt;li&gt;push to &lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt; / &lt;a href="https://gitlab.com/"&gt;GitLab&lt;/a&gt; / &lt;a href="https://bitbucket.org/"&gt;Bitbucket&lt;/a&gt; including tag&lt;/li&gt;
&lt;li&gt;push js to &lt;a href="https://www.npmjs.com"&gt;npmjs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;push to conda-forge (which includes sha256 hash)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to automate the process we advise to have a look at Maarten Breddels’ &lt;a href="https://github.com/maartenbreddels/releash"&gt;releash package&lt;/a&gt; (release with relish :-) ), and how it is used in the context of &lt;a href="https://github.com/maartenbreddels/ipyvolume/blob/master/.releash.py"&gt;ipyvolume&lt;/a&gt; and &lt;a href="https://github.com/QuantStack/ipysheet/blob/master/.releash.py"&gt;ipysheet&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="44-binder-and-nbviewer"&gt;4.4 — Binder and nbviewer&lt;/h3&gt;
&lt;p&gt;&lt;a href="http://nbviewer.org/"&gt;nbviewer&lt;/a&gt; and &lt;a href="http://mybinder.org/"&gt;Binder&lt;/a&gt; are two fantastic tools to share both static and live notebooks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;nbviewer&lt;/strong&gt; requires a URL to a valid notebook JSON file — typically hosted on GitHub/GitLab. &lt;a href="https://ipywidgets.readthedocs.io/en/latest/embedding.html"&gt;To make widgets render in nbviewer&lt;/a&gt;, you need (1) to make the JavaScript package for your widget available on npm, (2) to run the notebook with the corresponding JavaScript extension (with the same version), and (3) to save the notebook widget state (in the ‘Widgets’ tab) before pushing it to GitHub / GitLab.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binder&lt;/strong&gt; requires a URL to a GitHub repository containing notebooks and a manifest of the dependencies required to run this notebook, which is used to produce a Docker image including all the resources to run the notebook. Check out the &lt;a href="http://mybinder.readthedocs.io/en/latest/faq.html"&gt;mybinder.org documentation&lt;/a&gt; to know how exactly to make use of it. Another resource is the &lt;a href="https://binderhub.readthedocs.io/en/latest/"&gt;BinderHub documentation&lt;/a&gt; if you want to host your own deployment of Binder. Either way we also highly recommend the article &lt;a href="https://blog.jupyter.org/posts/2017/binder-2-0-a-tech-guide-2017/"&gt;Binder 2.0, a Tech Guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="5-conclusion"&gt;5 — Conclusion&lt;/h2&gt;
&lt;p&gt;Hopefully you will have learned something reading this article. We believe in the potential of Jupyter widgets and hope that this intermediate-level article will help getting more people involved in the development of the ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: This article only covers the case of the classic Jupyter notebook. The integration with JupyterLab will be covered in a future article!&lt;/p&gt;
&lt;p&gt;If you find bugs or are interested in improving the example widgets presented here, please do not hesitate contact the authors or open a pull request!&lt;/p&gt;
&lt;div class="main-content-end" aria-hidden="true"&gt;&lt;/div&gt;
&lt;h2 id="about-the-authors"&gt;About the Authors&lt;/h2&gt;
&lt;p&gt;Alphabetical order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Olivier Borderies, Société Générale&lt;/li&gt;
&lt;li&gt;Olivier Coudray, Student at École Polytechnique&lt;/li&gt;
&lt;li&gt;Pierre Marion, Student at École Polytechnique&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The software presented in this post was built upon the work of a large number of people including the &lt;strong&gt;Jupyter&lt;/strong&gt; team. We are especially grateful to &lt;a href="https://twitter.com/SylvainCorlay"&gt;Sylvain Corlay&lt;/a&gt;, &lt;a href="https://twitter.com/jason_grout"&gt;Jason Grout&lt;/a&gt;, &lt;a href="https://twitter.com/ivanov"&gt;Paul Ivanov&lt;/a&gt;, and &lt;a href="https://twitter.com/steve_silvester"&gt;Steven Silvester&lt;/a&gt; from the Jupyter Steering Council, as well as &lt;a href="https://twitter.com/maartenbreddels"&gt;Maarten Breddels&lt;/a&gt; and &lt;a href="https://twitter.com/pascalbugnion"&gt;Pascal Bugnion&lt;/a&gt;.&lt;/p&gt;
</content><category term="JavaScript"/><category term="widgets"/></entry></feed>