我正在尝试使用Bokeh制作一个应用程序,该应用程序需要一个小的数据集并根据Select
菜单(一个下拉菜单)的选定值加载部分数据集。过滤后的数据稍后将用于生成一些条形图(数量,例如比率,得分和归一化分数。我将仅提及比率)。但是,对于下拉菜单中的两个选项,还可以将数据映射到地理地图中,我希望在可能的情况下同时拥有这两个数据(法线贴图和地理映射)。但是,挑战在于这两个地区的地理划分是不同的:一个对应于一个国家的省,另一个对应于县。
鉴于此,我决定
我从硬盘驱动器上检索了border
,provinces
和cities
的shapefile,并设法绘制了条形图,地理图和选择菜单。只要关注条形图(在更改菜单中的选项时也会更改),回调函数也可以正常工作。然而,对于地理图则并非如此。他们保持原样(应有的状态),因为我没有找到为他们更改data_source
的正确方法。
这是我的代码的简化版本。
import pandas as pd
import geopandas as gpd
import json
from bokeh.io import show, output_notebook
from bokeh.layouts import column, row
from bokeh.models import GeoJSONDataSource, Panel, Tabs, LinearColorMapper,
from bokeh.models.widgets import Select
from bokeh.palettes import Viridis256
from bokeh.plotting import figure, ColumnDataSource, curdoc
# this is needed for some preprocessing
from bokeh.models.glyphs import MultiPolygons
#1. loading shapefiles
shapefile_dir = 'a_directory/'
border = gpd.read_file(shapefile_dir + 'border.shp', encoding='utf8')
prov = gpd.read_file(shapefile_dir + 'provinces.shp',encoding='utf8')
city = gpd.read_file(shapefile_dir + 'counties.shp',encoding='utf8')
prov = prov.reindex(['ADM1_EN', 'ADM1_FA','Shape_Leng','Shape_Area','geometry'], axis=1)
city = city.reindex(['ADM2_EN', 'ADM2_FA','ADM1_EN', 'ADM1_FA','Shape_Leng','Shape_Area','geometry'], axis=1)
# some preprocessing
# ...
#2. loading the data
data = pd.read_excel('GeneralReport.xlsx')[0:-1]
data.rename(columns={'پاسخ کافی نیست':'insufficient',\
'از پاسخ رضایت دارم':'satisfied',\
'پاسخ بی ارتباط است':'irrelevant',\
'نام سازمان':'name'}, inplace=True)
data.name = data.name.map(lambda x: x.strip())
data.set_index('name', inplace=True)
# some simple data manipulations
# ...
# 3. defining some functions that will be used later
def selector(select):
"""this function reads the menu value and reforms it if it is needed. the returned value is a string or a list of strings."""
# ...
def filtering(df, name_str):
"""filters the dataset based on the `name_str` and sorts it by the `rate` property`"""
cond = pd.Series(index = df.index, data=False)
if type(name_str)==type([]):
for s in name_str:
cond += data.index.str.startswith(s)
else:
cond += data.index.str.startswith(name_str)
filtered = df[cond]
return filtered.sort_values('rate').reset_index()
def filter_for_map(df, name_str):
"""checks the `name_sting` and returns the proper geopandas dataframe"""
_df = df.copy()
_df.index = _df.name.map(lambda x: x.replace(name_str,'').strip())
if (name_str=='شهرداری' or name_str=='استانداری'):
if name_str=='شهرداری': # counties shall be returned
R = pd.Series(index=city.city_fa)
geo = gpd.GeoDataFrame(geometry = city.geometry)
else: # provinces shall be returned
R = pd.Series(index=prov.prov_fa)
geo = gpd.GeoDataFrame(geometry = prov.geometry)
S = R.copy()
SN = R.copy()
R[R.index.isin(_df.index)]=_df.rate
S[S.index.isin(_df.index)]=_df.score
SN[SN.index.isin(_df.index)]=_df['score-normalized']
geo['name'] = R.index
geo['score'] = S.values
geo['rate'] = R.values
geo['score_normalized'] = SN.values
else: # borders shall be returned
geo = gpd.GeoDataFrame(geometry = border.geometry)
geo['name'] = pd.np.random.randint(len(geo))
geo['score'] = None
geo['rate'] = None
geo['score_normalized'] = None
return GeoJSONDataSource(geojson=geo.to_json())
#4. plotting
name_str = 'استانداری'
TOOLS = "pan,wheel_zoom,box_zoom,reset,hover,save"
FIG_SETTING = {'plot_width':900, 'plot_height':400, 'tools':"hover,wheel_zoom,pan,reset"}
BAR_SETTING = {'x':'name', 'width':0.9, 'line_color':'white' }
MAP_SETTING = {'fill_alpha':1, 'line_color':'black', 'line_width':0.25}
# bar plots
df = filtering(data, name_str)
source = ColumnDataSource.from_df(df)
p1 = figure(x_range=source['name'],**FIG_SETTING)
r1= p1.vbar(top='rate',fill_color={'field': 'rate','transform': color_mapper}, source=source, **BAR_SETTING )
t1 = Panel(child=p1, title='Rate')
p1.xaxis[0].major_label_orientation=pd.np.pi/2
# map plots
geo_source= filter_for_map(df, name_str)
p4 = figure(title="Rate", tooltips=[("نام", "@name"), ("ًنرخ پاسخگویی", "@rate")], tools=TOOLS,)
r4 = p4.patches('xs', 'ys', fill_color={'field': 'rate','transform': color_mapper}, source=geo_source, **MAP_SETTING,)
t4 = Panel(child=p4, title='Rate')
def callback(attr, old, new):
name_str = selector(select)
df = filtering(data, name_str)
src = ColumnDataSource.from_df(df)
geo_src = filter_for_map(df, name_str)
r1.data_source.data= src
p1.x_range.factors= list(src['name'])
# I'm not sure about the following line
r4.update(data_source= geo_src)
select = Select(title="دستهبندی", value="شهرداری",\
options=["استانداری", "وزارت", "دانشگاه", "بانک", "سازمان", "شرکت", "مرکز", "شهرداری", "صندوق", "موسسه", "معاونت", "بنیاد", "بیمه", "اداره","غیره"])
select.on_change('value', callback)
tabs1 = Tabs(tabs=[ t1 ])
tabs2 = Tabs(tabs=[ t4 ])
layout = row(tabs1,tabs2)
curdoc().add_root(column(select,layout))
我想引起您对回调函数的注意,在该函数中我不确定如何更新/重新定义geo_src
,以使Bokeh每当触发回调时都会对其进行更新。到目前为止,只有条形图t1
的行为正确,而另一张地图t4
保持静态(丢失了源,因此,在更改菜单的选择之后,悬停将不起作用)。
感谢您的帮助。
答案 0 :(得分:1)
我希望这对将来的某人有用。我通过更新地理资源的geojson解决了这个问题。在上述情况下,您将像这样更新回调:
def filter_for_map(df, name_str):
... #removed code
return geo #just return your modified data frame
def callback(attr, old, new):
...
geo_src = filter_for_map(df, name_str)
....
geo_source.geojson = geo_src.to_json() #set the existing source's geojson attribute to your dataframe converted in json file
请勿创建新的GeoJSONDataSource,必须使用可用参数(例如to_json)更新现有源。
由于我没有您的代码,因此无法检查此解决方案,但是此方法确实会更新本地的地理资源。最好的祝福。