获取GeoJSONDataSource中所选项的属性

时间:2017-11-10 14:53:13

标签: python bokeh

我想将包含补丁(来自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

加载

enter image description here

2 个答案:

答案 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))