Skip to content

Commit

Permalink
Plotly map tiles support (#4686)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmmease committed Nov 18, 2020
1 parent 4b34e5f commit b1e606d
Show file tree
Hide file tree
Showing 29 changed files with 1,974 additions and 306 deletions.
4 changes: 3 additions & 1 deletion examples/reference/elements/bokeh/Tiles.ipynb
Expand Up @@ -8,7 +8,9 @@
"<dl class=\"dl-horizontal\">\n",
" <dt>Title</dt> <dd> Tiles Element</dd>\n",
" <dt>Dependencies</dt> <dd>Bokeh</dd>\n",
" <dt>Backends</dt> <dd><a href='./Tiles.ipynb'>Bokeh</a></dd>\n",
" <dt>Backends</dt>\n",
" <dd><a href='./Tiles.ipynb'>Bokeh</a></dd>\n",
" <dd><a href='../plotly/Tiles.ipynb'>Plotly</a></dd>\n",
"</dl>\n",
"</div>"
]
Expand Down
130 changes: 130 additions & 0 deletions examples/reference/elements/plotly/Tiles.ipynb
@@ -0,0 +1,130 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"contentcontainer med left\" style=\"margin-left: -50px;\">\n",
"<dl class=\"dl-horizontal\">\n",
" <dt>Title</dt> <dd> Tiles Element</dd>\n",
" <dt>Dependencies</dt> <dd>Plotly</dd>\n",
" <dt>Backends</dt>\n",
" <dd><a href='../bokeh/Tiles.ipynb'>Bokeh</a></dd>\n",
" <dd><a href='Tiles.ipynb'>Plotly</a></dd>\n",
"</dl>\n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import holoviews as hv\n",
"from holoviews import opts\n",
"hv.extension('plotly')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``Tiles`` element represents a so called web mapping tile source usually used for geographic plots, which fetches tiles appropriate to the current zoom level. To declare a ``Tiles`` element simply provide a URL to the tile server. A standard tile server URL has a number of templated variables that describe the location and zoom level. In the most common case of a WMTS tile source, the URL looks like this:\n",
"\n",
" 'https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png'\n",
"\n",
"Here ``{X}``, ``{Y}`` and ``{Z}`` describe the location and zoom level of each tile.\n",
"A simple example of a WMTS tile source is the Wikipedia maps:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hv.Tiles('https://maps.wikimedia.org/osm-intl/{Z}/{X}/{Y}@2x.png', name=\"Wikipedia\").opts(width=600, height=550)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One thing to note about tile sources is that they are always defined in the [pseudo-Mercator projection](https://epsg.io/3857), which means that if you want to overlay any data on top of a tile source the values have to be expressed as eastings and northings. If you have data in another projection, e.g. latitudes and longitudes, it may make sense to use [GeoViews](http://geoviews.org/) for it to handle the projections for you.\n",
"\n",
"Both HoloViews and GeoViews provides a number of tile sources by default, provided by CartoDB, Stamen, OpenStreetMap, Esri and Wikipedia. These can be imported from the ``holoviews.element.tiles`` module and are provided as callable functions that return a ``Tiles`` element:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The full set of predefined tile sources can be accessed on the ``holoviews.element.tiles.tile_sources`` dictionary:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"hv.Layout([ts().relabel(name) for name, ts in hv.element.tiles.tile_sources.items()]).opts(\n",
" opts.Tiles(xaxis=None, yaxis=None, width=225, height=225),\n",
" opts.Layout(hspacing=10, vspacing=40)\n",
").cols(4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For full documentation and the available style and plot options, use ``hv.help(hv.Tiles).``"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Vector Mapbox Tiles\n",
"\n",
"In addition to displaying raster tiles loaded from a tile source URL, the Plotly backend can also display vector tiles provided by Mapbox. A vector tile style is specified using the `mapboxstyle` option, and requires a Mapbox\n",
"access token to be provided as the `accesstoken` option.\n",
"\n",
"```python\n",
"hv.Tiles('').opts(\n",
" mapboxstyle=\"dark\",\n",
" accesstoken=\"pk...\",\n",
" width=600,\n",
" height=600\n",
")\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
23 changes: 23 additions & 0 deletions holoviews/element/tiles.py
Expand Up @@ -8,6 +8,7 @@
from ..core import util
from ..core.dimension import Dimension
from ..core.element import Element2D
from ..util.transform import lon_lat_to_easting_northing, easting_northing_to_lon_lat


class Tiles(Element2D):
Expand Down Expand Up @@ -54,6 +55,28 @@ def range(self, dim, data_range=True, dimension_range=True):
def dimension_values(self, dimension, expanded=True, flat=True):
return np.array([])

@staticmethod
def lon_lat_to_easting_northing(longitude, latitude):
"""
Projects the given longitude, latitude values into Web Mercator
(aka Pseudo-Mercator or EPSG:3857) coordinates.
See docstring for holoviews.util.transform.lon_lat_to_easting_northing
for more information
"""
return lon_lat_to_easting_northing(longitude, latitude)

@staticmethod
def easting_northing_to_lon_lat(easting, northing):
"""
Projects the given easting, northing values into
longitude, latitude coordinates.
See docstring for holoviews.util.transform.easting_northing_to_lon_lat
for more information
"""
return easting_northing_to_lon_lat(easting, northing)


# Mapping between patterns to match specified as tuples and tuples containing attributions
_ATTRIBUTIONS = {
Expand Down
10 changes: 10 additions & 0 deletions holoviews/plotting/plotly/__init__.py
Expand Up @@ -15,6 +15,7 @@
from .renderer import PlotlyRenderer

from .annotation import * # noqa (API import)
from .tiles import * # noqa (API import)
from .element import * # noqa (API import)
from .chart import * # noqa (API import)
from .chart3d import * # noqa (API import)
Expand Down Expand Up @@ -72,6 +73,7 @@

# Annotations
Labels: LabelPlot,
Tiles: TilePlot,

# Shapes
Box: PathShapePlot,
Expand Down Expand Up @@ -102,6 +104,7 @@
plot.padding = 0

dflt_cmap = 'fire'
dflt_shape_line_color = '#2a3f5f' # Line color of default plotly template

point_size = np.sqrt(6) # Matches matplotlib default
Cycle.default_cycles['default_colors'] = ['#30a2da', '#fc4f30', '#e5ae38',
Expand Down Expand Up @@ -129,3 +132,10 @@
# Annotations
options.VSpan = Options('style', fillcolor=Cycle(), opacity=0.5)
options.HSpan = Options('style', fillcolor=Cycle(), opacity=0.5)

# Shapes
options.Rectangles = Options('style', line_color=dflt_shape_line_color)
options.Bounds = Options('style', line_color=dflt_shape_line_color)
options.Path = Options('style', line_color=dflt_shape_line_color)
options.Segments = Options('style', line_color=dflt_shape_line_color)
options.Box = Options('style', line_color=dflt_shape_line_color)
21 changes: 16 additions & 5 deletions holoviews/plotting/plotly/annotation.py
Expand Up @@ -3,6 +3,7 @@
import param

from .chart import ScatterPlot
from ...element import Tiles


class LabelPlot(ScatterPlot):
Expand All @@ -17,12 +18,16 @@ class LabelPlot(ScatterPlot):

_nonvectorized_styles = []

trace_kwargs = {'type': 'scatter', 'mode': 'text'}

_style_key = 'textfont'

def get_data(self, element, ranges, style):
x, y = ('y', 'x') if self.invert_axes else ('x', 'y')
@classmethod
def trace_kwargs(cls, is_geo=False, **kwargs):
if is_geo:
return {'type': 'scattermapbox', 'mode': 'text'}
else:
return {'type': 'scatter', 'mode': 'text'}

def get_data(self, element, ranges, style, is_geo=False, **kwargs):
text_dim = element.vdims[0]
xs = element.dimension_values(0)
if self.xoffset:
Expand All @@ -31,4 +36,10 @@ def get_data(self, element, ranges, style):
if self.yoffset:
ys = ys + self.yoffset
text = [text_dim.pprint_value(v) for v in element.dimension_values(2)]
return [{x: xs, y: ys, 'text': text}]

if is_geo:
lon, lat = Tiles.easting_northing_to_lon_lat(xs, ys)
return [{"lon": lon, "lat": lat, 'text': text}]
else:
x, y = ('y', 'x') if self.invert_axes else ('x', 'y')
return [{x: xs, y: ys, 'text': text}]

0 comments on commit b1e606d

Please sign in to comment.