我想将包含补丁(来自GeoJSONDataSource
)的图表与折线图链接,但我无法获取所选补丁的属性。
它基本上是一个显示多边形的图,当选择多边形时,我想用该多边形的数据时间序列更新折线图。折线图由正常ColumnDataSource
驱动。
我可以通过添加与geo_source.selected['1d']['indices']
结合的回调来获取所选补丁的索引。但是,我如何获得与该索引相对应的数据/属性?我需要在属性中获得一个“键”,然后我可以使用它来更新折线图。
GeoJSONDataSource
没有data
属性,我可以在其中查找数据本身。 Bokeh可以使用诸如着色/工具提示之类的东西的属性,所以我假设必须有一种方法将这些从GeoJSONDataSource
中取出,我很遗憾地发现它。
这是一个有效的玩具示例,显示了我到目前为止所拥有的内容。
import pandas as pd
import numpy as np
from bokeh import events
from bokeh.models import (Select, Column, Row, ColumnDataSource, HoverTool,
Range1d, LinearAxis, GeoJSONDataSource)
from bokeh.plotting import figure
from bokeh.io import curdoc
import os
import datetime
from collections import OrderedDict
def make_plot(src):
# function to create the line chart
p = figure(width=500, height=200, x_axis_type='datetime', title='Some parameter',
tools=['xwheel_zoom', 'xpan'], logo=None, toolbar_location='below', toolbar_sticky=False)
p.circle('index', 'var1', color='black', fill_alpha=0.2, size=10, source=src)
return p
def make_geo_plot(src):
# function to create the spatial plot with polygons
p = figure(width=300, height=300, title="Select area", tools=['tap', 'pan', 'box_zoom', 'wheel_zoom','reset'], logo=None)
p.patches('xs', 'ys', fill_alpha=0.2, fill_color='black',
line_color='black', line_width=0.5, source=src)
p.on_event(events.SelectionGeometry, update_plot_from_geo)
return p
def update_plot_from_geo(event):
# update the line chart based on the selected polygon
selected = geo_source.selected['1d']['indices']
if (len(selected) > 0):
first = selected[0]
print(geo_source.selected['1d']['indices'])
def update_plot(attrname, old, new):
# Callback for the dropdown menu which updates the line chart
new_src = get_source(df, area_select.value)
src.data.update(new_src.data)
def get_source(df, fieldid):
# function to get a subset of the multi-hierarchical DataFrame
# slice 'out' the selected area
dfsub = df.xs(fieldid, axis=1, level=0)
src = ColumnDataSource(dfsub)
return src
# example timeseries
n_points = 100
df = pd.DataFrame({('area_a','var1'): np.sin(np.linspace(0,5,n_points)) + np.random.rand(100)*0.1,
('area_b','var1'): np.sin(np.linspace(0,2,n_points)) + np.random.rand(100)*0.1,
('area_c','var1'): np.sin(np.linspace(0,3,n_points)) + np.random.rand(100)*0.1,
('area_d','var1'): np.sin(np.linspace(0,4,n_points)) + np.random.rand(100)*0.1},
index=pd.DatetimeIndex(start='2017-01-01', freq='D', periods=100))
# example polygons
geojson = """{
"type":"FeatureCollection",
"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[
{"type":"Feature","properties":{"key":"area_a"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-108.8,42.7],[-104.5,42.0],[-108.3,39.3],[-108.8,42.7]]]]}},
{"type":"Feature","properties":{"key":"area_b"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-106.3,44.0],[-106.2,42.6],[-103.3,42.6],[-103.4,44.0],[-106.3,44.0]]]]}},
{"type":"Feature","properties":{"key":"area_d"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-104.3,41.0],[-101.5,41.0],[-102.9,37.8],[-104.3,41.0]]]]}},
{"type":"Feature","properties":{"key":"area_c"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-105.8,40.3],[-108.3,37.7],[-104.0,37.4],[-105.8,40.3]]]]}}
]
}"""
geo_source = GeoJSONDataSource(geojson=geojson)
# populate a drop down menu with the area's
area_ids = sorted(df.columns.get_level_values(0).unique().values.tolist())
area_ids = [str(x) for x in area_ids]
area_select = Select(value=area_ids[0], title='Select area', options=area_ids)
area_select.on_change('value', update_plot)
src = get_source(df, area_select.value)
p = make_plot(src)
pgeo = make_geo_plot(geo_source)
# add to document
curdoc().add_root(Row(Column(area_select, p), pgeo))
将代码保存在.py
文件中,并使用bokeh serve example.py --show
答案 0 :(得分:2)
传递给GeoJSONDataSource
的geojson数据存储在其geojson
属性中 - 作为字符串。我的建议并不特别优雅:你可以使用内置的json
模块解析json字符串。这是update_plot_from_geo
的工作版本,可根据所选多边形更新折线图:
def update_plot_from_geo(event):
# update the line chart based on the selected polygon
indices = geo_source.selected['1d']['indices']
if indices:
parsed_geojson = json.loads(geo_source.geojson)
features = parsed_geojson['features']
series_key = features[indices[0]]['properties']['key']
new_source = get_source(df, series_key)
src.data.update(new_source.data)
您还需要在顶部import json
。
我有点意外,并不是一个明显的方法来获取解析的 json数据。 GeoJSONDataSource文档表明存在geojson
属性,但表示它是JSON
对象。 JSON
文档似乎暗示您应该能够执行src.geojson.parse
之类的操作。但geojson
的类型只是str
。经过仔细检查,似乎文档正在使用" JSON"含糊不清,在某些情况下指的是Bokeh JSON
类,在其他情况下指的是内置的JavaScript JSON
对象。
所以目前,我并不相信有更好的方式来获取这些数据。
答案 1 :(得分:2)
您应该为GeoJSONDataSource编写自定义扩展
以下是GeoJSONDataSource https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/models/sources/geojson_data_source.coffee
的coffeescript我对自定义扩展程序不太满意。所以我只是完全复制了GeoJSONDataSource并将其称为CustomGeo。我刚刚移动了数据'从@internal到@define。然后bingo,你得到了一个带有数据的GeoJSONDataSource'属性。
在下面的示例中,我使用'键'进行了回调。列表,但既然你现在有这样的数据,你可以写一些东西来重复检查,如果你担心改组,它对应于适当的多边形
import pandas as pd
import numpy as np
from bokeh.core.properties import Instance, Dict, JSON, Any
from bokeh import events
from bokeh.models import (Select, Column, Row, ColumnDataSource, HoverTool,
Range1d, LinearAxis, GeoJSONDataSource, ColumnarDataSource)
from bokeh.plotting import figure
from bokeh.io import curdoc
import os
import datetime
from collections import OrderedDict
def make_plot(src):
# function to create the line chart
p = figure(width=500, height=200, x_axis_type='datetime', title='Some parameter',
tools=['xwheel_zoom', 'xpan'], logo=None, toolbar_location='below', toolbar_sticky=False)
p.circle('index', 'var1', color='black', fill_alpha=0.2, size=10, source=src)
return p
def make_geo_plot(src):
# function to create the spatial plot with polygons
p = figure(width=300, height=300, title="Select area", tools=['tap', 'pan', 'box_zoom', 'wheel_zoom','reset'], logo=None)
a=p.patches('xs', 'ys', fill_alpha=0.2, fill_color='black',
line_color='black', line_width=0.5, source=src,name='poly')
p.on_event(events.SelectionGeometry, update_plot_from_geo)
return p
def update_plot_from_geo(event):
# update the line chart based on the selected polygon
try:
selected = geo_source.selected['1d']['indices'][0]
except IndexError:
return
print geo_source.data
print geo_source.data['key'][selected]
new_src = get_source(df,geo_source.data['key'][selected])
src.data.update(new_src.data)
def update_plot(attrname, old, new):
# Callback for the dropdown menu which updates the line chart
print area_select.value
new_src = get_source(df, area_select.value)
src.data.update(new_src.data)
def get_source(df, fieldid):
# function to get a subset of the multi-hierarchical DataFrame
# slice 'out' the selected area
dfsub = df.xs(fieldid, axis=1, level=0)
src = ColumnDataSource(dfsub)
return src
# example timeseries
n_points = 100
df = pd.DataFrame({('area_a','var1'): np.sin(np.linspace(0,5,n_points)) + np.random.rand(100)*0.1,
('area_b','var1'): np.sin(np.linspace(0,2,n_points)) + np.random.rand(100)*0.1,
('area_c','var1'): np.sin(np.linspace(0,3,n_points)) + np.random.rand(100)*0.1,
('area_d','var1'): np.sin(np.linspace(0,4,n_points)) + np.random.rand(100)*0.1},
index=pd.DatetimeIndex(start='2017-01-01', freq='D', periods=100))
# example polygons
geojson = """{
"type":"FeatureCollection",
"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[
{"type":"Feature","properties":{"key":"area_a"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-108.8,42.7],[-104.5,42.0],[-108.3,39.3],[-108.8,42.7]]]]}},
{"type":"Feature","properties":{"key":"area_b"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-106.3,44.0],[-106.2,42.6],[-103.3,42.6],[-103.4,44.0],[-106.3,44.0]]]]}},
{"type":"Feature","properties":{"key":"area_d"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-104.3,41.0],[-101.5,41.0],[-102.9,37.8],[-104.3,41.0]]]]}},
{"type":"Feature","properties":{"key":"area_c"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-105.8,40.3],[-108.3,37.7],[-104.0,37.4],[-105.8,40.3]]]]}}
]
}"""
implementation = """
import {ColumnarDataSource} from "models/sources/columnar_data_source"
import {logger} from "core/logging"
import * as p from "core/properties"
export class CustomGeo extends ColumnarDataSource
type: 'CustomGeo'
@define {
geojson: [ p.Any ] # TODO (bev)
data: [ p.Any, {} ]
}
initialize: (options) ->
super(options)
@_update_data()
@connect(@properties.geojson.change, () => @_update_data())
_update_data: () -> @data = @geojson_to_column_data()
_get_new_list_array: (length) -> ([] for i in [0...length])
_get_new_nan_array: (length) -> (NaN for i in [0...length])
_flatten_function: (accumulator, currentItem) ->
return accumulator.concat([[NaN, NaN, NaN]]).concat(currentItem)
_add_properties: (item, data, i, item_count) ->
for property of item.properties
if !data.hasOwnProperty(property)
data[property] = @_get_new_nan_array(item_count)
data[property][i] = item.properties[property]
_add_geometry: (geometry, data, i) ->
switch geometry.type
when "Point"
coords = geometry.coordinates
data.x[i] = coords[0]
data.y[i] = coords[1]
data.z[i] = coords[2] ? NaN
when "LineString"
coord_list = geometry.coordinates
for coords, j in coord_list
data.xs[i][j] = coords[0]
data.ys[i][j] = coords[1]
data.zs[i][j] = coords[2] ? NaN
when "Polygon"
if geometry.coordinates.length > 1
logger.warn('Bokeh does not support Polygons with holes in, only exterior ring used.')
exterior_ring = geometry.coordinates[0]
for coords, j in exterior_ring
data.xs[i][j] = coords[0]
data.ys[i][j] = coords[1]
data.zs[i][j] = coords[2] ? NaN
when "MultiPoint"
logger.warn('MultiPoint not supported in Bokeh')
when "MultiLineString"
flattened_coord_list = geometry.coordinates.reduce(@_flatten_function)
for coords, j in flattened_coord_list
data.xs[i][j] = coords[0]
data.ys[i][j] = coords[1]
data.zs[i][j] = coords[2] ? NaN
when "MultiPolygon"
exterior_rings = []
for polygon in geometry.coordinates
if polygon.length > 1
logger.warn('Bokeh does not support Polygons with holes in, only exterior ring used.')
exterior_rings.push(polygon[0])
flattened_coord_list = exterior_rings.reduce(@_flatten_function)
for coords, j in flattened_coord_list
data.xs[i][j] = coords[0]
data.ys[i][j] = coords[1]
data.zs[i][j] = coords[2] ? NaN
else
throw new Error('Invalid type ' + geometry.type)
_get_items_length: (items) ->
count = 0
for item, i in items
geometry = if item.type == 'Feature' then item.geometry else item
if geometry.type == 'GeometryCollection'
for g, j in geometry.geometries
count += 1
else
count += 1
return count
geojson_to_column_data: () ->
geojson = JSON.parse(@geojson)
if geojson.type not in ['GeometryCollection', 'FeatureCollection']
throw new Error('Bokeh only supports type GeometryCollection and FeatureCollection at top level')
if geojson.type == 'GeometryCollection'
if not geojson.geometries?
throw new Error('No geometries found in GeometryCollection')
if geojson.geometries.length == 0
throw new Error('geojson.geometries must have one or more items')
items = geojson.geometries
if geojson.type == 'FeatureCollection'
if not geojson.features?
throw new Error('No features found in FeaturesCollection')
if geojson.features.length == 0
throw new Error('geojson.features must have one or more items')
items = geojson.features
item_count = @_get_items_length(items)
data = {
'x': @_get_new_nan_array(item_count),
'y': @_get_new_nan_array(item_count),
'z': @_get_new_nan_array(item_count),
'xs': @_get_new_list_array(item_count),
'ys': @_get_new_list_array(item_count),
'zs': @_get_new_list_array(item_count)
}
arr_index = 0
for item, i in items
geometry = if item.type == 'Feature' then item.geometry else item
if geometry.type == 'GeometryCollection'
for g, j in geometry.geometries
@_add_geometry(g, data, arr_index)
if item.type == 'Feature'
@_add_properties(item, data, arr_index, item_count)
arr_index += 1
else
# Now populate based on Geometry type
@_add_geometry(geometry, data, arr_index)
if item.type == 'Feature'
@_add_properties(item, data, arr_index, item_count)
arr_index += 1
return data
"""
class CustomGeo(ColumnarDataSource):
__implementation__ = implementation
geojson = JSON(help="""
GeoJSON that contains features for plotting. Currently GeoJSONDataSource can
only process a FeatureCollection or GeometryCollection.
""")
data = Dict(Any,Any,default={},help="wooo")
geo_source = CustomGeo(geojson=geojson)
# populate a drop down menu with the area's
area_ids = sorted(df.columns.get_level_values(0).unique().values.tolist())
area_ids = [str(x) for x in area_ids]
area_select = Select(value=area_ids[0], title='Select area', options=area_ids)
area_select.on_change('value', update_plot)
src = get_source(df, area_select.value)
p = make_plot(src)
pgeo = make_geo_plot(geo_source)
# add to document
curdoc().add_root(Row(Column(area_select, p), pgeo))