Skip to content

Commit

Permalink
Merge 88b4e6a into 4ce7925
Browse files Browse the repository at this point in the history
  • Loading branch information
ajduberstein committed Jun 22, 2019
2 parents 4ce7925 + 88b4e6a commit f2842e8
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 46 deletions.
31 changes: 27 additions & 4 deletions bindings/python/pydeck/pydeck/bindings/deck.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from .json_tools import JSONMixin
import os
import warnings

from .json_tools import JSONMixin
from .layer import Layer
from .view import View
from .view_state import ViewState
from ..io.html import deck_to_html
from ..widget import DeckGLWidget


Expand All @@ -13,10 +16,13 @@ def __init__(
layers=[],
views=[View()],
map_style='mapbox://styles/mapbox/dark-v9',
mapbox_key=None,
initial_view_state=ViewState(),
):
"""Constructor for a Deck object, similar to the `Deck`_ class from deck.gl
Requires a Mapbox API token to display a basemap, see notes below.
Parameters
----------
layers : :obj:`pydeck.Layer` or :obj:`list` of :obj:`pydeck.Layer`, default []
Expand All @@ -28,7 +34,9 @@ def __init__(
initial_view_state : pydeck.ViewState, default pydeck.ViewState()
Initial camera angle relative to the map, defaults to a fully zoomed out 0, 0-centered map
To compute a viewport from data, see `pydeck.data_utils.autocompute_viewport`
mapbox_key : str, default None
Read on inititialization from the MAPBOX_API_KEY environment variable. Defaults to None if not set.
See https://docs.mapbox.com/help/how-mapbox-works/access-tokens/#mapbox-account-dashboard
.. _Deck:
https://deck.gl/#/documentation/deckgl-api-reference/deck
Expand All @@ -43,6 +51,11 @@ def __init__(
# Use passed view state
self.initial_view_state = initial_view_state
self.deck_widget = DeckGLWidget()
self.mapbox_key = mapbox_key or os.getenv('MAPBOX_API_KEY')
self.deck_widget.mapbox_key = self.mapbox_key
if not self.mapbox_key:
warnings.warn(
'Mapbox API key is not set. This may impact available features of pydeck.', UserWarning)

def __add__(self, obj):
"""
Expand Down Expand Up @@ -84,5 +97,15 @@ def update(self):
"""
self.deck_widget.json_input = self.to_json()

def to_html(self, filename=None):
pass
def to_html(self, filename=None, open_browser=False):
"""Writes a file and loads it to an iframe, if in a Jupyter notebook
Otherwise writes a file and optionally opens it in a web browser
Parameters
----------
filename : str, default None
Name of the file. If no name is provided, a randomly named file will be written locally.
open_browser : bool, default False
Whether a browser window will open or not after write
"""
deck_to_html(self.to_json(), self.mapbox_key)
11 changes: 9 additions & 2 deletions bindings/python/pydeck/pydeck/bindings/json_tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""
Support serializing objects into JSON
"""
import json

# Ignore deck_widget and mapbox_key attributes
IGNORE_KEYS = ['mapbox_key', 'deck_widget']


