Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions gmaps/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .maps import (
Map, InitialViewport, GMapsWidgetMixin, map_params_doc_snippets
)
from .geotraitlets import MapType, MouseHandling, Tilt
from .geotraitlets import MapType, MouseHandling, Tilt, StylesString
from .toolbar import Toolbar
from .errors_box import ErrorsBox
from ._docutils import doc_subst
Expand Down Expand Up @@ -45,6 +45,8 @@ class Figure(GMapsWidgetMixin, widgets.DOMWidget):
layout = widgets.trait_types.InstanceDict(FigureLayout).tag(
sync=True, **widgets.widget_serialization)

styles = StylesString('{}')

def __init__(self, *args, **kwargs):
if kwargs.get('layout') is None:
kwargs['layout'] = self._default_layout()
Expand All @@ -58,6 +60,9 @@ def __init__(self, *args, **kwargs):
self._map.mouse_handling = self.mouse_handling
link((self._map, 'mouse_handling'), (self, 'mouse_handling'))

self._map.styles = self.styles
link((self._map, 'styles'), (self, 'styles'))

@default('layout')
def _default_layout(self):
return FigureLayout()
Expand Down Expand Up @@ -116,7 +121,7 @@ def add_layer(self, layer):
def figure(
display_toolbar=True, display_errors=True, zoom_level=None, tilt=45,
center=None, layout=None, map_type='ROADMAP',
mouse_handling='COOPERATIVE'):
mouse_handling='COOPERATIVE', styles='{}'):
"""
Create a gmaps figure

Expand Down Expand Up @@ -157,6 +162,8 @@ def figure(

{mouse_handling}

{styles}

:param layout:
Control the layout of the figure, e.g. its width, height, border etc.
For instance, passing ``layout={{'width': '400px', 'height': '300px'}}``
Expand Down Expand Up @@ -194,6 +201,22 @@ def figure(

>>> fig = gmaps.figure(map_type='HYBRID')

To have a map with custom styles:

styles = '''[{{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{{
"visibility": "on"
}},
{{
"color": "#000000"
}}
]
}}]'''

>>> fig = gmaps.figure(styles=styles)
""" # noqa: E501
if zoom_level is not None or center is not None:
if zoom_level is None or center is None:
Expand All @@ -218,5 +241,5 @@ def figure(
fig = Figure(
_map=_map, _toolbar=_toolbar, _errors_box=_errors_box,
layout=layout, map_type=map_type, tilt=tilt,
mouse_handling=mouse_handling)
mouse_handling=mouse_handling, styles=styles)
return fig
21 changes: 21 additions & 0 deletions gmaps/geotraitlets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

import re

import json

import traitlets

from .locations import locations_to_list
Expand Down Expand Up @@ -323,3 +325,22 @@ def _validate_longitude(longitude):
'Longitudes must lie between '
'-180 and 180.'.format(longitude)
)


class StylesString(traitlets.Unicode):
"""
A string holding a google maps styles as JSON formatted string

Using `this <https://developers.google.com/maps/documentation/javascript/styling>` page # noqa: E501
for reference.
"""
info_text = 'JSON formatted styles string'
default_value = traitlets.Undefined

def validate(self, obj, value):
try:
value_as_string = super(StylesString, self).validate(obj, value)
json.loads(value_as_string)
return value_as_string
except TypeError:
return self.error(obj, value)
51 changes: 44 additions & 7 deletions gmaps/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
observe, Dict, HasTraits, Enum, Union)

from .bounds import merge_longitude_bounds
from .geotraitlets import Point, ZoomLevel, MapType, MouseHandling, Tilt
from .geotraitlets import (Point, ZoomLevel, MapType,
MouseHandling, Tilt, StylesString)
from ._docutils import doc_subst
from ._version import CLIENT_VERSION

Expand Down Expand Up @@ -38,6 +39,16 @@
"""


map_params_doc_snippets['styles'] = """
:param styles:
A JSON formatted Google Maps styles string.

Using `this <https://developers.google.com/maps/documentation/javascript/styling>` page # noqa: E501
for reference.
:type styles: str, optional
"""


def configure(api_key=None):
"""
Configure access to the GoogleMaps API.
Expand Down Expand Up @@ -73,10 +84,11 @@ class InitialViewport(Union):
"""
Traitlet defining the initial viewport for a map.
"""

def __init__(self, **metadata):
trait_types = [
Enum(['DATA_BOUNDS']),
Instance(_ZoomCenter)
Enum(['DATA_BOUNDS']),
Instance(_ZoomCenter)
]
super(InitialViewport, self).__init__(trait_types, **metadata)

Expand Down Expand Up @@ -135,9 +147,9 @@ def _serialize_viewport(viewport, manager):
else:
try:
payload = {
'type': 'ZOOM_CENTER',
'center': viewport.center,
'zoom_level': viewport.zoom_level
'type': 'ZOOM_CENTER',
'center': viewport.center,
'zoom_level': viewport.zoom_level
}
except AttributeError:
raise ValueError('viewport')
Expand Down Expand Up @@ -166,6 +178,8 @@ class Map(ConfigurationMixin, GMapsWidgetMixin, widgets.DOMWidget):

{mouse_handling}

{styles}

:Examples:

>>> m = gmaps.Map()
Expand All @@ -185,18 +199,41 @@ class Map(ConfigurationMixin, GMapsWidgetMixin, widgets.DOMWidget):
You can also change this dynamically:

>>> m.map_type = 'TERRAIN'

To have a map with custom styles:

styles = '''[{{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{{
"visibility": "on"
}},
{{
"color": "#000000"
}}
]
}}]'''

>>> m = gmaps.Map(styles=styles)

