diff --git a/docs-markdown/utils.md b/docs-markdown/utils.md index 8b73d62..d8f61cb 100644 --- a/docs-markdown/utils.md +++ b/docs-markdown/utils.md @@ -92,6 +92,20 @@ Parameter | Description --|-- breaks | List of float values + +## create_numeric_stops +Convert a Python list of a data domain (such as `[0, 1, 5, 100, 10000]`) into a generic numeric ramp between a minimum and maximum value, as in for a heatmap, choropleth 3-D extrusions, etc. + +### Params +**create_numeric_stops**(_breaks, min_value, max_value_) + +Parameter | Description +--|-- +breaks | List of float values +min_value | Minimum ramp value +max_value | Maximum ramp value + + ## create_color_stops Convert a Python list of a data domain (such as `[0, 1, 5, 100, 10000]`) into color ramp stops. Color ramps can be from colorBrewer, or a custom list of color values. @@ -181,3 +195,25 @@ match_color_stops = [ ] color = color_map('California', match_color_stops, default_color='grey)') ``` + + +## height_map +Return a height value (in meters) interpolated from given height_stops; for use with vector-based visualizations using fill-extrusion layers. + +### Params +**height_map**(_lookup, height_stops, default_height=10.0_) + +Parameter | Description +--|-- +lookup | value is numeric for interpolated heights or string for categorical height stops +height_stops | height ramp stops generated from `create_numeric_stops`, or custom list of numeric or categorical stops with paired heights +default_height | height, in meters, for display of 3-D extrusion on map + +### Usage +```python +from mapboxgl.utils import create_numeric_stops, height_map + +# interpolate height +height_stops = create_numeric_stops([0, 50, 100, 500, 1500], 0, 150000) +height = height_map(117, height_stops) +``` diff --git a/docs-markdown/viz.md b/docs-markdown/viz.md index 388d9da..25133b5 100644 --- a/docs-markdown/viz.md +++ b/docs-markdown/viz.md @@ -21,7 +21,7 @@ The `MapViz` class is the parent class of the various `mapboxgl-jupyter` visualizations. You can use this class to set default values for all visualizations rather than calling them directly from the other visualization objects. ### Params -**MapViz**(_data, access_token=None, center=(0, 0), below_layer='', opacity=1, div_id='map', height='500px', style='mapbox://styles/mapbox/light-v9?optimize=true', width='100%', zoom=0, min_zoom=0, max_zoom=24_) +**MapViz**(_data, access_token=None, center=(0, 0), below_layer='', opacity=1, div_id='map', height='500px', style='mapbox://styles/mapbox/light-v9?optimize=true', width='100%', zoom=0, min_zoom=0, max_zoom=24, pitch=0, bearing=0_) Parameter | Description --|-- @@ -34,6 +34,8 @@ width | The CSS width of the HTML div id in % or pixels. height | The CSS height of the HTML map div in % or pixels. zoom | starting zoom level for map opacity | opacity of map data layer +pitch | starting pitch (in degrees) for map +bearing | starting bearing (in degrees) for map ### Methods **as_iframe**(_self, html_data_) @@ -320,7 +322,7 @@ viz.show() The `ChoroplethViz` object handles the creation of a choropleth map and inherits from the `MapViz` class. It applies a thematic map style to polygon features with color shading in proportion to the intensity of the data being displayed. Choropleth polygons can be initialized with geojson source or vector source styled using the data-join technique. ### Params -**ChoroplethViz**(_data, vector_url=None, vector_layer_name=None, vector_join_property=None, data_join_property=None, # vector only label_property=None, color_property=None, color_stops=None, color_default='grey', color_function_type='interpolate', line_color='white', line_stroke='solid', line_width=1, *args, **kwargs_) +**ChoroplethViz**(_data, vector_url=None, vector_layer_name=None, vector_join_property=None, data_join_property=None, # vector only label_property=None, color_property=None, color_stops=None, color_default='grey', color_function_type='interpolate', line_color='white', line_stroke='solid', line_width=1, height_property=None, height_stops=None, height_default=0.0, height_function_type='interpolate', *args, **kwargs_) Parameter | Description | Example --|--|-- @@ -337,6 +339,10 @@ color_function_type | property to determine type of expression used by Mapbox to line_color | property to determine choropleth border line color | "#FFFFFF" line_stroke | property to determine choropleth border line stroke (one of solid (-), dashed (--), dotted (:), dash dot (-.)) | "solid" or "-" line_width | property to determine choropleth border line width | 1 +height_property | feature property for determining polygon height in 3D extruded choropleth map | "density" +height_stops | property for determining 3D extrusion height | [[0, 0], [500, 50000], [1500, 150000]] +height_default | default height (in meters) for 3D extruded polygons on map | 1500.0 +height_function_type | roperty to determine `type` used by Mapbox to assign height | "interpolate" [View options](https://github.com/mapbox/mapboxgl-jupyter/blob/master/docs-markdown/viz.md#params) diff --git a/examples/notebooks/choropleth-viz-example.ipynb b/examples/notebooks/choropleth-viz-example.ipynb index d79f763..c880bc5 100644 --- a/examples/notebooks/choropleth-viz-example.ipynb +++ b/examples/notebooks/choropleth-viz-example.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -32,308 +32,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# create choropleth from polygon features stored as GeoJSON\n", "viz = ChoroplethViz('https://raw.githubusercontent.com/mapbox/mapboxgl-jupyter/master/examples/data/us-states.geojson', \n", @@ -351,6 +54,32 @@ "viz.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add 3-D Extrusion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# adjust view angle\n", + "viz.bearing = -15\n", + "viz.pitch = 45\n", + "\n", + "# add extrusion to viz using interpolation keyed on density in GeoJSON features\n", + "viz.height_property = 'density'\n", + "viz.height_stops = create_numeric_stops([0, 50, 100, 500, 1500, 5000], 0, 500000)\n", + "viz.height_function_type = 'interpolate'\n", + "\n", + "# render again\n", + "viz.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -360,306 +89,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "match_color_stops = [\n", " ['Massachusetts', 'rgb(46,204,113)'],\n", @@ -691,334 +123,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# must be JSON object (need to extend to use referenced JSON file)\n", "data = [{\"id\": \"01\", \"name\": \"Alabama\", \"density\": 94.65}, {\"id\": \"02\", \"name\": \"Alaska\", \"density\": 1.264}, {\"id\": \"04\", \"name\": \"Arizona\", \"density\": 57.05}, {\"id\": \"05\", \"name\": \"Arkansas\", \"density\": 56.43}, {\"id\": \"06\", \"name\": \"California\", \"density\": 241.7}, {\"id\": \"08\", \"name\": \"Colorado\", \"density\": 49.33}, {\"id\": \"09\", \"name\": \"Connecticut\", \"density\": 739.1}, {\"id\": \"10\", \"name\": \"Delaware\", \"density\": 464.3}, {\"id\": \"11\", \"name\": \"District of Columbia\", \"density\": 10065}, {\"id\": \"12\", \"name\": \"Florida\", \"density\": 353.4}, {\"id\": \"13\", \"name\": \"Georgia\", \"density\": 169.5}, {\"id\": \"15\", \"name\": \"Hawaii\", \"density\": 214.1}, {\"id\": \"16\", \"name\": \"Idaho\", \"density\": 19.15}, {\"id\": \"17\", \"name\": \"Illinois\", \"density\": 231.5}, {\"id\": \"18\", \"name\": \"Indiana\", \"density\": 181.7}, {\"id\": \"19\", \"name\": \"Iowa\", \"density\": 54.81}, {\"id\": \"20\", \"name\": \"Kansas\", \"density\": 35.09}, {\"id\": \"21\", \"name\": \"Kentucky\", \"density\": 110}, {\"id\": \"22\", \"name\": \"Louisiana\", \"density\": 105}, {\"id\": \"23\", \"name\": \"Maine\", \"density\": 43.04}, {\"id\": \"24\", \"name\": \"Maryland\", \"density\": 596.3}, {\"id\": \"25\", \"name\": \"Massachusetts\", \"density\": 840.2}, {\"id\": \"26\", \"name\": \"Michigan\", \"density\": 173.9}, {\"id\": \"27\", \"name\": \"Minnesota\", \"density\": 67.14}, {\"id\": \"28\", \"name\": \"Mississippi\", \"density\": 63.5}, {\"id\": \"29\", \"name\": \"Missouri\", \"density\": 87.26}, {\"id\": \"30\", \"name\": \"Montana\", \"density\": 6.858}, {\"id\": \"31\", \"name\": \"Nebraska\", \"density\": 23.97}, {\"id\": \"32\", \"name\": \"Nevada\", \"density\": 24.8}, {\"id\": \"33\", \"name\": \"New Hampshire\", \"density\": 147}, {\"id\": \"34\", \"name\": \"New Jersey\", \"density\": 1189}, {\"id\": \"35\", \"name\": \"New Mexico\", \"density\": 17.16}, {\"id\": \"36\", \"name\": \"New York\", \"density\": 412.3}, {\"id\": \"37\", \"name\": \"North Carolina\", \"density\": 198.2}, {\"id\": \"38\", \"name\": \"North Dakota\", \"density\": 9.916}, {\"id\": \"39\", \"name\": \"Ohio\", \"density\": 281.9}, {\"id\": \"40\", \"name\": \"Oklahoma\", \"density\": 55.22}, {\"id\": \"41\", \"name\": \"Oregon\", \"density\": 40.33}, {\"id\": \"42\", \"name\": \"Pennsylvania\", \"density\": 284.3}, {\"id\": \"44\", \"name\": \"Rhode Island\", \"density\": 1006}, {\"id\": \"45\", \"name\": \"South Carolina\", \"density\": 155.4}, {\"id\": \"46\", \"name\": \"South Dakota\", \"density\": 98.07}, {\"id\": \"47\", \"name\": \"Tennessee\", \"density\": 88.08}, {\"id\": \"48\", \"name\": \"Texas\", \"density\": 98.07}, {\"id\": \"49\", \"name\": \"Utah\", \"density\": 34.3}, {\"id\": \"50\", \"name\": \"Vermont\", \"density\": 67.73}, {\"id\": \"51\", \"name\": \"Virginia\", \"density\": 204.5}, {\"id\": \"53\", \"name\": \"Washington\", \"density\": 102.6}, {\"id\": \"54\", \"name\": \"West Virginia\", \"density\": 77.06}, {\"id\": \"55\", \"name\": \"Wisconsin\", \"density\": 105.2}, {\"id\": \"56\", \"name\": \"Wyoming\", \"density\": 5.851}, {\"id\": \"72\", \"name\": \"Puerto Rico\", \"density\": 1082}]\n", @@ -1050,334 +157,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# must be JSON object (need to extend to use referenced JSON file)\n", "data = [{\"id\": \"01\", \"name\": \"Alabama\", \"density\": 94.65}, {\"id\": \"02\", \"name\": \"Alaska\", \"density\": 1.264}, {\"id\": \"04\", \"name\": \"Arizona\", \"density\": 57.05}, {\"id\": \"05\", \"name\": \"Arkansas\", \"density\": 56.43}, {\"id\": \"06\", \"name\": \"California\", \"density\": 241.7}, {\"id\": \"08\", \"name\": \"Colorado\", \"density\": 49.33}, {\"id\": \"09\", \"name\": \"Connecticut\", \"density\": 739.1}, {\"id\": \"10\", \"name\": \"Delaware\", \"density\": 464.3}, {\"id\": \"11\", \"name\": \"District of Columbia\", \"density\": 10065}, {\"id\": \"12\", \"name\": \"Florida\", \"density\": 353.4}, {\"id\": \"13\", \"name\": \"Georgia\", \"density\": 169.5}, {\"id\": \"15\", \"name\": \"Hawaii\", \"density\": 214.1}, {\"id\": \"16\", \"name\": \"Idaho\", \"density\": 19.15}, {\"id\": \"17\", \"name\": \"Illinois\", \"density\": 231.5}, {\"id\": \"18\", \"name\": \"Indiana\", \"density\": 181.7}, {\"id\": \"19\", \"name\": \"Iowa\", \"density\": 54.81}, {\"id\": \"20\", \"name\": \"Kansas\", \"density\": 35.09}, {\"id\": \"21\", \"name\": \"Kentucky\", \"density\": 110}, {\"id\": \"22\", \"name\": \"Louisiana\", \"density\": 105}, {\"id\": \"23\", \"name\": \"Maine\", \"density\": 43.04}, {\"id\": \"24\", \"name\": \"Maryland\", \"density\": 596.3}, {\"id\": \"25\", \"name\": \"Massachusetts\", \"density\": 840.2}, {\"id\": \"26\", \"name\": \"Michigan\", \"density\": 173.9}, {\"id\": \"27\", \"name\": \"Minnesota\", \"density\": 67.14}, {\"id\": \"28\", \"name\": \"Mississippi\", \"density\": 63.5}, {\"id\": \"29\", \"name\": \"Missouri\", \"density\": 87.26}, {\"id\": \"30\", \"name\": \"Montana\", \"density\": 6.858}, {\"id\": \"31\", \"name\": \"Nebraska\", \"density\": 23.97}, {\"id\": \"32\", \"name\": \"Nevada\", \"density\": 24.8}, {\"id\": \"33\", \"name\": \"New Hampshire\", \"density\": 147}, {\"id\": \"34\", \"name\": \"New Jersey\", \"density\": 1189}, {\"id\": \"35\", \"name\": \"New Mexico\", \"density\": 17.16}, {\"id\": \"36\", \"name\": \"New York\", \"density\": 412.3}, {\"id\": \"37\", \"name\": \"North Carolina\", \"density\": 198.2}, {\"id\": \"38\", \"name\": \"North Dakota\", \"density\": 9.916}, {\"id\": \"39\", \"name\": \"Ohio\", \"density\": 281.9}, {\"id\": \"40\", \"name\": \"Oklahoma\", \"density\": 55.22}, {\"id\": \"41\", \"name\": \"Oregon\", \"density\": 40.33}, {\"id\": \"42\", \"name\": \"Pennsylvania\", \"density\": 284.3}, {\"id\": \"44\", \"name\": \"Rhode Island\", \"density\": 1006}, {\"id\": \"45\", \"name\": \"South Carolina\", \"density\": 155.4}, {\"id\": \"46\", \"name\": \"South Dakota\", \"density\": 98.07}, {\"id\": \"47\", \"name\": \"Tennessee\", \"density\": 88.08}, {\"id\": \"48\", \"name\": \"Texas\", \"density\": 98.07}, {\"id\": \"49\", \"name\": \"Utah\", \"density\": 34.3}, {\"id\": \"50\", \"name\": \"Vermont\", \"density\": 67.73}, {\"id\": \"51\", \"name\": \"Virginia\", \"density\": 204.5}, {\"id\": \"53\", \"name\": \"Washington\", \"density\": 102.6}, {\"id\": \"54\", \"name\": \"West Virginia\", \"density\": 77.06}, {\"id\": \"55\", \"name\": \"Wisconsin\", \"density\": 105.2}, {\"id\": \"56\", \"name\": \"Wyoming\", \"density\": 5.851}, {\"id\": \"72\", \"name\": \"Puerto Rico\", \"density\": 1082}]\n", @@ -1405,6 +187,32 @@ "viz.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add 3-D Extrusion to Vector Choropleth Map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# adjust view angle\n", + "viz.bearing = -15\n", + "viz.pitch = 45\n", + "\n", + "# add extrusion to viz using interpolation keyed on density in GeoJSON features\n", + "viz.height_property = 'density'\n", + "viz.height_stops = create_numeric_stops([0, 50, 100, 500, 1500, 5000], 0, 500000)\n", + "viz.height_function_type = 'interpolate'\n", + "\n", + "# render again\n", + "viz.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1414,60 +222,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Zip Code ZCTA2010 Census Population
0100116769
1100229049
\n", - "
" - ], - "text/plain": [ - " Zip Code ZCTA 2010 Census Population\n", - "0 1001 16769\n", - "1 1002 29049" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Load data from sample csv\n", "import pandas as pd\n", @@ -1479,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1495,334 +252,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Create the viz\n", "viz = ChoroplethViz(data, \n", @@ -1850,9 +282,9 @@ "summary": "Mapboxgl Python Data Visualization example" }, "kernelspec": { - "display_name": "Python3.6 Conda", + "display_name": "Python 3", "language": "python", - "name": "myenv" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1864,7 +296,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.1" } }, "nbformat": 4, diff --git a/mapboxgl/templates/base.html b/mapboxgl/templates/base.html index 7a054cc..e44948c 100644 --- a/mapboxgl/templates/base.html +++ b/mapboxgl/templates/base.html @@ -9,6 +9,8 @@ style: {{ style }}, center: {{ center }}, zoom: {{ zoom }}, + pitch: {{ pitch }}, + bearing: {{ bearing }}, transformRequest: (url, resourceType)=> { if ( url.slice(0,22) == 'https://api.mapbox.com' ) { //Add Python Plugin identifier for Mapbox API traffic diff --git a/mapboxgl/templates/choropleth.html b/mapboxgl/templates/choropleth.html index 3e72f16..be2b605 100644 --- a/mapboxgl/templates/choropleth.html +++ b/mapboxgl/templates/choropleth.html @@ -74,7 +74,24 @@ "text-halo-color": "white", "text-halo-width": 1 } - }, "{{belowLayer}}" ); + }, "{{ belowLayer }}" ); + + // Optional extrusion layer + {% if extrudeChoropleth %} + + map.addLayer({ + id: "choropleth-extrusion", + type: "fill-extrusion", + source: "data", + paint: { + "fill-extrusion-opacity": {{ opacity }}, + "fill-extrusion-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), + "fill-extrusion-height": generatePropertyExpression("{{ heightType }}", "{{ heightProperty }}", {{ heightStops }}, {{ defaultHeight }}), + } + }, "{{ belowLayer }}"); + + {% endif %} + {% endblock choropleth %} diff --git a/mapboxgl/templates/vector_choropleth.html b/mapboxgl/templates/vector_choropleth.html index 3bb0ee4..582b33a 100644 --- a/mapboxgl/templates/vector_choropleth.html +++ b/mapboxgl/templates/vector_choropleth.html @@ -6,13 +6,19 @@ {% if joinData %} let joinData = {{ joinData }}; - var popUpKeys = {}; + var popUpKeys = {}, + heightPopUpKeys = {}; // Create filter for layers from join data - let layerFilter = ['in', "{{ vectorJoinColorProperty }}"] + let layerFilter = ['in', "{{ vectorJoinDataProperty }}"] joinData.forEach(function(row, index) { popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + + {% if extrudeChoropleth %} + heightPopUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ heightProperty }}"]; + {% endif %} + layerFilter.push(row["{{ dataJoinProperty }}"]); }); @@ -33,7 +39,7 @@ "paint": { "fill-color": { "type": "categorical", - "property": "{{ vectorJoinColorProperty }}", + "property": "{{ vectorJoinDataProperty }}", "stops": {{ vectorColorStops }}, "default": "{{ defaultColor }}" }, @@ -81,7 +87,35 @@ "text-halo-width": 1 }, filter: layerFilter - }, "{{belowLayer}}" ); + }, "{{ belowLayer }}" ); + + // Optional extrusion layer + {% if extrudeChoropleth %} + + map.addLayer({ + id: "choropleth-extrusion", + type: "fill-extrusion", + "source": "vector-data", + "source-layer": "{{ vectorLayer }}", + paint: { + "fill-extrusion-opacity": {{ opacity }}, + "fill-extrusion-color": { + "type": "categorical", + "property": "{{ vectorJoinDataProperty }}", + "stops": {{ vectorColorStops }}, + "default": "{{ defaultColor }}" + }, + "fill-extrusion-height": { + "type": "categorical", + "property": "{{ vectorJoinDataProperty }}", + "stops": {{ vectorHeightStops }}, + "default": {{ defaultHeight }} + } + }, + filter: layerFilter + }, "{{ belowLayer }}"); + + {% endif %} {% endblock choropleth %} @@ -99,7 +133,13 @@ } // Add property from joined data to vector's feature popup - popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + '
  • ' + popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + + {% if extrudeChoropleth %} + + popup_html += '
  • ' + "{{ heightProperty }}".toUpperCase() + ': ' + heightPopUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + + {% endif %} popup_html += '' popup.setLngLat(e.lngLat) diff --git a/mapboxgl/utils.py b/mapboxgl/utils.py index 0951b25..73bdcad 100644 --- a/mapboxgl/utils.py +++ b/mapboxgl/utils.py @@ -110,6 +110,13 @@ def create_weight_stops(breaks): return stops +def create_numeric_stops(breaks, min_value, max_value): + """Convert data breaks into a general numeric ramp (height, radius, weight, etc.) + """ + weight_breaks = scale_between(min_value, max_value, len(breaks)) + return [list(x) for x in zip(breaks, weight_breaks)] + + def create_color_stops(breaks, colors='RdYlGn', color_ramps=color_ramps): """Convert a list of breaks into color stops using colors from colorBrewer or a custom list of color values in RGB, RGBA, HSL, CSS text, or HEX format. @@ -258,3 +265,66 @@ def img_encode(arr, **kwargs): img_str = base64.b64encode(sio.getvalue()).decode() return 'data:image/{};base64,{}'.format(img_format, img_str) + + +def height_map(lookup, height_stops, default_height=0.0): + """Return a height value (in meters) interpolated from given height_stops; + for use with vector-based visualizations using fill-extrusion layers + """ + # if no height_stops, use default height + if len(height_stops) == 0: + return default_height + + # dictionary to lookup height from match-type height_stops + match_map = dict((x, y) for (x, y) in height_stops) + + # if lookup matches stop exactly, return corresponding height (first priority) + # (includes non-numeric height_stop "keys" for finding height by match) + if lookup in match_map.keys(): + return match_map.get(lookup) + + # if lookup value numeric, map height by interpolating from height scale + if isinstance(lookup, (int, float, complex)): + + # try ordering stops + try: + stops, heights = zip(*sorted(height_stops)) + + # if not all stops are numeric, attempt looking up as if categorical stops + except TypeError: + return match_map.get(lookup, default_height) + + # for interpolation, all stops must be numeric + if not all(isinstance(x, (int, float, complex)) for x in stops): + return default_height + + # check if lookup value in stops bounds + if float(lookup) <= stops[0]: + return heights[0] + + elif float(lookup) >= stops[-1]: + return heights[-1] + + # check if lookup value matches any stop value + elif float(lookup) in stops: + return heights[stops.index(lookup)] + + # interpolation required + else: + + # identify bounding height stop values + lower = max([stops[0]] + [x for x in stops if x < lookup]) + upper = min([stops[-1]] + [x for x in stops if x > lookup]) + + # heights from bounding stops + lower_height = heights[stops.index(lower)] + upper_height = heights[stops.index(upper)] + + # compute linear "relative distance" from lower bound height to upper bound height + distance = (lookup - lower) / (upper - lower) + + # return string representing rgb height value + return lower_height + distance * (upper_height - lower_height) + + # default height value catch-all + return default_height diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 1af855f..2b28d90 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -6,7 +6,7 @@ import numpy from mapboxgl.errors import TokenError -from mapboxgl.utils import color_map +from mapboxgl.utils import color_map, height_map from mapboxgl import templates from mapboxgl.utils import img_encode @@ -28,7 +28,9 @@ def __init__(self, width='100%', zoom=0, min_zoom=0, - max_zoom=24): + max_zoom=24, + pitch=0, + bearing=0): """Construct a MapViz object :param data: GeoJSON Feature Collection @@ -40,6 +42,8 @@ def __init__(self, :param height: The CSS height of the HTML map div in % or pixels. :param zoom: starting zoom level for map :param opacity: opacity of map data layer + :param pitch: starting pitch (in degrees) for map + :param bearing: starting bearing (in degrees) for map """ if access_token is None: @@ -62,6 +66,8 @@ def __init__(self, self.label_property = None self.min_zoom = min_zoom self.max_zoom = max_zoom + self.pitch = pitch + self.bearing = bearing def as_iframe(self, html_data): """Build the HTML representation for the mapviz.""" @@ -102,7 +108,9 @@ def create_html(self): belowLayer=self.below_layer, opacity=self.opacity, minzoom=self.min_zoom, - maxzoom=self.max_zoom) + maxzoom=self.max_zoom, + pitch=self.pitch, + bearing=self.bearing) if self.label_property is None: options.update(labelProperty=None) @@ -385,6 +393,10 @@ def __init__(self, line_color='white', line_stroke='solid', line_width=1, + height_property=None, + height_stops=None, + height_default=0.0, + height_function_type='interpolate', *args, **kwargs): """Construct a Mapviz object @@ -395,14 +407,17 @@ def __init__(self, :param vector_join_property: property to aid in determining color for styling vector polygons :param data_join_property: property to join json data to vector features :param label_property: property to use for marker label - :param color_property: property to determine circle color - :param color_stops: property to determine circle color - :param color_default: property to determine default circle color if match lookup fails + :param color_property: property to determine polygon color + :param color_stops: property to determine polygon color + :param color_default: property to determine default polygon color if match lookup fails :param color_function_type: property to determine `type` used by Mapbox to assign color :param line_color: property to determine choropleth line color :param line_stroke: property to determine choropleth line stroke (solid, dashed, dotted, dash dot) :param line_width: property to determine choropleth line width - + :param height_property: feature property for determining polygon height in 3D extruded choropleth map + :param height_stops: property for determining 3D extrusion height + :param height_default: default height for 3D extruded polygons + :param height_function_type: roperty to determine `type` used by Mapbox to assign height """ super(ChoroplethViz, self).__init__(data, *args, **kwargs) @@ -426,6 +441,10 @@ def __init__(self, self.line_color = line_color self.line_stroke = line_stroke self.line_width = line_width + self.height_property = height_property + self.height_stops = height_stops + self.height_default = height_default + self.height_function_type = height_function_type def generate_vector_color_map(self): """Generate color stops array for use with match expression in mapbox template""" @@ -440,6 +459,23 @@ def generate_vector_color_map(self): return vector_stops + def generate_vector_height_map(self): + """Generate height stops array for use with match expression in mapbox template""" + vector_stops = [] + + if self.height_function_type == 'match': + match_height = self.height_stops + + for row in self.data: + + # map height to JSON feature using height_property + height = height_map(row[self.height_property], self.height_stops, self.height_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], height]) + + return vector_stops + def add_unique_template_variables(self, options): """Update map template variables specific to heatmap visual""" @@ -456,6 +492,9 @@ def add_unique_template_variables(self, options): # default to solid line self.line_dash_array = [1, 0] + # check if choropleth map should include 3-D extrusion + self.extrude = all([bool(self.height_property), bool(self.height_stops)]) + # common variables for vector and geojson-based choropleths options.update(dict( colorStops=self.color_stops, @@ -466,7 +505,15 @@ def add_unique_template_variables(self, options): lineDashArray=self.line_dash_array, lineStroke=self.line_stroke, lineWidth=self.line_width, + extrudeChoropleth=self.extrude, )) + if self.extrude: + options.update(dict( + heightType=self.height_function_type, + heightProperty=self.height_property, + heightStops=self.height_stops, + defaultHeight=self.height_default, + )) # vector-based choropleth map variables if self.vector_source: @@ -474,11 +521,15 @@ def add_unique_template_variables(self, options): vectorUrl=self.vector_url, vectorLayer=self.vector_layer_name, vectorColorStops=self.generate_vector_color_map(), - vectorJoinColorProperty=self.vector_join_property, + vectorJoinDataProperty=self.vector_join_property, joinData=json.dumps(self.data, ensure_ascii=False), dataJoinProperty=self.data_join_property, )) - + if self.extrude: + options.update(dict( + vectorHeightStops=self.generate_vector_height_map(), + )) + # geojson-based choropleth map variables else: options.update(dict( diff --git a/tests/test_utils.py b/tests/test_utils.py index 9065707..82af9dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,8 +7,8 @@ from matplotlib.pyplot import imread from mapboxgl.utils import (df_to_geojson, scale_between, create_radius_stops, - create_weight_stops, create_color_stops, img_encode, - rgb_tuple_from_str, color_map) + create_weight_stops, create_numeric_stops, create_color_stops, + img_encode, rgb_tuple_from_str, color_map, height_map) @pytest.fixture() @@ -153,3 +153,14 @@ def test_color_map_interp_exact(): assert color_map(0.0, interp_stops, 'rgb(32,32,32)') == 'rgb(255,0,0)' +def test_create_numeric_stops(): + """Create numeric stops from custom breaks""" + domain = [7678.214347826088, 5793.63142857143, 1200] + stops = create_numeric_stops(domain, 1, 10) + assert stops == [[7678.214347826088, 1.0], [5793.63142857143, 4.0], [1200, 7.0]] + + +def test_height_map(): + """Interpolate height from numeric height stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert height_map(117.0, stops, default_height=0.0) == 11700.0