Search code examples
pythongeojsonbokeh

Bokeh is not rendering properly multipolygon (islands) from GeoJson


I'm rendering a choropleth map on Bokeh. My geodata is an GeoJSON with Polygons and MultiPolygons.

I have trouble rendering the Multipolygons:

If I extract all geometries of an feature (four islands, for example, in one list) their plot doesn't 'cut' between figures, and they seems all one. It shows some 'spiderweb' stuff, crossing unorderly all the points.

If I create one list for island (I assume that it is the correct way to work this), Bokeh doesn't plot anything. Not even the grid (only the toolbar)....and doesn't show any error.

Probably it's some issue with the output of the function 'obtCoordMultipoligono'.

I've searched for examples on islands, but nothing coudn't help me.

Thanks in advance.

Update: I add my snippets. They are fragments of original, but functional. The idea is an output like the 'Texas unemployment' on the BokehGallery, but my country has islands on it.

My GeoJSON in argentina.json (extract 1 multipolygon only; i don't have issues with polygons):

    {
    "type": "FeatureCollection",
    "features": [

    {
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
            [
              [
                -59.68266601562502,
                -52.231640624999976
              ],
              [
                -59.74658203124997,
                -52.25087890624999
              ],
              [
                -59.76445312499996,
                -52.2421875
              ],
              [
                -59.784863281249955,
                -52.2046875
              ],
              [
                -59.78593749999999,
                -52.156152343749966
              ],
              [
                -59.79331054687498,
                -52.134179687500016
              ],
              [
                -59.75322265624999,
                -52.14140624999998
              ],
              [
                -59.681005859375034,
                -52.18007812499995
              ],
              [
                -59.68266601562502,
                -52.231640624999976
              ]
            ]
          ],
          [
            [
              [
                -58.438818359375006,
                -52.011035156249974
              ],
              [
                -58.432714843750006,
                -52.09902343749996
              ],
              [
                -58.512841796874966,
                -52.071093750000045
              ],
              [
                -58.54140625000002,
                -52.02841796874996
              ],
              [
                -58.49707031249997,
                -51.99941406250001
              ],
              [
                -58.46054687499998,
                -52.0015625
              ],
              [
                -58.438818359375006,
                -52.011035156249974
              ]
            ]
          ],
          [
            [
              [
                -61.01875,
                -51.7857421875
              ],
              [
                -60.94726562499997,
                -51.79951171875005
              ],
              [
                -60.87597656250003,
                -51.79423828125004
              ],
              [
                -60.91616210937494,
                -51.89697265625001
              ],
              [
                -60.94755859374996,
                -51.94628906250002
              ],
              [
                -61.031982421875,
                -51.94248046875004
              ],
              [
                -61.11577148437493,
                -51.87529296875003
              ],
              [
                -61.14501953125003,
                -51.83945312500001
              ],
              [
                -61.05166015625002,
                -51.81396484374997
              ],
              [
                -61.01875,
                -51.7857421875
              ]
            ]
          ],
          [
            [
              [
                -60.11171875000002,
                -51.39589843749998
              ],
              [
                -60.24882812499996,
                -51.39599609375
              ],
              [
                -60.27587890624997,
                -51.36318359374997
              ],
              [
                -60.275341796874955,
                -51.28056640625002
              ],
              [
                -60.17138671875,
                -51.273437499999986
              ],
              [
                -60.06982421875,
                -51.307910156249996
              ],
              [
                -60.07646484374993,
                -51.34257812500004
              ],
              [
                -60.11171875000002,
                -51.39589843749998
              ]
            ]
          ]

        ]
      },
      "type": "Feature",
      "properties": {
        "perimeter": 0,
        "vista": "mim",
        "provincia": "Islas Malvinas",
        "objectid": 24,
        "prov": 0,
        "bounds": [
          0,
          0
        ],
        "provif3_": 27.0,
        "ogc_fid": 26,
        "provif3_id": 26.0
      }
    }
    ]
    }

