Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plotly map tiles support #4686

Merged
merged 18 commits into from Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 which describe the location and zoom level. In the most common case that is a WMTS tile source which looks like this:\n",
jbednar marked this conversation as resolved.
Show resolved Hide resolved
"\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 which return a ``Tiles`` element:"
jbednar marked this conversation as resolved.
Show resolved Hide resolved
]
},
{
"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
}
71 changes: 71 additions & 0 deletions holoviews/element/tiles.py
Expand Up @@ -54,6 +54,77 @@ 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.

Longitude and latitude can be provided as scalars, Pandas columns,
or Numpy arrays, and will be returned in the same form. Lists
or tuples will be converted to Numpy arrays.

Args:
longitude
latitude

Returns:
(easting, northing)

Examples:
easting, northing = lon_lat_to_easting_northing(-74,40.71)

easting, northing = lon_lat_to_easting_northing(
np.array([-74]),np.array([40.71])
)

df=pandas.DataFrame(dict(longitude=np.array([-74]),latitude=np.array([40.71])))
df.loc[:, 'longitude'], df.loc[:, 'latitude'] = lon_lat_to_easting_northing(
df.longitude,df.latitude
)
"""
if isinstance(longitude, (list, tuple)):
longitude = np.array(longitude)
if isinstance(latitude, (list, tuple)):
latitude = np.array(latitude)

origin_shift = np.pi * 6378137
easting = longitude * origin_shift / 180.0
with np.errstate(divide='ignore', invalid='ignore'):
northing = np.log(
np.tan((90 + latitude) * np.pi / 360.0)
) * origin_shift / np.pi
return easting, northing

@staticmethod
def easting_northing_to_lon_lat(easting, northing):
"""
Projects the given easting, northing values into
longitude, latitude coordinates.

easting and northing values are assumed to be in Web Mercator
(aka Pseudo-Mercator or EPSG:3857) coordinates.

Args:
easting
northing

Returns:
(longitude, latitude)
"""
if isinstance(easting, (list, tuple)):
easting = np.array(easting)
if isinstance(northing, (list, tuple)):
northing = np.array(northing)

origin_shift = np.pi * 6378137
longitude = easting * 180.0 / origin_shift
with np.errstate(divide='ignore'):
latitude = np.arctan(
np.exp(northing * np.pi / origin_shift)
) * 360.0 / np.pi - 90
return longitude, latitude


# 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}]