@philippjfr Of course, sorry about that. I’ve also provided a sample input file.
I have the following package versions:
GeoViews: 1.8.1
HoloViews: 1.13.3
Panel: 0.9.7
Param: 1.9.3
ebird_10000.csv (1.5 MB)
import panel as pn
import param
import geoviews as gv
import holoviews as hv
import pandas as pd
import geopandas as gpd
from cartopy import crs
from datashader.utils import lnglat_to_meters
from shapely.geometry import Point, box
gv.extension('bokeh',logo=False)
pn.extension()
class Inputs(param.Parameterized):
input_file = param.FileSelector(path='.\sample_data\*.csv', doc='A .csv file selector for input data')
x_field = param.Selector(doc='A field selector for longitude')
y_field = param.Selector(doc='A field selector for latitude')
id_field = param.Selector(doc='A field selector for the identifier')
data = None
@param.depends('input_file', watch=True)
def update_inputs(self):
df = pd.read_csv(self.input_file)
numeric_cols = list(df.select_dtypes(include=['float64']).columns)
columns = [x for i,x in enumerate(df.columns) if x !='Unnamed: 0']
self.param.x_field.objects = numeric_cols
self.x_field = numeric_cols[0]
self.param.y_field.objects = numeric_cols
self.x_field = numeric_cols[0]
self.param.id_field.objects = columns
self.id_field = columns[0]
self.data = df
class Mapview(param.Parameterized):
opts = dict(width=1200,height=750,xaxis=None,yaxis=None,show_grid=False)
tiles = gv.tile_sources.CartoEco().apply.opts(**opts)
extents = param.Parameter(default=(-168, -60, 168, 83), precedence=-1)
tiles.extents = extents.default
box_polygons = gv.Polygons([]).opts(fill_alpha=0.1)
box_colours = ['red','blue','green','orange','purple']
box_stream = hv.streams.BoxEdit(source=box_polygons, num_objects=5, styles={'fill_color': box_colours})
template_df = pd.DataFrame({'x_meters': [], 'y_meters': []}, columns=['x_meters', 'y_meters'])
dfstream = hv.streams.Buffer(template_df, index=False, length=10000)
points = hv.DynamicMap(hv.Points, streams=[dfstream]).opts(
size=5, color='green', fill_alpha=0.3, line_alpha=0.4)
def show_map(self):
return self.tiles * self.box_polygons * self.points
class Dashboard(param.Parameterized):
input1 = Inputs()
input2 = Inputs()
input3 = Inputs()
input4 = Inputs()
input5 = Inputs()
mapview = Mapview()
button = param.Action(lambda x: x.param.trigger('button'), label='Run')
def project_to_meters(self, df, x, y):
try:
df.loc[:,'x_meters'], df.loc[:,'y_meters'] = lnglat_to_meters(df[x],df[y])
geometry = [Point(xy) for xy in zip(df.x_meters, df.y_meters)]
web_merc = {'init': 'epsg:3857'}
return gpd.GeoDataFrame(df, crs=web_merc, geometry=geometry)
except:
print("""Could not project specified coordinate fields to meters. Make sure the X Field and Y Field
values have been set correctly.\n""")
def convert_box_stream_data_to_geometry(self):
try:
box_data = self.mapview.box_stream.data
try:
num_boxes = len(box_data['x0'])
except:
num_boxes = 0
boxes = []
for i in range(0,num_boxes):
x0 = box_data['x0'][i]
y0 = box_data['y0'][i]
x1 = box_data['x1'][i]
y1 = box_data['y1'][i]
boxes.append(box(x0,y0,x1,y1))
if len(boxes)>0:
return boxes
except:
print('Could not convert boxes drawn on map into geometries.\n')
@param.depends('button')
def run_analysis(self):
try:
# clear previous results
self.mapview.dfstream.clear()
# Convert each input into a GeoDataFrame and project coordinates to meters
datasets = []
id_fields = []
for i in [self.input1, self.input2, self.input3, self.input4, self.input5]:
if isinstance(i.data, pd.core.frame.DataFrame):
gdf = self.project_to_meters(i.data, i.x_field, i.y_field)
datasets.append(gdf)
id_fields.append(i.id_field)
# Convert boxes drawn on the map into Shapely geometries
if len(datasets)>0:
boxes = []
box_data = self.mapview.box_stream.data
for i in range(0,len(box_data['x0'])):
x0 = box_data['x0'][i]
y0 = box_data['y0'][i]
x1 = box_data['x1'][i]
y1 = box_data['y1'][i]
boxes.append(box(x0,y0,x1,y1))
# Do intersections of each box on each GeoDataFrame & send results to the map
df_all_boxes = pd.DataFrame()
df_final = pd.DataFrame()
box_labels = []
for i in range(0,len(datasets)):
for b in boxes:
box_label = self.mapview.box_colours[boxes.index(b)] + '_box'
box_labels.append(box_label)
datasets[i][box_label] = datasets[i].intersects(b)
selected = datasets[i].loc[datasets[i][box_label]==True].drop(columns='geometry')
self.mapview.dfstream.send(pd.DataFrame(selected[['x_meters','y_meters']]))
selected.rename(columns={id_fields[i]:'identifier'},inplace=True)
df_all_boxes = pd.concat([df_all_boxes,selected[['identifier',box_label]]],axis=0,ignore_index=True)
for ident in pd.unique(df_all_boxes['identifier']):
dfx = df_all_boxes.loc[df_all_boxes['identifier']==ident]
box_values = {'identifier':[ident]}
for bl in box_labels:
# returns true if any of the values are true
# else returns false
box_values[bl] = [dfx[bl].any()]
df_ident = pd.DataFrame.from_dict(box_values)
df_final = pd.concat([df_final, df_ident],axis=0)
def summarize_boxes(row):
cols = list(row.axes[0])
s = []
for c in cols:
if 'box' in c:
if row.values[cols.index(c)]:
s.append(c.split('_')[0])
return ','.join(s)
summary = df_final.copy()
try:
summary['Found_In'] = summary.apply(lambda row: summarize_boxes(row),axis=1)
return pn.widgets.DataFrame(summary, disabled=True, fit_columns=True, width=700)
except:
print('There was an error creating the final output summary table.\n')
else:
print('You must select at least one input dataset.\n')
except:
print('Something went wrong in the run_analysis() function.\n')
return('Something went wrong in the run_analysis() function.')
def view(self):
desc = """This is a demonstration dashboard with incomplete functionality. Its purpose
is to sit here and look pretty. We can put graphics and stuff in here to
make it look all fancy."""
logo = '.\images\logo_panel_stacked_s.png'
button_desc = """The <i>Run</i> button will execute the spatial intersection and generate
a table of identifiers found within each box and in multiple boxes.<br><br>
Push the <i>Run</i> button after configuring all inputs and drawing boxes on the map."""
return pn.Row(
pn.Column(
'## Description',
desc,logo),
pn.Column(
'### Configure Inputs',
pn.Tabs(
('Input 1',self.input1),
('Input 2',self.input2),
('Input 3',self.input3),
('Input 4',self.input4),
('Input 5',self.input5)),
button_desc,
self.param['button']),
pn.Tabs(
('Map View', pn.Column(self.mapview.show_map())),
('Results Table', self.run_analysis)))
d = Dashboard()
d.view().show()