My data in PBIArg.csv:

24,AR-V,Islas,13245

My code:

<!-- language: lang-py -->

import json,pprint,csv
pp = pprint.PrettyPrinter(indent=4)
from bokeh.io import output_file, show
from bokeh.models import HoverTool
from bokeh.plotting import figure, show, output_file, ColumnDataSource
import pandas as pd
from osgeo import ogr

fname = r'islas.json' # constante hasta conseguir algo mejor
dname = r'PBIArg.csv' # variable estadística a graficar.



paleta = ["#FFF5F0", "#FEE0D2", "#FCBBA1", "#FC9272", "#FB6A4A", "#EF3B2C", "#CB181D", "#99000D"]


def colorante(rate,max_value,min_value,paleta):
    try:
        intensidad = int(float(len(paleta)-1) * float(rate - min_value) / float(max_value - min_value))
        return paleta[intensidad]
    except:
        intensidad = int(float(len(paleta)-1) * float(rate - min_value) / float(max_value - min_value))
        return paleta[intensidad]


def obtCoordMultipoligono(pcia):
    mpoly = ogr.CreateGeometryFromJson(pcia)

    print('pcia-MPOLY tiene esta cantidad de islas: ', mpoly.GetGeometryCount()) 
    coordX,coordY = [],[]

    # idx poly mpoly
    for idx, poly in enumerate(mpoly): #itero mpoly
        print('POLY tiene esta cantidad de islas: ', poly.GetGeometryCount()) 
        innerX,innerY = [],[]
        # ind 
        for ind in range(0, poly.GetGeometryCount()): #itero poly
            innerPoly = poly.GetGeometryRef(ind)
            print('INNERPOLY tiene esta cantidad de PUNTOS: ', innerPoly.GetPointCount()) 
            lastX,lastY = [],[]
            for i in range(0, innerPoly.GetPointCount()): #itero innerpoly
                # GetPoint returns a tuple not a Geometry
                punto = innerPoly.GetPoint(i)
                print('pto obtenido en X',punto[0])
                # Asigno a lista local
                lastX.append(punto[0])
                lastY.append(punto[1])
            print('LastX:')
            pp.pprint(lastX)
            innerX.append(lastX)
            innerY.append(lastY)
        print('InnerX:')
        pp.pprint(innerX)
        coordX.append(innerX)
        coordY.append(innerY)
        print('CoordX:')
        pp.pprint(coordX)
    dictCoord = dict(coordX=coordX,coordY=coordY)
    print('DictCoord:')
    pp.pprint(dictCoord)
    return dictCoord

############  MAIN  ##################
######## Leo csv estadísticas ########

with open(dname, 'r') as f:
    '''Leo el CSV, creo diccio de pciaID: estadísticaPcial.
    Abajo busco la estadísticaPcial max y min para luego calcular los colores'''
    max_value, min_value = 0,0
    datos = {}
    for row in csv.reader(f):
        estadistica = int(row[3])
        datos[row[0]] = estadistica
        if estadistica > max_value:
            max_value = estadistica
        if estadistica < min_value:
            min_value = estadistica

######## Leo geojson ########            
with open(fname, 'r') as f:
    geojson = f.read()
    geoDict = json.loads(geojson)

######## Parseo geojson ########

dictArg = {}
for pcia in geoDict['features']:
    pciaID = str(pcia['properties']['objectid'])
    nombrePcia = pcia['properties']['provincia']
    coordX = []
    coordY = []

    if pcia['geometry']['type'] == 'Polygon':
        for punto in pcia['geometry']['coordinates'][0]:
            if len(punto) == 2:
                coordX.append(punto[0])
                coordY.append(punto[1])
    elif pcia['geometry']['type'] == 'MultiPolygon': 
        multiJSON = json.dumps(pcia['geometry'])
        dictCoord = obtCoordMultipoligono(multiJSON)
        # print(dictCoord)
        coordX = dictCoord['coordX']
        coordY = dictCoord['coordY']

    # Handling states without data
    try:
        info=int(datos[pciaID])
    except KeyError:
        info = 0    
    color = colorante(info,max_value,min_value,paleta)
    dictPcia = dict(nombre=nombrePcia,coordX=coordX,coordY=coordY, info=info,color=color)
    dictArg[pciaID] = dictPcia
    print('dict',dictArg['19'])