You can also change this dynamically:

>>> m.styles = 'styles'
"""
_view_name = Unicode('PlainmapView').tag(sync=True)
_model_name = Unicode('PlainmapModel').tag(sync=True)
layers = List(trait=Instance(widgets.Widget)).tag(
sync=True, **widgets.widget_serialization)
data_bounds = List(DEFAULT_BOUNDS).tag(sync=True)
initial_viewport = InitialViewport(default_value='DATA_BOUNDS').tag(
sync=True, to_json=_serialize_viewport)
sync=True, to_json=_serialize_viewport)
map_type = MapType('ROADMAP').tag(sync=True)
tilt = Tilt().tag(sync=True)
mouse_handling = MouseHandling('COOPERATIVE').tag(sync=True)

styles = StylesString('{}').tag(sync=True)

def add_layer(self, layer):
self.layers = tuple([l for l in self.layers] + [layer])

Expand Down
18 changes: 18 additions & 0 deletions gmaps/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from ..toolbar import Toolbar
from ..errors_box import ErrorsBox

STYLES = '[{}]'
STYLES_1 = '[{}, {}]'


class TestFigure(unittest.TestCase):

Expand Down Expand Up @@ -63,6 +66,16 @@ def test_catch_mouse_handling_change_in_map(self):
fig._map.mouse_handling = 'NONE'
assert fig.mouse_handling == 'NONE'

def test_proxy_styles(self):
fig = Figure(_map=Map(), styles=STYLES)
assert fig.styles == STYLES
assert fig._map.styles == STYLES

def test_proxy_styles_change(self):
fig = Figure(_map=Map(), styles=STYLES)
fig.styles = STYLES_1
assert fig._map.styles == STYLES_1


class TestFigureFactory(unittest.TestCase):

Expand All @@ -72,6 +85,7 @@ def test_defaults(self):
assert fig._errors_box is not None
assert fig.map_type == 'ROADMAP'
assert fig.mouse_handling == 'COOPERATIVE'
assert fig.styles == '{}'
map_ = fig._map
assert map_ is not None
assert map_.initial_viewport == 'DATA_BOUNDS'
Expand Down Expand Up @@ -142,3 +156,7 @@ def test_default_tilt(self):
def test_custom_mouse_handling(self):
fig = figure(mouse_handling='NONE')
assert fig.mouse_handling == 'NONE'

def test_custom_styles(self):
fig = figure(styles=STYLES_1)
assert fig.styles == STYLES_1
29 changes: 29 additions & 0 deletions gmaps/tests/test_geotraitlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,32 @@ def test_over_max(self):
def test_wrong_type(self):
with self.assertRaises(traitlets.TraitError):
self.A(x='not-a-float')


STYLES = '''[{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"visibility": "on"
},
{
"color": "#000000"
}
]
}]'''


class StylesString(unittest.TestCase):
def setUp(self):
class A(traitlets.HasTraits):
x = geotraitlets.StylesString()
self.A = A

def test_accept_styles_json_string(self):
a = self.A(x=STYLES)
assert a.x == STYLES

def test_reject_invalid_json_string(self):
with self.assertRaises(ValueError):
self.A(x='{')
7 changes: 6 additions & 1 deletion gmaps/tests/test_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from .. import maps, heatmap_layer

STYLES = '[{}]'


class Map(unittest.TestCase):

Expand All @@ -15,6 +17,7 @@ def test_defaults(self):
assert state['mouse_handling'] == 'COOPERATIVE'
assert state['initial_viewport'] == {'type': 'DATA_BOUNDS'}
assert state['layers'] == []
assert state['styles'] == '{}'

def test_custom_traits(self):
test_layer = heatmap_layer([(1.0, 2.0), (3.0, 4.0)])
Expand All @@ -23,7 +26,8 @@ def test_custom_traits(self):
mouse_handling='NONE',
initial_viewport=maps.InitialViewport.from_zoom_center(
10, (5.0, 10.0)),
layers=[test_layer]
layers=[test_layer],
styles=STYLES
)
state = m.get_state()
assert state['map_type'] == 'HYBRID'
Expand All @@ -34,6 +38,7 @@ def test_custom_traits(self):
}
assert state['layers'] == ['IPY_MODEL_' + test_layer.model_id]
assert state['mouse_handling'] == 'NONE'
assert state['styles'] == STYLES

def test_change_layer(self):
test_layer = heatmap_layer([(1.0, 2.0), (3.0, 4.0)])
Expand Down
8 changes: 8 additions & 0 deletions js/src/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,12 @@ export class PlainmapView extends ConfigurationMixin(widgets.DOMWidgetView) {
}

readOptions(google) {
const styles = this.model.get('styles');
const options = {
mapTypeId: stringToMapType(google, this.model.get('map_type')),
gestureHandling: this.model.get('mouse_handling').toLowerCase(),
tilt: this.model.get('tilt'),
styles: styles ? JSON.parse(styles) : '',
};
return options;
}
Expand All @@ -137,6 +139,11 @@ export class PlainmapView extends ConfigurationMixin(widgets.DOMWidgetView) {
.toLowerCase();
this.setMapOptions({gestureHandling});
});

this.model.on('change:styles', () => {
const styles = JSON.parse(this.model.get('styles'));
this.setMapOptions({styles});
});
}

_viewEvents(google) {
Expand Down Expand Up @@ -240,6 +247,7 @@ export class PlainmapModel extends widgets.DOMWidgetModel {
initial_viewport: {type: DATA_BOUNDS},
map_type: 'ROADMAP',
mouse_handling: 'COOPERATIVE',
styles: '{}',
};
}

Expand Down