{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a039bdf7",
   "metadata": {},
   "source": [
    "# Interaction\n",
    "\n",
    "_“A graphic is not ‘drawn’ once and for all; it is ‘constructed’ and reconstructed until it reveals all the relationships constituted by the interplay of the data. The best graphic operations are those carried out by the decision-maker themself.”_ &mdash; [Jacques Bertin](https://books.google.com/books?id=csqX_xnm4tcC)\n",
    "\n",
    "Visualization provides a powerful means of making sense of data. A single image, however, typically provides answers to, at best, a handful of questions. Through _interaction_ we can transform static images into tools for exploration: highlighting points of interest, zooming in to reveal finer-grained patterns, and linking across multiple views to reason about multi-dimensional relationships.\n",
    "\n",
    "At the core of interaction is the notion of a _selection_: a means of indicating to the computer which elements or regions we are interested in. For example, we might hover the mouse over a point, click multiple marks, or draw a bounding box around a region to highlight subsets of the data for further scrutiny.\n",
    "\n",
    "Alongside visual encodings and data transformations, Altair provides a _selection_ abstraction for authoring interactions. These selections encompass three aspects:\n",
    "\n",
    "1. Input event handling to select points or regions of interest, such as mouse hover, click, drag, scroll, and touch events.\n",
    "2. Generalizing from the input to form a selection rule (or [_predicate_](https://en.wikipedia.org/wiki/Predicate_%28mathematical_logic%29)) that determines whether or not a given data record lies within the selection.\n",
    "3. Using the selection predicate to dynamically configure a visualization by driving _conditional encodings_, _filter transforms_, or _scale domains_.\n",
    "\n",
    "This notebook introduces interactive selections and explores how to use them to author a variety of interaction techniques, such as dynamic queries, panning &amp; zooming, details-on-demand, and brushing &amp; linking.\n",
    "\n",
    "_This notebook is part of the [data visualization curriculum](https://github.com/uwdata/visualization-curriculum)._"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "04c4fe3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import altair as alt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32f0f71c",
   "metadata": {},
   "source": [
    "## Datasets\n",
    "\n",
    "We will visualize a variety of datasets from the [vega-datasets](https://github.com/vega/vega-datasets) collection:\n",
    "\n",
    "- A dataset of `cars` from the 1970s and early 1980s,\n",
    "- A dataset of `movies`, previously used in the [Data Transformation](https://github.com/uwdata/visualization-curriculum/blob/master/altair_data_transformation.ipynb) notebook,\n",
    "- A dataset containing ten years of [S&amp;P 500](https://en.wikipedia.org/wiki/S%26P_500_Index) (`sp500`) stock prices,\n",
    "- A dataset of technology company `stocks`, and\n",
    "- A dataset of `flights`, including departure time, distance, and arrival delay."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "77655ebc",
   "metadata": {},
   "outputs": [],
   "source": [
    "cars = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json'\n",
    "movies = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json'\n",
    "sp500 = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/sp500.csv'\n",
    "stocks = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/stocks.csv'\n",
    "flights = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/flights-5k.json'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c512831",
   "metadata": {},
   "source": [
    "## Introducing Selections\n",
    "\n",
    "Let's start with a basic selection: simply clicking a point to highlight it. Using the `cars` dataset, we'll start with a scatter plot of horsepower versus miles per gallon, with a color encoding for the number cylinders in the car engine.\n",
    "\n",
    "In addition, we'll create a selection instance by calling `alt.selection_single()`, indicating we want a selection defined over a _single value_. By default, the selection uses a mouse click to determine the selected value. To register a selection with a chart, we must add it using the `.add_selection()` method.\n",
    "\n",
    "Once our selection has been defined, we can use it as a parameter for _conditional encodings_, which apply a different encoding depending on whether a data record lies in or out of the selection. For example, consider the following code:\n",
    "\n",
    "~~~ python\n",
    "color=alt.condition(selection, 'Cylinders:O', alt.value('grey'))\n",
    "~~~\n",
    "\n",
    "This encoding definition states that data points contained within the `selection` should be colored according to the `Cylinder` field, while non-selected data points should use a default `grey`. An empty selection includes _all_ data points, and so initially all points will be colored.\n",
    "\n",
    "_Try clicking different points in the chart below. What happens? (Click the background to clear the selection state and return to an \"empty\" selection.)_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "a5326f4c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-62b9068157144f0389216a288b24a4c5\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-62b9068157144f0389216a288b24a4c5\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-62b9068157144f0389216a288b24a4c5\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json\"}, \"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector001\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector001\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"selection\": {\"selector001\": {\"type\": \"single\"}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "selection = alt.selection_single();\n",
    "  \n",
    "alt.Chart(cars).mark_circle().add_selection(\n",
    "    selection\n",
    ").encode(\n",
    "    x='Horsepower:Q',\n",
    "    y='Miles_per_Gallon:Q',\n",
    "    color=alt.condition(selection, 'Cylinders:O', alt.value('grey')),\n",
    "    opacity=alt.condition(selection, alt.value(0.8), alt.value(0.1))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29881c95",
   "metadata": {},
   "source": [
    "Of course, highlighting individual data points one-at-a-time is not particularly exciting! As we'll see, however, single value selections provide a useful building block for more powerful interactions. Moreover, single value selections are just one of the three selection types provided by Altair:\n",
    "\n",
    "- `selection_single` - select a single discrete value, by default on click events. \n",
    "- `selection_multi` - select multiple discrete values. The first value is selected on mouse click and additional values toggled using shift-click. \n",
    "- `selection_interval` - select a continuous range of values, initiated by mouse drag.\n",
    "\n",
    "Let's compare each of these selection types side-by-side. To keep our code tidy we'll first define a function (`plot`) that generates a scatter plot specification just like the one above. We can pass a selection to the `plot` function to have it applied to the chart:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "7352a2f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot(selection):\n",
    "    return alt.Chart(cars).mark_circle().add_selection(\n",
    "        selection\n",
    "    ).encode(\n",
    "        x='Horsepower:Q',\n",
    "        y='Miles_per_Gallon:Q',\n",
    "        color=alt.condition(selection, 'Cylinders:O', alt.value('grey')),\n",
    "        opacity=alt.condition(selection, alt.value(0.8), alt.value(0.1))\n",
    "    ).properties(\n",
    "        width=240,\n",
    "        height=180\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1850a3bf",
   "metadata": {},
   "source": [
    "Let's use our `plot` function to create three chart variants, one per selection type.\n",
    "\n",
    "The first (`single`) chart replicates our earlier example. The second (`multi`) chart supports shift-click interactions to toggle inclusion of multiple points within the selection. The third (`interval`) chart generates a selection region (or _brush_) upon mouse drag. Once created, you can drag the brush around to select different points, or scroll when the cursor is inside the brush to scale (zoom) the brush size.\n",
    "\n",
    "_Try interacting with each of the charts below!_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "660939c6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-37560940e05846639cdeca17a01b42b1\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-37560940e05846639cdeca17a01b42b1\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-37560940e05846639cdeca17a01b42b1\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"hconcat\": [{\"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector002\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector002\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"height\": 180, \"selection\": {\"selector002\": {\"type\": \"single\"}}, \"title\": \"Single (Click)\", \"width\": 240}, {\"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector003\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector003\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"height\": 180, \"selection\": {\"selector003\": {\"type\": \"multi\"}}, \"title\": \"Multi (Shift-Click)\", \"width\": 240}, {\"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector004\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector004\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"height\": 180, \"selection\": {\"selector004\": {\"type\": \"interval\"}}, \"title\": \"Interval (Drag)\", \"width\": 240}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json\"}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.HConcatChart(...)"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alt.hconcat(\n",
    "  plot(alt.selection_single()).properties(title='Single (Click)'),\n",
    "  plot(alt.selection_multi()).properties(title='Multi (Shift-Click)'),\n",
    "  plot(alt.selection_interval()).properties(title='Interval (Drag)')\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f122b1a",
   "metadata": {},
   "source": [
    "The examples above use default interactions (click, shift-click, drag) for each selection type. We can further customize the interactions by providing input event specifications using [Vega event selector syntax](https://vega.github.io/vega/docs/event-streams/). For example, we can modify our `single` and `multi` charts to trigger upon `mouseover` events instead of `click` events.\n",
    "\n",
    "_Hold down the shift key in the second chart to \"paint\" with data!_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "f7b0bf8f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-afb6955a2708478085a5aa5715bce6e2\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-afb6955a2708478085a5aa5715bce6e2\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-afb6955a2708478085a5aa5715bce6e2\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"hconcat\": [{\"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector005\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector005\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"height\": 180, \"selection\": {\"selector005\": {\"type\": \"single\", \"on\": \"mouseover\"}}, \"title\": \"Single (Mouseover)\", \"width\": 240}, {\"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector006\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector006\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": \"Horsepower\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"Miles_per_Gallon\"}}, \"height\": 180, \"selection\": {\"selector006\": {\"type\": \"multi\", \"on\": \"mouseover\"}}, \"title\": \"Multi (Shift-Mouseover)\", \"width\": 240}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json\"}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.HConcatChart(...)"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alt.hconcat(\n",
    "  plot(alt.selection_single(on='mouseover')).properties(title='Single (Mouseover)'),\n",
    "  plot(alt.selection_multi(on='mouseover')).properties(title='Multi (Shift-Mouseover)')\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9369ce3e",
   "metadata": {},
   "source": [
    "Now that we've covered the basics of Altair selections, let's take a tour through the various interaction techniques they enable!\n",
    "\n",
    "## Dynamic Queries\n",
    "\n",
    "_Dynamic queries_ enables rapid, reversible exploration of data to isolate patterns of interest. As defined by [Ahlberg, Williamson, &amp; Shneiderman](https://www.cs.umd.edu/~ben/papers/Ahlberg1992Dynamic.pdf), a dynamic query:\n",
    "\n",
    "- represents a query graphically,\n",
    "- provides visible limits on the query range,\n",
    "- provides a graphical representation of the data and query result,\n",
    "- gives immediate feedback of the result after every query adjustment,\n",
    "- and allows novice users to begin working with little training.\n",
    "\n",
    "A common approach is to manipulate query parameters using standard user interface widgets such as sliders, radio buttons, and drop-down menus. To generate dynamic query widgets, we can apply a selection's `bind` operation to one or more data fields we wish to query.\n",
    "\n",
    "Let's build an interactive scatter plot that uses a dynamic query to filter the display. Given a scatter plot of movie ratings (from Rotten Tomates and IMDB), we can add a selection over the `Major_Genre` field to enable interactive filtering by film genre.\n",
    "\n",
    "To start, let's extract the unique (non-null) genres from the `movies` data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "81b758e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_json(movies) # load movies data\n",
    "genres = df['Major_Genre'].unique() # get unique field values\n",
    "genres = list(filter(lambda d: d is not None, genres)) # filter out None values\n",
    "genres.sort() # sort alphabetically"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f94021d",
   "metadata": {},
   "source": [
    "For later use, let's also define a list of unique `MPAA_Rating` values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "07c60846",
   "metadata": {},
   "outputs": [],
   "source": [
    "mpaa = ['G', 'PG', 'PG-13', 'R', 'NC-17', 'Not Rated']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97fde546",
   "metadata": {},
   "source": [
    "Now let's create a `single` selection bound to a drop-down menu.\n",
    "\n",
    "*Use the dynamic query menu below to explore the data. How do ratings vary by genre? How would you revise the code to filter `MPAA_Rating` (G, PG, PG-13, etc.) instead of `Major_Genre`?*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "c06e250a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-71b28d79a49b4ded819d2438ea5908b8\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-71b28d79a49b4ded819d2438ea5908b8\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-71b28d79a49b4ded819d2438ea5908b8\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"mark\": \"circle\", \"encoding\": {\"opacity\": {\"condition\": {\"value\": 0.75, \"selection\": \"Select\"}, \"value\": 0.05}, \"tooltip\": {\"type\": \"nominal\", \"field\": \"Title\"}, \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"selection\": {\"Select\": {\"type\": \"single\", \"fields\": [\"Major_Genre\"], \"init\": {\"Major_Genre\": \"Action\"}, \"bind\": {\"input\": \"select\", \"options\": [\"Action\", \"Adventure\", \"Black Comedy\", \"Comedy\", \"Concert/Performance\", \"Documentary\", \"Drama\", \"Horror\", \"Musical\", \"Romantic Comedy\", \"Thriller/Suspense\", \"Western\"]}}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "selectGenre = alt.selection_single(\n",
    "    name='Select', # name the selection 'Select'\n",
    "    fields=['Major_Genre'], # limit selection to the Major_Genre field\n",
    "    init={'Major_Genre': genres[0]}, # use first genre entry as initial value\n",
    "    bind=alt.binding_select(options=genres) # bind to a menu of unique genre values\n",
    ")\n",
    "\n",
    "alt.Chart(movies).mark_circle().add_selection(\n",
    "    selectGenre\n",
    ").encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y='IMDB_Rating:Q',\n",
    "    tooltip='Title:N',\n",
    "    opacity=alt.condition(selectGenre, alt.value(0.75), alt.value(0.05))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a36334ee",
   "metadata": {},
   "source": [
    "Our construction above leverages multiple aspects of selections:\n",
    "\n",
    "- We give the selection a name (`'Select'`). This name is not required, but allows us to influence the label text of the generated dynamic query menu. (_What happens if you remove the name? Try it!_)\n",
    "- We constrain the selection to a specific data field (`Major_Genre`). Earlier when we used a `single` selection, the selection mapped to individual data points. By limiting the selection to a specific field, we can select _all_ data points whose `Major_Genre` field value matches the single selected value.\n",
    "- We initialize `init=...` the selection to a starting value.\n",
    "- We `bind` the selection to an interface widget, in this case a drop-down menu via `binding_select`.\n",
    "- As before, we then use a conditional encoding to control the opacity channel.\n",
    "\n",
    "### Binding Selections to Multiple Inputs\n",
    "\n",
    "One selection instance can be bound to _multiple_ dynamic query widgets. Let's modify the example above to provide filters for _both_ `Major_Genre` and `MPAA_Rating`, using radio buttons instead of a menu. Our `single` selection is now defined over a single _pair_ of genre and MPAA rating values\n",
    "\n",
    "_Look for surprising conjunctions of genre and rating. Are there any G or PG-rated horror films?_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "5d4701c7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-e30bc2cc7a194017bae17cc65fa7150f\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-e30bc2cc7a194017bae17cc65fa7150f\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-e30bc2cc7a194017bae17cc65fa7150f\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"mark\": \"circle\", \"encoding\": {\"opacity\": {\"condition\": {\"value\": 0.75, \"selection\": \"Select\"}, \"value\": 0.05}, \"tooltip\": {\"type\": \"nominal\", \"field\": \"Title\"}, \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"selection\": {\"Select\": {\"type\": \"single\", \"fields\": [\"Major_Genre\", \"MPAA_Rating\"], \"init\": {\"Major_Genre\": \"Drama\", \"MPAA_Rating\": \"R\"}, \"bind\": {\"Major_Genre\": {\"input\": \"select\", \"options\": [\"Action\", \"Adventure\", \"Black Comedy\", \"Comedy\", \"Concert/Performance\", \"Documentary\", \"Drama\", \"Horror\", \"Musical\", \"Romantic Comedy\", \"Thriller/Suspense\", \"Western\"]}, \"MPAA_Rating\": {\"input\": \"radio\", \"options\": [\"G\", \"PG\", \"PG-13\", \"R\", \"NC-17\", \"Not Rated\"]}}}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# single-value selection over [Major_Genre, MPAA_Rating] pairs\n",
    "# use specific hard-wired values as the initial selected values\n",
    "selection = alt.selection_single(\n",
    "    name='Select',\n",
    "    fields=['Major_Genre', 'MPAA_Rating'],\n",
    "    init={'Major_Genre': 'Drama', 'MPAA_Rating': 'R'},\n",
    "    bind={'Major_Genre': alt.binding_select(options=genres), 'MPAA_Rating': alt.binding_radio(options=mpaa)}\n",
    ")\n",
    "  \n",
    "# scatter plot, modify opacity based on selection\n",
    "alt.Chart(movies).mark_circle().add_selection(\n",
    "    selection\n",
    ").encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y='IMDB_Rating:Q',\n",
    "    tooltip='Title:N',\n",
    "    opacity=alt.condition(selection, alt.value(0.75), alt.value(0.05))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2a5f5ff",
   "metadata": {},
   "source": [
    "_Fun facts: The PG-13 rating didn't exist when the movies [Jaws](https://www.imdb.com/title/tt0073195/) and [Jaws 2](https://www.imdb.com/title/tt0077766/) were released. The first film to receive a PG-13 rating was 1984's [Red Dawn](https://www.imdb.com/title/tt0087985/)._\n",
    "\n",
    "### Using Visualizations as Dynamic Queries\n",
    "\n",
    "Though standard interface widgets show the _possible_ query parameter values, they do not visualize the _distribution_ of those values. We might also wish to use richer interactions, such as multi-value or interval selections, rather than input widgets that select only a single value at a time.\n",
    "\n",
    "To address these issues, we can author additional charts to both visualize data and support dynamic queries. Let's add a histogram of the count of films per year and use an interval selection to dynamically highlight films over selected time periods.\n",
    "\n",
    "*Interact with the year histogram to explore films from different time periods. Do you seen any evidence of [sampling bias](https://en.wikipedia.org/wiki/Sampling_bias) across the years? (How do year and critics' ratings relate?)*\n",
    "\n",
    "_The years range from 1930 to 2040! Are future films in pre-production, or are there \"off-by-one century\" errors? Also, depending on which time zone you're in, you may see a small bump in either 1969 or 1970. Why might that be? (See the end of the notebook for an explanation!)_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "360dcb65",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-02643093db76439e8f88edefbbd980ea\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-02643093db76439e8f88edefbbd980ea\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-02643093db76439e8f88edefbbd980ea\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"vconcat\": [{\"mark\": \"bar\", \"encoding\": {\"x\": {\"type\": \"temporal\", \"field\": \"Release_Date\", \"timeUnit\": \"year\", \"title\": \"Films by Release Year\"}, \"y\": {\"type\": \"quantitative\", \"aggregate\": \"count\", \"title\": null}}, \"height\": 50, \"selection\": {\"selector007\": {\"type\": \"interval\", \"encodings\": [\"x\"]}}, \"width\": 650}, {\"mark\": \"circle\", \"encoding\": {\"opacity\": {\"condition\": {\"value\": 0.75, \"selection\": \"selector007\"}, \"value\": 0.05}, \"tooltip\": {\"type\": \"nominal\", \"field\": \"Title\"}, \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"height\": 400, \"width\": 650}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"spacing\": 5, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.VConcatChart(...)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "brush = alt.selection_interval(\n",
    "    encodings=['x'] # limit selection to x-axis (year) values\n",
    ")\n",
    "\n",
    "# dynamic query histogram\n",
    "years = alt.Chart(movies).mark_bar().add_selection(\n",
    "    brush\n",
    ").encode(\n",
    "    alt.X('year(Release_Date):T', title='Films by Release Year'),\n",
    "    alt.Y('count():Q', title=None)\n",
    ").properties(\n",
    "    width=650,\n",
    "    height=50\n",
    ")\n",
    "\n",
    "# scatter plot, modify opacity based on selection\n",
    "ratings = alt.Chart(movies).mark_circle().encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y='IMDB_Rating:Q',\n",
    "    tooltip='Title:N',\n",
    "    opacity=alt.condition(brush, alt.value(0.75), alt.value(0.05))\n",
    ").properties(\n",
    "    width=650,\n",
    "    height=400\n",
    ")\n",
    "\n",
    "alt.vconcat(years, ratings).properties(spacing=5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d0d5cd1",
   "metadata": {},
   "source": [
    "The example above provides dynamic queries using a _linked selection_ between charts:\n",
    "\n",
    "- We create an `interval` selection (`brush`), and set `encodings=['x']` to limit the selection to the x-axis only, resulting in a one-dimensional selection interval.\n",
    "- We register `brush` with our histogram of films per year via `.add_selection(brush)`.\n",
    "- We use `brush` in a conditional encoding to adjust the scatter plot `opacity`.\n",
    "\n",
    "This interaction technique of selecting elements in one chart and seeing linked highlights in one or more other charts is known as [_brushing &amp; linking_](https://en.wikipedia.org/wiki/Brushing_and_linking).\n",
    "\n",
    "## Panning &amp; Zooming\n",
    "\n",
    "The movie rating scatter plot is a bit cluttered in places, making it hard to examine points in denser regions. Using the interaction techniques of _panning_ and _zooming_, we can inspect dense regions more closely.\n",
    "\n",
    "Let's start by thinking about how we might express panning and zooming using Altair selections. What defines the \"viewport\" of a chart? _Axis scale domains!_\n",
    "\n",
    "We can change the scale domains to modify the visualized range of data values. To do so interactively, we can bind an `interval` selection to scale domains with the code `bind='scales'`. The result is that instead of an interval brush that we can drag and zoom, we instead can drag and zoom the entire plotting area!\n",
    "\n",
    "_In the chart below, click and drag to pan (translate) the view, or scroll to zoom (scale) the view. What can you discover about the precision of the provided rating values?_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "2ab5621c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-15063704909a4941bb8dc092df828afe\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-15063704909a4941bb8dc092df828afe\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-15063704909a4941bb8dc092df828afe\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"mark\": \"circle\", \"encoding\": {\"tooltip\": [{\"type\": \"nominal\", \"field\": \"Title\"}, {\"type\": \"nominal\", \"field\": \"Release_Date\"}, {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}, {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}], \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"axis\": {\"minExtent\": 30}, \"field\": \"IMDB_Rating\"}}, \"height\": 400, \"selection\": {\"selector008\": {\"type\": \"interval\", \"bind\": \"scales\"}}, \"width\": 600, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alt.Chart(movies).mark_circle().add_selection(\n",
    "    alt.selection_interval(bind='scales')\n",
    ").encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement\n",
    "    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']\n",
    ").properties(\n",
    "    width=600,\n",
    "    height=400\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db8142fb",
   "metadata": {},
   "source": [
    "_Zooming in, we can see that the rating values have limited precision! The Rotten Tomatoes ratings are integers, while the IMDB ratings are truncated to tenths. As a result, there is overplotting even when we zoom, with multiple movies sharing the same rating values._\n",
    "\n",
    "Reading the code above, you may notice the code `alt.Axis(minExtent=30)` in the `y` encoding channel. The `minExtent` parameter ensures a minimum amount of space is reserved for axis ticks and labels. Why do this? When we pan and zoom, the axis labels may change and cause the axis title position to shift. By setting a minimum extent we can reduce distracting movements in the plot. _Try changing the `minExtent` value, for example setting it to zero, and then zoom out to see what happens when longer axis labels enter the view._\n",
    "\n",
    "Altair also includes a shorthand for adding panning and zooming to a plot. Instead of directly creating a selection, you can call `.interactive()` to have Altair automatically generate an interval selection bound to the chart's scales:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "90ff5c44",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-2ad510d9e79941bb8f5902cb0f9abdb8\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-2ad510d9e79941bb8f5902cb0f9abdb8\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-2ad510d9e79941bb8f5902cb0f9abdb8\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"mark\": \"circle\", \"encoding\": {\"tooltip\": [{\"type\": \"nominal\", \"field\": \"Title\"}, {\"type\": \"nominal\", \"field\": \"Release_Date\"}, {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}, {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}], \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"axis\": {\"minExtent\": 30}, \"field\": \"IMDB_Rating\"}}, \"height\": 400, \"selection\": {\"selector009\": {\"type\": \"interval\", \"bind\": \"scales\", \"encodings\": [\"x\", \"y\"]}}, \"width\": 600, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alt.Chart(movies).mark_circle().encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement\n",
    "    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']\n",
    ").properties(\n",
    "    width=600,\n",
    "    height=400\n",
    ").interactive()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce8aa19e",
   "metadata": {},
   "source": [
    "By default, scale bindings for selections include both the `x` and `y` encoding channels. What if we want to limit panning and zooming along a single dimension? We can invoke `encodings=['x']` to constrain the selection to the `x` channel only:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "7c51f571",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-4ea45efbc99e4b84b0d2917c0db1b14e\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-4ea45efbc99e4b84b0d2917c0db1b14e\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-4ea45efbc99e4b84b0d2917c0db1b14e\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"mark\": \"circle\", \"encoding\": {\"tooltip\": [{\"type\": \"nominal\", \"field\": \"Title\"}, {\"type\": \"nominal\", \"field\": \"Release_Date\"}, {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}, {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}], \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"axis\": {\"minExtent\": 30}, \"field\": \"IMDB_Rating\"}}, \"height\": 400, \"selection\": {\"selector010\": {\"type\": \"interval\", \"bind\": \"scales\", \"encodings\": [\"x\"]}}, \"width\": 600, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.Chart(...)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alt.Chart(movies).mark_circle().add_selection(\n",
    "    alt.selection_interval(bind='scales', encodings=['x'])\n",
    ").encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y=alt.Y('IMDB_Rating:Q', axis=alt.Axis(minExtent=30)), # use min extent to stabilize axis title placement\n",
    "    tooltip=['Title:N', 'Release_Date:N', 'IMDB_Rating:Q', 'Rotten_Tomatoes_Rating:Q']\n",
    ").properties(\n",
    "    width=600,\n",
    "    height=400\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f40012c2",
   "metadata": {},
   "source": [
    "_When zooming along a single axis only, the shape of the visualized data can change, potentially affecting our perception of relationships in the data. [Choosing an appropriate aspect ratio](http://vis.stanford.edu/papers/arclength-banking) is an important visualization design concern!_\n",
    "\n",
    "## Navigation: Overview + Detail\n",
    "\n",
    "When panning and zooming, we directly adjust the \"viewport\" of a chart. The related navigation strategy of _overview + detail_ instead uses an overview display to show _all_ of the data, while supporting selections that pan and zoom a separate focus display.\n",
    "\n",
    "Below we have two area charts showing a decade of price fluctuations for the S&amp;P 500 stock index. Initially both charts show the same data range. _Click and drag in the bottom overview chart to update the focus display and examine specific time spans._"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "f78d6bb3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-3ff61249d49a4c45a232775b65c55202\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-3ff61249d49a4c45a232775b65c55202\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-3ff61249d49a4c45a232775b65c55202\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"vconcat\": [{\"mark\": \"area\", \"encoding\": {\"x\": {\"type\": \"temporal\", \"field\": \"date\", \"scale\": {\"domain\": {\"selection\": \"selector011\"}}, \"title\": null}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\"}}, \"width\": 700}, {\"mark\": \"area\", \"encoding\": {\"x\": {\"type\": \"temporal\", \"field\": \"date\", \"title\": null}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\"}}, \"height\": 60, \"selection\": {\"selector011\": {\"type\": \"interval\", \"encodings\": [\"x\"]}}, \"width\": 700}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/sp500.csv\"}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.VConcatChart(...)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "brush = alt.selection_interval(encodings=['x']);\n",
    "\n",
    "base = alt.Chart().mark_area().encode(\n",
    "    alt.X('date:T', title=None),\n",
    "    alt.Y('price:Q')\n",
    ").properties(\n",
    "    width=700\n",
    ")\n",
    "  \n",
    "alt.vconcat(\n",
    "    base.encode(alt.X('date:T', title=None, scale=alt.Scale(domain=brush))),\n",
    "    base.add_selection(brush).properties(height=60),\n",
    "    data=sp500\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a1dba6e",
   "metadata": {},
   "source": [
    "Unlike our earlier panning &amp; zooming case, here we don't want to bind a selection directly to the scales of a single interactive chart. Instead, we want to bind the selection to a scale domain in _another_ chart. To do so, we update the `x` encoding channel for our focus chart, setting the scale `domain` property to reference our `brush` selection. If no interval is defined (the selection is empty), Altair ignores the brush and uses the underlying data to determine the domain. When a brush interval is created, Altair instead uses that as the scale `domain` for the focus chart.\n",
    "\n",
    "## Details on Demand\n",
    "\n",
    "Once we spot points of interest within a visualization, we often want to know more about them. _Details-on-demand_ refers to interactively querying for more information about selected values. _Tooltips_ are one useful means of providing details on demand. However, tooltips typically only show information for one data point at a time. How might we show more?\n",
    "\n",
    "The movie ratings scatterplot includes a number of potentially interesting outliers where the Rotten Tomatoes and IMDB ratings disagree. Let's create a plot that allows us to interactively select points and show their labels. To trigger the filter query on either the hover or click interaction, we will use the [Altair composition operator](https://altair-viz.github.io/user_guide/interactions.html#composing-multiple-selections) `|` (\"or\").\n",
    "\n",
    "_Mouse over points in the scatter plot below to see a highlight and title label. Shift-click points to make annotations persistent and view multiple labels at once. Which movies are loved by Rotten Tomatoes critics, but not the general audience on IMDB (or vice versa)? See if you can find possible errors, where two different movies with the same name were accidentally combined!_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "2dd424da",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-26965ece48d842a08a8cc43e7fb0e59d\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-26965ece48d842a08a8cc43e7fb0e59d\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-26965ece48d842a08a8cc43e7fb0e59d\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"layer\": [{\"mark\": \"circle\", \"encoding\": {\"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"selection\": {\"selector012\": {\"type\": \"single\", \"on\": \"mouseover\", \"nearest\": true, \"empty\": \"none\"}, \"selector013\": {\"type\": \"multi\", \"empty\": \"none\"}}}, {\"mark\": {\"type\": \"point\", \"size\": 100, \"stroke\": \"firebrick\", \"strokeWidth\": 1}, \"encoding\": {\"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"transform\": [{\"filter\": {\"selection\": {\"or\": [\"selector012\", \"selector013\"]}}}]}, {\"mark\": {\"type\": \"text\", \"align\": \"right\", \"dx\": 4, \"dy\": -8, \"stroke\": \"white\", \"strokeWidth\": 2}, \"encoding\": {\"text\": {\"type\": \"nominal\", \"field\": \"Title\"}, \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"transform\": [{\"filter\": {\"selection\": {\"or\": [\"selector012\", \"selector013\"]}}}]}, {\"mark\": {\"type\": \"text\", \"align\": \"right\", \"dx\": 4, \"dy\": -8}, \"encoding\": {\"text\": {\"type\": \"nominal\", \"field\": \"Title\"}, \"x\": {\"type\": \"quantitative\", \"field\": \"Rotten_Tomatoes_Rating\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"IMDB_Rating\"}}, \"transform\": [{\"filter\": {\"selection\": {\"or\": [\"selector012\", \"selector013\"]}}}]}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json\"}, \"height\": 450, \"width\": 600, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.LayerChart(...)"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hover = alt.selection_single(\n",
    "    on='mouseover',  # select on mouseover\n",
    "    nearest=True,    # select nearest point to mouse cursor\n",
    "    empty='none'     # empty selection should match nothing\n",
    ")\n",
    "\n",
    "click = alt.selection_multi(\n",
    "    empty='none' # empty selection matches no points\n",
    ")\n",
    "\n",
    "# scatter plot encodings shared by all marks\n",
    "plot = alt.Chart().mark_circle().encode(\n",
    "    x='Rotten_Tomatoes_Rating:Q',\n",
    "    y='IMDB_Rating:Q'\n",
    ")\n",
    "  \n",
    "# shared base for new layers\n",
    "base = plot.transform_filter(\n",
    "    hover | click # filter to points in either selection\n",
    ")\n",
    "\n",
    "# layer scatter plot points, halo annotations, and title labels\n",
    "alt.layer(\n",
    "    plot.add_selection(hover).add_selection(click),\n",
    "    base.mark_point(size=100, stroke='firebrick', strokeWidth=1),\n",
    "    base.mark_text(dx=4, dy=-8, align='right', stroke='white', strokeWidth=2).encode(text='Title:N'),\n",
    "    base.mark_text(dx=4, dy=-8, align='right').encode(text='Title:N'),\n",
    "    data=movies\n",
    ").properties(\n",
    "    width=600,\n",
    "    height=450\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99b97222",
   "metadata": {},
   "source": [
    "The example above adds three new layers to the scatter plot: a circular annotation, white text to provide a legible background, and black text showing a film title. In addition, this example uses two selections in tandem:\n",
    "\n",
    "1. A single selection (`hover`) that includes `nearest=True` to automatically select the nearest data point as the mouse moves.\n",
    "2. A multi selection (`click`) to create persistent selections via shift-click.\n",
    "\n",
    "Both selections include the set `empty='none'` to indicate that no points should be included if a selection is empty. These selections are then combined into a single filter predicate &mdash; the logical _or_ of `hover` and `click` &mdash; to include points that reside in _either_ selection. We use this predicate to filter the new layers to show annotations and labels for selected points only.\n",
    "\n",
    "Using selections and layers, we can realize a number of different designs for details on demand! For example, here is a log-scaled time series of technology stock prices, annotated with a guideline and labels for the date nearest the mouse cursor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "aab815f1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-0de08a41e0ba42f6b102d9c71659f5b9\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-0de08a41e0ba42f6b102d9c71659f5b9\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-0de08a41e0ba42f6b102d9c71659f5b9\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"layer\": [{\"mark\": \"line\", \"encoding\": {\"color\": {\"type\": \"nominal\", \"field\": \"symbol\"}, \"x\": {\"type\": \"temporal\", \"field\": \"date\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\", \"scale\": {\"type\": \"log\"}}}}, {\"mark\": {\"type\": \"rule\", \"color\": \"#aaa\"}, \"encoding\": {\"x\": {\"type\": \"temporal\", \"field\": \"date\"}}, \"transform\": [{\"filter\": {\"selection\": \"selector014\"}}]}, {\"mark\": \"circle\", \"encoding\": {\"color\": {\"type\": \"nominal\", \"field\": \"symbol\"}, \"opacity\": {\"condition\": {\"value\": 1, \"selection\": \"selector014\"}, \"value\": 0}, \"x\": {\"type\": \"temporal\", \"field\": \"date\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\", \"scale\": {\"type\": \"log\"}}}, \"selection\": {\"selector014\": {\"type\": \"single\", \"encodings\": [\"x\"], \"on\": \"mouseover\", \"nearest\": true, \"empty\": \"none\"}}}, {\"mark\": {\"type\": \"text\", \"align\": \"left\", \"dx\": 5, \"dy\": -5, \"stroke\": \"white\", \"strokeWidth\": 2}, \"encoding\": {\"color\": {\"type\": \"nominal\", \"field\": \"symbol\"}, \"text\": {\"type\": \"quantitative\", \"field\": \"price\"}, \"x\": {\"type\": \"temporal\", \"field\": \"date\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\", \"scale\": {\"type\": \"log\"}}}, \"transform\": [{\"filter\": {\"selection\": \"selector014\"}}]}, {\"mark\": {\"type\": \"text\", \"align\": \"left\", \"dx\": 5, \"dy\": -5}, \"encoding\": {\"color\": {\"type\": \"nominal\", \"field\": \"symbol\"}, \"text\": {\"type\": \"quantitative\", \"field\": \"price\"}, \"x\": {\"type\": \"temporal\", \"field\": \"date\"}, \"y\": {\"type\": \"quantitative\", \"field\": \"price\", \"scale\": {\"type\": \"log\"}}}, \"transform\": [{\"filter\": {\"selection\": \"selector014\"}}]}], \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/stocks.csv\"}, \"height\": 400, \"width\": 700, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.LayerChart(...)"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# select a point for which to provide details-on-demand\n",
    "label = alt.selection_single(\n",
    "    encodings=['x'], # limit selection to x-axis value\n",
    "    on='mouseover',  # select on mouseover events\n",
    "    nearest=True,    # select data point nearest the cursor\n",
    "    empty='none'     # empty selection includes no data points\n",
    ")\n",
    "\n",
    "# define our base line chart of stock prices\n",
    "base = alt.Chart().mark_line().encode(\n",
    "    alt.X('date:T'),\n",
    "    alt.Y('price:Q', scale=alt.Scale(type='log')),\n",
    "    alt.Color('symbol:N')\n",
    ")\n",
    "\n",
    "alt.layer(\n",
    "    base, # base line chart\n",
    "    \n",
    "    # add a rule mark to serve as a guide line\n",
    "    alt.Chart().mark_rule(color='#aaa').encode(\n",
    "        x='date:T'\n",
    "    ).transform_filter(label),\n",
    "    \n",
    "    # add circle marks for selected time points, hide unselected points\n",
    "    base.mark_circle().encode(\n",
    "        opacity=alt.condition(label, alt.value(1), alt.value(0))\n",
    "    ).add_selection(label),\n",
    "\n",
    "    # add white stroked text to provide a legible background for labels\n",
    "    base.mark_text(align='left', dx=5, dy=-5, stroke='white', strokeWidth=2).encode(\n",
    "        text='price:Q'\n",
    "    ).transform_filter(label),\n",
    "\n",
    "    # add text labels for stock prices\n",
    "    base.mark_text(align='left', dx=5, dy=-5).encode(\n",
    "        text='price:Q'\n",
    "    ).transform_filter(label),\n",
    "    \n",
    "    data=stocks\n",
    ").properties(\n",
    "    width=700,\n",
    "    height=400\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7da8b5e",
   "metadata": {},
   "source": [
    "_Putting into action what we've learned so far: can you modify the movie scatter plot above (the one with the dynamic query over years) to include a `rule` mark that shows the average IMDB (or Rotten Tomatoes) rating for the data contained within the year `interval` selection?_\n",
    "\n",
    "## Brushing &amp; Linking, Revisited\n",
    "\n",
    "Earlier in this notebook we saw an example of _brushing &amp; linking_: using a dynamic query histogram to highlight points in a movie rating scatter plot. Here, we'll visit some additional examples involving linked selections.\n",
    "\n",
    "Returning to the `cars` dataset, we can use the `repeat` operator to build a [scatter plot matrix (SPLOM)](https://en.wikipedia.org/wiki/Scatter_plot#Scatterplot_matrices) that shows associations between mileage, acceleration, and horsepower. We can define an `interval` selection and include it _within_ our repeated scatter plot specification to enable linked selections among all the plots.\n",
    "\n",
    "_Click and drag in any of the plots below to perform brushing &amp; linking!_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "e36e3172",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-e981e8317dee4365886f5d8403f8c72d\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-e981e8317dee4365886f5d8403f8c72d\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-e981e8317dee4365886f5d8403f8c72d\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"repeat\": {\"column\": [\"Acceleration\", \"Horsepower\", \"Miles_per_Gallon\"], \"row\": [\"Miles_per_Gallon\", \"Horsepower\", \"Acceleration\"]}, \"spec\": {\"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/cars.json\"}, \"mark\": \"circle\", \"encoding\": {\"color\": {\"condition\": {\"type\": \"ordinal\", \"field\": \"Cylinders\", \"selection\": \"selector015\"}, \"value\": \"grey\"}, \"opacity\": {\"condition\": {\"value\": 0.8, \"selection\": \"selector015\"}, \"value\": 0.1}, \"x\": {\"type\": \"quantitative\", \"field\": {\"repeat\": \"column\"}}, \"y\": {\"type\": \"quantitative\", \"field\": {\"repeat\": \"row\"}}}, \"height\": 140, \"selection\": {\"selector015\": {\"type\": \"interval\", \"resolve\": \"global\"}}, \"width\": 140}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.RepeatChart(...)"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "brush = alt.selection_interval(\n",
    "    resolve='global' # resolve all selections to a single global instance\n",
    ")\n",
    "\n",
    "alt.Chart(cars).mark_circle().add_selection(\n",
    "    brush\n",
    ").encode(\n",
    "    alt.X(alt.repeat('column'), type='quantitative'),\n",
    "    alt.Y(alt.repeat('row'), type='quantitative'),\n",
    "    color=alt.condition(brush, 'Cylinders:O', alt.value('grey')),\n",
    "    opacity=alt.condition(brush, alt.value(0.8), alt.value(0.1))\n",
    ").properties(\n",
    "    width=140,\n",
    "    height=140\n",
    ").repeat(\n",
    "    column=['Acceleration', 'Horsepower', 'Miles_per_Gallon'],\n",
    "    row=['Miles_per_Gallon', 'Horsepower', 'Acceleration']\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "38fb670b",
   "metadata": {},
   "source": [
    "Note above the use of `resolve='global'` on the `interval` selection. The default setting of `'global'` indicates that across all plots only one brush can be active at a time. However, in some cases we might want to define brushes in multiple plots and combine the results. If we use `resolve='union'`, the selection will be the _union_ of all brushes: if a point resides within any brush it will be selected. Alternatively, if we use `resolve='intersect'`, the selection will consist of the _intersection_ of all brushes: only points that reside within all brushes will be selected.\n",
    "\n",
    "_Try setting the `resolve` parameter to `'union'` and `'intersect'` and see how it changes the resulting selection logic._\n",
    "\n",
    "### Cross-Filtering\n",
    "\n",
    "The brushing &amp; linking examples we've looked at all use conditional encodings, for example to change opacity values in response to a selection. Another option is to use a selection defined in one view to _filter_ the content of another view.\n",
    "\n",
    "Let's build a collection of histograms for the `flights` dataset: arrival `delay` (how early or late a flight arrives, in minutes), `distance` flown (in miles), and `time` of departure (hour of the day). We'll use the `repeat` operator to create the histograms, and add an `interval` selection for the `x` axis with brushes resolved via intersection.\n",
    "\n",
    "In particular, each histogram will consist of two layers: a gray background layer and a blue foreground layer, with the foreground layer filtered by our intersection of brush selections. The result is a _cross-filtering_ interaction across the three charts!\n",
    "\n",
    "_Drag out brush intervals in the charts below. As you select flights with longer or shorter arrival delays, how do the distance and time distributions respond?_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "041e726c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<div id=\"altair-viz-19ac01021f3d4af483b8d1e4c2d05bd6\"></div>\n",
       "<script type=\"text/javascript\">\n",
       "  (function(spec, embedOpt){\n",
       "    let outputDiv = document.currentScript.previousElementSibling;\n",
       "    if (outputDiv.id !== \"altair-viz-19ac01021f3d4af483b8d1e4c2d05bd6\") {\n",
       "      outputDiv = document.getElementById(\"altair-viz-19ac01021f3d4af483b8d1e4c2d05bd6\");\n",
       "    }\n",
       "    const paths = {\n",
       "      \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n",
       "      \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n",
       "      \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n",
       "      \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n",
       "    };\n",
       "\n",
       "    function loadScript(lib) {\n",
       "      return new Promise(function(resolve, reject) {\n",
       "        var s = document.createElement('script');\n",
       "        s.src = paths[lib];\n",
       "        s.async = true;\n",
       "        s.onload = () => resolve(paths[lib]);\n",
       "        s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n",
       "        document.getElementsByTagName(\"head\")[0].appendChild(s);\n",
       "      });\n",
       "    }\n",
       "\n",
       "    function showError(err) {\n",
       "      outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n",
       "      throw err;\n",
       "    }\n",
       "\n",
       "    function displayChart(vegaEmbed) {\n",
       "      vegaEmbed(outputDiv, spec, embedOpt)\n",
       "        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n",
       "    }\n",
       "\n",
       "    if(typeof define === \"function\" && define.amd) {\n",
       "      requirejs.config({paths});\n",
       "      require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n",
       "    } else if (typeof vegaEmbed === \"function\") {\n",
       "      displayChart(vegaEmbed);\n",
       "    } else {\n",
       "      loadScript(\"vega\")\n",
       "        .then(() => loadScript(\"vega-lite\"))\n",
       "        .then(() => loadScript(\"vega-embed\"))\n",
       "        .catch(showError)\n",
       "        .then(() => displayChart(vegaEmbed));\n",
       "    }\n",
       "  })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300, \"stroke\": \"transparent\"}}, \"repeat\": {\"row\": [\"delay\", \"distance\", \"time\"]}, \"spec\": {\"layer\": [{\"mark\": \"bar\", \"encoding\": {\"color\": {\"value\": \"lightgrey\"}, \"x\": {\"type\": \"quantitative\", \"axis\": {\"format\": \"d\", \"titleAnchor\": \"start\"}, \"bin\": {\"maxbins\": 100, \"minstep\": 1}, \"field\": {\"repeat\": \"row\"}}, \"y\": {\"type\": \"quantitative\", \"aggregate\": \"count\", \"title\": null}}, \"selection\": {\"selector016\": {\"type\": \"interval\", \"encodings\": [\"x\"], \"resolve\": \"intersect\"}}}, {\"mark\": \"bar\", \"encoding\": {\"x\": {\"type\": \"quantitative\", \"axis\": {\"format\": \"d\", \"titleAnchor\": \"start\"}, \"bin\": {\"maxbins\": 100, \"minstep\": 1}, \"field\": {\"repeat\": \"row\"}}, \"y\": {\"type\": \"quantitative\", \"aggregate\": \"count\", \"title\": null}}, \"transform\": [{\"filter\": {\"selection\": \"selector016\"}}]}], \"height\": 100, \"width\": 900}, \"data\": {\"url\": \"https://cdn.jsdelivr.net/npm/vega-datasets@1/data/flights-5k.json\"}, \"transform\": [{\"calculate\": \"datum.delay < 180 ? datum.delay : 180\", \"as\": \"delay\"}, {\"calculate\": \"hours(datum.date) + minutes(datum.date) / 60\", \"as\": \"time\"}], \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\"}, {\"mode\": \"vega-lite\"});\n",
       "</script>"
      ],
      "text/plain": [
       "alt.RepeatChart(...)"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "brush = alt.selection_interval(\n",
    "    encodings=['x'],\n",
    "    resolve='intersect'\n",
    ");\n",
    "\n",
    "hist = alt.Chart().mark_bar().encode(\n",
    "    alt.X(alt.repeat('row'), type='quantitative',\n",
    "        bin=alt.Bin(maxbins=100, minstep=1), # up to 100 bins\n",
    "        axis=alt.Axis(format='d', titleAnchor='start') # integer format, left-aligned title\n",
    "    ),\n",
    "    alt.Y('count():Q', title=None) # no y-axis title\n",
    ")\n",
    "  \n",
    "alt.layer(\n",
    "    hist.add_selection(brush).encode(color=alt.value('lightgrey')),\n",
    "    hist.transform_filter(brush)\n",
    ").properties(\n",
    "    width=900,\n",
    "    height=100\n",
    ").repeat(\n",
    "    row=['delay', 'distance', 'time'],\n",
    "    data=flights\n",
    ").transform_calculate(\n",
    "    delay='datum.delay < 180 ? datum.delay : 180', # clamp delays > 3 hours\n",
    "    time='hours(datum.date) + minutes(datum.date) / 60' # fractional hours\n",
    ").configure_view(\n",
    "    stroke='transparent' # no outline\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "729235d7",
   "metadata": {},
   "source": [
    "_By cross-filtering you can observe that delayed flights are more likely to depart at later hours. This phenomenon is familiar to frequent fliers: a delay can propagate through the day, affecting subsequent travel by that plane. For the best odds of an on-time arrival, book an early flight!_\n",
    "\n",
    "The combination of multiple views and interactive selections can enable valuable forms of multi-dimensional reasoning, turning even basic histograms into powerful input devices for asking questions of a dataset!\n",
    "\n",
    "## Summary\n",
    "\n",
    "For more information about the supported interaction options in Altair, please consult the [Altair interactive selection documentation](https://altair-viz.github.io/user_guide/interactions.html). For details about customizing event handlers, for example to compose multiple interaction techniques or support touch-based input on mobile devices, see the [Vega-Lite selection documentation](https://vega.github.io/vega-lite/docs/selection.html).\n",
    "\n",
    "Interested in learning more?\n",
    "- The _selection_ abstraction was introduced in the paper [Vega-Lite: A Grammar of Interactive Graphics](http://idl.cs.washington.edu/papers/vega-lite/), by Satyanarayan, Moritz, Wongsuphasawat, &amp; Heer.\n",
    "- The PRIM-9 system (for projection, rotation, isolation, and masking in up to 9 dimensions) is one of the earliest interactive visualization tools, built in the early 1970s by Fisherkeller, Tukey, &amp; Friedman. [A retro demo video survives!](http://stat-graphics.org/movies/prim9.html)\n",
    "- The concept of brushing &amp; linking was crystallized by Becker, Cleveland, &amp; Wilks in their 1987 article [Dynamic Graphics for Data Analysis](https://scholar.google.com/scholar?cluster=14817303117298653693).\n",
    "- For a comprehensive summary of interaction techniques for visualization, see [Interactive Dynamics for Visual Analysis](https://queue.acm.org/detail.cfm?id=2146416) by Heer &amp; Shneiderman.\n",
    "- Finally, for a treatise on what makes interaction effective, read the classic [Direct Manipulation Interfaces](https://scholar.google.com/scholar?cluster=15702972136892195211) paper by Hutchins, Hollan, &amp; Norman.\n",
    "\n",
    "#### Appendix: On The Representation of Time\n",
    "\n",
    "Earlier we observed a small bump in the number of movies in either 1969 and 1970. Where does that bump come from? And why 1969 _or_ 1970? The answer stems from a combination of missing data and how your computer represents time. \n",
    "\n",
    "Internally, dates and times are represented relative to the [UNIX epoch](https://en.wikipedia.org/wiki/Unix_time), in which time \"zero\" corresponds to the stroke of midnight on January 1, 1970 in [UTC time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), which runs along the [prime meridian](https://en.wikipedia.org/wiki/Prime_meridian). It turns out there are a few movies with missing (`null`) release dates. Those `null` values get interpreted as time `0`, and thus map to January 1, 1970 in UTC time. If you live in the Americas &ndash; and thus in \"earlier\" time zones &ndash; this precise point in time corresponds to an earlier hour on December 31, 1969 in your local time zone. On the other hand, if you live near or east of the prime meridian, the date in your local time zone will be January 1, 1970.\n",
    "\n",
    "The takeaway? Always be skeptical of your data, and be mindful that how data is represented (whether as date times, or floating point numbers, or latitudes and longitudes, _etc._) can sometimes lead to artifacts that impact analysis!\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
