如何根据 GeoJSON 数据过滤 Bokeh 视觉效果?

How to filter a Bokeh visual based on GeoJSON data?

我在 Bokeh 中使用了一些 shapefile,并且在遵循 this tutorial 背后的基本原理之后,我想添加到我的绘图中的一个功能是可以基于单个或多个选择来过滤值多选小部件:

import geopandas as gpd
from shapely.geometry import Polygon

from bokeh.io import show
from bokeh.models import (GeoJSONDataSource, HoverTool, BoxZoomTool,
                          MultiChoice)
from bokeh.models.callbacks import CustomJS
from bokeh.layouts import column
from bokeh.plotting import figure

# Creating a GeoDataFrame as an example
data = {
    'type':['alpha', 'beta', 'gaga', 'alpha'],
    'age':['young', 'adult', 'really old', 'Methuselah'],
    'favourite_food':['banana', 'fish & chips', 'cookieeeeees!', 'pies'],
    'geometry':[Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
                Polygon([(2, 2), (2, 3), (3, 3), (3, 2)]),
                Polygon([(4, 4), (4, 3), (3, 3), (3, 4)]),
                Polygon([(1, 3), (2, 3), (2, 4), (1, 4)])]}

df = gpd.GeoDataFrame(data)

# Creating the Bokeh data source
gs = GeoJSONDataSource(geojson=df.to_json())

# Our MultiChoice widget accepts the values in the "type" column
ops = ['alpha', 'beta', 'gaga']

mc = MultiChoice(title='Type', options=ops)

# The frame of the Bokeh figure
f = figure(title='Three little squares',
           toolbar_location='below',
           tools='pan, wheel_zoom, reset, save',
           match_aspect=True)

# The GeoJSON data added to the figure
plot_area = f.patches('xs', 'ys', source=gs, line_color=None, line_width=0.25,
                      fill_alpha=1, fill_color='blue')

# Additional tools (may be relevant for the case in question)
f.add_tools(
    HoverTool(renderers=[plot_area], tooltips=[
        ('Type', '@type'),
        ('Age', '@age'),
        ('Favourite Food', '@favourite_food')]),
    BoxZoomTool(match_aspect=True))

# EDIT: creating the callback
tjs = '''
var gjs = JSON.parse(map.geojson);
var n_instances = Object.keys(gjs.features).length;

var txt_mc = choice.value;

if (txt_mc == "") {
    alert("empty string means no filter!");
} else {
    var n_filter = String(txt_mc).split(",").length;

    if (n_filter == max_filter) {
        alert("all values are selected: reset filter!");
    } else {
        var keepers = [];
        for (var i = 0; i < n_instances; i++){
            if (txt_mc.includes(gjs.features[i].properties.type)){
                keepers.push(i);
            }
        }
        alert("Objects to be kept visible: " + String(keepers));
    }
}
'''

cjs = CustomJS(args={'choice':mc, 'map':gs, 'max_filter':len(ops)}, code=tjs)

mc.js_on_change('value', cjs)

show(column(mc, f))

但是,我完全不知道应该如何将自定义 JavaScript 回调编写到 GeoJSONDataSource,或者这是否 even 可能,鉴于我在 SO 中找到的其他示例改为处理 ColumnDataSource 对象,例如 , and Bokeh's tutorial 似乎更喜欢静态过滤器。

是否可以动态过滤数据?如果是这样,回调应该如何构造?


编辑

我已经设法至少构建了回调逻辑,通过变量 tjscjs 将对象绑定到 MultiChoice 上选择的值。然而,最后一块拼图仍然存在:因为 gs 不是 ColumnDataSource,我无法使用 CDSViewCustomJSFilter 来完成这项工作,比如 做到了(“滑块工具”部分)。有什么想法吗?

基本上,我设法找到的解决方案涉及创建 GeoJSON 结构的副本作为“原始”,因此只要过滤器更新,就会将值与这个“原始”进行比较确保:

  • 已添加 个过滤器反映了额外的数据点;
  • 清除过滤器重新创建整个原始结构。

连同 JSONs 的 splice 功能,我们能够更新 JSON 就地:

gs = GeoJSONDataSource(geojson=inter_f.to_json())

# gs_org = gs creates a shallow copy, not ideal
gs_org = GeoJSONDataSource(geojson=inter_f.to_json())

cjs = CustomJS(args={'choice':mc, 'map':gs, 'map_org':gs_org, 'max_filter':len(ops)},
               code=tjs)

tjs会是一个字符串变量,JS代码如下:

var gjs = JSON.parse(map_org.geojson);
var n_instances = Object.keys(gjs.features).length;

var txt_mc = choice.value;

if (txt_mc == "") {
    map.geojson = map_org.geojson;
} else {
    var n_filter = String(txt_mc).split(",").length;

    if (n_filter == max_filter) {
        map.geojson = map_org.geojson;
    } else {
        var deleters = [];
        for (var i = 0; i < n_instances; i++){
            if (!txt_mc.includes(gjs.features[i].properties.type)){
                deleters.push(i);
            }
        }
        deleters.reverse();
        for (var i = 0; i < deleters.length; i++){
            gjs.features.splice(deleters[i], 1);
        }
        map.geojson = JSON.stringify(gjs);
    }
}

map.change.emit();

我很确定它的性能很差(我对JavaScript一无所知),所以如果有人愿意提出优化的解决方案,我洗耳恭听。

下面的解决方案

  1. 只是重置显示的数据并且
  2. 复制源中MultiChoice选中的数据
  3. 发出更改
callback = CustomJS(args=dict(gs=gs, gs_org=gs_org, mc=mc),
    code="""
    const show = gs.data;
    var base = gs_org.data;
    var array = ['xs', 'ys', 'type', 'age', 'favourite_food']
    
    // clear all old data
    for(let element of array)
        show[element] = []
    
    // set default if no label is selected
    if(mc.value.length==0)
        mc.value = ['alpha', 'beta', 'gaga']

    // loop trough all values in MultiChoice
    for(let i=0; i<mc.value.length;i++){
        let value = mc.value[i]
        var idx = base['type'].indexOf(value);
        
        // search for multiple indexes for "value"
        while(idx!=-1){
            // set new data
            for(let element of array)
                show[element].push(base[element][idx])
            idx = base['type'].indexOf(value, idx + 1);
        }
    }
    gs.change.emit()
    """
                   )
mc.js_on_change('value', callback)

输出