MultiPolygons

The MultiPolygons glyphs is modeled closely on the GeoJSON spec for Polygon and MultiPolygon. The data that are used to construct MultiPolygons are nested 3 deep. In the top level of nesting, each item in the list represents a MultiPolygon - an entity like a state or a contour level. Each MultiPolygon is composed of Polygons representing different parts of the MultiPolygon. Each Polygon contains a list of coordinates representing the exterior bounds of the Polygon followed by lists of coordinates of any holes contained within the Polygon.

Polygon with no holes

We'll start with one square with bottom left corner at (1, 3) and top right corner at (2, 4). The simple case of one Polygon with no holes is represented in geojson as follows:

geojson
 {
     "type": "Polygon",
     "coordinates": [
         [
             [1, 3],
             [2, 3],
             [2, 4],
             [1, 4],
             [1, 3]
         ]
     ]
 }

In geojson this list of coordinates is nested 1 deep to allow for passing lists of holes within the polygon. In bokeh (using MultiPolygon) the coordinates for this same polygon will be nested 3 deep to allow space for other entities and for other parts of the MultiPolygon.


In [ ]:
from bokeh.plotting import figure, output_notebook, show

output_notebook()

In [ ]:
p = figure(plot_width=300, plot_height=300, tools='hover,tap,wheel_zoom,pan,reset,help')
p.multi_polygons(xs=[[[[1, 2, 2, 1, 1]]]],
                 ys=[[[[3, 3, 4, 4, 3]]]])
show(p)

Notice that in the geojson Polygon always starts and ends at the same point and that the direction in which the Polygon is drawn (winding) must be counter-clockwise. In bokeh we don't have these two restrictions, the direction doesn't matter, and the polygon will be closed even if the starting end ending point are not the same.


In [ ]:
p = figure(plot_width=300, plot_height=300, tools='hover,tap,wheel_zoom,pan,reset,help')
p.multi_polygons(xs=[[[[1, 1, 2, 2]]]],
                 ys=[[[[3, 4, 4, 3]]]])
show(p)

Polygon with holes

Now we'll add some holes to the square polygon defined above. We'll add a triangle in the lower left corner and another in the upper right corner. In geojson this can be represented as follows:

geojson
 {
     "type": "Polygon",
     "coordinates": [
         [
             [1, 3],
             [2, 3],
             [2, 4],
             [1, 4],
             [1, 3]
         ],
         [
             [1.2, 3.2],
             [1.6, 3.6],
             [1.6, 3.2],
             [1.2, 3.2]
         ],
         [
             [1.8, 3.8],
             [1.8, 3.4],
             [1.6, 3.8],
             [1.8, 3.8]
         ]
     ]
 }

Once again notice that the direction in which the polygons are drawn doesn't matter and the last point in a polygon does not need to match the first. Hover over the holes to demonstrate that they aren't considered part of the Polygon.


In [ ]:
p = figure(plot_width=300, plot_height=300, tools='hover,tap,wheel_zoom,pan,reset,help')
p.multi_polygons(xs=[[[ [1, 2, 2, 1], [1.2, 1.6, 1.6], [1.8, 1.8, 1.6] ]]],
                 ys=[[[ [3, 3, 4, 4], [3.2, 3.6, 3.2], [3.4, 3.8, 3.8] ]]])
show(p)

MultiPolygon

Now we'll examine a MultiPolygon. A MultiPolygon is composed of different parts each of which is a Polygon and each of which can have or not have holes. To create a MultiPolygon from the Polygon that we are using above, we'll add a triangle below the square with holes. Here is how this shape would be represented in geojson:

geojson
 {
     "type": "MultiPolygon",
     "coordinates": [
         [
             [
                 [1, 3],
                 [2, 3],
                 [2, 4],
                 [1, 4],
                 [1, 3]
             ],
             [
                 [1.2, 3.2],
                 [1.6, 3.6],
                 [1.6, 3.2],
                 [1.2, 3.2]
             ],
             [
                 [1.8, 3.8],
                 [1.8, 3.4],
                 [1.6, 3.8],
                 [1.8, 3.8]
             ]
         ],
         [
             [
                 [3, 1],
                 [4, 1],
                 [3, 3],
                 [3, 1]
             ]
         ]
     ]
 }

In [ ]:
p = figure(plot_width=300, plot_height=300, tools='hover,tap,wheel_zoom,pan,reset,help')
p.multi_polygons(xs=[[[ [1, 1, 2, 2], [1.2, 1.6, 1.6], [1.8, 1.8, 1.6] ], [ [3, 4, 3] ]]],
                 ys=[[[ [4, 3, 3, 4], [3.2, 3.2, 3.6], [3.4, 3.8, 3.8] ], [ [1, 1, 3] ]]])
show(p)

It is important to understand that the Polygons that make up this MultiPolygon are part of the same entity. It can be helpful to think of representing physically separate areas that are part of the same entity such as the islands of Hawaii.

MultiPolygons

Finally, we'll take a look at how we can represent a list of MultiPolygons. Each Mulipolygon represents a different entity. In geojson this would be a FeatureCollection:

geojson
{
    "type": "FeatureCollection",
    "features": [
    {
        "type": "Feature",
        "properties": {
            "fill": "blue"
        },
        "geometry": {
            "type": "MultiPolygon",
            "coordinates": [
                [
                    [
                        [1, 3],
                        [2, 3],
                        [2, 4],
                        [1, 4],
                        [1, 3]
                    ],
                    [
                        [1.2, 3.2],
                        [1.6, 3.6],
                        [1.6, 3.2],
                        [1.2, 3.2]
                    ],
                    [
                        [1.8, 3.8],
                        [1.8, 3.4],
                        [1.6, 3.8],
                        [1.8, 3.8]
                    ]
                ],
                [
                    [
                        [3, 1],
                        [4, 1],
                        [3, 3],
                        [3, 1]
                    ]
                ]
            ]
        }
    },
    {
        "type": "Feature",
        "properties": {
            "fill": "red"
        },
         "geometry": {
            "type": "Polygon",
            "coordinates": [
                [
                    [1, 1],
                    [2, 1],
                    [2, 2],
                    [1, 2],
                    [1, 1]
                ],
                [
                    [1.3, 1.3],
                    [1.3, 1.7],
                    [1.7, 1.7],
                    [1.7, 1.3]
                    [1.3, 1.3]
                ]
            ]
        }
    }
]}

In [ ]:
p = figure(plot_width=300, plot_height=300, tools='hover,tap,wheel_zoom,pan,reset,help')
p.multi_polygons(
    xs=[
        [[ [1, 1, 2, 2], [1.2, 1.6, 1.6], [1.8, 1.8, 1.6] ], [ [3, 3, 4] ]],
        [[ [1, 2, 2, 1], [1.3, 1.3, 1.7, 1.7] ]]],
    ys=[
        [[ [4, 3, 3, 4], [3.2, 3.2, 3.6], [3.4, 3.8, 3.8] ], [ [1, 3, 1] ]],
        [[ [1, 1, 2, 2], [1.3, 1.7, 1.7, 1.3] ]]],
    color=['blue', 'red'])
show(p)

Using MultiPolygons glyph directly


In [ ]:
from bokeh.models import ColumnDataSource, Plot, LinearAxis, Grid
from bokeh.models.glyphs import MultiPolygons
from bokeh.models.tools import TapTool, WheelZoomTool, ResetTool, HoverTool

In [ ]:
source = ColumnDataSource(dict(
    xs=[
        [
            [ 
                [1, 1, 2, 2], 
                [1.2, 1.6, 1.6], 
                [1.8, 1.8, 1.6] 
            ], 
            [ 
                [3, 3, 4] 
            ]
        ],
        [
            [ 
                [1, 2, 2, 1], 
                [1.3, 1.3, 1.7, 1.7] 
            ]
        ]
    ],
    ys=[
        [
            [ 
                [4, 3, 3, 4], 
                [3.2, 3.2, 3.6], 
                [3.4, 3.8, 3.8] 
            ], 
            [ 
                [1, 3, 1] 
            ]
        ],
        [
            [ 
                [1, 1, 2, 2], 
                [1.3, 1.7, 1.7, 1.3] 
            ]
        ]
    ],
    color=["blue", "red"],
    label=["A", "B"]
))

By looking at the dataframe for this ColumnDataSource object, we can see that each MultiPolygon is represented by one row.


In [ ]:
source.to_df()

In [ ]:
hover = HoverTool(tooltips=[("Label", "@label")])
plot = Plot(plot_width=300, plot_height=300, tools=[hover, TapTool(), WheelZoomTool()])

glyph = MultiPolygons(xs="xs", ys="ys", fill_color='color')
plot.add_glyph(source, glyph)

xaxis = LinearAxis()
plot.add_layout(xaxis, 'below')

yaxis = LinearAxis()
plot.add_layout(yaxis, 'left')

plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker))
plot.add_layout(Grid(dimension=1, ticker=yaxis.ticker))

show(plot)

Using numpy arrays with MultiPolygons

Numpy arrays can be used instead of python native lists. In the following example, we'll generate concentric circles and used them to make rings. Similar methods could be used to generate contours.


In [ ]:
import numpy as np

from bokeh.palettes import Viridis10 as palette

In [ ]:
def circle(radius):
    angles = np.linspace(0, 2*np.pi, 100)
    return {'x': radius*np.sin(angles), 'y': radius*np.cos(angles), 'radius': radius}

In [ ]:
radii = np.geomspace(1, 100, 10)
source = dict(xs=[], 
              ys=[], 
              color=[palette[i] for i in range(10)], 
              outer_radius=radii)

for i, r in enumerate(radii):
    exterior = circle(r)
    if i == 0:
        polygon_xs = [exterior['x']]
        polygon_ys = [exterior['y']]
    else:
        hole = circle(radii[i-1])

        polygon_xs = [exterior['x'], hole['x']]
        polygon_ys = [exterior['y'], hole['y']]

    source['xs'].append([polygon_xs])
    source['ys'].append([polygon_ys])

In [ ]:
p = figure(plot_width=300, plot_height=300, 
           tools='hover,tap,wheel_zoom,pan,reset,help',
           tooltips=[("Outer Radius", "@outer_radius")])

p.multi_polygons('xs', 'ys', fill_color='color', source=source)
show(p)