def to_camel_case(snake_case):
"""Makes a snake case string into a camel case one
Expand Down Expand Up @@ -43,8 +49,9 @@ def default_serialize(o, remap_function=lower_camel_case_keys):
"""Default method for rendering JSON from a dictionary"""
attrs = vars(o)
attrs = {k: v for k, v in attrs.items() if v is not None}
if attrs.get('deck_widget'):
del attrs['deck_widget']
for ignore_attr in IGNORE_KEYS:
if attrs.get(ignore_attr):
del attrs[ignore_attr]
if remap_function:
remap_function(attrs)
return attrs
Expand Down
19 changes: 9 additions & 10 deletions bindings/python/pydeck/pydeck/bindings/layer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import uuid

from .colors import BLACK_RGBA, COLOR_BREWER
from ..data_utils import is_pandas_df
from .json_tools import JSONMixin

Expand All @@ -25,8 +24,8 @@ def __init__(
id=None,
get_position='-',
color_range=None,
opacity=1,
radius=1000,
opacity=None,
radius=None,
get_radius=None,
light_settings=None,
coverage=None,
Expand All @@ -36,7 +35,7 @@ def __init__(
upper_percentile=None,
get_fill_color=None,
get_color=None,
stroked=False,
stroked=None,
filled=None,
radius_scale=None,
radius_min_pixels=None,
Expand Down Expand Up @@ -93,13 +92,13 @@ def __init__(
Boolean to determine if layer rises from map. Defaults to `True` for aggregate layers.
get_fill_color : :obj:`list` of `float` or str
*Not valid on all deck.gl layers.* Specifies fill color as an RGBA value or field name in the data.
stroked : bool, default False
stroked : bool, default None
*Valid on ScatterplotLayer.* Draw online the outline of a point
filled : bool, default True
*Valid on ScatterplotLayer.* Draw the filled area of a point
radius_scale : int, default 1
Global radius multiplier for all points
radius_min_pixels : int, default 1
radius_min_pixels : int, default None
Minimum radius in pixels, prevents stroke from getting too small when zoomed out
radius_max_pixels : int, default 100
Maximum radius in pixels, prevents stroke from getting too big when zoomed out
Expand All @@ -115,9 +114,9 @@ def __init__(
self.get_position = get_position
self.opacity = opacity
self.get_radius = get_radius or radius
self.get_color = get_color or BLACK_RGBA
self.get_fill_color = get_fill_color or BLACK_RGBA
self.get_line_color = get_line_color or BLACK_RGBA
self.get_color = get_color
self.get_fill_color = get_fill_color
self.get_line_color = get_line_color
self.get_line_width = get_line_width
# ScatterplotLayer
self.radius_scale = radius_scale
Expand Down Expand Up @@ -151,7 +150,7 @@ def __init__(
if is_aggregate_layer(type):
self.radius = radius
self.upper_percentile = upper_percentile
self.color_range = color_range or COLOR_BREWER['BuRd']
self.color_range = color_range
self.light_settings = light_settings
self.extruded = extruded if extruded is not None else True

Expand Down
Empty file.
58 changes: 58 additions & 0 deletions bindings/python/pydeck/pydeck/io/html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import time
import jinja2
import os
import os.path
import tempfile

import webbrowser
from IPython.display import IFrame


TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), './templates/')
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATES_PATH),
trim_blocks=True)

def render_json_to_html(json_input, mapbox_api_key=None):
js = j2_env.get_template('index.j2')
html = js.render(
mapbox_api_key=mapbox_api_key,
json_input=json_input)
return html


def display_html(filename=None):
"""Converts HTML into a temporary file and open it in the system browser or IPython/Jupyter Notebook IFrame."""
try:
return IFrame(filename, height=500, width=500)
except Exception:
try:
url = 'file://{}'.format(filename)
# Hack to prevent blank page
time.sleep(0.5)
webbrowser.open(url)
except Exception:
raise


def open_named_or_temporary_file(filename=''):
if filename:
filename = add_html_extension(filename)
return open(filename, 'w+')
return tempfile.NamedTemporaryFile(
suffix='.html', dir=os.cwd(), delete=False)


def add_html_extension(fname):
if fname.endswith('.html') or fname.endswith('.htm'):
return fname
if fname is None:
raise Exception("File has no name")
return fname + '.html'


def deck_to_html(deck_json, mapbox_key, filename=None):
html = render_json_to_html(deck_json, mapbox_key)
f = open_named_or_temporary_file(filename)
f.write(html)
f.close()
display_html(f.name)
4 changes: 4 additions & 0 deletions bindings/python/pydeck/pydeck/io/templates/imports.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script src='https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js'></script>
<link rel="stylesheet" href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" />
23 changes: 23 additions & 0 deletions bindings/python/pydeck/pydeck/io/templates/index.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>pydeck</title>
{% include 'imports.j2' %}
<style>
{% include 'style.j2' %}
</style>
</head>
<body>
<div id="deck-container">
<div id="deck-map-wrapper">
<canvas id="deck-map-container">
</canvas>
<div id="map">
</div>
</div>
</div>
</body>
<script>
{% include 'js.j2' %}
</script>
</html>
76 changes: 76 additions & 0 deletions bindings/python/pydeck/pydeck/io/templates/js.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
requirejs.config({
paths: {
deck: 'https://unpkg.com/@deck.gl/core@7.1.0/dist.min',
deckjson: 'https://unpkg.com/@deck.gl/json@7.1.0/dist.min',
decklayers: 'https://unpkg.com/@deck.gl/layers@7.1.0/dist.min',
deckaggregate: 'https://unpkg.com/@deck.gl/aggregation-layers@7.1.0/dist.min',
mapboxgl: 'https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl',
luma: 'https://unpkg.com/@luma.gl/core@7.1.0/dist/dist.min'
},
shim : {
'deckaggregate': ['deck', 'decklayers', 'luma']
}
});

let config, mapLayer, jsonInput, jsonConverter;

jsonInput = JSON.parse(JSON.stringify({{json_input}}));

function setMapProps(map, props) {
if ('viewState' in props && props.viewState.longitude && props.viewState.latitude) {
const {viewState} = props;
map.jumpTo({
center: [viewState.longitude, viewState.latitude],
zoom: Number.isFinite(viewState.zoom) ? viewState.zoom : 10,
bearing: viewState.bearing || 0,
pitch: viewState.pitch || 0
});
}

if (props.map && 'style' in props.map) {
if (props.map.style !== map.deckStyle) {
map.setStyle(props.map.style);
map.deckStyle = props.map.style;
}
}
}


function onViewportChange({viewState}) {
config.setProps({viewState});
setMapProps(mapLayer, {viewState});
}


function valueChanged() {
const jsonProps = jsonConverter.convertJsonToDeckProps(jsonInput);
config.setProps(jsonProps);
setMapProps(mapLayer, config.props);
}


function init(luma, deckJson, deckgl, deckglLayers, mapboxgl, deckAggregationLayers) {
mapboxgl.accessToken = '{{mapbox_api_key}}',
mapLayer = new mapboxgl.Map({
container: 'map',
interactive: false,
style: null
});

jsonConverter = new deckJson._JSONConverter({
configuration: {
layers: {...deckglLayers, ...deckAggregationLayers}
}
});

config = new deckgl.Deck({
canvas: 'deck-map-container',
height: '100%',
width: '100%',
onLoad: valueChanged.bind(this),
views: [new deckgl.MapView()],
onViewStateChange: onViewportChange.bind(this)
});
}

requirejs(['luma', 'deckjson', 'deck', 'decklayers', 'mapboxgl', 'deckaggregate'], init);
33 changes: 33 additions & 0 deletions bindings/python/pydeck/pydeck/io/templates/style.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
body {
margin: 0;
padding: 0;
overflow: hidden;
}

#deck-map-container {
width: 100%;
height: 100%;
background-color: black;
}

#map {
pointer-events: none;
height: 100%;
width: 100%;
position: absolute;
z-index: 1;
}

#deckgl-overlay {
z-index: 2;
}

#deck-map-wrapper {
width: 100%;
height: 100%;
}

#deck-container {
width: 100vw;
height: 100vh;
}
11 changes: 1 addition & 10 deletions bindings/python/pydeck/pydeck/widget/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# coding: utf-8

from __future__ import unicode_literals
import os
import warnings

import ipywidgets as widgets
from traitlets import Int, Unicode
Expand All @@ -19,7 +17,6 @@ class DeckGLWidget(widgets.DOMWidget):
Attributes
----------
mapbox_key (str) - Read on inititialization from the MAPBOX_API_KEY environment variable. Defaults to None if not set.
json_input (str) - JSON as a string meant for reading into deck.gl JSON API
"""
_model_name = Unicode('DeckGLModel').tag(sync=True)
Expand All @@ -28,13 +25,7 @@ class DeckGLWidget(widgets.DOMWidget):
_view_name = Unicode('DeckGLView').tag(sync=True)
_view_module = Unicode(module_name).tag(sync=True)
_view_module_version = Unicode(module_version).tag(sync=True)
mapbox_key = Unicode(os.getenv('MAPBOX_API_KEY'), allow_none=True).tag(sync=True)
mapbox_key = Unicode('', allow_none=True).tag(sync=True)
json_input = Unicode('').tag(sync=True)
height = Int(500).tag(sync=True)
width = Int(500).tag(sync=True)

def __init__(self, suppress_warning=False):
super(DeckGLWidget, self).__init__()
if not os.environ.get('MAPBOX_API_KEY') and not suppress_warning:
warnings.warn('MAPBOX_API_KEY is not set. This may impact available features of the pydeck library.', UserWarning)

Empty file.

0 comments on commit f2842e8

Please sign in to comment.