{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "09a64ad7",
   "metadata": {},
   "source": [
    "## Introduction\n",
    "\n",
    "Jupyter notebooks have become the de facto standard for interactive computing\n",
    "and data analysis, combining code, prose, and visualizations in a single\n",
    "document.\n",
    "\n",
    "This blog post was written in a notebook!\n",
    "\n",
    "Since v1.37, Deno has included a built-in Jupyter kernel, bringing JavaScript\n",
    "and TypeScript to data science and machine learning. Having worked extensively\n",
    "with computational notebooks (mainly in Python), I find this exciting for\n",
    "several reasons:\n",
    "\n",
    "- **Easier setup** – The kernel is built into the Deno CLI, so there’s no need\n",
    "  for additional installation. Just install Deno.\n",
    "\n",
    "- **Improved dependency management** – Notebooks often behave like standalone\n",
    "  scripts, making dependency management a challenge in other languages and\n",
    "  contributing to\n",
    "  [reproducibility issues](https://leomurta.github.io/papers/pimentel2019a.pdf).\n",
    "  Deno's use of ECMAScript modules allows dependencies to be declared directly\n",
    "  in code, improving self-containment and reliability of analysis code.\n",
    "\n",
    "- **A unified ecosystem for interactive data analysis** – Jupyter frontends rely\n",
    "  on web technologies and support rich outputs in HTML, CSS, and JavaScript. The\n",
    "  JavaScript ecosystem is mature and among the most accessible for UI\n",
    "  development, and Deno bridges the gap between the kernel and front end,\n",
    "  unlocking new possibilities for data science and machine learning.\n",
    "\n",
    "In this post, we'll analyze the National Gallery of Art's Open Access data in\n",
    "Jupyter with Deno. We'll start with basic data cleaning, manipulation, and\n",
    "plotting to explore which artworks are public domain, who created them, where\n",
    "they're from, and any interesting patterns. The techniques should feel familiar\n",
    "to those who've worked in Jupyter before, but we'll introduce some of the key\n",
    "libraries available for working with data in Deno (e.g., `npm:nodejs-polars`,\n",
    "`npm:@observablehq/plot`). Finally, we'll integrate interactive widgets and\n",
    "extend Jupyter with a custom display to make exploring the data more dynamic and\n",
    "open-ended."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f99a40c",
   "metadata": {},
   "source": [
    "## The Dataset\n",
    "\n",
    "The **National Gallery of Art (NGA)\n",
    "[Open Data Program](https://www.nga.gov/open-access-images/open-data.html)**\n",
    "provides access to over **130,000 artworks** and their creators, available\n",
    "[on GitHub](https://github.com/NationalGalleryOfArt/opendata/tree/main/data).\n",
    "This dataset includes valuable metadata such as titles, dates, artists, and\n",
    "classifications, all under a\n",
    "[Creative Commons 0 (CC0)](https://creativecommons.org/public-domain/cc0/)\n",
    "license, meaning it's free to use and share.\n",
    "\n",
    "The collection spans a wide variety of artworks, from sculptures to paintings,\n",
    "by famous artists like Mary Cassatt, M.C. Escher, Vincent van Gogh, Pablo\n",
    "Picasso, and Georgia O'Keeffe.\n",
    "\n",
    "However, navigating this resource is challenging. The National Gallery's website\n",
    "is not very user-friendly, making it difficult to make sense of what is actually\n",
    "_in_ the dataset. It's nearly impossible to get any high-level insights, like\n",
    "the number of paintings, which time periods are most represented, or which\n",
    "artists are most prevalent–let alone make specific queries.\n",
    "\n",
    "Fortunately, the dataset is available\n",
    "[on GitHub](https://github.com/NationalGalleryOfArt/opendata/tree/main/data) as\n",
    "a collection of related tables, exported as CSV files. For our analysis, we'll\n",
    "focus on three key tables:\n",
    "\n",
    "- **`objects.csv`** – Metadata about artworks, including titles, dates,\n",
    "  materials, and classifications.\n",
    "- **`constituents.csv`** – Artist details such as names, nationalities, and\n",
    "  lifespans.\n",
    "- **`published_images.csv`** – Links to artwork images via the NGA’s IIIF API.\n",
    "\n",
    "We'll **clean** and **join** these tables to create a **unified dataset**.\n",
    "\n",
    "**But wait!** While these NGA tables are free to use, they only includes\n",
    "_metadata_. The actual artwork images have separate licensing, with only about\n",
    "half being CC0-licensed. We'll gather this info separately to identify freely\n",
    "available images.\n",
    "\n",
    "Our goal is to unify the data to identify public domain images, explore subsets,\n",
    "and view available artworks. Maybe you'll even find an upgrade for that default\n",
    "OS wallpaper."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2070c1a",
   "metadata": {},
   "source": [
    "## Loading & cleaning the data\n",
    "\n",
    "### Open Access Dataset\n",
    "\n",
    "To get started, we first need to download the data. If you’ve worked with CSVs\n",
    "in JavaScript before, this typically looks like a `fetch` request (or reading from a local cache) followed by\n",
    "some parsing. Here, we’re using `jsr:@std/csv` to stream the data, which gives\n",
    "us an array of objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ed80427b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[\n",
       "  {\n",
       "    objectid: \u001b[33m0\u001b[39m,\n",
       "    title: \u001b[32m\"Saint James Major\"\u001b[39m,\n",
       "    year: \u001b[33m1310\u001b[39m,\n",
       "    medium: \u001b[32m\"tempera on panel\"\u001b[39m,\n",
       "    type: \u001b[32m\"painting\"\u001b[39m\n",
       "  },\n",
       "  {\n",
       "    objectid: \u001b[33m1\u001b[39m,\n",
       "    title: \u001b[32m\"Saint Paul and a Group of Worshippers\"\u001b[39m,\n",
       "    year: \u001b[33m1333\u001b[39m,\n",
       "    medium: \u001b[32m\"tempera on panel\"\u001b[39m,\n",
       "    type: \u001b[32m\"painting\"\u001b[39m\n",
       "  },\n",
       "  {\n",
       "    objectid: \u001b[33m4\u001b[39m,\n",
       "    title: \u001b[32m\"Saint Bernard and Saint Catherine of Alexandria with the Virgin of the Annunciation [right panel]\"\u001b[39m,\n",
       "    year: \u001b[33m1387\u001b[39m,\n",
       "    medium: \u001b[32m\"tempera on poplar panel\"\u001b[39m,\n",
       "    type: \u001b[32m\"painting\"\u001b[39m\n",
       "  }\n",
       "]"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import * as csv from \"jsr:@std/csv@1.0.5\";\n",
    "\n",
    "async function fetchCached(url: string | URL, filename: string): Promise<string> {\n",
    "  try {\n",
    "    return await Deno.readTextFile(filename);\n",
    "  } catch (error) {\n",
    "    if (!(error instanceof Deno.errors.NotFound)) throw error;\n",
    "    console.log(`Fetching ${url} and caching to ${filename}...`);\n",
    "    const response = await fetch(url);\n",
    "    const text = await response.text();\n",
    "    await Deno.writeTextFile(filename, text);\n",
    "    return text;\n",
    "  }\n",
    "}\n",
    "\n",
    "let baseUrl = new URL(\n",
    "  \"https://github.com/NationalGalleryOfArt/opendata/raw/refs/heads/main/data/\",\n",
    ");\n",
    "let objectsText = await fetchCached(new URL(\"objects.csv\", baseUrl), \"objects.csv\");\n",
    "\n",
    "let objects = await Array.fromAsync(\n",
    "  ReadableStream.from([objectsText])\n",
    "    .pipeThrough(new csv.CsvParseStream({ skipFirstRow: true })),\n",
    "  (row) => ({\n",
    "    objectid: +row.objectid,\n",
    "    title: row.title,\n",
    "    year: +row.beginyear,\n",
    "    medium: row.medium,\n",
    "    type: row.visualbrowserclassification,\n",
    "  }),\n",
    ");\n",
    "\n",
    "objects.slice(0, 3);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c35ebb0d-63c3-46a5-9a59-0f3cf97ad7a5",
   "metadata": {},
   "source": [
    "If we were just plotting the data, this would work fine. But an array of objects\n",
    "isn’t ideal for building our dataset. The NGA data is relational and needs\n",
    "merging, cleaning, and restructuring. A **DataFrame** is better suited for our\n",
    "use case, offering higher-level, efficient APIs that eliminate the need to write\n",
    "manipulation functions by hand.\n",
    "\n",
    "Let's see how we can load the same dataset using a DataFrame from\n",
    "`npm:nodejs-polars`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f7984430",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.dataresource+json": {
       "data": [
        {
         "medium": "tempera on panel",
         "objectid": 0,
         "title": "Saint James Major",
         "type": "painting",
         "year": 1310
        },
        {
         "medium": "tempera on panel",
         "objectid": 1,
         "title": "Saint Paul and a Group of Worshippers",
         "type": "painting",
         "year": 1333
        },
        {
         "medium": "tempera on poplar panel",
         "objectid": 4,
         "title": "Saint Bernard and Saint Catherine of Alexandria with the Virgin of the Annunciation [right panel]",
         "type": "painting",
         "year": 1387
        },
        {
         "medium": "tempera (and oil?) on panel transferred to canvas",
         "objectid": 17,
         "title": "Matteo Olivieri (?)",
         "type": "painting",
         "year": 1430
        },
        {
         "medium": "tempera on panel",
         "objectid": 19,
         "title": "Portrait of a Man",
         "type": "painting",
         "year": 1450
        }
       ],
       "schema": {
        "fields": [
         {
          "name": "objectid",
          "type": "integer"
         },
         {
          "name": "title",
          "type": "string"
         },
         {
          "name": "year",
          "type": "integer"
         },
         {
          "name": "medium",
          "type": "string"
         },
         {
          "name": "type",
          "type": "string"
         }
        ]
       }
      },
      "text/html": [
       "<table><thead><tr><th>objectid</th><th>title</th><th>year</th><th>medium</th><th>type</th></tr></thead><tbody><tr><td>0</td><td>Saint James Major</td><td>1310</td><td>tempera on panel</td><td>painting</td></tr><tr><td>1</td><td>Saint Paul and a Group of Worshippers</td><td>1333</td><td>tempera on panel</td><td>painting</td></tr><tr><td>4</td><td>Saint Bernard and Saint Catherine of Alexandria with the Virgin of the Annunciation [right panel]</td><td>1387</td><td>tempera on poplar panel</td><td>painting</td></tr><tr><td>17</td><td>Matteo Olivieri (?)</td><td>1430</td><td>tempera (and oil?) on panel transferred to canvas</td><td>painting</td></tr><tr><td>19</td><td>Portrait of a Man</td><td>1450</td><td>tempera on panel</td><td>painting</td></tr></tbody></table>"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import * as pl from \"npm:nodejs-polars@0.18.0\";\n",
    "\n",
    "let objectsText = await fetchCached(new URL(\"objects.csv\", baseUrl), \"objects.csv\");\n",
    "\n",
    "let objects: pl.DataFrame = pl.readCSV(objectsText, {\n",
    "  quoteChar: '\"',\n",
    "});\n",
    "\n",
    "objects = objects.select(\n",
    "  pl.col(\"objectid\"),\n",
    "  pl.col(\"title\"),\n",
    "  pl.col(\"beginyear\").as(\"year\"),\n",
    "  pl.col(\"medium\"),\n",
    "  pl.col(\"visualbrowserclassification\").as(\"type\"),\n",
    ");\n",
    "\n",
    "objects.head();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a406b1a6-605d-4d49-acc9-cf342ac3330e",
   "metadata": {},
   "source": [
    "Here, we `fetch` the dataset as before, but instead of parsing the CSV\n",
    "ourselves, we read the text response directly with Polars to create a\n",
    "`pl.DataFrame`.\n",
    "\n",
    "We then chain a `.select` expression to choose and rename the columns we want.\n",
    "Notice how, rather than operating on each row in a loop, with Polars we work\n",
    "directly with _columns_. This API allows us to express complex, efficient\n",
    "operations (in Rust) without materializing JavaScript objects.\n",
    "\n",
    "Let's load our other two datasets as `pl.DataFrames` as well:\n",
    "\n",
    "The `constituents.csv` table contains information about any person or entity\n",
    "associated with a piece of art, such as artists, curators, or collectors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8c958438",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.dataresource+json": {
       "data": [
        {
         "constituentid": 9,
         "name": "Anonymous",
         "nationality": "Other"
        },
        {
         "constituentid": 11,
         "name": "Hans Baldung",
         "nationality": "Other"
        },
        {
         "constituentid": 12,
         "name": "National Gallery of Art",
         "nationality": "Other"
        },
        {
         "constituentid": 13,
         "name": "Anonymous Artist",
         "nationality": "Other"
        },
        {
         "constituentid": 14,
         "name": "Mrs. Thomas M. Evans",
         "nationality": "American"
        }
       ],
       "schema": {
        "fields": [
         {
          "name": "constituentid",
          "type": "integer"
         },
         {
          "name": "name",
          "type": "string"
         },
         {
          "name": "nationality",
          "type": "string"
         }
        ]
       }
      },
      "text/html": [
       "<table><thead><tr><th>constituentid</th><th>name</th><th>nationality</th></tr></thead><tbody><tr><td>9</td><td>Anonymous</td><td>Other</td></tr><tr><td>11</td><td>Hans Baldung</td><td>Other</td></tr><tr><td>12</td><td>National Gallery of Art</td><td>Other</td></tr><tr><td>13</td><td>Anonymous Artist</td><td>Other</td></tr><tr><td>14</td><td>Mrs. Thomas M. Evans</td><td>American</td></tr></tbody></table>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "let constituentsText = await fetchCached(new URL(\"constituents.csv\", baseUrl), \"constituents.csv\");\n",
    "let constituents = pl.readCSV(constituentsText, { quoteChar: '\"' })\n",
    "  .select(\n",
    "    \"constituentid\",\n",
    "    pl.col(\"forwarddisplayname\").alias(\"name\"),\n",
    "    pl.col(\"visualbrowsernationality\").alias(\"nationality\"),\n",
    "  );\n",
    "\n",
    "constituents.head();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36997d1b-2c3e-4102-ab80-81de168cd909",
   "metadata": {},
   "source": [
    "The `published_images.csv` table contains additional information about the\n",
    "artwork's image, including a URL for the thumbnail and mapping the objectid to\n",
    "the [IIIF (International Image Interoperability Framework)](https://iiif.io/)\n",
    "for the image."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "6b19e1af",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.dataresource+json": {
       "data": [
        {
         "objectid": 17387,
         "thumburl": "https://api.nga.gov/iiif/00007f61-4922-417b-8f27-893ea328206c/full/!200,200/0/default.jpg",
         "uuid": "00007f61-4922-417b-8f27-893ea328206c"
        },
        {
         "objectid": 19245,
         "thumburl": "https://api.nga.gov/iiif/0000bd8c-39de-4453-b55d-5e28a9beed38/full/!200,200/0/default.jpg",
         "uuid": "0000bd8c-39de-4453-b55d-5e28a9beed38"
        },
        {
         "objectid": 23830,
         "thumburl": "https://api.nga.gov/iiif/0001668a-dd1c-48e8-9267-b6d1697d43c8/full/!200,200/0/default.jpg",
         "uuid": "0001668a-dd1c-48e8-9267-b6d1697d43c8"
        },
        {
         "objectid": 232817,
         "thumburl": "https://api.nga.gov/iiif/0002e210-8bc2-4471-bb0b-e7bb51d01a40/full/!200,200/0/default.jpg",
         "uuid": "0002e210-8bc2-4471-bb0b-e7bb51d01a40"
        },
        {
         "objectid": 713,
         "thumburl": "https://api.nga.gov/iiif/00032658-8a7a-44e3-8bb8-df8c172f521d/full/!200,200/0/default.jpg",
         "uuid": "00032658-8a7a-44e3-8bb8-df8c172f521d"
        }
       ],
       "schema": {
        "fields": [
         {
          "name": "objectid",
          "type": "integer"
         },
         {
          "name": "uuid",
          "type": "string"
         },
         {
          "name": "thumburl",
          "type": "string"
         }
        ]
       }
      },
      "text/html": [
       "<table><thead><tr><th>objectid</th><th>uuid</th><th>thumburl</th></tr></thead><tbody><tr><td>17387</td><td>00007f61-4922-417b-8f27-893ea328206c</td><td>https://api.nga.gov/iiif/00007f61-4922-417b-8f27-893ea328206c/full/!200,200/0/default.jpg</td></tr><tr><td>19245</td><td>0000bd8c-39de-4453-b55d-5e28a9beed38</td><td>https://api.nga.gov/iiif/0000bd8c-39de-4453-b55d-5e28a9beed38/full/!200,200/0/default.jpg</td></tr><tr><td>23830</td><td>0001668a-dd1c-48e8-9267-b6d1697d43c8</td><td>https://api.nga.gov/iiif/0001668a-dd1c-48e8-9267-b6d1697d43c8/full/!200,200/0/default.jpg</td></tr><tr><td>232817</td><td>0002e210-8bc2-4471-bb0b-e7bb51d01a40</td><td>https://api.nga.gov/iiif/0002e210-8bc2-4471-bb0b-e7bb51d01a40/full/!200,200/0/default.jpg</td></tr><tr><td>713</td><td>00032658-8a7a-44e3-8bb8-df8c172f521d</td><td>https://api.nga.gov/iiif/00032658-8a7a-44e3-8bb8-df8c172f521d/full/!200,200/0/default.jpg</td></tr></tbody></table>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "let publishedImagesText = await fetchCached(new URL(\"published_images.csv\", baseUrl), \"published_images.csv\");\n",
    "let publishedImages = pl.readCSV(publishedImagesText, { quoteChar: '\"' })\n",
    "  .select(\n",
    "    pl.col(\"depictstmsobjectid\").alias(\"objectid\"),\n",
    "    pl.col(\"uuid\"),\n",
    "    pl.col(\"iiifthumburl\").alias(\"thumburl\"),\n",
    "  );\n",
    "publishedImages.head();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c4717524-3f11-47e7-965e-9bbb0cf73333",
   "metadata": {},
   "source": [
    "The final table, `objects_constituents.csv`, represents a many-to-many\n",
    "relationship between artworks and the people or entities associated with them.\n",
    "Each artwork may have multiple people linked to it.\n",
    "\n",
    "For our analysis, we're interested in selecting the \"primary\" constituent (i.e.,\n",
    "the artist). We define the primary artist as the one with the highest\n",
    "`displayorder` for each object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "01f1ba87",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.dataresource+json": {
       "data": [
        {
         "constituentid": 14997,
         "objectid": 91245
        },
        {
         "constituentid": 2368,
         "objectid": 67787
        },
        {
         "constituentid": 50669,
         "objectid": 221275
        },
        {
         "constituentid": 2686,
         "objectid": 106349
        },
        {
         "constituentid": 5015,
         "objectid": 9176
        }
       ],
       "schema": {
        "fields": [
         {
          "name": "objectid",
          "type": "integer"
         },
         {
          "name": "constituentid",
          "type": "integer"
         }
        ]
       }
      },
      "text/html": [
       "<table><thead><tr><th>objectid</th><th>constituentid</th></tr></thead><tbody><tr><td>91245</td><td>14997</td></tr><tr><td>67787</td><td>2368</td></tr><tr><td>221275</td><td>50669</td></tr><tr><td>106349</td><td>2686</td></tr><tr><td>9176</td><td>5015</td></tr></tbody></table>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "let objectToArtistText = await fetchCached(new URL(\"objects_constituents.csv\", baseUrl), \"objects_constituents.csv\");\n",
    "let objectToArtist = pl.readCSV(objectToArtistText, { quoteChar: '\"' })\n",
    "  .filter(pl.col(\"role\").eq(pl.lit(\"artist\")))\n",
    "  .sort({ by: \"displayorder\" })\n",
    "  .groupBy(\"objectid\")\n",
    "  .first()\n",
    "  .select(\"objectid\", \"constituentid\");\n",
    "\n",
    "objectToArtist.head();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8bd4edcc-45b8-48b6-9e60-a39b8c62e601",
   "metadata": {},
   "source": [
    "This is a much more advanced usage of Polars, so let’s break down what's going\n",
    "on:\n",
    "\n",
    "- **Filter** the table to select only rows where the role is \"artist\".\n",
    "- **Sort** the filtered data by `displayorder` to ensure all entries are at the\n",
    "  top.\n",
    "- **Group** the rows **by** `objectid`, ensuring one row per artwork.\n",
    "- Get the **first** in each group (i.e., artist with highest display order).\n",
    "- **Select** just the `objectid` and `constituentid` for joining our tables.\n",
    "\n",
    "Writing this logic by hand in JavaScript would be tedius, but with Polars it's\n",
    "declarative and reads like a sentence. This type of chaining is common when\n",
    "working with relational data."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5891b298-76a1-4f94-aec6-0f1ecc634ec7",
   "metadata": {},
   "source": [
    "### Public domain images IDs\n",
    "\n",
    "We've loaded the relevant tables from the Open Access dataset, but these data\n",
    "**do not** indicate whether the images associated with an artwork are also in\n",
    "the public domain. This is an interesting omission both for understanding the\n",
    "overall \"openness\" of the dataset and because CC0 images can be freely\n",
    "downloaded, shared, and repurposed.\n",
    "\n",
    "This information _is_ available on the NGA website, but only through the search\n",
    "user interface. Extracting it manually would be impractical, so I\n",
    "reverse-engineered an API call to retrieve the IDs of artworks with public\n",
    "domain images.\n",
    "\n",
    "We only need the IDs, not the full data, but the query is slow. Since this isn't\n",
    "an official API, I've saved the results separately.\n",
    "\n",
    "There are about 50k IDs identifying public domain images in our dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2e2b86e4-9971-4676-bbc2-f6f02add8b88",
   "metadata": {},
   "outputs": [],
   "source": [
    "// This is not an official API, so I’ve cached the results to avoid repeatedly querying the server.\n",
    "//\n",
    "// let response = await fetch(\n",
    "//   \"https://www.nga.gov/bin/ngaweb/collection-search-result/search.pageSize__100000.pageNumber__1.lastFacet__artobj_downloadable.json?artobj_downloadable=Image_download_available\",\n",
    "// );\n",
    "// let data = await response.json();\n",
    "// Deno.writeTextFileSync(\n",
    "//   \"public-domain-ids.txt\",\n",
    "//   data.results.map((object) => object.id).join(\"\\n\"),\n",
    "// );\n",
    "\n",
    "let publicDomainIds = Deno.readTextFileSync(\"public-domain-ids.txt\")\n",
    "  .split(\"\\n\")\n",
    "  .map((d) => +d);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b61b3bca-ab15-4c57-a1fa-1343d5d3afc1",
   "metadata": {},
   "source": [
    "### Putting it together\n",
    "\n",
    "Now that we have all our data cleaned and loaded, we'll perform a large join to\n",
    "combine all these tables into a **single unified table**.\n",
    "\n",
    "Again, this challenging to do manually, but with Polars, we can nicely express\n",
    "these complex join operations. Finally, we use a `.isIn` expression to derive a\n",
    "new column that indicates whether a piece of art is public domain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e29b04ef",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.dataresource+json": {
       "data": [
        {
         "medium": "aventurine, malachite, glass and acrylic on panel",
         "name": "Alteronce Gumby",
         "nationality": "American",
         "objectid": 231342,
         "public": false,
         "thumburl": "https://api.nga.gov/iiif/eaf0bd42-3792-4617-beee-9949e421be75/full/!200,200/0/default.jpg",
         "title": "Energy Flows Where Focus Goes",
         "type": "painting",
         "uuid": "eaf0bd42-3792-4617-beee-9949e421be75",
         "year": 2024
        },
        {
         "medium": "oil on panel",
         "name": "Kay WalkingStick",
         "nationality": "American",
         "objectid": 231344,
         "public": false,
         "thumburl": "https://api.nga.gov/iiif/7c90a2c9-f365-4982-afb9-e0f2686e2d84/full/!200,200/0/default.jpg",
         "title": "The Niagara Cascades",
         "type": "painting",
         "uuid": "7c90a2c9-f365-4982-afb9-e0f2686e2d84",
         "year": 2024
        },
        {
         "medium": "4 woven inkjet prints, tape, wax, cotton thread",
         "name": "Sarah Sense",
         "nationality": "Other",
         "objectid": 231353,
         "public": false,
         "thumburl": "https://api.nga.gov/iiif/137e64b1-0616-43c7-8e18-1083d05a08bc/full/!200,200/0/default.jpg",
         "title": "Hinushi Bayou Teche",
         "type": "photograph",
         "uuid": "137e64b1-0616-43c7-8e18-1083d05a08bc",
         "year": 2024
        },
        {
         "medium": "4 woven inkjet prints, tape, wax, cotton thread",
         "name": "Sarah Sense",
         "nationality": "Other",
         "objectid": 231353,
         "public": false,
         "thumburl": "https://api.nga.gov/iiif/2ba97773-a770-40d2-a496-3ed6cb964d31/full/!200,200/0/default.jpg",
         "title": "Hinushi Bayou Teche",
         "type": "photograph",
         "uuid": "2ba97773-a770-40d2-a496-3ed6cb964d31",
         "year": 2024
        },
        {
         "medium": "4 woven inkjet prints, tape, wax, cotton thread",
         "name": "Sarah Sense",
         "nationality": "Other",
         "objectid": 231353,
         "public": false,
         "thumburl": "https://api.nga.gov/iiif/e4e08675-fcf4-418a-80a1-b1ea5d8389d8/full/!200,200/0/default.jpg",
         "title": "Hinushi Bayou Teche",
         "type": "photograph",
         "uuid": "e4e08675-fcf4-418a-80a1-b1ea5d8389d8",
         "year": 2024
        }
       ],
       "schema": {
        "fields": [
         {
          "name": "objectid",
          "type": "integer"
         },
         {
          "name": "title",
          "type": "string"
         },
         {
          "name": "year",
          "type": "integer"
         },
         {
          "name": "medium",
          "type": "string"
         },
         {
          "name": "type",
          "type": "string"
         },
         {
          "name": "name",
          "type": "string"
         },
         {
          "name": "nationality",
          "type": "string"
         },
         {
          "name": "uuid",
          "type": "string"
         },
         {
          "name": "thumburl",
          "type": "string"
         },
         {
          "name": "public",
          "type": "boolean"
         }
        ]
       }
      },
      "text/html": [
       "<table><thead><tr><th>objectid</th><th>title</th><th>year</th><th>medium</th><th>type</th><th>name</th><th>nationality</th><th>uuid</th><th>thumburl</th><th>public</th></tr></thead><tbody><tr><td>231342</td><td>Energy Flows Where Focus Goes</td><td>2024</td><td>aventurine, malachite, glass and acrylic on panel</td><td>painting</td><td>Alteronce Gumby</td><td>American</td><td>eaf0bd42-3792-4617-beee-9949e421be75</td><td>https://api.nga.gov/iiif/eaf0bd42-3792-4617-beee-9949e421be75/full/!200,200/0/default.jpg</td><td>false</td></tr><tr><td>231344</td><td>The Niagara Cascades</td><td>2024</td><td>oil on panel</td><td>painting</td><td>Kay WalkingStick</td><td>American</td><td>7c90a2c9-f365-4982-afb9-e0f2686e2d84</td><td>https://api.nga.gov/iiif/7c90a2c9-f365-4982-afb9-e0f2686e2d84/full/!200,200/0/default.jpg</td><td>false</td></tr><tr><td>231353</td><td>Hinushi Bayou Teche</td><td>2024</td><td>4 woven inkjet prints, tape, wax, cotton thread</td><td>photograph</td><td>Sarah Sense</td><td>Other</td><td>137e64b1-0616-43c7-8e18-1083d05a08bc</td><td>https://api.nga.gov/iiif/137e64b1-0616-43c7-8e18-1083d05a08bc/full/!200,200/0/default.jpg</td><td>false</td></tr><tr><td>231353</td><td>Hinushi Bayou Teche</td><td>2024</td><td>4 woven inkjet prints, tape, wax, cotton thread</td><td>photograph</td><td>Sarah Sense</td><td>Other</td><td>2ba97773-a770-40d2-a496-3ed6cb964d31</td><td>https://api.nga.gov/iiif/2ba97773-a770-40d2-a496-3ed6cb964d31/full/!200,200/0/default.jpg</td><td>false</td></tr><tr><td>231353</td><td>Hinushi Bayou Teche</td><td>2024</td><td>4 woven inkjet prints, tape, wax, cotton thread</td><td>photograph</td><td>Sarah Sense</td><td>Other</td><td>e4e08675-fcf4-418a-80a1-b1ea5d8389d8</td><td>https://api.nga.gov/iiif/e4e08675-fcf4-418a-80a1-b1ea5d8389d8/full/!200,200/0/default.jpg</td><td>false</td></tr></tbody></table>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "let df = objects\n",
    "  .join(objectToArtist, { on: \"objectid\" })\n",
    "  .join(constituents, { on: \"constituentid\" })\n",
    "  .join(publishedImages, { on: \"objectid\" })\n",
    "  .select(pl.exclude(\"constituentid\"))\n",
    "  .withColumns(pl.col(\"objectid\").isIn(publicDomainIds).alias(\"public\"))\n",
    "  .sort({ by: \"public\" })\n",
    "  .sort({ by: \"year\", descending: true, nullsLast: true });\n",
    "\n",
    "df.head();"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67baa204-937b-4b08-8d77-cb3417b6f874",
   "metadata": {},
   "source": [
    "With our unified dataset finally loaded, let's move on to some plotting."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e0b909d8",
   "metadata": {},
   "source": [
    "## Plotting\n",
    "\n",
    "The NGA portal allows some exploration of these data but focuses on _specific\n",
    "artworks_. We're more interested in broader insights--where most art comes from,\n",
    "who created it, when it was made, and whether it's public domain.\n",
    "\n",
    "As a JavaScript/TypeScript runtime, Deno gives us access to many visualization\n",
    "libraries to ask these kinds of questions. We'll use `npm:@observablehq/plot`\n",
    "(Observable Plot) to create plots, spot patterns, and better understand the\n",
    "collection.\n",
    "\n",
    "Keep in mind, this post is not a tutorial on Observable Plot, so don't worry if\n",
    "some of the code below is not immediately clear. The goal here is to demonstrate\n",
    "how to use Observable Plot in Jupyter with Deno to extract insights from our\n",
    "data. I'll highlight any Deno-specific details or useful tips along the way.\n",
    "\n",
    "Let’s start by examining the distribution of artworks across different \"types\"\n",
    "in the NGA collection."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "b965c83c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<figure style=\"max-width:initial\" class=\"plot-d6a7b5-figure\"><div class=\"plot-d6a7b5-swatches plot-d6a7b5-swatches-wrap\"><style>:where(.plot-d6a7b5-swatches) {\n",
       "  font-family: system-ui, sans-serif;\n",
       "  font-size: 10px;\n",
       "  margin-bottom: 0.5em;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatch > svg) {\n",
       "  margin-right: 0.5em;\n",
       "  overflow: visible;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap) {\n",
       "  display: flex;\n",
       "  align-items: center;\n",
       "  min-height: 33px;\n",
       "  flex-wrap: wrap;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap .plot-d6a7b5-swatch) {\n",
       "  display: inline-flex;\n",
       "  align-items: center;\n",
       "  margin-right: 1em;\n",
       "}</style><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#4269d0\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Copyrighted</span><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#efb118\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Public domain</span></div><svg viewBox=\"0 0 900 400\" height=\"400\" width=\"900\" text-anchor=\"middle\" font-size=\"10\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" class=\"plot-d6a7b5\"><style>:where(.plot-d6a7b5) {\n",
       "  --plot-background: white;\n",
       "  display: block;\n",
       "  height: auto;\n",
       "  height: intrinsic;\n",
       "  max-width: 100%;\n",
       "}\n",
       ":where(.plot-d6a7b5 text),\n",
       ":where(.plot-d6a7b5 tspan) {\n",
       "  white-space: pre;\n",
       "}</style><g stroke=\"currentColor\" fill=\"none\" aria-hidden=\"true\" aria-label=\"y-axis tick\"><path d=\"M0,0L-6,0\" transform=\"translate(50,370)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,333.8257849804659)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,297.6515699609318)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,261.4773549413978)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,225.30313992186367)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,189.1289249023296)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,152.95470988279553)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,116.78049486326145)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,80.60627984372738)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,44.43206482419333)\" /></g><g transform=\"translate(-9,0)\" font-variant=\"tabular-nums\" text-anchor=\"end\" aria-label=\"y-axis tick label\"><text transform=\"translate(50,370)\" y=\"0.32em\">0</text><text transform=\"translate(50,333.8257849804659)\" y=\"0.32em\">5,000</text><text transform=\"translate(50,297.6515699609318)\" y=\"0.32em\">10,000</text><text transform=\"translate(50,261.4773549413978)\" y=\"0.32em\">15,000</text><text transform=\"translate(50,225.30313992186367)\" y=\"0.32em\">20,000</text><text transform=\"translate(50,189.1289249023296)\" y=\"0.32em\">25,000</text><text transform=\"translate(50,152.95470988279553)\" y=\"0.32em\">30,000</text><text transform=\"translate(50,116.78049486326145)\" y=\"0.32em\">35,000</text><text transform=\"translate(50,80.60627984372738)\" y=\"0.32em\">40,000</text><text transform=\"translate(50,44.43206482419333)\" y=\"0.32em\">45,000</text></g><g transform=\"translate(-47,-17)\" text-anchor=\"start\" aria-label=\"y-axis label\"><text transform=\"translate(50,20)\" y=\"0.71em\">↑ Frequency</text></g><g transform=\"translate(37,0)\" stroke=\"currentColor\" fill=\"none\" aria-hidden=\"true\" aria-label=\"x-axis tick\"><path d=\"M0,0L0,6\" transform=\"translate(59,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(141,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(223,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(305,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(387,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(469,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(551,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(633,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(715,370)\" /><path d=\"M0,0L0,6\" transform=\"translate(797,370)\" /></g><g transform=\"translate(37,9)\" aria-label=\"x-axis tick label\"><text transform=\"translate(59,370)\" y=\"0.71em\">print</text><text transform=\"translate(141,370)\" y=\"0.71em\">drawing</text><text transform=\"translate(223,370)\" y=\"0.71em\">photograph</text><text transform=\"translate(305,370)\" y=\"0.71em\">portfolio</text><text transform=\"translate(387,370)\" y=\"0.71em\">sculpture</text><text transform=\"translate(469,370)\" y=\"0.71em\">volume</text><text transform=\"translate(551,370)\" y=\"0.71em\">painting</text><text transform=\"translate(633,370)\" y=\"0.71em\">decorative art</text><text transform=\"translate(715,370)\" y=\"0.71em\">technical material</text><text transform=\"translate(797,370)\" y=\"0.71em\">new media</text></g><g transform=\"translate(0,27)\" aria-label=\"x-axis label\"><text transform=\"translate(465,370)\">type</text></g><g aria-label=\"bar\"><rect fill=\"#4269d0\" height=\"5.353783822891046\" y=\"364.64621617710895\" width=\"74\" x=\"551\" /><rect fill=\"#4269d0\" height=\"143.69121690059325\" y=\"226.30878309940675\" width=\"74\" x=\"223\" /><rect fill=\"#4269d0\" height=\"4.717117638547222\" y=\"365.2828823614528\" width=\"74\" x=\"387\" /><rect fill=\"#4269d0\" height=\"164.43351179279412\" y=\"205.56648820720588\" width=\"74\" x=\"59\" /><rect fill=\"#4269d0\" height=\"42.36724063087826\" y=\"327.63275936912174\" width=\"74\" x=\"305\" /><rect fill=\"#4269d0\" height=\"0.07234843003908509\" y=\"369.9276515699609\" width=\"74\" x=\"797\" /><rect fill=\"#4269d0\" height=\"12.284763420633794\" y=\"357.7152365793662\" width=\"74\" x=\"469\" /><rect fill=\"#4269d0\" height=\"2.7637100274923796\" y=\"367.2362899725076\" width=\"74\" x=\"633\" /><rect fill=\"#4269d0\" height=\"58.24048618144991\" y=\"311.7595138185501\" width=\"74\" x=\"141\" /><rect fill=\"#4269d0\" height=\"0.9549992765157072\" y=\"369.0450007234843\" width=\"74\" x=\"715\" /><rect fill=\"#efb118\" height=\"179.49645492692804\" y=\"132.26305889162205\" width=\"74\" x=\"141\" /><rect fill=\"#efb118\" height=\"185.56648820720588\" y=\"20\" width=\"74\" x=\"59\" /><rect fill=\"#efb118\" height=\"16.78483576906379\" y=\"209.52394733034296\" width=\"74\" x=\"223\" /><rect fill=\"#efb118\" height=\"11.170597598032145\" y=\"353.4756185790768\" width=\"74\" x=\"551\" /><rect fill=\"#efb118\" height=\"12.183475618579052\" y=\"345.53176096078715\" width=\"74\" x=\"469\" /><rect fill=\"#efb118\" height=\"5.8529879901606705\" y=\"321.7797713789611\" width=\"74\" x=\"305\" /><rect fill=\"#efb118\" height=\"22.66676313124003\" y=\"342.61611923021275\" width=\"74\" x=\"387\" /><rect fill=\"#efb118\" height=\"0.26045434814062673\" y=\"368.78454637534367\" width=\"74\" x=\"715\" /><rect fill=\"#efb118\" height=\"1.7146577919259585\" y=\"365.52163218058166\" width=\"74\" x=\"633\" /></g></svg></figure>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import * as Plot from \"npm:@observablehq/plot\";\n",
    "import { document } from \"jsr:@manzt/jupyter-helper\";\n",
    "\n",
    "// Convert our DataFrame to Array<Object>\n",
    "let records = df.toRecords();\n",
    "\n",
    "Plot.plot({\n",
    "  width: 900,\n",
    "  marginLeft: 50,\n",
    "  color: { legend: true },\n",
    "  marks: [\n",
    "    Plot.barY(\n",
    "      records,\n",
    "      Plot.groupX({ y: \"count\" }, {\n",
    "        x: \"type\",\n",
    "        sort: { x: \"-y\" },\n",
    "        fill: (d) => d.public ? \"Public domain\" : \"Copyrighted\",\n",
    "      }),\n",
    "    ),\n",
    "  ],\n",
    "  // Provide a custom `document`\n",
    "  document,\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f408ed5c-4b80-488b-abf2-bb9711408cb8",
   "metadata": {},
   "source": [
    "Right away, we can see that **prints, drawings, and photographs** make up most\n",
    "of the collection. Each category varies in how many images are public\n",
    "domain—images of **prints, drawings, sculptures, and paintings** are mostly\n",
    "public domain, while images of **photographs and portfolios** are largely\n",
    "copyrighted.\n",
    "\n",
    "Observable Plot's API can feel a bit overwhelming at first, but you don’t need\n",
    "to understand it completely to start using it. The nice thing is that you can\n",
    "copy examples, tweak them, and quickly see results. For the code above, the key\n",
    "things to keep in mind are:\n",
    "\n",
    "- Custom `document` – Observable Plot relies on the browser DOM, but since we're\n",
    "  in Deno, we provide a custom document from `jsr:@manzt/jupyter-helper` to\n",
    "  enable rendering.\n",
    "- Converting our Polars `DataFrame` to records – Observable Plot works best with\n",
    "  arrays of objects, so we use `df.toRecords()` to make the data easier to work\n",
    "  with.\n",
    "\n",
    "For specifics on the `Plot` API, see the\n",
    "[documentation](https://observablehq.com/plot/) and\n",
    "[examples](https://observablehq.com/@observablehq/plot-gallery) avaiable.\n",
    "\n",
    "Observable Plot is **composable and expressive**. With just a few tweaks, you\n",
    "can completely change how the data is represented. For example, starting from\n",
    "the plot above, we can modify a couple of lines can produce an entirely\n",
    "different encoding, allowing for quick exploration and iteration – like this\n",
    "\"waffle\" bar chart."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "7481c14b-e884-4a58-9a8f-b9c2a0499948",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<figure style=\"max-width:initial\" class=\"plot-d6a7b5-figure\"><div class=\"plot-d6a7b5-swatches plot-d6a7b5-swatches-wrap\"><style>:where(.plot-d6a7b5-swatches) {\n",
       "  font-family: system-ui, sans-serif;\n",
       "  font-size: 10px;\n",
       "  margin-bottom: 0.5em;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatch > svg) {\n",
       "  margin-right: 0.5em;\n",
       "  overflow: visible;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap) {\n",
       "  display: flex;\n",
       "  align-items: center;\n",
       "  min-height: 33px;\n",
       "  flex-wrap: wrap;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap .plot-d6a7b5-swatch) {\n",
       "  display: inline-flex;\n",
       "  align-items: center;\n",
       "  margin-right: 1em;\n",
       "}</style><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#4269d0\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Copyrighted</span><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#efb118\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Public Domain</span></div><svg viewBox=\"0 0 900 430\" height=\"430\" width=\"900\" text-anchor=\"middle\" font-size=\"10\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" class=\"plot-d6a7b5\"><style>:where(.plot-d6a7b5) {\n",
       "  --plot-background: white;\n",
       "  display: block;\n",
       "  height: auto;\n",
       "  height: intrinsic;\n",
       "  max-width: 100%;\n",
       "}\n",
       ":where(.plot-d6a7b5 text),\n",
       ":where(.plot-d6a7b5 tspan) {\n",
       "  white-space: pre;\n",
       "}</style><g transform=\"translate(0,9)\" aria-label=\"fx-axis tick label\"><g transform=\"translate(4,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">print</text></g><g transform=\"translate(87,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">drawing</text></g><g transform=\"translate(170,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">photograph</text></g><g transform=\"translate(253,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">portfolio</text></g><g transform=\"translate(336,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">sculpture</text></g><g transform=\"translate(419,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">volume</text></g><g transform=\"translate(502,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">painting</text></g><g transform=\"translate(585,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">decorative art</text></g><g transform=\"translate(668,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">technical material</text></g><g transform=\"translate(751,0)\"><text transform=\"translate(87.5,400)\" y=\"0.71em\">new media</text></g></g><g transform=\"translate(0,27)\" aria-label=\"fx-axis label\"><text transform=\"translate(465,400)\">type</text></g><g aria-hidden=\"true\" aria-label=\"y-axis tick\"><g transform=\"translate(4,0)\" stroke=\"currentColor\" fill=\"none\"><path d=\"M0,0L-6,0\" transform=\"translate(50,400)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,360.72513797879157)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,321.45027595758313)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,282.17541393637475)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,242.9005519151663)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,203.62568989395785)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,164.35082787274945)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,125.07596585154101)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,85.80110383033261)\" /><path d=\"M0,0L-6,0\" transform=\"translate(50,46.52624180912419)\" /></g></g><g transform=\"translate(-9,0)\" aria-label=\"y-axis tick label\"><g transform=\"translate(4,0)\" font-variant=\"tabular-nums\" text-anchor=\"end\"><text transform=\"translate(50,400)\" y=\"0.32em\">0</text><text transform=\"translate(50,360.72513797879157)\" y=\"0.32em\">5,000</text><text transform=\"translate(50,321.45027595758313)\" y=\"0.32em\">10,000</text><text transform=\"translate(50,282.17541393637475)\" y=\"0.32em\">15,000</text><text transform=\"translate(50,242.9005519151663)\" y=\"0.32em\">20,000</text><text transform=\"translate(50,203.62568989395785)\" y=\"0.32em\">25,000</text><text transform=\"translate(50,164.35082787274945)\" y=\"0.32em\">30,000</text><text transform=\"translate(50,125.07596585154101)\" y=\"0.32em\">35,000</text><text transform=\"translate(50,85.80110383033261)\" y=\"0.32em\">40,000</text><text transform=\"translate(50,46.52624180912419)\" y=\"0.32em\">45,000</text></g></g><g transform=\"translate(-47,-17)\" text-anchor=\"start\" aria-label=\"y-axis label\"><text transform=\"translate(54,20)\" y=\"0.71em\">↑ Frequency</text></g><g aria-label=\"waffle\"><g transform=\"translate(4,0)\"><pattern id=\"plot-pattern-1-11\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-1-12\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-1-11)\" d=\"M0,0L0,0L0,0L0,0L0,0L58.91229303181263,0L58.91229303181263,-176.7368790954379L11.782458606362527,-176.7368790954379L11.782458606362527,-185.69154763627347L0,-185.69154763627347Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-1-12)\" d=\"M0,-188.51933770180042L0,-188.51933770180042L0,-185.69154763627347L11.782458606362527,-185.69154763627347L11.782458606362527,-176.7368790954379L58.91229303181263,-176.7368790954379L58.91229303181263,-377.03867540360085L23.564917212725053,-377.03867540360085L23.564917212725053,-380.0628397792338L11.782458606362527,-380.0628397792338L11.782458606362527,-388.8211340099634L0,-388.8211340099634Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(87,0)\"><pattern id=\"plot-pattern-2-2\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-2-3\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-2-2)\" d=\"M0,0L0,0L0,0L0,0L0,0L58.91229303181263,0L58.91229303181263,-58.91229303181263L23.564917212725053,-58.91229303181263L23.564917212725053,-68.73100853711472L11.782458606362527,-68.73100853711472L11.782458606362527,-70.69475163817516L0,-70.69475163817516Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-2-3)\" d=\"M0,-70.69475163817516L11.782458606362527,-70.69475163817516L11.782458606362527,-68.73100853711472L23.564917212725053,-68.73100853711472L23.564917212725053,-58.91229303181263L58.91229303181263,-58.91229303181263L58.91229303181263,-247.43163073361305L58.91229303181263,-247.43163073361305L58.91229303181263,-253.71560865700638L47.129834425450106,-253.71560865700638L47.129834425450106,-259.2140893399756L0,-259.2140893399756Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(170,0)\"><pattern id=\"plot-pattern-3-7\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-3-8\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-3-7)\" d=\"M0,0L0,0L0,0L0,0L0,0L58.91229303181263,0L58.91229303181263,-153.17196188271285L23.564917212725053,-153.17196188271285L23.564917212725053,-155.56772846600654L11.782458606362527,-155.56772846600654L11.782458606362527,-164.95442048907537L0,-164.95442048907537Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-3-8)\" d=\"M0,-164.95442048907537L11.782458606362527,-164.95442048907537L11.782458606362527,-155.56772846600654L23.564917212725053,-155.56772846600654L23.564917212725053,-153.17196188271285L58.91229303181263,-153.17196188271285L58.91229303181263,-164.95442048907537L47.129834425450106,-164.95442048907537L47.129834425450106,-175.99065671703494L35.34737581908758,-175.99065671703494L35.34737581908758,-176.7368790954379L0,-176.7368790954379Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(253,0)\"><pattern id=\"plot-pattern-4-9\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-4-10\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-4-9)\" d=\"M0,0L0,0L0,0L0,0L0,0L58.91229303181263,0L58.91229303181263,-35.34737581908758L58.91229303181263,-35.34737581908758L58.91229303181263,-41.47425429439609L47.129834425450106,-41.47425429439609L47.129834425450106,-47.129834425450106L0,-47.129834425450106Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-4-10)\" d=\"M0,-47.129834425450106L47.129834425450106,-47.129834425450106L47.129834425450106,-41.47425429439609L58.91229303181263,-41.47425429439609L58.91229303181263,-47.129834425450106L35.34737581908758,-47.129834425450106L35.34737581908758,-49.682700456828634L23.564917212725053,-49.682700456828634L23.564917212725053,-58.91229303181263L0,-58.91229303181263Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(336,0)\"><pattern id=\"plot-pattern-5-13\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-5-14\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-5-13)\" d=\"M0,0L0,0L0,0L0,0L0,0L35.34737581908758,0L35.34737581908758,-2.042292825102838L23.564917212725053,-2.042292825102838L23.564917212725053,-11.782458606362527L0,-11.782458606362527Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-5-14)\" d=\"M0,-11.782458606362527L23.564917212725053,-11.782458606362527L23.564917212725053,-2.042292825102838L35.34737581908758,-2.042292825102838L35.34737581908758,0L58.91229303181263,0L58.91229303181263,-23.564917212725053L35.34737581908758,-23.564917212725053L35.34737581908758,-30.830766686648616L23.564917212725053,-30.830766686648616L23.564917212725053,-35.34737581908758L0,-35.34737581908758Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(419,0)\"><pattern id=\"plot-pattern-6-17\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-6-18\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-6-17)\" d=\"M0,0L0,0L0,0L0,0L0,0L58.91229303181263,0L58.91229303181263,-11.782458606362527L11.782458606362527,-11.782458606362527L11.782458606362527,-19.558881286561796L0,-19.558881286561796Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-6-18)\" d=\"M0,-23.564917212725053L0,-23.564917212725053L0,-19.558881286561796L11.782458606362527,-19.558881286561796L11.782458606362527,-11.782458606362527L58.91229303181263,-11.782458606362527L58.91229303181263,-23.564917212725053L23.564917212725053,-23.564917212725053L23.564917212725053,-26.785455898464146L11.782458606362527,-26.785455898464146L11.782458606362527,-35.34737581908758L0,-35.34737581908758Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(502,0)\"><pattern id=\"plot-pattern-7-5\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-7-6\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-7-5)\" d=\"M0,0L0,0L0,0L0,0L0,0L35.34737581908758,0L35.34737581908758,-5.498480682969181L23.564917212725053,-5.498480682969181L23.564917212725053,-11.782458606362527L0,-11.782458606362527Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-7-6)\" d=\"M0,-11.782458606362527L23.564917212725053,-11.782458606362527L23.564917212725053,-5.498480682969181L35.34737581908758,-5.498480682969181L35.34737581908758,0L58.91229303181263,0L58.91229303181263,-11.782458606362527L35.34737581908758,-11.782458606362527L35.34737581908758,-19.009033218264875L23.564917212725053,-19.009033218264875L23.564917212725053,-23.564917212725053L0,-23.564917212725053Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(585,0)\"><pattern id=\"plot-pattern-8-0\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-8-1\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-8-0)\" d=\"M0,0L0,0L0,0L0,0L0,0L23.564917212725053,0L23.564917212725053,-3.2205386857390916L11.782458606362527,-3.2205386857390916L11.782458606362527,-11.782458606362527L0,-11.782458606362527Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-8-1)\" d=\"M11.782458606362527,-11.782458606362527L11.782458606362527,-3.2205386857390916L23.564917212725053,-3.2205386857390916L23.564917212725053,0L35.34737581908758,0L35.34737581908758,-0.7462223784029616L23.564917212725053,-0.7462223784029616L23.564917212725053,-11.782458606362527Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(668,0)\"><pattern id=\"plot-pattern-9-15\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><pattern id=\"plot-pattern-9-16\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#efb118\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-9-15)\" d=\"M0,0L0,0L0,0L0,0L0,0L11.782458606362527,0L11.782458606362527,-5.184281786799512L0,-5.184281786799512Z\" transform=\"translate(58.04385348409369,400)\" /><path fill=\"url(#plot-pattern-9-16)\" d=\"M0,-11.782458606362527L0,-5.184281786799512L11.782458606362527,-5.184281786799512L11.782458606362527,0L11.782458606362527,0L11.782458606362527,-6.598176819563015L0,-6.598176819563015Z\" transform=\"translate(58.04385348409369,400)\" /></g><g transform=\"translate(751,0)\"><pattern id=\"plot-pattern-10-4\" patternUnits=\"userSpaceOnUse\" height=\"11.782458606362527\" width=\"11.782458606362527\"><rect fill=\"#4269d0\" height=\"10.782458606362527\" width=\"10.782458606362527\" y=\"0.5\" x=\"0.5\" /></pattern><path fill=\"url(#plot-pattern-10-4)\" d=\"M0,0L0,0L0,0L0,0L0,0L11.782458606362527,0L11.782458606362527,-0.39274862021208423L0,-0.39274862021208423Z\" transform=\"translate(58.04385348409369,400)\" /></g></g><g aria-label=\"rule\"><g transform=\"translate(4,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(87,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(170,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(253,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(336,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(419,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(502,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(585,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(668,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g><g transform=\"translate(751,0)\" stroke=\"currentColor\"><line y2=\"400\" y1=\"400\" x2=\"125\" x1=\"50\" /></g></g></svg></figure>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Plot.plot({\n",
    "  width: 900,\n",
    "  marginLeft: 50,\n",
    "  color: { legend: true },\n",
    "  marks: [\n",
    "    Plot.waffleY(\n",
    "      /* changed, Plot.barY */\n",
    "      records,\n",
    "      Plot.groupZ(\n",
    "        /* changed, Plot.groupX */\n",
    "        { y: \"count\" },\n",
    "        {\n",
    "          fx: \"type\",\n",
    "          fill: (d) => d.public ? \"Public Domain\" : \"Copyrighted\",\n",
    "          sort: { fx: \"-y\" }, /* changed, sort: { x: \"-y\"  } */\n",
    "          unit: 300, /* new */\n",
    "        },\n",
    "      ),\n",
    "    ),\n",
    "    Plot.ruleY([0]),\n",
    "  ],\n",
    "  document,\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6116bbd2-9c3f-410b-97aa-b73c82432dad",
   "metadata": {},
   "source": [
    "We just needed to change the mark and some encoding fields.\n",
    "\n",
    "With both Polars and Plot, we can ask more specific questions about the data,\n",
    "such as **\"Which artists have the most artwork in the collection?\"** Here, we\n",
    "first use Polars to group the data by artist (`name`) and whether the artwork is\n",
    "public domain (`public`), counting the number of works for each. Then, we use\n",
    "Plot to visualize the top 25 artists."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bf0828e9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<figure style=\"max-width:initial\" class=\"plot-d6a7b5-figure\"><div class=\"plot-d6a7b5-swatches plot-d6a7b5-swatches-wrap\"><style>:where(.plot-d6a7b5-swatches) {\n",
       "  font-family: system-ui, sans-serif;\n",
       "  font-size: 10px;\n",
       "  margin-bottom: 0.5em;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatch > svg) {\n",
       "  margin-right: 0.5em;\n",
       "  overflow: visible;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap) {\n",
       "  display: flex;\n",
       "  align-items: center;\n",
       "  min-height: 33px;\n",
       "  flex-wrap: wrap;\n",
       "}\n",
       ":where(.plot-d6a7b5-swatches-wrap .plot-d6a7b5-swatch) {\n",
       "  display: inline-flex;\n",
       "  align-items: center;\n",
       "  margin-right: 1em;\n",
       "}</style><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#4269d0\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Copyrighted</span><span class=\"plot-d6a7b5-swatch\"><svg fill=\"#efb118\" height=\"15\" width=\"15\"><rect height=\"100%\" width=\"100%\" /></svg>Public domain</span></div><svg viewBox=\"0 0 640 540\" height=\"540\" width=\"640\" text-anchor=\"middle\" font-size=\"10\" font-family=\"system-ui, sans-serif\" fill=\"currentColor\" class=\"plot-d6a7b5\"><style>:where(.plot-d6a7b5) {\n",
       "  --plot-background: white;\n",
       "  display: block;\n",
       "  height: auto;\n",
       "  height: intrinsic;\n",
       "  max-width: 100%;\n",
       "}\n",
       ":where(.plot-d6a7b5 text),\n",
       ":where(.plot-d6a7b5 tspan) {\n",
       "  white-space: pre;\n",
       "}</style><g transform=\"translate(0,9)\" stroke=\"currentColor\" fill=\"none\" aria-hidden=\"true\" aria-label=\"y-axis tick\"><path d=\"M0,0L-6,0\" transform=\"translate(200,26)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,46)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,66)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,86)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,106)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,126)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,146)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,166)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,186)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,206)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,226)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,246)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,266)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,286)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,306)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,326)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,346)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,366)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,386)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,406)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,426)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,446)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,466)\" /><path d=\"M0,0L-6,0\" transform=\"translate(200,486)\" /></g><g transform=\"translate(-9,9)\" text-anchor=\"end\" aria-label=\"y-axis tick label\"><text transform=\"translate(200,26)\" y=\"0.32em\">Robert Frank</text><text transform=\"translate(200,46)\" y=\"0.32em\">Honoré Daumier</text><text transform=\"translate(200,66)\" y=\"0.32em\">Alfred Stieglitz</text><text transform=\"translate(200,86)\" y=\"0.32em\">Jasper Johns</text><text transform=\"translate(200,106)\" y=\"0.32em\">Richard Diebenkorn</text><text transform=\"translate(200,126)\" y=\"0.32em\">American 20th Century</text><text transform=\"translate(200,146)\" y=\"0.32em\">Jacques Callot</text><text transform=\"translate(200,166)\" y=\"0.32em\">Max Beckmann</text><text transform=\"translate(200,186)\" y=\"0.32em\">Mark Rothko</text><text transform=\"translate(200,206)\" y=\"0.32em\">Charles B. J. Févret de Saint-Mémin</text><text transform=\"translate(200,226)\" y=\"0.32em\">Alphonse Legros</text><text transform=\"translate(200,246)\" y=\"0.32em\">James McNeill Whistler</text><text transform=\"translate(200,266)\" y=\"0.32em\">Lee Friedlander</text><text transform=\"translate(200,286)\" y=\"0.32em\">David Young Cameron</text><text transform=\"translate(200,306)\" y=\"0.32em\">American 19th Century</text><text transform=\"translate(200,326)\" y=\"0.32em\">Unknown 19th Century</text><text transform=\"translate(200,346)\" y=\"0.32em\">Eadweard Muybridge</text><text transform=\"translate(200,366)\" y=\"0.32em\">John Marin</text><text transform=\"translate(200,386)\" y=\"0.32em\">German 15th Century</text><text transform=\"translate(200,406)\" y=\"0.32em\">Robert Havell, Jr.</text><text transform=\"translate(200,426)\" y=\"0.32em\">M.C. Escher</text><text transform=\"translate(200,446)\" y=\"0.32em\">Allen Ginsberg</text><text transform=\"translate(200,466)\" y=\"0.32em\">Albrecht Dürer</text><text transform=\"translate(200,486)\" y=\"0.32em\">Ernst Ludwig Kirchner</text></g><g transform=\"translate(-197,0)\" aria-label=\"y-axis label\"><text transform=\"translate(200,265) rotate(-90)\" y=\"0.71em\">name</text></g><g stroke=\"currentColor\" fill=\"none\" aria-hidden=\"true\" aria-label=\"x-axis tick\"><path d=\"M0,0L0,6\" transform=\"translate(200,510)\" /><path d=\"M0,0L0,6\" transform=\"translate(283.3168022217814,510)\" /><path d=\"M0,0L0,6\" transform=\"translate(366.63360444356283,510)\" /><path d=\"M0,0L0,6\" transform=\"translate(449.95040666534413,510)\" /><path d=\"M0,0L0,6\" transform=\"translate(533.2672088871257,510)\" /><path d=\"M0,0L0,6\" transform=\"translate(616.584011108907,510)\" /></g><g transform=\"translate(0,9)\" font-variant=\"tabular-nums\" aria-label=\"x-axis tick label\"><text transform=\"translate(200,510)\" y=\"0.71em\">0</text><text transform=\"translate(283.3168022217814,510)\" y=\"0.71em\">1,000</text><text transform=\"translate(366.63360444356283,510)\" y=\"0.71em\">2,000</text><text transform=\"translate(449.95040666534413,510)\" y=\"0.71em\">3,000</text><text transform=\"translate(533.2672088871257,510)\" y=\"0.71em\">4,000</text><text transform=\"translate(616.584011108907,510)\" y=\"0.71em\">5,000</text></g><g transform=\"translate(17,27)\" text-anchor=\"end\" aria-label=\"x-axis label\"><text transform=\"translate(620,510)\">name_count →</text></g><g aria-label=\"bar\"><rect fill=\"#4269d0\" height=\"18\" y=\"26\" width=\"420\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"46\" width=\"190.79547708787942\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"66\" width=\"145.80440388811746\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"86\" width=\"140.38881174370164\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"106\" width=\"119.97619519936518\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"146\" width=\"114.64391985717123\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"166\" width=\"109.9781789327514\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"186\" width=\"78.98432850624874\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"206\" width=\"74.98512199960328\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"226\" width=\"58.73834556635586\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"126\" width=\"57.90517754413804\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"246\" width=\"57.32195992858561\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"126\" width=\"56.822059115254945\" x=\"257.90517754413804\" /><rect fill=\"#4269d0\" height=\"18\" y=\"266\" width=\"43.74132116643523\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"286\" width=\"40.90854989089465\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"306\" width=\"40.07538186867686\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"326\" width=\"38.82562983535013\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"346\" width=\"36.74270977980561\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"366\" width=\"35.49295774647888\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"386\" width=\"35.40964094425709\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"406\" width=\"35.326324142035304\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"426\" width=\"34.74310652648285\" x=\"200\" /><rect fill=\"#4269d0\" height=\"18\" y=\"446\" width=\"33.07677048204721\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"466\" width=\"31.07716722872445\" x=\"200\" /><rect fill=\"#efb118\" height=\"18\" y=\"486\" width=\"30.99385042650266\" x=\"200\" /></g></svg></figure>"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "const artworkTotals = df\n",
    "  .groupBy(\"name\", \"public\")\n",
    "  .len()\n",
    "  .sort(\"name_count\", true)\n",
    "  .head(25)\n",
    "  .toRecords();\n",
    "\n",
    "Plot.plot({\n",
    "  marginLeft: 200,\n",
    "  color: { legend: true },\n",
    "  marks: [\n",
    "    Plot.barX(artworkTotals, {\n",
    "      x: \"name_count\",\n",
    "      y: \"name\",\n",
    "      sort: { y: \"-x\" },\n",
    "      fill: (d) => d.public ? \"Public domain\" : \"Copyrighted\",\n",
    "    }),\n",
    "  ],\n",
    "  document,\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f6e9f6c-d0d6-4226-83cb-2b3e88c7df51",
   "metadata": {},
   "source": [
    "Again, we can draw some high-level takeaways from this plot: The photographer\n",
    "[Robert Frank](https://en.wikipedia.org/wiki/Robert_Frank) has nearly twice as\n",
    "many works as the next highest artist. Some entries, like **\"American 20th\n",
    "Century\"** and **\"German 15th Century,\"** represent groups rather than\n",
    "individuals. Additionally, an artist’s work is typically **either entirely\n",
    "public domain or not at all**–likely because public domain status is determined\n",
    "at the **collection level**, with institutions clearing entire groups of works\n",
    "at once rather than evaluating individual pieces separately.\n",
    "\n",
    "With our tidy and clean dataset, we can quickly iterate in Deno to ask both\n",
    "broad questions (\"What even is _in_ this collection?\") and very specific ones\n",
    "(\"Who created this artwork in a given year?\").\n",
    "\n",
    "## Interactivity\n",
    "\n",
    "So far, we've used **static plots** to explore our data, but Jupyter notebooks\n",
    "support rich, interactive outputs using **HTML, CSS, and JavaScript**.\n",
    "\n",
    "With Deno, we can add more interactivity to our data exploration by either using\n",
    "**off-the-shelf interactive [anywidgets](https://github.com/manzt/anywidget)**\n",
    "by quickly building **custom components** for visualizing subsets in ways\n",
    "prebuilt tools don't support. Since we're already in JavaScript, experimenting\n",
    "with small, task-specific utilities is effortless. Unlike other Jupyter runtimes\n",
    "that juggle multiple languages, Deno provides direct access to the full web\n",
    "ecosystem, making custom visualizations seamless.\n",
    "\n",
    "In this section, we'll look at:\n",
    "\n",
    "- Prebuilt interactive table anywidgets for exploring data\n",
    "- Create custom `<Gallery />` component to visualize subsets of the dataset\n",
    "\n",
    "### DataFrame viewer anywidgets\n",
    "\n",
    "In Jupyter, \"widgets\" extend notebook outputs with interactive views and\n",
    "controls for objects in the kernel (i.e., backend). Unlike standard outputs,\n",
    "they consist of both kernel-side and front-end code, communicating directly via\n",
    "custom messages. While the Jupyter Widget ecosystem is largely Python-centric,\n",
    "the [**anywidget** project](https://github.com/manzt/anywidget) provides a\n",
    "kernel-agnostic way to create and share these components.\n",
    "\n",
    "With anywidget, widgets can be published to [JSR](https://jsr.io) and used in\n",
    "the Deno Jupyter kernel. For example, `jsr:@manzt/jupyter-helper` provides some\n",
    "off-the-shelf anywidgets for interactively viewing Polars data frames.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "c799edbb-5642-497c-a4af-b3f3a39b8308",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e942993b-b77a-49b2-8f27-84f976237ee4",
       "version_major": 2,
       "version_minor": 1
      }
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import { agGrid } from \"jsr:@manzt/jupyter-helper\";\n",
    "\n",
    "agGrid(df.head(100)); // just look at the first 200 items"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d99a03fc-04f0-419e-935b-14cfa75e6e20",
   "metadata": {},
   "source": [
    "The `agGrid` export renders the `pl.DataFrame` using the popular\n",
    "[AG Grid](https://www.ag-grid.com/) library, allowing for interactive sorting,\n",
    "filtering, and pagination. This default view is minimal but can be easily\n",
    "extended with AG Grid's many features."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "25a4a052-215d-42f5-9770-875919dbc9c1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "6cfc1fe7-6f32-498a-aeb8-6f897ae35427",
       "version_major": 2,
       "version_minor": 1
      }
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import { quak } from \"jsr:@manzt/jupyter-helper\";\n",
    "\n",
    "quak(df);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ee3d329-c06e-45ae-a517-a294602631c6",
   "metadata": {},
   "source": [
    "The `quak` export renders the `pl.DataFrame` with the\n",
    "[quak](https://github.com/manzt/quak) data table viewer. Like `agGrid`, it also\n",
    "includes summary visualizations above each column, showing 1D distributions, and\n",
    "can scale to much more data (e.g., the entire collection). These 1D summary\n",
    "visualizations are _interactive_, enabling _cross-filtering_ across columns.\n",
    "\n",
    "Together, the programmatic and interactive layers enable unique hybrid workflows\n",
    "working with data. Some queries are easier to express in code, while others are\n",
    "more intuitive through direct manipulation. In notebooks, the two approaches\n",
    "come together naturally.\n",
    "\n",
    "For example, `polars` can be used to subset the data to just public domain\n",
    "paintings (~2k out of ~115k artworks), and then a date range can be selected\n",
    "interactively by _brushing_ in the UI to explore variation across other columns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2fdd3d7f-700d-4517-ad08-24dbf7c1994c",
   "metadata": {},
   "outputs": [],
   "source": [
    "const paintings = df.filter(pl.col(\"type\").eq(pl.lit(\"painting\")));\n",
    "quak(paintings);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67f56e17-95e9-4c5d-9754-a1fe594d74e6",
   "metadata": {},
   "source": [
    "### Custom `<Gallery/>` component\n",
    "\n",
    "Being in Deno gives us direct access to the web ecosystem, making it easy to\n",
    "build small, useful UIs for exploring our data in ways that off-the-shelf tools\n",
    "don't support.\n",
    "\n",
    "For example, our dataset contains many image URLs, and it would be helpful to\n",
    "view selections of images in a \"gallery view.\" If we filter down to a subset of\n",
    "artworks, actually seeing some of the images can provide valuable intuition\n",
    "about what's in that subset.\n",
    "\n",
    "We'll create a custom **JSX component** that server-renders (SSR) a gallery view\n",
    "for any dataset subset using the `render` export from\n",
    "`jsr:@manzt/jupyter-helper`. This helper simply converts JSX to an HTML string,\n",
    "which we display using the\n",
    "[`Deno.jupyter.Displayable`](https://docs.deno.com/api/deno/~/Deno.jupyter.Displayable)\n",
    "interface."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "569b4216-7b75-4a56-a875-283b7a3caefc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import * as React from \"npm:react\";\n",
    "import { render } from \"jsr:@manzt/jupyter-helper\";\n",
    "\n",
    "function Gallery({ objects, size = 100 }) {\n",
    "  return (\n",
    "    <div\n",
    "      style={{\n",
    "        display: \"grid\",\n",
    "        gridTemplateColumns: `repeat(auto-fill, minmax(${size}px, 1fr))`,\n",
    "        gap: \"4px\",\n",
    "      }}\n",
    "    >\n",
    "      {objects\n",
    "        .select(\"objectid\", \"thumburl\", \"title\", \"public\")\n",
    "        .map(([objectid, thumburl, title, publicDomain]) => (\n",
    "          <div\n",
    "            key={objectid}\n",
    "            style={{ position: \"relative\", textAlign: \"center\" }}\n",
    "          >\n",
    "            <a\n",
    "              href={`https://www.nga.gov/collection/art-object-page.${objectid}.html`}\n",
    "              style={{\n",
    "                display: \"block\",\n",
    "                width: `${size}px`,\n",
    "                height: `${size}px`,\n",
    "                position: \"relative\",\n",
    "              }}\n",
    "            >\n",
    "              <img\n",
    "                src={thumburl}\n",
    "                alt={title}\n",
    "                style={{\n",
    "                  width: \"100%\",\n",
    "                  height: \"100%\",\n",
    "                  objectFit: \"cover\",\n",
    "                  borderRadius: \"5px\",\n",
    "                }}\n",
    "              />\n",
    "              {publicDomain && (\n",
    "                <img\n",
    "                  src=\"https://mirrors.creativecommons.org/presskit/icons/zero.svg\"\n",
    "                  alt=\"Public Domain\"\n",
    "                  style={{\n",
    "                    position: \"absolute\",\n",
    "                    bottom: \"3px\",\n",
    "                    right: \"3px\",\n",
    "                    width: \"20px\",\n",
    "                    height: \"20px\",\n",
    "                    opacity: 0.60,\n",
    "                  }}\n",
    "                />\n",
    "              )}\n",
    "            </a>\n",
    "          </div>\n",
    "        ))}\n",
    "    </div>\n",
    "  );\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b22e6a0-9db2-4236-a1ba-1b9ef6d251d5",
   "metadata": {},
   "source": [
    "`Gallery` is a custom component that displays a grid of artwork thumbnails from\n",
    "a given dataset. Each image **links to its NGA collection page**, and public\n",
    "domain works are marked with a CC0 icon.\n",
    "\n",
    "We can SSR `Gallery` with our data to better understand a smaller subset, making\n",
    "it easier to explore specific artists, time periods, or categories visually. For\n",
    "example, a **random sample** from _all_ the collection:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "db3a8ad1-4846-44a9-925f-db11787220d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "render(<Gallery objects={df.sample(20)} />);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92ace3f3-88ac-4e18-8a5a-a86b48a9d7af",
   "metadata": {},
   "source": [
    "Or sample of just paintings:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d92f0c51-b8a1-42ac-ba6d-19efd6ec7cbf",
   "metadata": {},
   "outputs": [],
   "source": [
    "render(<Gallery objects={paintings.sample(20)} />);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "febbdc86-02cb-40ba-ac06-485694ccd67d",
   "metadata": {},
   "source": [
    "It's worth taking a moment to appreciate that in just a few lines of code, we've\n",
    "built a **domain-specific utility** that makes our data more intuitive to\n",
    "explore. This kind of flexibility is powerful and unique to Deno, letting us\n",
    "quickly craft custom utilities that provide deeper insights.\n",
    "\n",
    "With our toolkit, let's take a closer look at the NGA dataset:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b402f8d-0d7f-49de-a527-01ad62475c6f",
   "metadata": {},
   "source": [
    "## A closer look\n",
    "\n",
    "So far, our data exploration has focused on **high-level summaries** of the NGA\n",
    "collection by artist and artwork type using static plots. Now, let's drill down\n",
    "into something potentially more interesting using both our complete toolkit:\n",
    "_when_ these works were created.\n",
    "\n",
    "We'll start with a stacked histogram to see the overall distribution of artwork\n",
    "types over time, then facet by public domain status to uncover any patterns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a4819f1-b3da-4f4e-938b-1f4dd2c48c70",
   "metadata": {},
   "outputs": [],
   "source": [
    "Plot.plot({\n",
    "  y: { grid: true },\n",
    "  color: { legend: true },\n",
    "  marks: [\n",
    "    Plot.rectY(\n",
    "      records.filter((r) => r.year > 1401), // 15th century or later\n",
    "      Plot.binX({ y: \"count\" }, {\n",
    "        x: (d) => new Date(d.year, 0, 1),\n",
    "        fill: \"type\",\n",
    "        fy: (d) => d.public ? \"Public Domain\" : \"Copyrighted\",\n",
    "      }),\n",
    "    ),\n",
    "    Plot.ruleY([0]),\n",
    "  ],\n",
    "  marginLeft: 100,\n",
    "  marginRight: 100,\n",
    "  width: 1000,\n",
    "  height: 400,\n",
    "  document,\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b95decd3-9561-4a97-9f8c-bdfdb8be57d8",
   "metadata": {},
   "source": [
    "The distribution of artworks with **copyrighted** and **public domain** images\n",
    "are noticeably different. Nearly all **copyrighted works appear after 1850**,\n",
    "while **public domain artworks are more evenly spread** over time—_except_ for a\n",
    "**sharp spike in public domain drawings from the 1940s**, with over 15k artworks\n",
    "added.\n",
    "\n",
    "This is a striking anomaly. Are they all from the same artist? Why only\n",
    "drawings? Let’s take a closer look.\n",
    "\n",
    "We'll filter down the public domain data to this span."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ac12287-5668-4437-bf4c-e23153ac784b",
   "metadata": {},
   "outputs": [],
   "source": [
    "let notablePeriod = df\n",
    "  .filter(\"public\") // just public domain\n",
    "  .filter(pl.col(\"year\").gt(pl.lit(1925))) // between 1925 - 1955\n",
    "  .filter(pl.col(\"year\").lt(pl.lit(1955)))\n",
    "  .sort({ by: \"year\" });\n",
    "\n",
    "Plot.plot({\n",
    "  y: { grid: true },\n",
    "  color: { legend: true },\n",
    "  marks: [\n",
    "    Plot.rectY(\n",
    "      notablePeriod.toRecords(),\n",
    "      Plot.binX({ y: \"count\" }, {\n",
    "        x: (d) => new Date(d.year, 0, 1),\n",
    "        fill: \"type\",\n",
    "      }),\n",
    "    ),\n",
    "  ],\n",
    "  document,\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "790271a4-da6f-4e72-877b-15582e68d81a",
   "metadata": {},
   "source": [
    "Zooming into the **1925–1955 range**, we see that almost all the data falls\n",
    "within a much narrower window. Interestingly, the spike is highly concentrated\n",
    "between **1935 and 1942**, with very little artwork outside this period.\n",
    "\n",
    "Narrowing in closer, we can see that in this window there are ~18k individual\n",
    "drawings:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f30168f-1125-43c5-b08a-a28c898a5721",
   "metadata": {},
   "outputs": [],
   "source": [
    "const totalNumberOfDrawings = notablePeriod\n",
    "  .filter(pl.col(\"year\").gt(pl.lit(1934)))\n",
    "  .filter(pl.col(\"year\").lt(pl.lit(1943)))\n",
    "  .shape\n",
    "  .height;\n",
    "\n",
    "totalNumberOfDrawings;"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "defd431e-20bf-42df-964c-7a8655df451e",
   "metadata": {},
   "source": [
    "made by over 1000 individuals:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8ec4023-2bf6-4cfc-96b2-232d127dcff6",
   "metadata": {},
   "outputs": [],
   "source": [
    "const numberOfArtists = notablePeriod\n",
    "  .filter(pl.col(\"year\").gt(pl.lit(1934)))\n",
    "  .filter(pl.col(\"year\").lt(pl.lit(1943)))\n",
    "  .groupBy(\"name\")\n",
    "  .len()\n",
    "  .shape\n",
    "  .height;\n",
    "\n",
    "numberOfArtists;"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08d310ad-b5e4-43c6-a306-9e8e82b3c87a",
   "metadata": {},
   "source": [
    "This gets even more interesting! The artwork is not from just one specific\n",
    "source—there was a **surge of public works between 1935 and 1942**.\n",
    "\n",
    "Let's use our custom `<Gallery />` view to explore a subset of these works—maybe\n",
    "seeing them will give us some insight."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eceb7b72-d546-43f6-824e-01209866651c",
   "metadata": {},
   "outputs": [],
   "source": [
    "render(<Gallery objects={notablePeriod.sample(100)} />);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d4ec2ae4-c82b-432e-b6cb-151660c87825",
   "metadata": {},
   "source": [
    "So all these artworks, despite being from different individuals, have a very\n",
    "similar style and medium. That’s something worth digging into—what might explain\n",
    "this pattern?\n",
    "\n",
    "Thanks to our custom `<Gallery />` component, we can click on artworks and view\n",
    "their metadata. Sampling a few, we see that they all belong to the\n",
    "[\"Index of American Design\"](https://www.nga.gov/features/exhibitions/outliers-and-american-vanguard-artist-biographies/index-of-american-design.html)\n",
    "collection.\n",
    "\n",
    "> Conceived as an effort to document and preserve American folk and decorative\n",
    "> arts, the Index of American Design consists of 18,257 watercolor renderings\n",
    "> created between **1935 and 1942** as part of a Federal Art Project (FAP)\n",
    "> work-relief program. Around 400 artists meticulously recreated textiles,\n",
    "> woodcarvings, weathervanes, and other objects from across the U.S., aiming to\n",
    "> establish a distinct American visual lineage. The project was ultimately\n",
    "> housed at the National Gallery of Art, becoming a widely exhibited visual\n",
    "> archive.\n",
    "\n",
    "That’s it! We’re seeing a **collection of public works funded by a federal\n",
    "program during the Great Depression**—clear in both the data and historical\n",
    "context."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33981752-9831-4d80-869a-d45cb202b0d5",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "\n",
    "Using Deno’s Jupyter kernel, we explored the NGA Open Access dataset, combining\n",
    "static plots, interactive widgets, and custom JSX components to uncover patterns\n",
    "in the data. This led us to a historical insight—the Index of American Design, a\n",
    "public art project from the 1930s.\n",
    "\n",
    "With Deno, JSX, and anywidget, we seamlessly bridged data analysis with\n",
    "web-based visualization, making it easy to build domain-specific tools for\n",
    "deeper exploration."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e1f4181-5375-43ac-be6f-284585ad5d80",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14170f37-d4ae-49b1-b699-610ac904f1a6",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3cc84307-a0ff-4f88-a934-36b36bbb540a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Deno",
   "language": "typescript",
   "name": "deno"
  },
  "language_info": {
   "codemirror_mode": "typescript",
   "file_extension": ".ts",
   "mimetype": "text/x.typescript",
   "name": "typescript",
   "nbconvert_exporter": "script",
   "pygments_lexer": "typescript",
   "version": "5.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
