{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "pn.extension('vega')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``Vega`` pane renders Vega-based plots (including those from Altair) inside a panel. It optimizes the plot rendering by using binary serialization for any array data found on the Vega/Altair object, providing huge speedups over the standard JSON serialization employed by Vega natively. Note that to use the ``Vega`` pane in the notebook the Panel extension has to be loaded with 'vega' as an argument to ensure that vega.js is initialized.\n",
    "\n",
    "#### Parameters:\n",
    "\n",
    "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
    "\n",
    "* **``debounce``** (int or dict): The debounce timeout to apply to selection events, either specified as a single integer value (in milliseconds) or a dictionary that declares a debounce value per event. Debouncing ensures that events are only dispatched N milliseconds after a user is done interacting with the plot.\n",
    "* **``object``** (dict or altair Chart): Either a dictionary containing a Vega or Vega-Lite plot specification, or an Altair Chart\n",
    "* **``theme``** (str): A theme to apply to the plot, must be one of 'excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', 'latimes', 'urbaninstitute', 'googlecharts'.\n",
    "* **``show_actions``** (boolean): Whether to show chart actions menu such as save, edit etc.\n",
    "\n",
    "Readonly parameters:\n",
    "\n",
    "* **``selection``** (Selection): The Selection object exposes parameters which reflect the selections declared on the plot into Python. \n",
    "\n",
    "___"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``Vega`` pane supports both ``vega`` and ``vega-lite`` specs which may be provided in a raw form (i.e. a dictionary) or by defining an ``altair`` plot.\n",
    "\n",
    "To display ``vega`` and ``vega-lite`` specification simply construct a ``Vega`` pane directly or pass it to ``pn.panel``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vegalite = {\n",
    "  \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n",
    "  \"data\": {\"url\": \"https://raw.githubusercontent.com/vega/vega/master/docs/data/barley.json\"},\n",
    "  \"mark\": \"bar\",\n",
    "  \"encoding\": {\n",
    "    \"x\": {\"aggregate\": \"sum\", \"field\": \"yield\", \"type\": \"quantitative\"},\n",
    "    \"y\": {\"field\": \"variety\", \"type\": \"nominal\"},\n",
    "    \"color\": {\"field\": \"site\", \"type\": \"nominal\"}\n",
    "  }\n",
    "}\n",
    "vgl_pane = pn.panel(vegalite, height=240)\n",
    "vgl_pane"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Like all other panes, the ``Vega`` pane ``object`` can be updated, either in place and triggering an update:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vegalite['mark'] = 'area'\n",
    "vgl_pane.param.trigger('object')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "or by replacing the ``object`` entirely:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vgl_pane.object = {\n",
    "  \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n",
    "  \"data\": {\n",
    "    \"url\": \"https://raw.githubusercontent.com/vega/vega/master/docs/data/disasters.csv\"\n",
    "  },\n",
    "  \"width\": 600,\n",
    "  \"height\": 400,\n",
    "  \"transform\": [\n",
    "    {\"filter\": \"datum.Entity !== 'All natural disasters'\"}\n",
    "  ],\n",
    "  \"mark\": {\n",
    "    \"type\": \"circle\",\n",
    "    \"opacity\": 0.8,\n",
    "    \"stroke\": \"black\",\n",
    "    \"strokeWidth\": 1\n",
    "  },\n",
    "  \"encoding\": {\n",
    "    \"x\": {\n",
    "        \"field\": \"Year\",\n",
    "        \"type\": \"quantitative\",\n",
    "        \"axis\": {\"labelAngle\": 90},\n",
    "        \"scale\": {\"zero\": False}\n",
    "    },\n",
    "    \"y\": {\n",
    "        \"field\": \"Entity\",\n",
    "        \"type\": \"nominal\",\n",
    "        \"axis\": {\"title\": \"\"}\n",
    "    },\n",
    "    \"size\": {\n",
    "      \"field\": \"Deaths\",\n",
    "      \"type\": \"quantitative\",\n",
    "      \"legend\": {\"title\": \"Annual Global Deaths\", \"clipHeight\": 30},\n",
    "      \"scale\": {\"range\": [0, 5000]}\n",
    "    },\n",
    "    \"color\": {\"field\": \"Entity\", \"type\": \"nominal\", \"legend\": None}\n",
    "  }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `vega-lite` specification can also be responsively sized by declaring the width or height to match the container:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "responsive_spec = dict(vgl_pane.object, width='container')\n",
    "\n",
    "vgl_responsive_pane = pn.pane.Vega(responsive_spec)\n",
    "vgl_responsive_pane"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Please note that the `vega` specification does not support setting `width` and `height` to `container`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Altair\n",
    "\n",
    "A more convenient way of defining a Vega chart is to declare it using [altair](https://altair-viz.github.io), which provides a declarative API on top of vega-lite. The ``Vega`` pane will automatically render the Vega-Lite spec when passed an Altair chart:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import altair as alt\n",
    "from vega_datasets import data\n",
    "\n",
    "cars = data.cars()\n",
    "\n",
    "chart = alt.Chart(cars).mark_circle(size=60).encode(\n",
    "    x='Horsepower',\n",
    "    y='Miles_per_Gallon',\n",
    "    color='Origin',\n",
    "    tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']\n",
    ").interactive()\n",
    "\n",
    "altair_pane = pn.panel(chart)\n",
    "altair_pane"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Altair chart can also be updated by updating the pane ``object``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "altair_pane.object = chart.mark_circle(size=100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "All the usual layouts and composition operators that Altair supports can also be rendered:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "penguins_url = \"https://raw.githubusercontent.com/vega/vega/master/docs/data/penguins.json\"\n",
    "\n",
    "chart1 = alt.Chart(penguins_url).mark_point().encode(\n",
    "    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    color='Species:N'\n",
    ").properties(\n",
    "    height=300,\n",
    "    width=300,\n",
    ")\n",
    "\n",
    "chart2 = alt.Chart(penguins_url).mark_bar().encode(\n",
    "    x='count()',\n",
    "    y=alt.Y('Beak Depth (mm):Q', bin=alt.Bin(maxbins=30)),\n",
    "    color='Species:N'\n",
    ").properties(\n",
    "    height=300,\n",
    "    width=100\n",
    ")\n",
    "\n",
    "pn.panel(chart1 | chart2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Selections\n",
    "\n",
    "The `Vega` pane automatically syncs any selections expressed on the Vega/Altair chart. Three types of selections are currently supported:\n",
    "\n",
    "- `selection_interval`: Allows selecting a intervals using a box-select tool, returns data in the form of `{<x-axis-name: [xmin, xmax], <y-axis-name>: [ymin, ymax]}`\n",
    "- `selection_single`: Allows selecting a single point using clicks, returns a list of integer indices\n",
    "- `selection_multi`: Allows selecting a multiple points using (shift+) click, returns a list of integer indices.\n",
    "\n",
    "#### Interval selection\n",
    "\n",
    "As an example we can add an Altair `selection_interval` selection to our chart:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "df = pd.read_json(penguins_url)\n",
    "\n",
    "brush = alt.selection_interval(name='brush')  # selection of type \"interval\"\n",
    "\n",
    "chart = alt.Chart(penguins_url).mark_point().encode(\n",
    "    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    color=alt.condition(brush, 'Species:N', alt.value('lightgray'))\n",
    ").properties(\n",
    "    width=250,\n",
    "    height=250\n",
    ").add_selection(\n",
    "    brush\n",
    ")\n",
    "\n",
    "vega_pane = pn.pane.Vega(chart, debounce=10)\n",
    "\n",
    "vega_pane"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note we specified a single `debounce` value, if we declare multiple selections we can instead declare a debounce value per named event by specifying it as a dictionary, e.g. `debounce={'brush': 10, ...}`.\n",
    "\n",
    "The named selection will now appear on the `.selection` sub-object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vega_pane.selection"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By inspecting the JSON representation of the Altair chart we can see how to express these selections in vega(-lite):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chart.to_dict()['selection']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Single & multi-selection\n",
    "\n",
    "Both single and multi-selection return the indices of the selected data as a list (in the case of single selection the list is always of length 0 or 1)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "multi = alt.selection_multi(name='multi')  # selection of type \"multi\"\n",
    "\n",
    "multi_chart = alt.Chart(penguins_url).mark_point().encode(\n",
    "    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    color=alt.condition(multi, 'Species:N', alt.value('lightgray'))\n",
    ").properties(\n",
    "    width=250,\n",
    "    height=250\n",
    ").add_selection(\n",
    "    multi\n",
    ")\n",
    "\n",
    "vega_multi = pn.pane.Vega(multi_chart, debounce=10)\n",
    "\n",
    "vega_multi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `multi` value is now available on the `selection` object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vega_multi.selection"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To apply the selection we can simply use the `.iloc` method on the pandas DataFrame containing our data (try tapping on one or more points above and re-running the cell below):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.iloc[vega_multi.selection.multi]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more background see the [Altair](https://altair-viz.github.io/user_guide/interactions.html) documentation on available interactions.\n",
    "\n",
    "#### Filtering a table via a selection\n",
    "\n",
    "To filter a table via a chart selection we're first going to bind the `brush` selection to a function which filters the dataframe to display only the selected values in the table. To achieve this, we need to know that the selection returns a dictionary in the format `{'column_name': [min, max]}`, which for our Penguins examples can look like this:\n",
    "\n",
    "```python\n",
    "{'Beak Length (mm)': [51.824, 53.952], 'Beak Depth (mm)': [18.796, 18.904]}\n",
    "```\n",
    "\n",
    "To display the selected values in a table, we will use the selection dictionary to construct a pandas query string that can be used with `DataFrame.query()`. Finally we are returning both the query string and the filtered table in a Column:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def filtered_table(selection):\n",
    "    if not selection:\n",
    "        return '## No selection'\n",
    "    query = ' & '.join(\n",
    "        f'{crange[0]:.3f} <= `{col}` <= {crange[1]:.3f}'\n",
    "        for col, crange in selection.items()\n",
    "    )\n",
    "    return pn.Column(\n",
    "        f'Query: {query}',\n",
    "        pn.pane.DataFrame(df.query(query), width=600, height=300)\n",
    "    )\n",
    "\n",
    "pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that this way of constructing the query string means that Panel currently supports filtering the table via the max and min values of the selection area but does not check whether there are actually points present in this area of the chart."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Filtering another chart via a selection\n",
    "\n",
    "Altair already provides a syntax for filtering one chart based on the selection in another, but one limitation is that these charts need to be displayed in the same layout for the filtering to work. By using Panel to filter one Altair chart based on another, we can place the charts anywhere in our app and still have the filtering work as expected.\n",
    "\n",
    "One way to filter a chart based on the selection in another chart, is to to use the same approach as above and create the second chart with the dataframe filtered via `.query`. Altair also provides a way to do the filtering directly with the `transform_filter` method instead of using pandas. In the example below, we are constructing a [composed](https://vega.github.io/vega-lite/docs/predicate.html#composition) [range predicate](https://vega.github.io/vega-lite/docs/predicate.html#range-predicate) from our selection object and passing it to the `transform_filter` method of the second chart."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bar_counts(selection):\n",
    "    if not selection:\n",
    "        return '## No selection'\n",
    "    range_predicate = {\n",
    "        'and': [{\n",
    "            'field': key,\n",
    "            'range': [selection[key][0], selection[key][1]]\n",
    "        } for key in selection]\n",
    "    }\n",
    "    return alt.Chart(penguins_url, width=220).mark_bar().encode(\n",
    "        x='count()',\n",
    "        y='Species:N',\n",
    "        color=alt.Color('Species:N', legend=None)\n",
    "    ).transform_filter(\n",
    "        range_predicate\n",
    "    )\n",
    "\n",
    "pn.Column(vega_pane, pn.bind(bar_counts, vega_pane.selection.param.brush))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Filtering categorical data via a selection\n",
    "\n",
    "Selections on categorical columns ('nominal' and 'ordinal' in Altair) return all the selected values in a list rather than just the min and max of the selection interval. Therefore, we need to construct the query string as follows:\n",
    "\n",
    "```python\n",
    "query = ' & '.join([f'`{col}` in {values}' for col, values in selection.items()])\n",
    "```\n",
    "\n",
    "In the example below we first check the data type in the column and then use either the categorical and quantitative query string as appropriate, which allows us to filter on a combination on categorical and numerical data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chart = alt.Chart(df).mark_tick().encode(\n",
    "    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),\n",
    "    y='Species:N',\n",
    "    color=alt.condition(brush, 'Species:N', alt.value('lightgray'))\n",
    ").add_selection(\n",
    "    brush\n",
    ")\n",
    "\n",
    "def filtered_table(selection):\n",
    "    if not selection:\n",
    "        return '## No selection'\n",
    "    query = ' & '.join(\n",
    "        f'{values[0]} <= `{col}` <= {values[1]}'\n",
    "        if pd.api.types.is_numeric_dtype(df[col])\n",
    "        else f'`{col}` in {values}' \n",
    "        for col, values in selection.items()\n",
    "    )\n",
    "    return pn.Column(\n",
    "        f'Query: {query}',\n",
    "        pn.pane.DataFrame(df.query(query), width=600, height=300)\n",
    "    )\n",
    "\n",
    "\n",
    "vega_pane = pn.pane.Vega(chart, debounce=10)\n",
    "pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Filtering temporal data via a selection\n",
    "\n",
    "Selections on temporal columns return the max and min of the selection interval, just as for quantitative data. However, these are returned as a Unix timestamp in milliseconds by default and therefore need to be converted to a pandas timestamp before they can be used in a query string. We can do this using `pd.to_datetime(value, unit=\"ms\")` as in the example below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from vega_datasets import data\n",
    "\n",
    "temps = data.seattle_temps()[:300]\n",
    "\n",
    "brush = alt.selection_interval(name='brush')\n",
    "\n",
    "chart = alt.Chart(temps).mark_circle().encode(\n",
    "    x='date:T',\n",
    "    y=alt.Y('temp:Q', scale={'zero': False}),\n",
    "    color=alt.condition(brush, alt.value('coral'), alt.value('lightgray'))\n",
    ").properties(\n",
    "    width=500\n",
    ").add_selection(\n",
    "    brush\n",
    ")\n",
    "\n",
    "def filtered_table(selection):\n",
    "    if not selection:\n",
    "        return '## No selection'\n",
    "    query = ' & '.join(\n",
    "        f'\"{pd.to_datetime(values[0], unit=\"ms\")}\" <= `{col}` <= \"{pd.to_datetime(values[1], unit=\"ms\")}\"'\n",
    "        if pd.api.types.is_datetime64_any_dtype(temps[col]) else f'{values[0]} <= `{col}` <= {values[1]}'\n",
    "        for col, values in selection.items()\n",
    "    )\n",
    "    return pn.Column(\n",
    "        f'Query: {query}',\n",
    "        pn.pane.DataFrame(temps.query(query), width=600, height=300)\n",
    "    )\n",
    "\n",
    "\n",
    "vega_pane = pn.pane.Vega(chart, debounce=10)\n",
    "pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Controls\n",
    "\n",
    "The `Vega` pane exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.Row(vgl_responsive_pane.controls(jslink=True), vgl_responsive_pane, sizing_mode=\"stretch_width\")"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
