Viewing the THOR dataset with Bokeh and Panel

May 18 2021

Background

Following from the previous post, we can view the same THOR data interactively with Bokeh and Panel. These are Python libraries for interactive web visualization of data. The data is read in as in the previous post and we then write a function to plot the map data points using Bokeh with a Tile Renderer. We also use a function here to convert lat and long coordinates into web mercartor. The dashboard then consists of a date selector and some filter widgets that are connected to the update_map function via the param.watch mechanism. The code is all below in one block.

Result

The resulting widget is animated here.

.

The complete code can be found here. You can also view a demo of the dashboard hosted here. If this no longer available you can run the notebook from mybinder too.

Code

 colormap={'NORTH VIETNAM':'brown','SOUTH VIETNAM':'orange','LAOS':'red',
                 'CAMBODIA':'green','THAILAND':'blue','UNKNOWN':'gray'}

 providers = ['CARTODBPOSITRON','STAMEN_TERRAIN','OSM','ESRI_IMAGERY']
 cats = ['TGTCOUNTRY','WEAPONTYPE','MFUNC_DESC']

def wgs84_to_web_mercator(df, lon="LON", lat="LAT"):
    """convert mat long to web mercartor"""

    k = 6378137
    df.loc[:,"x"] = df[lon] * (k * np.pi/180.0)
    df.loc[:,"y"] = np.log(np.tan((90 + df[lat]) * np.pi/360.0)) * k
    return df

 def draw_map(df=None, long=None, lat=None, height=500, colorby='TGTCOUNTRY',
             point_size=5,
              tile_provider='CARTODBPOSITRON'):
    tile_provider = get_provider(tile_provider)
    tools = "pan,wheel_zoom,box_zoom,hover,tap,lasso_select,reset,save"
    sizing_mode='stretch_both'

    # range bounds supplied in web mercator coordinates
    k = 6378137
    pad = 700000
    if lat == None:
        lat = 16
    if long == None:
        long = 108
    x = long * (k * np.pi/180.0)
    y = np.log(np.tan((90 + lat) * np.pi/360.0)) * k

    p = figure(x_range=(x-pad, x+pad), y_range=(y-pad, y+pad),
               x_axis_type="mercator", y_axis_type="mercator", tools=tools,
               plot_width=height, plot_height=height, sizing_mode=sizing_mode)
    p.add_tile(tile_provider)
    if df is None:
        return
    df.loc[:,'color'] = [colormap[i] if i in colormap else 'gray' for i in df[colorby]]

    source = ColumnDataSource(df)    
    p.circle(x='x', y='y', size=point_size, alpha=0.7, color='color', source=source)
    p.toolbar.logo = None    
    p.title.text = "date"
    hover = p.select(dict(type=HoverTool))
    hover.tooltips = OrderedDict([
        ("TGTCOUNTRY", "@TGTCOUNTRY"),
        ("MSNDATE", "@MSNDATE"),
        ("TAKEOFFLOCATION", "@TAKEOFFLOCATION"),
        ("WEAPONTYPE", "@WEAPONTYPE"),
        ("MFUNC_DESC", "@MFUNC_DESC")     
    ])
    hover.formatters={'@MSNDATE': 'datetime'}
    return p

 def dashboard():
     cols = list(x.columns)
     colorby='TGTCOUNTRY'
     map_pane=pn.pane.Bokeh(width=700)
     df_pane = pn.pane.DataFrame(width=600,height=600)
     date_picker = pnw.DatePicker(name='Pick Date',width=200)
     from datetime import date  
     date_picker.value=date(1965, 1, 1)    
     date_slider = pnw.DateSlider(name='Date', start=dt.datetime(1965, 1, 1),
                                  end=dt.datetime(1973, 10, 31), value=dt.datetime(1968, 1, 1))      
     tile_select = pnw.Select(name='tile layer',options=providers,width=200)
     filterby_select = pnw.Select(name='filter by',value='',options=['']+cols[1:4],width=200)
     value_select = pnw.Select(name='value',value='',options=[],width=200)
     find_btn = pnw.Button(name='find in region',button_type='primary',width=200)

     def update_tile(event=None):
         p = map_pane.object
         p.renderers = [x for x in p.renderers if not str(x).startswith('TileRenderer')]
         rend = renderers.TileRenderer(tile_source= get_provider(tile_select.value))
         p.renderers.insert(0, rend)

     def update_filter(event):
         col=filterby_select.value
         if col=='':
             value_select.options = []
         else:
             value_select.options = sorted(list(x[col].dropna().unique()))

     def find_in_region(event):
         #get points in selected map area
         p = map_pane.object
         source = p.renderers[1].data_source
         d = x[(x.x>p.x_range.start) & (x.x<p.x_range.end) & (x.y>p.y_range.start) & (x.y<p.y_range.end)]
         #add any filter
         d = do_filter(d)
         if len(d)==0:
             return
         elif len(d)>25000:           
             p.title.text = 'too many points!'
         else:
             d.loc[:,'color'] = [colormap[i] if i in colormap else 'gray' for i in d[colorby]]        
             source.data = dict(d)
             p.title.text = 'selected %s points' %len(d)
         map_pane.param.trigger('object')     
         return

     def do_filter(d):
         col = filterby_select.value
         val = value_select.value
         if col != '':
             d = d[d[col]==val]
         return d

     def update_date(event):
         date_slider.value = date_picker.value     

     def update_map(event=None, date=None):
         p = map_pane.object
         source = p.renderers[1].data_source
         if date == None:
             date = str(date_slider.value)
         d = x[x.MSNDATE==date]
         d = do_filter(d)
         if len(d)==0:
             return  
         d.loc[:,'color'] = [colormap[i] if i in colormap else 'gray' for i in d[colorby]]
         source.data = dict(d)
         p.title.text = date

     sdate='1968-01-01'
     d = x[x.MSNDATE==sdate]
     map_pane.object=draw_map(d)

     date_slider.param.watch(update_map,'value')
     date_picker.param.watch(update_date,'value')
     tile_select.param.watch(update_tile,'value')
     filterby_select.param.watch(update_filter,'value')
     value_select.param.watch(update_map,'value')
     find_btn.on_click(find_in_region)

     dashboard = pn.Column(date_slider,pn.Row(
                pn.Column(date_picker,tile_select,filterby_select,
                value_select,find_btn),map_pane))
     return dashboard

 app=dashboard()