Vega#

Open this notebook in Jupyterlite | Download this notebook from GitHub (right-click to download).


import panel as pn
pn.extension('vega')

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.

Parameters:#

For details on other options for customizing the component see the layout and styling how-to guides.

  • 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.

  • object (dict or altair Chart): Either a dictionary containing a Vega or Vega-Lite plot specification, or an Altair Chart

  • theme (str): A theme to apply to the plot, must be one of ‘excel’, ‘ggplot2’, ‘quartz’, ‘vox’, ‘fivethirtyeight’, ‘dark’, ‘latimes’, ‘urbaninstitute’, ‘googlecharts’.

  • show_actions (boolean): Whether to show chart actions menu such as save, edit etc.

Readonly parameters:

  • selection (Selection): The Selection object exposes parameters which reflect the selections declared on the plot into Python.


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.

To display vega and vega-lite specification simply construct a Vega pane directly or pass it to pn.panel:

vegalite = {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/barley.json"},
  "mark": "bar",
  "encoding": {
    "x": {"aggregate": "sum", "field": "yield", "type": "quantitative"},
    "y": {"field": "variety", "type": "nominal"},
    "color": {"field": "site", "type": "nominal"}
  }
}
vgl_pane = pn.pane.Vega(vegalite, height=240)
vgl_pane

Like all other panes, the Vega pane object can be updated, either in place and triggering an update:

vegalite['mark'] = 'area'
vgl_pane.param.trigger('object')

or by replacing the object entirely:

vgl_pane.object = {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {
    "url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/disasters.csv"
  },
  "width": 600,
  "height": 400,
  "transform": [
    {"filter": "datum.Entity !== 'All natural disasters'"}
  ],
  "mark": {
    "type": "circle",
    "opacity": 0.8,
    "stroke": "black",
    "strokeWidth": 1
  },
  "encoding": {
    "x": {
        "field": "Year",
        "type": "quantitative",
        "axis": {"labelAngle": 90},
        "scale": {"zero": False}
    },
    "y": {
        "field": "Entity",
        "type": "nominal",
        "axis": {"title": ""}
    },
    "size": {
      "field": "Deaths",
      "type": "quantitative",
      "legend": {"title": "Annual Global Deaths", "clipHeight": 30},
      "scale": {"range": [0, 5000]}
    },
    "color": {"field": "Entity", "type": "nominal", "legend": None}
  }
}

The vega-lite specification can also be responsively sized by declaring the width or height to match the container:

responsive_spec = dict(vgl_pane.object, width='container')

vgl_responsive_pane = pn.pane.Vega(responsive_spec)
vgl_responsive_pane

Please note that the vega specification does not support setting width and height to container.

Altair#

A more convenient way of defining a Vega chart is to declare it using altair, 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:

import altair as alt
from vega_datasets import data

cars = data.cars()

chart = alt.Chart(cars).mark_circle(size=60).encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin',
    tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']
).interactive()

altair_pane = pn.panel(chart)
altair_pane

The Altair chart can also be updated by updating the pane object:

altair_pane.object = chart.mark_circle(size=100)

All the usual layouts and composition operators that Altair supports can also be rendered:

penguins_url = "https://raw.githubusercontent.com/vega/vega/master/docs/data/penguins.json"

chart1 = alt.Chart(penguins_url).mark_point().encode(
    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),
    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),
    color='Species:N'
).properties(
    height=300,
    width=300,
)

chart2 = alt.Chart(penguins_url).mark_bar().encode(
    x='count()',
    y=alt.Y('Beak Depth (mm):Q', bin=alt.Bin(maxbins=30)),
    color='Species:N'
).properties(
    height=300,
    width=100
)

pn.panel(chart1 | chart2)

Selections#

The Vega pane automatically syncs any selections expressed on the Vega/Altair chart. Three types of selections are currently supported:

  • 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]}

  • selection_single: Allows selecting a single point using clicks, returns a list of integer indices

  • selection_multi: Allows selecting a multiple points using (shift+) click, returns a list of integer indices.

Interval selection#

As an example we can add an Altair selection_interval selection to our chart:

import pandas as pd

df = pd.read_json(penguins_url)

brush = alt.selection_interval(name='brush')  # selection of type "interval"

chart = alt.Chart(penguins_url).mark_point().encode(
    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),
    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),
    color=alt.condition(brush, 'Species:N', alt.value('lightgray'))
).properties(
    width=250,
    height=250
).add_params(
    brush
)

vega_pane = pn.pane.Vega(chart, debounce=10)

vega_pane

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, ...}.

The named selection will now appear on the .selection sub-object:

vega_pane.selection
Selection(brush=None, name='Selection00138')

By inspecting the JSON representation of the Altair chart we can see how to express these selections in vega(-lite):

chart.to_dict()['params']
[{'name': 'brush', 'select': {'type': 'interval'}}]

Single & multi-selection#

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).

multi = alt.selection_point(name='multi')  # selection of type "multi"

multi_chart = alt.Chart(penguins_url).mark_point().encode(
    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),
    y=alt.Y('Beak Depth (mm):Q', scale=alt.Scale(zero=False)),
    color=alt.condition(multi, 'Species:N', alt.value('lightgray'))
).properties(
    width=250,
    height=250
).add_params(
    multi
)

vega_multi = pn.pane.Vega(multi_chart, debounce=10)

vega_multi

The multi value is now available on the selection object:

vega_multi.selection
Selection(multi=[], name='Selection00143')

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):

df.iloc[vega_multi.selection.multi]
Species Island Beak Length (mm) Beak Depth (mm) Flipper Length (mm) Body Mass (g) Sex

For more background see the Altair documentation on available interactions.

Filtering a table via a selection#

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:

{'Beak Length (mm)': [51.824, 53.952], 'Beak Depth (mm)': [18.796, 18.904]}

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:

def filtered_table(selection):
    if not selection:
        return '## No selection'
    query = ' & '.join(
        f'{crange[0]:.3f} <= `{col}` <= {crange[1]:.3f}'
        for col, crange in selection.items()
    )
    return pn.Column(
        f'Query: {query}',
        pn.pane.DataFrame(df.query(query), width=600, height=300)
    )

pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))

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.

Filtering another chart via a selection#

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.

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 range predicate from our selection object and passing it to the transform_filter method of the second chart.

def bar_counts(selection):
    if not selection:
        return '## No selection'
    range_predicate = {
        'and': [{
            'field': key,
            'range': [selection[key][0], selection[key][1]]
        } for key in selection]
    }
    return alt.Chart(penguins_url, width=220).mark_bar().encode(
        x='count()',
        y='Species:N',
        color=alt.Color('Species:N', legend=None)
    ).transform_filter(
        range_predicate
    )

pn.Column(vega_pane, pn.bind(bar_counts, vega_pane.selection.param.brush))

Filtering categorical data via a selection#

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:

query = ' & '.join([f'`{col}` in {values}' for col, values in selection.items()])

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.

chart = alt.Chart(df).mark_tick().encode(
    x=alt.X('Beak Length (mm):Q', scale=alt.Scale(zero=False)),
    y='Species:N',
    color=alt.condition(brush, 'Species:N', alt.value('lightgray'))
).add_params(
    brush
)

def filtered_table(selection):
    if not selection:
        return '## No selection'
    query = ' & '.join(
        f'{values[0]} <= `{col}` <= {values[1]}'
        if pd.api.types.is_numeric_dtype(df[col])
        else f'`{col}` in {values}' 
        for col, values in selection.items()
    )
    return pn.Column(
        f'Query: {query}',
        pn.pane.DataFrame(df.query(query), width=600, height=300)
    )


vega_pane = pn.pane.Vega(chart, debounce=10)
pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))

Filtering temporal data via a selection#

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.

from vega_datasets import data

temps = data.seattle_temps()[:300]

brush = alt.selection_interval(name='brush')

chart = alt.Chart(temps).mark_circle().encode(
    x='date:T',
    y=alt.Y('temp:Q', scale={'zero': False}),
    color=alt.condition(brush, alt.value('coral'), alt.value('lightgray'))
).properties(
    width=500
).add_params(
    brush
)

def filtered_table(selection):
    if not selection:
        return '## No selection'
    query = ' & '.join(
        f'"{pd.to_datetime(values[0], unit="ms")}" <= `{col}` <= "{pd.to_datetime(values[1], unit="ms")}"'
        if pd.api.types.is_datetime64_any_dtype(temps[col]) else f'{values[0]} <= `{col}` <= {values[1]}'
        for col, values in selection.items()
    )
    return pn.Column(
        f'Query: {query}',
        pn.pane.DataFrame(temps.query(query), width=600, height=300)
    )


vega_pane = pn.pane.Vega(chart, debounce=10)
pn.Row(vega_pane, pn.bind(filtered_table, vega_pane.selection.param.brush))

Controls#

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:

pn.Row(vgl_responsive_pane.controls(jslink=True), vgl_responsive_pane, sizing_mode="stretch_width")

Open this notebook in Jupyterlite | Download this notebook from GitHub (right-click to download).