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
.
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)
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)
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.
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)
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)
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)