散景回调:更改地理位置

时间:2019-08-27 13:35:19

标签: python callback bokeh geopandas

我正在尝试使用Bokeh制作一个应用程序,该应用程序需要一个小的数据集并根据Select菜单(一个下拉菜单)的选定值加载部分数据集。过滤后的数据稍后将用于生成一些条形图(数量,例如比率,得分和归一化分数。我将仅提及比率)。但是,对于下拉菜单中的两个选项,还可以将数据映射到地理地图中,我希望在可能的情况下同时拥有这两个数据(法线贴图和地理映射)。但是,挑战在于这两个地区的地理划分是不同的:一个对应于一个国家的省,另一个对应于县。

鉴于此,我决定

  • 总是绘制常用的条形图,
  • 也总是绘制地理图:根据菜单中选择的选项,必须绘制省或县/市。如果未选择这两个选项,则仅绘制国家边界。

我从硬盘驱动器上检索了borderprovincescities的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保持静态(丢失了源,因此,在更改菜单的选择之后,悬停将不起作用)。

感谢您的帮助。

1 个答案:

答案 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)更新现有源。

由于我没有您的代码,因此无法检查此解决方案,但是此方法确实会更新本地的地理资源。最好的祝福。