######## saco coord de las pcias ########
provincias = {
    codPcia: nombrePcia for codPcia, nombrePcia in dictArg.items()
}

# print(provincias)
pcia_xs = [provincia['coordX'] for provincia in provincias.values()]
pcia_ys = [provincia['coordY'] for provincia in provincias.values()]

nombres_provincias = [provincia['nombre'] for provincia in provincias.values()]

######## Saco estadísticas de las pcias ########
provincias_datos = [provincia['info'] for provincia in provincias.values()]

######## Coloreo el mapa a nivel datos ########

provincias_colores = [provincia['color'] for provincia in provincias.values()]

source = ColumnDataSource(data=dict(
    x=pcia_xs,
    y=pcia_ys,
    color=provincias_colores,
    nombre=nombres_provincias,
    dato=provincias_datos,
))

TOOLS="pan,wheel_zoom,box_zoom,reset,hover,save"

p = figure(title="PBI de Argentina por provincia", tools=TOOLS)

p.patches('x', 'y', source=source,
          fill_color='color', fill_alpha=0.9,
          line_color='#767676', line_width=1.5)

hover = p.select_one(HoverTool)
hover.point_policy = "follow_mouse"
hover.tooltips = [
    ("State:", "@nombre"),
    ("Nº:", "@dato"),

]

output_file("PBIar.html", title="Testing islands in bokeh")

show(p)

Solution

  • Instead of following the original 'Texas example', you can significantly simplify your code if you use Bokeh's GeoJSONDataSource.

    A trimmed down example using your geojson could look like:

    from bokeh.io import show, output_notebook, output_file
    from bokeh.models import (
        GeoJSONDataSource,
        HoverTool,
        LinearColorMapper
    )
    from bokeh.plotting import figure
    from bokeh.palettes import Viridis6
    
    with open(r'argentina.geojson', 'r') as f:
        geo_source = GeoJSONDataSource(geojson=f.read())
    
    
    color_mapper = LinearColorMapper(palette=Viridis6)
    
    TOOLS = "pan,wheel_zoom,box_zoom,reset,hover,save"
    
    p = figure(title="Argentina", tools=TOOLS, x_axis_location=None, y_axis_location=None, width=500, height=300)
    p.grid.grid_line_color = None
    
    p.patches('xs', 'ys', fill_alpha=0.7, fill_color={'field': 'objectid', 'transform': color_mapper}, 
              line_color='white', line_width=0.5, source=geo_source)
    
    
    hover = p.select_one(HoverTool)
    hover.point_policy = "follow_mouse"
    hover.tooltips = [("Provincia:", "@provincia")]
    
    output_file("PBIar.html", title="Testing islands in bokeh")
    
    show(p)
    

    The output then looks like: enter image description here

    Edit:

    This is what the output looks like when using the entire geojson as mentioned in the comments below.

    enter image description here

    And zoomed in on the islands in the south:

    enter image description here

    Update: functionality with variable data

    I added this function in order to edit the geoJSON in an interactive way, depending on a JSON data passed.

    The geoJSON now has the 'data' property and the state international code (property: 'ISO_3166-2').

    The JSON data is like this:

    {
    "AR-A": "7",
    "AR-B": "53",
    "AR-C": "46"
    }
    

    The function reads the geoJSON and asign the data:

    def asignDataToStates(geo,data):
        for pcia in geo['features']:
            codPcia = str(pcia['properties']['ISO_3166-2'])
            
            if codPcia in data.keys():
                if data.values() != 0:
                    pcia['properties']['data'] = data[codPcia]
        dataJson = json.dumps(geo,ensure_ascii=True)
        return dataJson