MultiSelect not working with Altair

I am using Altair to generate my plots (As i need the linked bar-chart selection) and Panel to create my dashboard. I have two dropdowns, where the values in the second are conditional on the value in the first.

When I use a Single Select dropdown the dashboard works as expected. However when I try and use any Mulitiple select widget I get no data rendered on my chart. See code below. Testdataset available here:https://github.com/KWSpittles/testdata

import panel as pn
import altair as alt
import pandas as pd
from vega_datasets import data
import datetime as dt
from altair import datum

alt.renderers.enable('default')
pn.extension('vega')

data = pd.read_excel('randomtestdata.xlsx')
df = pd.DataFrame(data, columns=['Parent Location','Location','Alert Definition','Alert Type','Initiated Date'])

df = df[(df['Parent Location'] == 'Zone 1') | (df['Parent Location'] == 'Zone 2' )| (df['Parent Location'] == 'Zone 3' )]

df.rename(columns={'Parent Location': 'ParentLocation'},
          inplace=True, errors='raise')

source = df

title = '##Dashboard'
subtitle = 'This is a test dashboard. Use widgets below to show desired chart.'

_locations = {
    'Zone 1': source.loc[source['ParentLocation'] == 'Zone 1']['Location'].unique().tolist(),
    'Zone 2'  : source.loc[source['ParentLocation'] == 'Zone 2']['Location'].unique().tolist(),
    'Zone 3': source.loc[source['ParentLocation'] == 'Zone 3']['Location'].unique().tolist()
}

zone = pn.widgets.Select(
    name = 'Select a Zone',
    value ='Zone 1', 
    options =['Zone 1', 'Zone 2', 'Zone 3']
)

#The following does not work
location = pn.widgets.MultiSelect(
    name = 'Select a Location',
    value =[True], 
    options =_locations[zone.value]
)

# The following does works:
# location = pn.widgets.Select(
#     name = 'Select a Location',
#     value = _locations[zone.value][0], 
#     options =_locations[zone.value]
# )


date_range_slider = pn.widgets.DateRangeSlider(
    name='Date range to consider',
    start=dt.datetime(2021, 1, 1), end=dt.datetime(2022, 1, 1),
    value=(dt.datetime(2021, 1, 1), dt.datetime(2022, 1, 1))
)

@pn.depends(zone.param.value, location.param.value, date_range_slider.param.value, watch=True)
def get_plot(zone, location, date_range): # start function

    df = source
    df['Initiated Date'] = pd.to_datetime(df['Initiated Date']) # format date as datetime
    
    start_date = date_range_slider.value[0] 
    end_date = date_range_slider.value[1] 
    mask = (df['Initiated Date'] > start_date) & (df['Initiated Date'] <= end_date)
    df = df.loc[mask] 
    
    selection2 = alt.selection_single(fields=['Alert Type'])

    chart = alt.Chart(df).mark_bar(
        color="#0c1944", 
        opacity=0.8).encode(
        x=alt.X('Alert Type:O', scale=alt.Scale(domain=source['Alert Type'].unique())),
        y='count(Alert Type)').transform_filter(
        (datum.Location == location) 
    ).add_selection(selection2)
    
    chart2 = alt.Chart(df).mark_bar(
        color="#0c1944", 
        opacity=0.8).encode(
        x='Alert Definition', 
        y='count(Alert Definition)').transform_filter(
        (datum.Location == location) 
    ).transform_filter(selection2)
            
    return (chart|chart2)

@pn.depends(zone.param.value, watch=True)
def _update_locations(zone):
    
    locations = _locations[zone]
    location.options = locations
    location.value = locations[0]
    
    return

pn.Row(
    pn.Column(title, subtitle, zone, location, date_range_slider,
    get_plot )
)

Hi @kieren and welcome!

Thanks for providing your complete code and test data, that was very useful!

I’m suggesting a few changes to your code which I’ve commented with ###, let me know if the behavior you get is the one you expected:

import panel as pn
import altair as alt
import pandas as pd
from vega_datasets import data
import datetime as dt
from altair import datum

alt.renderers.enable('default')
pn.extension('vega')

data = pd.read_excel('randomtestdata.xlsx')

df = pd.DataFrame(data, columns=['Parent Location','Location','Alert Definition','Alert Type','Initiated Date'])

df = df[(df['Parent Location'] == 'Zone 1') | (df['Parent Location'] == 'Zone 2' )| (df['Parent Location'] == 'Zone 3' )]

df.rename(columns={'Parent Location': 'ParentLocation'},
          inplace=True, errors='raise')

source = df

title = '##Dashboard'
subtitle = 'This is a test dashboard. Use widgets below to show desired chart.'

_locations = {
    'Zone 1': source.loc[source['ParentLocation'] == 'Zone 1']['Location'].unique().tolist(),
    'Zone 2'  : source.loc[source['ParentLocation'] == 'Zone 2']['Location'].unique().tolist(),
    'Zone 3': source.loc[source['ParentLocation'] == 'Zone 3']['Location'].unique().tolist()
}

zone = pn.widgets.Select(
    name = 'Select a Zone',
    value ='Zone 1', 
    options =['Zone 1', 'Zone 2', 'Zone 3']
)

### Setting the value of MultiSelect as the first element by default,
### wrapped in a list.
location = pn.widgets.MultiSelect(
    name = 'Select a Location',
    value =[_locations[zone.value][0]], 
    options =_locations[zone.value]
)

date_range_slider = pn.widgets.DateRangeSlider(
    name='Date range to consider',
    start=dt.datetime(2021, 1, 1), end=dt.datetime(2022, 1, 1),
    value=(dt.datetime(2021, 1, 1), dt.datetime(2022, 1, 1))
)

### Removed watch=True which isn't required because this function
### is passed to panel, which will take care of watching the Parameters
### and rerun the function when they change.

### Also remove the dependence on the zone Parameter since
### _update_locations already takes care of that.
@pn.depends(location.param.value, date_range_slider.param.value)
def get_plot(location, date_range): # start function
    print('get plot', 'location', location)
    df = source
    df['Initiated Date'] = pd.to_datetime(df['Initiated Date']) # format date as datetime
    
    start_date = date_range_slider.value[0] 
    end_date = date_range_slider.value[1] 
    mask = (df['Initiated Date'] > start_date) & (df['Initiated Date'] <= end_date)
    df = df.loc[mask]
    
    ### Instead of filtering the data with Altair, I filter
    ### it with pandas (which I'm more used to).
    df = df.loc[df['Location'].isin(location)]
    
    selection2 = alt.selection_single(fields=['Alert Type'])

    chart = alt.Chart(df).mark_bar(
        color="#0c1944", 
        opacity=0.8).encode(
        x=alt.X('Alert Type:O', scale=alt.Scale(domain=source['Alert Type'].unique())),
        y='count(Alert Type)').add_selection(selection2)
    
    chart2 = alt.Chart(df).mark_bar(
        color="#0c1944", 
        opacity=0.8).encode(
        x='Alert Definition', 
        y='count(Alert Definition)').transform_filter(selection2)
                
    return (chart|chart2)

@pn.depends(zone.param.value, watch=True)
def _update_locations(zone):
    print('update locations')
    locations = _locations[zone]
    location.options = locations
    ### The MultiSelect widget value must be a list
    location.value = [locations[0]]    
    return

pn.Row(
    pn.Column(title, subtitle, zone, location, date_range_slider,
    get_plot )
)
1 Like