From 2f6fbdfd13948d355bb40c40adbcc5cc68379d07 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Thu, 29 Mar 2018 14:57:53 -0700 Subject: [PATCH 01/16] Add LinestringViz class with associated geojson and vector-based templates --- mapboxgl/templates/linestring.html | 112 ++++++++++++++++++++ mapboxgl/templates/vector_linestring.html | 92 +++++++++++++++++ mapboxgl/viz.py | 120 ++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 mapboxgl/templates/linestring.html create mode 100644 mapboxgl/templates/vector_linestring.html diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html new file mode 100644 index 0000000..d433ab8 --- /dev/null +++ b/mapboxgl/templates/linestring.html @@ -0,0 +1,112 @@ +{% extends "base.html" %} + + +{% block extra_css %} + +{% endblock extra_css %} + +{% block legend %} + + var legend = document.getElementById('legend'); + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + +{% endblock legend %} + +{% block map %} + + map.on('style.load', function() { + + {% block linestring %} + + // Add geojson data source + map.addSource("data", { + "type": "geojson", + "data": {{ geojson_data }}, + "buffer": 1, + "maxzoom": 14 + }); + + // Add data layer + map.addLayer({ + "id": "linestring", + "source": "data", + "type": "line", + "layout": { + "line-join": "round", + "line-cap": "round" + }, + "paint": { + {% if lineDashArray %} + "line-dasharray": {{ lineDashArray }}, + {% endif %} + "line-color": "{{ lineColor }}", + "line-width": {{ lineWidth }}, + "line-opacity": {{ opacity }} + } + }, "{{ belowLayer }}" ); + + // Add label layer + map.addLayer({ + "id": "linestring-label", + "source": "data", + "type": "symbol", + "layout": { + {% if labelProperty %} + "text-field": "{{ labelProperty }}", + {% endif %} + "text-size" : generateInterpolateExpression('zoom', [[0,8],[22,16]] ), + "text-offset": [0,-1] + }, + "paint": { + "text-halo-color": "white", + "text-halo-width": 1 + } + }, "{{belowLayer}}" ); + + {% endblock linestring %} + + // Create a popup + var popup = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false + }); + + {% block linestring_popup %} + + // Show the popup on mouseover + map.on('mousemove', 'linestring', function(e) { + map.getCanvas().style.cursor = 'pointer'; + + let f = e.features[0]; + let popup_html = '
'; + + for (key in f.properties) { + popup_html += '
  • ' + key + ': ' + f.properties[key] + '
  • ' + } + + popup_html += '
    ' + popup.setLngLat(e.lngLat) + .setHTML(popup_html) + .addTo(map); + }); + + {% endblock linestring_popup %} + + map.on('mouseleave', 'linestring', function() { + map.getCanvas().style.cursor = ''; + popup.remove(); + }); + + // Fly to on click + map.on('click', 'linestring', function(e) { + map.flyTo({ + center: e.lngLat, + zoom: map.getZoom() + 1 + }); + }); + + }); + +{% endblock map %} diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html new file mode 100644 index 0000000..5376308 --- /dev/null +++ b/mapboxgl/templates/vector_linestring.html @@ -0,0 +1,92 @@ +{% extends "linestring.html" %} + +{% block linestring %} + + // extract JSON property used for data-driven styling to add to popup + {% if joinData %} + + let joinData = {{ joinData }}; + var popUpKeys = {}; + + // Create filter for layers from join data + let layerFilter = ['in', "{{ vectorJoinColorProperty }}"] + + joinData.forEach(function(row, index) { + popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + layerFilter.push(row["{{ dataJoinProperty }}"]); + }); + + {% endif %} + + // Add vector data source + map.addSource("vector-data", { + type: "vector", + url: "{{ vectorUrl }}", + }); + + // Add data layer from the vector tile source with data-driven style + map.addLayer({ + "id": "linestring", + "source": "vector-data", + "source-layer": "{{ vectorLayer }}", + "type": "line", + "layout": { + "line-join": "round", + "line-cap": "round" + }, + "paint": { + {% if lineDashArray %} + "line-dasharray": {{ lineDashArray }}, + {% endif %} + "line-color": "{{ lineColor }}", + "line-width": {{ lineWidth }}, + "line-opacity": {{ opacity }} + }, + filter: layerFilter + }, "{{ belowLayer }}" ); + + // Add label layer + map.addLayer({ + "id": "linestring-label", + "source": "vector-data", + "source-layer": "{{ vectorLayer }}", + "type": "symbol", + "layout": { + {% if labelProperty %} + "text-field": "{{ labelProperty }}", + {% endif %} + "text-size" : generateInterpolateExpression('zoom', [[0,8],[22,16]] ), + "text-offset": [0,-1] + }, + "paint": { + "text-halo-color": "white", + "text-halo-width": 1 + }, + filter: layerFilter + }, "{{belowLayer}}" ); + +{% endblock linestring %} + +{% block linestring_popup %} + + // Show the popup on mouseover + map.on('mousemove', 'linestring', function(e) { + map.getCanvas().style.cursor = 'pointer'; + + let f = e.features[0]; + let popup_html = '
    '; + + for (key in f.properties) { + popup_html += '
  • ' + key + ': ' + f.properties[key] + '
  • ' + } + + // Add property from joined data to vector's feature popup + popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + '
  • ' + + popup_html += '
    ' + popup.setLngLat(e.lngLat) + .setHTML(popup_html) + .addTo(map); + }); + +{% endblock linestring_popup %} diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 1af855f..7163f00 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -553,3 +553,123 @@ def add_unique_template_variables(self, options): tiles_minzoom=self.tiles_minzoom, tiles_maxzoom=self.tiles_maxzoom, tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) + + + +class LinestringViz(MapViz): + """Create a linestring viz""" + + def __init__(self, + 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): + """Construct a Mapviz object + + :param data: can be either GeoJSON (containing polygon features) or JSON for data-join technique with vector polygons + :param vector_url: optional property to define vector polygon source + :param vector_layer_name: property to define target layer of vector source + :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_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 + + """ + super(LinestringViz, self).__init__(data, *args, **kwargs) + + self.vector_url = vector_url + self.vector_layer_name = vector_layer_name + self.vector_join_property = vector_join_property + self.data_join_property = data_join_property + + if self.vector_url is not None and self.vector_layer_name is not None: + self.template = 'vector_linestring' + self.vector_source = True + else: + self.vector_source = False + self.template = 'linestring' + + self.label_property = label_property + self.color_property = color_property + self.color_stops = color_stops + self.color_default = color_default + self.color_function_type = color_function_type + self.line_color = line_color + self.line_stroke = line_stroke + self.line_width = line_width + + def generate_vector_color_map(self): + """Generate color stops array for use with match expression in mapbox template""" + vector_stops = [] + for row in self.data: + + # map color to JSON feature using color_property + color = color_map(row[self.color_property], self.color_stops, self.color_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], color]) + + return vector_stops + + def add_unique_template_variables(self, options): + """Update map template variables specific to heatmap visual""" + + # set line stroke dash interval based on line_stroke property + if self.line_stroke in ["dashed", "--"]: + self.line_dash_array = [6, 4] + elif self.line_stroke in ["dotted", ":"]: + self.line_dash_array = [0.5, 4] + elif self.line_stroke in ["dash dot", "-."]: + self.line_dash_array = [6, 4, 0.5, 4] + elif self.line_stroke in ["solid", "-"]: + self.line_dash_array = [1, 0] + else: + # default to solid line + self.line_dash_array = [1, 0] + + # common variables for vector and geojson-based choropleths + options.update(dict( + colorStops=self.color_stops, + colorProperty=self.color_property, + colorType=self.color_function_type, + defaultColor=self.color_default, + lineColor=self.line_color, + lineDashArray=self.line_dash_array, + lineStroke=self.line_stroke, + lineWidth=self.line_width, + )) + + # vector-based choropleth map variables + if self.vector_source: + options.update(dict( + vectorUrl=self.vector_url, + vectorLayer=self.vector_layer_name, + vectorColorStops=self.generate_vector_color_map(), + vectorJoinColorProperty=self.vector_join_property, + joinData=json.dumps(self.data, ensure_ascii=False), + dataJoinProperty=self.data_join_property, + )) + + # geojson-based choropleth map variables + else: + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + )) + From b1fe48cd616670c7118a9351f640dac8e80e7a2f Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sun, 1 Apr 2018 23:00:34 -0700 Subject: [PATCH 02/16] Add data-driven styling to vector linestring viz --- mapboxgl/templates/linestring.html | 2 +- mapboxgl/templates/vector_linestring.html | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index d433ab8..3a6c1df 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -41,7 +41,7 @@ {% if lineDashArray %} "line-dasharray": {{ lineDashArray }}, {% endif %} - "line-color": "{{ lineColor }}", + "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), "line-width": {{ lineWidth }}, "line-opacity": {{ opacity }} } diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 5376308..737e6ec 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -38,7 +38,12 @@ {% if lineDashArray %} "line-dasharray": {{ lineDashArray }}, {% endif %} - "line-color": "{{ lineColor }}", + "line-color": { + "type": "categorical", + "property": "{{ vectorJoinColorProperty }}", + "stops": {{ vectorColorStops }}, + "default": "{{ defaultColor }}" + }, "line-width": {{ lineWidth }}, "line-opacity": {{ opacity }} }, From 07bf51ed194bfc4586867bba8751d6467d9f5efb Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sun, 1 Apr 2018 23:26:08 -0700 Subject: [PATCH 03/16] Example linestring viz and vector linestring viz --- examples/notebooks/linestring-viz.ipynb | 164 ++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 examples/notebooks/linestring-viz.ipynb diff --git a/examples/notebooks/linestring-viz.ipynb b/examples/notebooks/linestring-viz.ipynb new file mode 100644 index 0000000..51d68df --- /dev/null +++ b/examples/notebooks/linestring-viz.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "import pandas as pd\n", + "\n", + "from mapboxgl.viz import *\n", + "from mapboxgl.utils import *\n", + "\n", + "# Must be a public token, starting with `pk`\n", + "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", + "\n", + "\n", + "geojson = {\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [{\n", + " \"type\": \"Feature\",\n", + " \"id\": \"01\", \n", + " \"properties\": {\"something\": 50, \"other\": 1}, \n", + " \"geometry\": {\n", + " \"type\": \"LineString\",\n", + " \"coordinates\": [\n", + " [-122.4833858013153, 37.829607404976734],\n", + " [-122.4830961227417, 37.82932776098012],\n", + " [-122.4830746650696, 37.82932776098012],\n", + " [-122.48218417167662, 37.82889558180985],\n", + " [-122.48218417167662, 37.82890193740421],\n", + " [-122.48221099376678, 37.82868372835086],\n", + " [-122.4822163581848, 37.82868372835086],\n", + " [-122.48205006122589, 37.82801003030873]\n", + " ]\n", + " }\n", + " }, {\n", + " \"type\": \"Feature\",\n", + " \"id\": \"02\",\n", + " \"properties\": {\"something\": 500, \"other\": 2},\n", + " \"geometry\": {\n", + " \"type\": \"LineString\",\n", + " \"coordinates\": [\n", + " [-122.4833858013153, 37.929607404976734],\n", + " [-122.4830961227417, 37.83],\n", + " ]\n", + " }\n", + " }]\n", + "}\n", + "\n", + "viz = LinestringViz(geojson, \n", + " color_property='something',\n", + " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'),\n", + " color_function_type='match',\n", + " color_default='red',\n", + " line_stroke='-',\n", + " line_width=1,\n", + " opacity=0.8,\n", + " center=(-122.48, 37.83),\n", + " zoom=16,\n", + " below_layer='waterway-label'\n", + " )\n", + "viz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "import pandas as pd\n", + "\n", + "from mapboxgl.viz import *\n", + "from mapboxgl.utils import *\n", + "\n", + "# Must be a public token, starting with `pk`\n", + "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", + "\n", + "data = [{\"elevation\": 0, \"something\": 0},\n", + " {\"elevation\": 10, \"something\": 200},\n", + " {\"elevation\": 20, \"something\": 200},\n", + " {\"elevation\": 30, \"something\": 30},\n", + " {\"elevation\": 40, \"something\": 200},\n", + " {\"elevation\": 50, \"something\": 20},\n", + " {\"elevation\": 60, \"something\": 200},\n", + " {\"elevation\": 70, \"something\": 40},\n", + " {\"elevation\": 80, \"something\": 200},\n", + " {\"elevation\": 90, \"something\": 200},\n", + " {\"elevation\": 100, \"something\": 200},\n", + " ]\n", + "# https://www.mapbox.com/mapbox-gl-js/example/vector-source/\n", + "\n", + "match_stops = [\n", + " [0, \"red\"], \n", + " [10, \"blue\"], \n", + " [20, \"green\"], \n", + " [30, \"yellow\"], \n", + " [40, \"pink\"],\n", + " [50, \"magenta\"],\n", + " [60, \"black\"],\n", + " [70, \"cyan\"],\n", + " [80, \"grey\"],\n", + " [90, \"brown\"],\n", + " [200, \"orange\"],\n", + "]\n", + "\n", + "viz = LinestringViz(data, \n", + " vector_url='mapbox://mapbox.mapbox-terrain-v2',\n", + " vector_layer_name='contour',\n", + " vector_join_property='ele',\n", + " data_join_property='elevation',\n", + " color_property='elevation',\n", + " color_stops=match_stops,\n", + " color_function_type='match',\n", + " color_default='rgb(255,255,0)',\n", + " line_stroke='-',\n", + " line_color='notused', ##########\n", + " line_width=1,\n", + " opacity=0.8,\n", + " center=(-122.48, 37.83),\n", + " zoom=16,\n", + " below_layer='waterway-label'\n", + " )\n", + "viz.show()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.6.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 3d6b878bd89fa814e0f63b43d6925de08041204c Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 14 Apr 2018 18:45:37 -0700 Subject: [PATCH 04/16] Refine linestring iz with data-driven styles for line width; update viz arguments; refine default color and line width handling --- mapboxgl/templates/linestring.html | 29 ++++- mapboxgl/templates/vector_linestring.html | 37 +++++- mapboxgl/viz.py | 137 ++++++++++++++++++---- 3 files changed, 170 insertions(+), 33 deletions(-) diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index 3a6c1df..c654ec9 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -3,14 +3,25 @@ {% block extra_css %} {% endblock extra_css %} {% block legend %} var legend = document.getElementById('legend'); - calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + + {% if colorStops and colorProperty and widthProperty %} + {% if colorProperty != widthProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ widthProperty }}"); + {% else %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% endif %} + {% elif colorStops and colorProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% else %} + document.getElementById('legend').style.visibility='hidden'; + {% endif %} {% endblock legend %} @@ -39,10 +50,18 @@ }, "paint": { {% if lineDashArray %} - "line-dasharray": {{ lineDashArray }}, + "line-dasharray": {{ lineDashArray }}, + {% endif %} + {% if colorProperty %} + "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), + {% else %} + "line-color": "{{ defaultColor }}", + {% endif %} + {% if widthProperty %} + "line-width": generatePropertyExpression("{{ widthType }}", "{{ widthProperty }}", {{ widthStops }}, "{{ defaultWidth }}"), + {% else %} + "line-width": {{ defaultWidth }}, {% endif %} - "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), - "line-width": {{ lineWidth }}, "line-opacity": {{ opacity }} } }, "{{ belowLayer }}" ); diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 737e6ec..55da9de 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -6,13 +6,24 @@ {% if joinData %} let joinData = {{ joinData }}; - var popUpKeys = {}; + var popUpKeys = {}, + lineWidthPopUpKeys = {}; // 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 colorProperty %} + popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + lineWidthPopUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ widthProperty }}"]; + {% endif %} + {% endif %} + layerFilter.push(row["{{ dataJoinProperty }}"]); }); @@ -40,11 +51,17 @@ {% endif %} "line-color": { "type": "categorical", - "property": "{{ vectorJoinColorProperty }}", + "property": "{{ vectorJoinDataProperty }}", "stops": {{ vectorColorStops }}, "default": "{{ defaultColor }}" }, - "line-width": {{ lineWidth }}, + "line-width": { + "type": "categorical", + "property": "{{ vectorJoinDataProperty }}", + "stops": {{ vectorWidthStops }}, + "default": {{ defaultWidth }} + }, + "line-opacity": {{ opacity }} }, filter: layerFilter @@ -86,7 +103,15 @@ } // Add property from joined data to vector's feature popup - popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + '
  • ' + {% if colorProperty %} + popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + popup_html += '
  • ' + "{{ widthProperty }}".toUpperCase() + ': ' + lineWidthPopUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + {% endif %} popup_html += '' popup.setLngLat(e.lngLat) diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 7163f00..cf0ae56 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -13,6 +13,67 @@ GL_JS_VERSION = 'v0.44.1' +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 class MapViz(object): @@ -555,7 +616,6 @@ def add_unique_template_variables(self, options): tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) - class LinestringViz(MapViz): """Create a linestring viz""" @@ -564,32 +624,36 @@ def __init__(self, vector_url=None, vector_layer_name=None, vector_join_property=None, - data_join_property=None, # vector only + data_join_property=None, 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, + line_width_property=None, + line_width_stops=None, + line_width_default=1, + line_width_function_type='interpolate', *args, **kwargs): """Construct a Mapviz object :param data: can be either GeoJSON (containing polygon features) or JSON for data-join technique with vector polygons - :param vector_url: optional property to define vector polygon source + :param vector_url: optional property to define vector linestring source :param vector_layer_name: property to define target layer of vector source - :param vector_join_property: property to aid in determining color for styling vector polygons + :param vector_join_property: property to aid in determining color for styling vector lines :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 line color + :param color_stops: property to determine line color + :param color_default: property to determine default line 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 line_stroke: property to determine line stroke (solid, dashed, dotted, dash dot) + :param line_width_property: property to determine line width + :param line_width_stops: property to determine line width + :param line_width_default: property to determine default line width if match lookup fails + :param line_width_function_type: property to determine `type` used by Mapbox to assign line width """ super(LinestringViz, self).__init__(data, *args, **kwargs) @@ -611,9 +675,11 @@ def __init__(self, self.color_stops = color_stops self.color_default = color_default self.color_function_type = color_function_type - self.line_color = line_color self.line_stroke = line_stroke - self.line_width = line_width + self.line_width_property = line_width_property + self.line_width_stops = line_width_stops + self.line_width_default = line_width_default + self.line_width_function_type = line_width_function_type def generate_vector_color_map(self): """Generate color stops array for use with match expression in mapbox template""" @@ -628,8 +694,25 @@ def generate_vector_color_map(self): return vector_stops + def generate_vector_width_map(self): + """Generate width stops array for use with match expression in mapbox template""" + vector_stops = [] + + if self.line_width_function_type == 'match': + match_width = self.line_width_stops + + for row in self.data: + + # map width to JSON feature using width_property + width = height_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], width]) + + return vector_stops + def add_unique_template_variables(self, options): - """Update map template variables specific to heatmap visual""" + """Update map template variables specific to linestring visual""" # set line stroke dash interval based on line_stroke property if self.line_stroke in ["dashed", "--"]: @@ -644,30 +727,40 @@ def add_unique_template_variables(self, options): # default to solid line self.line_dash_array = [1, 0] - # common variables for vector and geojson-based choropleths + # common variables for vector and geojson-based linestring maps options.update(dict( colorStops=self.color_stops, colorProperty=self.color_property, colorType=self.color_function_type, defaultColor=self.color_default, - lineColor=self.line_color, + lineColor=self.color_default, lineDashArray=self.line_dash_array, lineStroke=self.line_stroke, - lineWidth=self.line_width, + widthStops=self.line_width_stops, + widthProperty=self.line_width_property, + widthType=self.line_width_function_type, + defaultWidth=self.line_width_default, )) - # vector-based choropleth map variables + # vector-based linestring map variables if self.vector_source: options.update(dict( 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, + vectorColorStops=[[0,self.color_default]], + vectorWidthStops=[[0,self.line_width_default]], joinData=json.dumps(self.data, ensure_ascii=False), dataJoinProperty=self.data_join_property, )) + + if self.color_property: + options.update(dict(vectorColorStops=self.generate_vector_color_map())) - # geojson-based choropleth map variables + if self.line_width_property: + options.update(dict(vectorWidthStops=self.generate_vector_width_map())) + + # geojson-based linestring map variables else: options.update(dict( geojson_data=json.dumps(self.data, ensure_ascii=False), From 03db8aaca10a181db40390bfdcd4d01ae60d6323 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 14 Apr 2018 18:55:13 -0700 Subject: [PATCH 05/16] Testing linestring viz examples (further refinements pending) --- examples/notebooks/linestring-viz.ipynb | 155 +++++++++++++++++------- mapboxgl/utils.py | 62 ++++++++++ mapboxgl/viz.py | 65 +--------- 3 files changed, 177 insertions(+), 105 deletions(-) diff --git a/examples/notebooks/linestring-viz.ipynb b/examples/notebooks/linestring-viz.ipynb index 51d68df..8951c57 100644 --- a/examples/notebooks/linestring-viz.ipynb +++ b/examples/notebooks/linestring-viz.ipynb @@ -1,13 +1,19 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GeoJSON linestring source" + ] + }, { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ + "import random\n", "import sys\n", "import os\n", "import pandas as pd\n", @@ -18,13 +24,16 @@ "# Must be a public token, starting with `pk`\n", "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", "\n", + "# JSON join-data object\n", + "data = [{\"elevation\": x, \"something\": random.randint(0,100)} for x in range(0, 21000, 10)]\n", "\n", + "# GeoJSON data object\n", "geojson = {\n", " \"type\": \"FeatureCollection\",\n", " \"features\": [{\n", " \"type\": \"Feature\",\n", " \"id\": \"01\", \n", - " \"properties\": {\"something\": 50, \"other\": 1}, \n", + " \"properties\": {\"property-1\": 50, \"property-2\": 1}, \n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -41,7 +50,7 @@ " }, {\n", " \"type\": \"Feature\",\n", " \"id\": \"02\",\n", - " \"properties\": {\"something\": 500, \"other\": 2},\n", + " \"properties\": {\"property-1\": 500, \"property-2\": 2},\n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -49,16 +58,56 @@ " [-122.4830961227417, 37.83],\n", " ]\n", " }\n", + " }, {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {\"property-1\": 5000, \"property-2\": 1},\n", + " \"geometry\": {\n", + " \"type\": \"LineString\",\n", + " \"coordinates\": [\n", + " [-122.48369693756104, 37.83381888486939],\n", + " [-122.48348236083984, 37.83317489144141],\n", + " [-122.48339653015138, 37.83270036637107],\n", + " [-122.48356819152832, 37.832056363179625],\n", + " [-122.48404026031496, 37.83114119107971],\n", + " [-122.48404026031496, 37.83049717427869],\n", + " [-122.48348236083984, 37.829920943955045],\n", + " [-122.48356819152832, 37.82954808664175],\n", + " [-122.48507022857666, 37.82944639795659],\n", + " [-122.48610019683838, 37.82880236636284],\n", + " [-122.48695850372314, 37.82931081282506],\n", + " [-122.48700141906738, 37.83080223556934],\n", + " [-122.48751640319824, 37.83168351665737],\n", + " [-122.48803138732912, 37.832158048267786],\n", + " [-122.48888969421387, 37.83297152392784],\n", + " [-122.48987674713133, 37.83263257682617],\n", + " [-122.49043464660643, 37.832937629287755],\n", + " [-122.49125003814696, 37.832429207817725],\n", + " [-122.49163627624512, 37.832564787218985],\n", + " [-122.49223709106445, 37.83337825839438],\n", + " [-122.49378204345702, 37.83368330777276]\n", + " ]\n", + " }\n", " }]\n", - "}\n", - "\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# make viz with GeoJSON source\n", "viz = LinestringViz(geojson, \n", - " color_property='something',\n", + " color_property='property-1',\n", " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'),\n", - " color_function_type='match',\n", + " color_function_type='interpolate',\n", " color_default='red',\n", - " line_stroke='-',\n", - " line_width=1,\n", + " line_stroke='--',\n", + " line_width_property='property-2',\n", + " line_width_stops=create_radius_stops([0, 1, 2, 3, 4, 5], 0, 10),\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", @@ -67,36 +116,19 @@ "viz.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vector linestring source" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import sys\n", - "import os\n", - "import pandas as pd\n", - "\n", - "from mapboxgl.viz import *\n", - "from mapboxgl.utils import *\n", - "\n", - "# Must be a public token, starting with `pk`\n", - "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", - "\n", - "data = [{\"elevation\": 0, \"something\": 0},\n", - " {\"elevation\": 10, \"something\": 200},\n", - " {\"elevation\": 20, \"something\": 200},\n", - " {\"elevation\": 30, \"something\": 30},\n", - " {\"elevation\": 40, \"something\": 200},\n", - " {\"elevation\": 50, \"something\": 20},\n", - " {\"elevation\": 60, \"something\": 200},\n", - " {\"elevation\": 70, \"something\": 40},\n", - " {\"elevation\": 80, \"something\": 200},\n", - " {\"elevation\": 90, \"something\": 200},\n", - " {\"elevation\": 100, \"something\": 200},\n", - " ]\n", - "# https://www.mapbox.com/mapbox-gl-js/example/vector-source/\n", - "\n", "match_stops = [\n", " [0, \"red\"], \n", " [10, \"blue\"], \n", @@ -117,19 +149,58 @@ " vector_join_property='ele',\n", " data_join_property='elevation',\n", " color_property='elevation',\n", - " color_stops=match_stops,\n", - " color_function_type='match',\n", - " color_default='rgb(255,255,0)',\n", + " color_stops=create_color_stops([0, 25, 50, 75, 100], colors='YlOrRd'),\n", + " # color_stops=match_stops,\n", + " color_function_type='match', \n", + " line_width_property='something',\n", + " line_width_stops=create_radius_stops([0, 50, 100], 2, 6),\n", " line_stroke='-',\n", - " line_color='notused', ##########\n", - " line_width=1,\n", + " line_width_default=3,\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", " below_layer='waterway-label'\n", " )\n", - "viz.show()\n", - "\n" + "viz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# use defaults with geojson source (bare minimum args)\n", + "viz = LinestringViz(geojson)\n", + "\n", + "# important for visibility for this vector\n", + "viz.center = (-122.48, 37.83)\n", + "viz.zoom = 16\n", + "\n", + "# make viz\n", + "viz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# use defaults with vector source (bare minimum args)\n", + "viz = LinestringViz(data, \n", + " vector_url='mapbox://mapbox.mapbox-terrain-v2',\n", + " vector_layer_name='contour',\n", + " vector_join_property='ele',\n", + " data_join_property='elevation',\n", + " )\n", + "\n", + "# important for visibility for this vector\n", + "viz.center = (-122.48, 37.83)\n", + "viz.zoom = 16\n", + "\n", + "# make viz\n", + "viz.show()\n" ] }, { diff --git a/mapboxgl/utils.py b/mapboxgl/utils.py index 0951b25..2900714 100644 --- a/mapboxgl/utils.py +++ b/mapboxgl/utils.py @@ -243,6 +243,68 @@ def color_map(lookup, color_stops, default_color='rgb(122,122,122)'): return default_color +def numeric_map(lookup, numeric_stops, default=0.0): + """Return a number value interpolated from given numeric_stops + """ + # if no numeric_stops, use default + if len(numeric_stops) == 0: + return default + + # dictionary to lookup value from match-type numeric_stops + match_map = dict((x, y) for (x, y) in numeric_stops) + + # if lookup matches stop exactly, return corresponding stop (first priority) + # (includes non-numeric numeric_stop "keys" for finding value by match) + if lookup in match_map.keys(): + return match_map.get(lookup) + + # if lookup value numeric, map value by interpolating from scale + if isinstance(lookup, (int, float, complex)): + + # try ordering stops + try: + stops, values = zip(*sorted(numeric_stops)) + + # if not all stops are numeric, attempt looking up as if categorical stops + except TypeError: + return match_map.get(lookup, default) + + # for interpolation, all stops must be numeric + if not all(isinstance(x, (int, float, complex)) for x in stops): + return default + + # check if lookup value in stops bounds + if float(lookup) <= stops[0]: + return values[0] + + elif float(lookup) >= stops[-1]: + return values[-1] + + # check if lookup value matches any stop value + elif float(lookup) in stops: + return values[stops.index(lookup)] + + # interpolation required + else: + + # identify bounding 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]) + + # values from bounding stops + lower_value = values[stops.index(lower)] + upper_value = values[stops.index(upper)] + + # compute linear "relative distance" from lower bound to upper bound + distance = (lookup - lower) / (upper - lower) + + # return interpolated value + return lower_value + distance * (upper_value - lower_value) + + # default value catch-all + return default + + def img_encode(arr, **kwargs): """Encode ndarray to base64 string image data diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index cf0ae56..10c8b80 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -8,72 +8,11 @@ from mapboxgl.errors import TokenError from mapboxgl.utils import color_map from mapboxgl import templates -from mapboxgl.utils import img_encode +from mapboxgl.utils import img_encode, numeric_map GL_JS_VERSION = 'v0.44.1' -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 class MapViz(object): @@ -704,7 +643,7 @@ def generate_vector_width_map(self): for row in self.data: # map width to JSON feature using width_property - width = height_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) + width = numeric_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) # link to vector feature using data_join_property (from JSON object) vector_stops.append([row[self.data_join_property], width]) From e40d776416e83120b14600f6bb3a43063a3fa65f Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Mon, 16 Apr 2018 14:12:53 -0700 Subject: [PATCH 06/16] Add tests for numeric_map, LinestringViz --- tests/linestrings.geojson | 61 +++++++++++++++++++++++++++++++++++++ tests/test_html.py | 63 +++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 35 ++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 tests/linestrings.geojson diff --git a/tests/linestrings.geojson b/tests/linestrings.geojson new file mode 100644 index 0000000..9687388 --- /dev/null +++ b/tests/linestrings.geojson @@ -0,0 +1,61 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "id": "01", + "properties": {"sample": 50, "width": 1}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4833858013153, 37.829607404976734], + [-122.4830961227417, 37.82932776098012], + [-122.4830746650696, 37.82932776098012], + [-122.48218417167662, 37.82889558180985], + [-122.48218417167662, 37.82890193740421], + [-122.48221099376678, 37.82868372835086], + [-122.4822163581848, 37.82868372835086], + [-122.48205006122589, 37.82801003030873] + ] + } + }, { + "type": "Feature", + "id": "02", + "properties": {"sample": 500, "width": 2}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4833858013153, 37.929607404976734], + [-122.4830961227417, 37.83] + ] + } + }, { + "type": "Feature", + "properties": {"sample": 5000, "width": 1}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.48369693756104, 37.83381888486939], + [-122.48348236083984, 37.83317489144141], + [-122.48339653015138, 37.83270036637107], + [-122.48356819152832, 37.832056363179625], + [-122.48404026031496, 37.83114119107971], + [-122.48404026031496, 37.83049717427869], + [-122.48348236083984, 37.829920943955045], + [-122.48356819152832, 37.82954808664175], + [-122.48507022857666, 37.82944639795659], + [-122.48610019683838, 37.82880236636284], + [-122.48695850372314, 37.82931081282506], + [-122.48700141906738, 37.83080223556934], + [-122.48751640319824, 37.83168351665737], + [-122.48803138732912, 37.832158048267786], + [-122.48888969421387, 37.83297152392784], + [-122.48987674713133, 37.83263257682617], + [-122.49043464660643, 37.832937629287755], + [-122.49125003814696, 37.832429207817725], + [-122.49163627624512, 37.832564787218985], + [-122.49223709106445, 37.83337825839438], + [-122.49378204345702, 37.83368330777276] + ] + } + }] +} \ No newline at end of file diff --git a/tests/test_html.py b/tests/test_html.py index 5887474..ce07c3f 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,6 +1,7 @@ import os import json import base64 +import random from mock import patch @@ -24,6 +25,12 @@ def polygon_data(): return json.loads(fh.read()) +@pytest.fixture() +def linestring_data(): + with open('tests/linestrings.geojson') as fh: + return json.loads(fh.read()) + + TOKEN = 'pk.abc123' @@ -51,6 +58,14 @@ def test_secret_key_ChoroplethViz(polygon_data): ChoroplethViz(polygon_data, access_token=secret) +def test_secret_key_LinestringViz(linestring_data): + """Secret key raises a token error + """ + secret = 'sk.abc123' + with pytest.raises(TokenError): + LinestringViz(linestring_data, access_token=secret) + + def test_token_env_CircleViz(monkeypatch, data): """Viz can get token from environment if not specified """ @@ -77,6 +92,14 @@ def test_token_env_ChoroplethViz(monkeypatch, polygon_data): assert TOKEN in viz.create_html() +def test_token_env_LinestringViz(monkeypatch, linestring_data): + """Viz can get token from environment if not specified + """ + monkeypatch.setenv('MAPBOX_ACCESS_TOKEN', TOKEN) + viz = LinestringViz(linestring_data, color_property="sample") + assert TOKEN in viz.create_html() + + def test_html_color(data): viz = CircleViz(data, color_property="Avg Medicare Payments", @@ -100,6 +123,14 @@ def test_html_ChoroplethViz(polygon_data): assert "" in viz.create_html() +def test_html_LinestringViz(linestring_data): + viz = LinestringViz(linestring_data, + color_property="sample", + color_stops=[[0.0, "red"], [50.0, "gold"], [1000.0, "blue"]], + access_token=TOKEN) + assert "" in viz.create_html() + + @patch('mapboxgl.viz.display') def test_display_CircleViz(display, data): """Assert that show calls the mocked display function @@ -193,6 +224,38 @@ def test_display_vector_ChoroplethViz(display): display.assert_called_once() +@patch('mapboxgl.viz.display') +def test_display_LinestringViz(display, linestring_data): + """Assert that show calls the mocked display function + """ + viz = LinestringViz(linestring_data, + color_property="sample", + color_stops=[[0.0, "red"], [50.0, "gold"], [1000.0, "blue"]], + access_token=TOKEN) + viz.show() + display.assert_called_once() + + +@patch('mapboxgl.viz.display') +def test_display_vector_LinestringViz(display): + """Assert that show calls the mocked display function when using data-join technique + for LinestringViz. + """ + data = [{"elevation": x, "other": random.randint(0,100)} for x in range(0, 21000, 10)] + + viz = LinestringViz(data, + vector_url='mapbox://mapbox.mapbox-terrain-v2', + vector_layer_name='contour', + vector_join_property='ele', + data_join_property='elevation', + color_property="elevation", + color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'), + access_token=TOKEN + ) + viz.show() + display.assert_called_once() + + @patch('mapboxgl.viz.display') def test_min_zoom(display, data): viz = GraduatedCircleViz(data, diff --git a/tests/test_utils.py b/tests/test_utils.py index 9065707..45466ed 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,6 +15,7 @@ def df(): return pd.read_csv('tests/points.csv') + @pytest.fixture() def df_no_properties(): df = pd.read_csv('tests/points.csv') @@ -37,6 +38,7 @@ def test_df_no_properties(df_no_properties): 'features'] assert tuple(features[0]['properties'].keys()) == () + def test_df_geojson_file(df): features = df_to_geojson(df, filename='out.geojson') with open('out.geojson', 'r') as f: @@ -60,26 +62,31 @@ def test_scale_between_maxMin(): scale = scale_between(0,1,1) assert scale == [0,1] + def test_color_stops(): """Create color stops from breaks using colorBrewer""" stops = create_color_stops([0, 1, 2], colors='YlGn') assert stops == [[0,"rgb(247,252,185)"], [1,"rgb(173,221,142)"], [2,"rgb(49,163,84)"]] + def test_color_stops_custom(): """Create color stops from custom color breaks""" stops = create_color_stops([0, 1, 2], colors=['red', 'yellow', 'green']) assert stops == [[0,"red"], [1,"yellow"], [2,"green"]] + def test_color_stops_custom_invalid(): """Create invalid color stops from custom color breaks and throw value error""" with pytest.raises(ValueError): create_color_stops([0, 1, 2], colors=['x', 'yellow', 'green']) + def test_color_stops_custom_null(): """Create invalid number of color stops that do not match the number of breaks""" with pytest.raises(ValueError): create_color_stops([0, 1, 2], colors=['red', 'yellow', 'green', 'grey']) + def test_create_radius_stops(df): domain = [7678.214347826088, 5793.63142857143, 1200] radius_stops = create_radius_stops(domain, 1, 10) @@ -153,3 +160,31 @@ def test_color_map_interp_exact(): assert color_map(0.0, interp_stops, 'rgb(32,32,32)') == 'rgb(255,0,0)' +def test_numeric_map(): + """Map interpolated (or matched) value from numeric stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(117.0, stops, 0.0) == 11700.0 + + +def test_numeric_map_match(): + """Match value from numeric stops""" + match_stops = [['road', 1.0], ['fence', 15.0], ['wall', 10.0]] + assert numeric_map('fence', match_stops, 0.0) == 15.0 + + +def test_numeric_map_no_stops(): + """Return default if length of stops argument is 0""" + stops = [] + assert numeric_map(117.0, stops, 42) == 42 + + +def test_numeric_map_default(): + """Default value when look up does not match any stop in stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(-1.0, stops, 42) == 0 + + +def test_numeric_map_exact(): + """Compute mapping for lookup value exactly matching numeric stop in stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(50.0, stops, 42) == 5000.0 \ No newline at end of file From 70032a3f455d4da64916ba0c62a9d518e3bfa353 Mon Sep 17 00:00:00 2001 From: Ryan Baumann Date: Wed, 4 Apr 2018 15:26:49 -0700 Subject: [PATCH 07/16] Delete out.geojson --- out.geojson | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 out.geojson diff --git a/out.geojson b/out.geojson deleted file mode 100644 index 843bbc2..0000000 --- a/out.geojson +++ /dev/null @@ -1,5 +0,0 @@ -{"type": "FeatureCollection", "features": [ -{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.362856, 31.216215]}, "properties": {"Avg Medicare Payments": 7678.2143478261, "Avg Covered Charges": 35247.0281521739, "Avg Total Payments": 8749.0251086956}} -,{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-88.1427969, 32.4529763]}, "properties": {"Avg Medicare Payments": 5793.6314285714, "Avg Covered Charges": 16451.0920408163, "Avg Total Payments": 6812.1312244898}} -,{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-89.1427969, 31.4529763]}, "properties": {"Avg Medicare Payments": 1200.0, "Avg Covered Charges": 1800.0, "Avg Total Payments": 2200.0}} -]} \ No newline at end of file From 013daaed81c6ed8710362a783fe9b627c67acdbe Mon Sep 17 00:00:00 2001 From: Brett Naul Date: Sat, 7 Apr 2018 10:47:07 -0700 Subject: [PATCH 08/16] Fix link to example data (#91) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a35523..623cfea 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ from mapboxgl.utils import * from mapboxgl.viz import * # Load data from sample csv -data_url = 'https://raw.githubusercontent.com/mapbox/mapboxgl-jupyter/master/examples/points.csv' +data_url = 'https://raw.githubusercontent.com/mapbox/mapboxgl-jupyter/master/examples/data/points.csv' df = pd.read_csv(data_url) # Must be a public token, starting with `pk` From be7d60afc260722173d15fcd13ca21f3d1506b39 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 14 Apr 2018 18:45:37 -0700 Subject: [PATCH 09/16] Refine linestring iz with data-driven styles for line width; update viz arguments; refine default color and line width handling --- mapboxgl/templates/linestring.html | 29 ++++- mapboxgl/templates/vector_linestring.html | 37 +++++- mapboxgl/viz.py | 137 ++++++++++++++++++---- 3 files changed, 170 insertions(+), 33 deletions(-) diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index 3a6c1df..c654ec9 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -3,14 +3,25 @@ {% block extra_css %} {% endblock extra_css %} {% block legend %} var legend = document.getElementById('legend'); - calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + + {% if colorStops and colorProperty and widthProperty %} + {% if colorProperty != widthProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ widthProperty }}"); + {% else %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% endif %} + {% elif colorStops and colorProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% else %} + document.getElementById('legend').style.visibility='hidden'; + {% endif %} {% endblock legend %} @@ -39,10 +50,18 @@ }, "paint": { {% if lineDashArray %} - "line-dasharray": {{ lineDashArray }}, + "line-dasharray": {{ lineDashArray }}, + {% endif %} + {% if colorProperty %} + "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), + {% else %} + "line-color": "{{ defaultColor }}", + {% endif %} + {% if widthProperty %} + "line-width": generatePropertyExpression("{{ widthType }}", "{{ widthProperty }}", {{ widthStops }}, "{{ defaultWidth }}"), + {% else %} + "line-width": {{ defaultWidth }}, {% endif %} - "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), - "line-width": {{ lineWidth }}, "line-opacity": {{ opacity }} } }, "{{ belowLayer }}" ); diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 737e6ec..55da9de 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -6,13 +6,24 @@ {% if joinData %} let joinData = {{ joinData }}; - var popUpKeys = {}; + var popUpKeys = {}, + lineWidthPopUpKeys = {}; // 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 colorProperty %} + popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + lineWidthPopUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ widthProperty }}"]; + {% endif %} + {% endif %} + layerFilter.push(row["{{ dataJoinProperty }}"]); }); @@ -40,11 +51,17 @@ {% endif %} "line-color": { "type": "categorical", - "property": "{{ vectorJoinColorProperty }}", + "property": "{{ vectorJoinDataProperty }}", "stops": {{ vectorColorStops }}, "default": "{{ defaultColor }}" }, - "line-width": {{ lineWidth }}, + "line-width": { + "type": "categorical", + "property": "{{ vectorJoinDataProperty }}", + "stops": {{ vectorWidthStops }}, + "default": {{ defaultWidth }} + }, + "line-opacity": {{ opacity }} }, filter: layerFilter @@ -86,7 +103,15 @@ } // Add property from joined data to vector's feature popup - popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + '
  • ' + {% if colorProperty %} + popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + popup_html += '
  • ' + "{{ widthProperty }}".toUpperCase() + ': ' + lineWidthPopUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + {% endif %} popup_html += '' popup.setLngLat(e.lngLat) diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 7163f00..cf0ae56 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -13,6 +13,67 @@ GL_JS_VERSION = 'v0.44.1' +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 class MapViz(object): @@ -555,7 +616,6 @@ def add_unique_template_variables(self, options): tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) - class LinestringViz(MapViz): """Create a linestring viz""" @@ -564,32 +624,36 @@ def __init__(self, vector_url=None, vector_layer_name=None, vector_join_property=None, - data_join_property=None, # vector only + data_join_property=None, 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, + line_width_property=None, + line_width_stops=None, + line_width_default=1, + line_width_function_type='interpolate', *args, **kwargs): """Construct a Mapviz object :param data: can be either GeoJSON (containing polygon features) or JSON for data-join technique with vector polygons - :param vector_url: optional property to define vector polygon source + :param vector_url: optional property to define vector linestring source :param vector_layer_name: property to define target layer of vector source - :param vector_join_property: property to aid in determining color for styling vector polygons + :param vector_join_property: property to aid in determining color for styling vector lines :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 line color + :param color_stops: property to determine line color + :param color_default: property to determine default line 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 line_stroke: property to determine line stroke (solid, dashed, dotted, dash dot) + :param line_width_property: property to determine line width + :param line_width_stops: property to determine line width + :param line_width_default: property to determine default line width if match lookup fails + :param line_width_function_type: property to determine `type` used by Mapbox to assign line width """ super(LinestringViz, self).__init__(data, *args, **kwargs) @@ -611,9 +675,11 @@ def __init__(self, self.color_stops = color_stops self.color_default = color_default self.color_function_type = color_function_type - self.line_color = line_color self.line_stroke = line_stroke - self.line_width = line_width + self.line_width_property = line_width_property + self.line_width_stops = line_width_stops + self.line_width_default = line_width_default + self.line_width_function_type = line_width_function_type def generate_vector_color_map(self): """Generate color stops array for use with match expression in mapbox template""" @@ -628,8 +694,25 @@ def generate_vector_color_map(self): return vector_stops + def generate_vector_width_map(self): + """Generate width stops array for use with match expression in mapbox template""" + vector_stops = [] + + if self.line_width_function_type == 'match': + match_width = self.line_width_stops + + for row in self.data: + + # map width to JSON feature using width_property + width = height_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], width]) + + return vector_stops + def add_unique_template_variables(self, options): - """Update map template variables specific to heatmap visual""" + """Update map template variables specific to linestring visual""" # set line stroke dash interval based on line_stroke property if self.line_stroke in ["dashed", "--"]: @@ -644,30 +727,40 @@ def add_unique_template_variables(self, options): # default to solid line self.line_dash_array = [1, 0] - # common variables for vector and geojson-based choropleths + # common variables for vector and geojson-based linestring maps options.update(dict( colorStops=self.color_stops, colorProperty=self.color_property, colorType=self.color_function_type, defaultColor=self.color_default, - lineColor=self.line_color, + lineColor=self.color_default, lineDashArray=self.line_dash_array, lineStroke=self.line_stroke, - lineWidth=self.line_width, + widthStops=self.line_width_stops, + widthProperty=self.line_width_property, + widthType=self.line_width_function_type, + defaultWidth=self.line_width_default, )) - # vector-based choropleth map variables + # vector-based linestring map variables if self.vector_source: options.update(dict( 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, + vectorColorStops=[[0,self.color_default]], + vectorWidthStops=[[0,self.line_width_default]], joinData=json.dumps(self.data, ensure_ascii=False), dataJoinProperty=self.data_join_property, )) + + if self.color_property: + options.update(dict(vectorColorStops=self.generate_vector_color_map())) - # geojson-based choropleth map variables + if self.line_width_property: + options.update(dict(vectorWidthStops=self.generate_vector_width_map())) + + # geojson-based linestring map variables else: options.update(dict( geojson_data=json.dumps(self.data, ensure_ascii=False), From 7743631c6b502ba1ca84375f3f472f19201bd178 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 14 Apr 2018 18:55:13 -0700 Subject: [PATCH 10/16] Testing linestring viz examples (further refinements pending) --- examples/notebooks/linestring-viz.ipynb | 155 +++++++++++++++++------- mapboxgl/utils.py | 62 ++++++++++ mapboxgl/viz.py | 65 +--------- 3 files changed, 177 insertions(+), 105 deletions(-) diff --git a/examples/notebooks/linestring-viz.ipynb b/examples/notebooks/linestring-viz.ipynb index 51d68df..8951c57 100644 --- a/examples/notebooks/linestring-viz.ipynb +++ b/examples/notebooks/linestring-viz.ipynb @@ -1,13 +1,19 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GeoJSON linestring source" + ] + }, { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ + "import random\n", "import sys\n", "import os\n", "import pandas as pd\n", @@ -18,13 +24,16 @@ "# Must be a public token, starting with `pk`\n", "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", "\n", + "# JSON join-data object\n", + "data = [{\"elevation\": x, \"something\": random.randint(0,100)} for x in range(0, 21000, 10)]\n", "\n", + "# GeoJSON data object\n", "geojson = {\n", " \"type\": \"FeatureCollection\",\n", " \"features\": [{\n", " \"type\": \"Feature\",\n", " \"id\": \"01\", \n", - " \"properties\": {\"something\": 50, \"other\": 1}, \n", + " \"properties\": {\"property-1\": 50, \"property-2\": 1}, \n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -41,7 +50,7 @@ " }, {\n", " \"type\": \"Feature\",\n", " \"id\": \"02\",\n", - " \"properties\": {\"something\": 500, \"other\": 2},\n", + " \"properties\": {\"property-1\": 500, \"property-2\": 2},\n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -49,16 +58,56 @@ " [-122.4830961227417, 37.83],\n", " ]\n", " }\n", + " }, {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {\"property-1\": 5000, \"property-2\": 1},\n", + " \"geometry\": {\n", + " \"type\": \"LineString\",\n", + " \"coordinates\": [\n", + " [-122.48369693756104, 37.83381888486939],\n", + " [-122.48348236083984, 37.83317489144141],\n", + " [-122.48339653015138, 37.83270036637107],\n", + " [-122.48356819152832, 37.832056363179625],\n", + " [-122.48404026031496, 37.83114119107971],\n", + " [-122.48404026031496, 37.83049717427869],\n", + " [-122.48348236083984, 37.829920943955045],\n", + " [-122.48356819152832, 37.82954808664175],\n", + " [-122.48507022857666, 37.82944639795659],\n", + " [-122.48610019683838, 37.82880236636284],\n", + " [-122.48695850372314, 37.82931081282506],\n", + " [-122.48700141906738, 37.83080223556934],\n", + " [-122.48751640319824, 37.83168351665737],\n", + " [-122.48803138732912, 37.832158048267786],\n", + " [-122.48888969421387, 37.83297152392784],\n", + " [-122.48987674713133, 37.83263257682617],\n", + " [-122.49043464660643, 37.832937629287755],\n", + " [-122.49125003814696, 37.832429207817725],\n", + " [-122.49163627624512, 37.832564787218985],\n", + " [-122.49223709106445, 37.83337825839438],\n", + " [-122.49378204345702, 37.83368330777276]\n", + " ]\n", + " }\n", " }]\n", - "}\n", - "\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# make viz with GeoJSON source\n", "viz = LinestringViz(geojson, \n", - " color_property='something',\n", + " color_property='property-1',\n", " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'),\n", - " color_function_type='match',\n", + " color_function_type='interpolate',\n", " color_default='red',\n", - " line_stroke='-',\n", - " line_width=1,\n", + " line_stroke='--',\n", + " line_width_property='property-2',\n", + " line_width_stops=create_radius_stops([0, 1, 2, 3, 4, 5], 0, 10),\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", @@ -67,36 +116,19 @@ "viz.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vector linestring source" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import sys\n", - "import os\n", - "import pandas as pd\n", - "\n", - "from mapboxgl.viz import *\n", - "from mapboxgl.utils import *\n", - "\n", - "# Must be a public token, starting with `pk`\n", - "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", - "\n", - "data = [{\"elevation\": 0, \"something\": 0},\n", - " {\"elevation\": 10, \"something\": 200},\n", - " {\"elevation\": 20, \"something\": 200},\n", - " {\"elevation\": 30, \"something\": 30},\n", - " {\"elevation\": 40, \"something\": 200},\n", - " {\"elevation\": 50, \"something\": 20},\n", - " {\"elevation\": 60, \"something\": 200},\n", - " {\"elevation\": 70, \"something\": 40},\n", - " {\"elevation\": 80, \"something\": 200},\n", - " {\"elevation\": 90, \"something\": 200},\n", - " {\"elevation\": 100, \"something\": 200},\n", - " ]\n", - "# https://www.mapbox.com/mapbox-gl-js/example/vector-source/\n", - "\n", "match_stops = [\n", " [0, \"red\"], \n", " [10, \"blue\"], \n", @@ -117,19 +149,58 @@ " vector_join_property='ele',\n", " data_join_property='elevation',\n", " color_property='elevation',\n", - " color_stops=match_stops,\n", - " color_function_type='match',\n", - " color_default='rgb(255,255,0)',\n", + " color_stops=create_color_stops([0, 25, 50, 75, 100], colors='YlOrRd'),\n", + " # color_stops=match_stops,\n", + " color_function_type='match', \n", + " line_width_property='something',\n", + " line_width_stops=create_radius_stops([0, 50, 100], 2, 6),\n", " line_stroke='-',\n", - " line_color='notused', ##########\n", - " line_width=1,\n", + " line_width_default=3,\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", " below_layer='waterway-label'\n", " )\n", - "viz.show()\n", - "\n" + "viz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# use defaults with geojson source (bare minimum args)\n", + "viz = LinestringViz(geojson)\n", + "\n", + "# important for visibility for this vector\n", + "viz.center = (-122.48, 37.83)\n", + "viz.zoom = 16\n", + "\n", + "# make viz\n", + "viz.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# use defaults with vector source (bare minimum args)\n", + "viz = LinestringViz(data, \n", + " vector_url='mapbox://mapbox.mapbox-terrain-v2',\n", + " vector_layer_name='contour',\n", + " vector_join_property='ele',\n", + " data_join_property='elevation',\n", + " )\n", + "\n", + "# important for visibility for this vector\n", + "viz.center = (-122.48, 37.83)\n", + "viz.zoom = 16\n", + "\n", + "# make viz\n", + "viz.show()\n" ] }, { diff --git a/mapboxgl/utils.py b/mapboxgl/utils.py index 0951b25..2900714 100644 --- a/mapboxgl/utils.py +++ b/mapboxgl/utils.py @@ -243,6 +243,68 @@ def color_map(lookup, color_stops, default_color='rgb(122,122,122)'): return default_color +def numeric_map(lookup, numeric_stops, default=0.0): + """Return a number value interpolated from given numeric_stops + """ + # if no numeric_stops, use default + if len(numeric_stops) == 0: + return default + + # dictionary to lookup value from match-type numeric_stops + match_map = dict((x, y) for (x, y) in numeric_stops) + + # if lookup matches stop exactly, return corresponding stop (first priority) + # (includes non-numeric numeric_stop "keys" for finding value by match) + if lookup in match_map.keys(): + return match_map.get(lookup) + + # if lookup value numeric, map value by interpolating from scale + if isinstance(lookup, (int, float, complex)): + + # try ordering stops + try: + stops, values = zip(*sorted(numeric_stops)) + + # if not all stops are numeric, attempt looking up as if categorical stops + except TypeError: + return match_map.get(lookup, default) + + # for interpolation, all stops must be numeric + if not all(isinstance(x, (int, float, complex)) for x in stops): + return default + + # check if lookup value in stops bounds + if float(lookup) <= stops[0]: + return values[0] + + elif float(lookup) >= stops[-1]: + return values[-1] + + # check if lookup value matches any stop value + elif float(lookup) in stops: + return values[stops.index(lookup)] + + # interpolation required + else: + + # identify bounding 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]) + + # values from bounding stops + lower_value = values[stops.index(lower)] + upper_value = values[stops.index(upper)] + + # compute linear "relative distance" from lower bound to upper bound + distance = (lookup - lower) / (upper - lower) + + # return interpolated value + return lower_value + distance * (upper_value - lower_value) + + # default value catch-all + return default + + def img_encode(arr, **kwargs): """Encode ndarray to base64 string image data diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index cf0ae56..10c8b80 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -8,72 +8,11 @@ from mapboxgl.errors import TokenError from mapboxgl.utils import color_map from mapboxgl import templates -from mapboxgl.utils import img_encode +from mapboxgl.utils import img_encode, numeric_map GL_JS_VERSION = 'v0.44.1' -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 class MapViz(object): @@ -704,7 +643,7 @@ def generate_vector_width_map(self): for row in self.data: # map width to JSON feature using width_property - width = height_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) + width = numeric_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) # link to vector feature using data_join_property (from JSON object) vector_stops.append([row[self.data_join_property], width]) From 02bca0e84151b4022502afd9f950b377f7542590 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Mon, 16 Apr 2018 14:12:53 -0700 Subject: [PATCH 11/16] Add tests for numeric_map, LinestringViz --- tests/linestrings.geojson | 61 +++++++++++++++++++++++++++++++++++++ tests/test_html.py | 63 +++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 35 ++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 tests/linestrings.geojson diff --git a/tests/linestrings.geojson b/tests/linestrings.geojson new file mode 100644 index 0000000..9687388 --- /dev/null +++ b/tests/linestrings.geojson @@ -0,0 +1,61 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "id": "01", + "properties": {"sample": 50, "width": 1}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4833858013153, 37.829607404976734], + [-122.4830961227417, 37.82932776098012], + [-122.4830746650696, 37.82932776098012], + [-122.48218417167662, 37.82889558180985], + [-122.48218417167662, 37.82890193740421], + [-122.48221099376678, 37.82868372835086], + [-122.4822163581848, 37.82868372835086], + [-122.48205006122589, 37.82801003030873] + ] + } + }, { + "type": "Feature", + "id": "02", + "properties": {"sample": 500, "width": 2}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.4833858013153, 37.929607404976734], + [-122.4830961227417, 37.83] + ] + } + }, { + "type": "Feature", + "properties": {"sample": 5000, "width": 1}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-122.48369693756104, 37.83381888486939], + [-122.48348236083984, 37.83317489144141], + [-122.48339653015138, 37.83270036637107], + [-122.48356819152832, 37.832056363179625], + [-122.48404026031496, 37.83114119107971], + [-122.48404026031496, 37.83049717427869], + [-122.48348236083984, 37.829920943955045], + [-122.48356819152832, 37.82954808664175], + [-122.48507022857666, 37.82944639795659], + [-122.48610019683838, 37.82880236636284], + [-122.48695850372314, 37.82931081282506], + [-122.48700141906738, 37.83080223556934], + [-122.48751640319824, 37.83168351665737], + [-122.48803138732912, 37.832158048267786], + [-122.48888969421387, 37.83297152392784], + [-122.48987674713133, 37.83263257682617], + [-122.49043464660643, 37.832937629287755], + [-122.49125003814696, 37.832429207817725], + [-122.49163627624512, 37.832564787218985], + [-122.49223709106445, 37.83337825839438], + [-122.49378204345702, 37.83368330777276] + ] + } + }] +} \ No newline at end of file diff --git a/tests/test_html.py b/tests/test_html.py index 5887474..ce07c3f 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,6 +1,7 @@ import os import json import base64 +import random from mock import patch @@ -24,6 +25,12 @@ def polygon_data(): return json.loads(fh.read()) +@pytest.fixture() +def linestring_data(): + with open('tests/linestrings.geojson') as fh: + return json.loads(fh.read()) + + TOKEN = 'pk.abc123' @@ -51,6 +58,14 @@ def test_secret_key_ChoroplethViz(polygon_data): ChoroplethViz(polygon_data, access_token=secret) +def test_secret_key_LinestringViz(linestring_data): + """Secret key raises a token error + """ + secret = 'sk.abc123' + with pytest.raises(TokenError): + LinestringViz(linestring_data, access_token=secret) + + def test_token_env_CircleViz(monkeypatch, data): """Viz can get token from environment if not specified """ @@ -77,6 +92,14 @@ def test_token_env_ChoroplethViz(monkeypatch, polygon_data): assert TOKEN in viz.create_html() +def test_token_env_LinestringViz(monkeypatch, linestring_data): + """Viz can get token from environment if not specified + """ + monkeypatch.setenv('MAPBOX_ACCESS_TOKEN', TOKEN) + viz = LinestringViz(linestring_data, color_property="sample") + assert TOKEN in viz.create_html() + + def test_html_color(data): viz = CircleViz(data, color_property="Avg Medicare Payments", @@ -100,6 +123,14 @@ def test_html_ChoroplethViz(polygon_data): assert "" in viz.create_html() +def test_html_LinestringViz(linestring_data): + viz = LinestringViz(linestring_data, + color_property="sample", + color_stops=[[0.0, "red"], [50.0, "gold"], [1000.0, "blue"]], + access_token=TOKEN) + assert "" in viz.create_html() + + @patch('mapboxgl.viz.display') def test_display_CircleViz(display, data): """Assert that show calls the mocked display function @@ -193,6 +224,38 @@ def test_display_vector_ChoroplethViz(display): display.assert_called_once() +@patch('mapboxgl.viz.display') +def test_display_LinestringViz(display, linestring_data): + """Assert that show calls the mocked display function + """ + viz = LinestringViz(linestring_data, + color_property="sample", + color_stops=[[0.0, "red"], [50.0, "gold"], [1000.0, "blue"]], + access_token=TOKEN) + viz.show() + display.assert_called_once() + + +@patch('mapboxgl.viz.display') +def test_display_vector_LinestringViz(display): + """Assert that show calls the mocked display function when using data-join technique + for LinestringViz. + """ + data = [{"elevation": x, "other": random.randint(0,100)} for x in range(0, 21000, 10)] + + viz = LinestringViz(data, + vector_url='mapbox://mapbox.mapbox-terrain-v2', + vector_layer_name='contour', + vector_join_property='ele', + data_join_property='elevation', + color_property="elevation", + color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'), + access_token=TOKEN + ) + viz.show() + display.assert_called_once() + + @patch('mapboxgl.viz.display') def test_min_zoom(display, data): viz = GraduatedCircleViz(data, diff --git a/tests/test_utils.py b/tests/test_utils.py index 9065707..45466ed 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,6 +15,7 @@ def df(): return pd.read_csv('tests/points.csv') + @pytest.fixture() def df_no_properties(): df = pd.read_csv('tests/points.csv') @@ -37,6 +38,7 @@ def test_df_no_properties(df_no_properties): 'features'] assert tuple(features[0]['properties'].keys()) == () + def test_df_geojson_file(df): features = df_to_geojson(df, filename='out.geojson') with open('out.geojson', 'r') as f: @@ -60,26 +62,31 @@ def test_scale_between_maxMin(): scale = scale_between(0,1,1) assert scale == [0,1] + def test_color_stops(): """Create color stops from breaks using colorBrewer""" stops = create_color_stops([0, 1, 2], colors='YlGn') assert stops == [[0,"rgb(247,252,185)"], [1,"rgb(173,221,142)"], [2,"rgb(49,163,84)"]] + def test_color_stops_custom(): """Create color stops from custom color breaks""" stops = create_color_stops([0, 1, 2], colors=['red', 'yellow', 'green']) assert stops == [[0,"red"], [1,"yellow"], [2,"green"]] + def test_color_stops_custom_invalid(): """Create invalid color stops from custom color breaks and throw value error""" with pytest.raises(ValueError): create_color_stops([0, 1, 2], colors=['x', 'yellow', 'green']) + def test_color_stops_custom_null(): """Create invalid number of color stops that do not match the number of breaks""" with pytest.raises(ValueError): create_color_stops([0, 1, 2], colors=['red', 'yellow', 'green', 'grey']) + def test_create_radius_stops(df): domain = [7678.214347826088, 5793.63142857143, 1200] radius_stops = create_radius_stops(domain, 1, 10) @@ -153,3 +160,31 @@ def test_color_map_interp_exact(): assert color_map(0.0, interp_stops, 'rgb(32,32,32)') == 'rgb(255,0,0)' +def test_numeric_map(): + """Map interpolated (or matched) value from numeric stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(117.0, stops, 0.0) == 11700.0 + + +def test_numeric_map_match(): + """Match value from numeric stops""" + match_stops = [['road', 1.0], ['fence', 15.0], ['wall', 10.0]] + assert numeric_map('fence', match_stops, 0.0) == 15.0 + + +def test_numeric_map_no_stops(): + """Return default if length of stops argument is 0""" + stops = [] + assert numeric_map(117.0, stops, 42) == 42 + + +def test_numeric_map_default(): + """Default value when look up does not match any stop in stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(-1.0, stops, 42) == 0 + + +def test_numeric_map_exact(): + """Compute mapping for lookup value exactly matching numeric stop in stops""" + stops = [[0.0, 0], [50.0, 5000.0], [1000.0, 100000.0]] + assert numeric_map(50.0, stops, 42) == 5000.0 \ No newline at end of file From 0ada9227ca735ab75b611152887043cd9c89ffcd Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Mon, 16 Apr 2018 22:00:38 -0700 Subject: [PATCH 12/16] Fix utils import for test_utils.py --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 37f0a12..fb876ca 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from mapboxgl.utils import (df_to_geojson, scale_between, create_radius_stops, create_weight_stops, create_numeric_stops, create_color_stops, - img_encode, rgb_tuple_from_str, color_map, height_map) + img_encode, rgb_tuple_from_str, color_map, height_map, numeric_map) @pytest.fixture() From c39306bf60899d4b7292125d163c0ee20e96ab1d Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Mon, 16 Apr 2018 22:34:35 -0700 Subject: [PATCH 13/16] Update linestring viz notebook --- examples/notebooks/linestring-viz.ipynb | 108 ++++++------------------ 1 file changed, 26 insertions(+), 82 deletions(-) diff --git a/examples/notebooks/linestring-viz.ipynb b/examples/notebooks/linestring-viz.ipynb index 8951c57..44660d0 100644 --- a/examples/notebooks/linestring-viz.ipynb +++ b/examples/notebooks/linestring-viz.ipynb @@ -4,7 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## GeoJSON linestring source" + "# Mapboxgl Python Library for location data visualizaiton\n", + "\n", + "https://github.com/mapbox/mapboxgl-jupyter\n", + "\n", + "\n", + "# Linestring Visualizations" ] }, { @@ -14,18 +19,16 @@ "outputs": [], "source": [ "import random\n", - "import sys\n", "import os\n", - "import pandas as pd\n", "\n", - "from mapboxgl.viz import *\n", - "from mapboxgl.utils import *\n", + "from mapboxgl.viz import LinestringViz\n", + "from mapboxgl.utils import create_color_stops, create_numeric_stops\n", "\n", "# Must be a public token, starting with `pk`\n", "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", "\n", "# JSON join-data object\n", - "data = [{\"elevation\": x, \"something\": random.randint(0,100)} for x in range(0, 21000, 10)]\n", + "data = [{\"elevation\": x, \"weight\": random.randint(0,100)} for x in range(0, 21000, 10)]\n", "\n", "# GeoJSON data object\n", "geojson = {\n", @@ -33,7 +36,7 @@ " \"features\": [{\n", " \"type\": \"Feature\",\n", " \"id\": \"01\", \n", - " \"properties\": {\"property-1\": 50, \"property-2\": 1}, \n", + " \"properties\": {\"sample\": 50, \"weight\": 1}, \n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -50,17 +53,17 @@ " }, {\n", " \"type\": \"Feature\",\n", " \"id\": \"02\",\n", - " \"properties\": {\"property-1\": 500, \"property-2\": 2},\n", + " \"properties\": {\"sample\": 500, \"weight\": 2},\n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", " [-122.4833858013153, 37.929607404976734],\n", - " [-122.4830961227417, 37.83],\n", + " [-122.4830961227417, 37.83]\n", " ]\n", " }\n", " }, {\n", " \"type\": \"Feature\",\n", - " \"properties\": {\"property-1\": 5000, \"property-2\": 1},\n", + " \"properties\": {\"sample\": 5000, \"weight\": 1},\n", " \"geometry\": {\n", " \"type\": \"LineString\",\n", " \"coordinates\": [\n", @@ -91,6 +94,13 @@ "}" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GeoJSON Test Linestring Source" + ] + }, { "cell_type": "code", "execution_count": null, @@ -101,13 +111,11 @@ "source": [ "# make viz with GeoJSON source\n", "viz = LinestringViz(geojson, \n", - " color_property='property-1',\n", - " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'),\n", - " color_function_type='interpolate',\n", - " color_default='red',\n", + " color_property='sample',\n", + " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='Blues'),\n", " line_stroke='--',\n", - " line_width_property='property-2',\n", - " line_width_stops=create_radius_stops([0, 1, 2, 3, 4, 5], 0, 10),\n", + " line_width_property='weight',\n", + " line_width_stops=create_numeric_stops([0, 1, 2, 3, 4, 5], 0, 10),\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", @@ -120,7 +128,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Vector linestring source" + "## Vector Linestring Source (Topography)" ] }, { @@ -129,20 +137,6 @@ "metadata": {}, "outputs": [], "source": [ - "match_stops = [\n", - " [0, \"red\"], \n", - " [10, \"blue\"], \n", - " [20, \"green\"], \n", - " [30, \"yellow\"], \n", - " [40, \"pink\"],\n", - " [50, \"magenta\"],\n", - " [60, \"black\"],\n", - " [70, \"cyan\"],\n", - " [80, \"grey\"],\n", - " [90, \"brown\"],\n", - " [200, \"orange\"],\n", - "]\n", - "\n", "viz = LinestringViz(data, \n", " vector_url='mapbox://mapbox.mapbox-terrain-v2',\n", " vector_layer_name='contour',\n", @@ -150,12 +144,8 @@ " data_join_property='elevation',\n", " color_property='elevation',\n", " color_stops=create_color_stops([0, 25, 50, 75, 100], colors='YlOrRd'),\n", - " # color_stops=match_stops,\n", - " color_function_type='match', \n", - " line_width_property='something',\n", - " line_width_stops=create_radius_stops([0, 50, 100], 2, 6),\n", " line_stroke='-',\n", - " line_width_default=3,\n", + " line_width_default=2,\n", " opacity=0.8,\n", " center=(-122.48, 37.83),\n", " zoom=16,\n", @@ -163,52 +153,6 @@ " )\n", "viz.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# use defaults with geojson source (bare minimum args)\n", - "viz = LinestringViz(geojson)\n", - "\n", - "# important for visibility for this vector\n", - "viz.center = (-122.48, 37.83)\n", - "viz.zoom = 16\n", - "\n", - "# make viz\n", - "viz.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# use defaults with vector source (bare minimum args)\n", - "viz = LinestringViz(data, \n", - " vector_url='mapbox://mapbox.mapbox-terrain-v2',\n", - " vector_layer_name='contour',\n", - " vector_join_property='ele',\n", - " data_join_property='elevation',\n", - " )\n", - "\n", - "# important for visibility for this vector\n", - "viz.center = (-122.48, 37.83)\n", - "viz.zoom = 16\n", - "\n", - "# make viz\n", - "viz.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 370933f0af86f6327d678d5075dbe0fec5cbe38e Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Mon, 16 Apr 2018 22:38:53 -0700 Subject: [PATCH 14/16] Add line_width_property to vector LinestringViz test --- tests/test_html.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_html.py b/tests/test_html.py index 4fb7e5d..aa86dbf 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -269,7 +269,7 @@ def test_display_vector_LinestringViz(display): """Assert that show calls the mocked display function when using data-join technique for LinestringViz. """ - data = [{"elevation": x, "other": random.randint(0,100)} for x in range(0, 21000, 10)] + data = [{"elevation": x, "weight": random.randint(0,100)} for x in range(0, 21000, 10)] viz = LinestringViz(data, vector_url='mapbox://mapbox.mapbox-terrain-v2', @@ -278,6 +278,8 @@ def test_display_vector_LinestringViz(display): data_join_property='elevation', color_property="elevation", color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='YlOrRd'), + line_width_property='weight', + line_width_stops=create_numeric_stops([0, 25, 50, 75, 100], 1, 6), access_token=TOKEN ) viz.show() From 3e9fac4b9e8208556bd1bde6f4f9cd4d7807d530 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 21 Apr 2018 18:31:05 -0700 Subject: [PATCH 15/16] Update label style to match CircleViz properties --- mapboxgl/templates/linestring.html | 5 +++-- mapboxgl/templates/vector_linestring.html | 5 +++-- mapboxgl/viz.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index c654ec9..10b5b04 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -79,8 +79,9 @@ "text-offset": [0,-1] }, "paint": { - "text-halo-color": "white", - "text-halo-width": 1 + "text-halo-color": "{{ labelHaloColor }}", + "text-halo-width": generatePropertyExpression('interpolate', 'zoom', [[0,{{ labelHaloWidth }}], [18,5* {{ labelHaloWidth }}]]), + "text-color": "{{ labelColor }}" } }, "{{belowLayer}}" ); diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 55da9de..9b099f0 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -81,8 +81,9 @@ "text-offset": [0,-1] }, "paint": { - "text-halo-color": "white", - "text-halo-width": 1 + "text-halo-color": "{{ labelHaloColor }}", + "text-halo-width": generatePropertyExpression('interpolate', 'zoom', [[0,{{ labelHaloWidth }}], [18,5* {{ labelHaloWidth }}]]), + "text-color": "{{ labelColor }}" }, filter: layerFilter }, "{{belowLayer}}" ); diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 846c2e0..d5b8ba1 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -616,6 +616,10 @@ def __init__(self, vector_join_property=None, data_join_property=None, label_property=None, + label_size=8, + label_color='#131516', + label_halo_color='white', + label_halo_width=1, color_property=None, color_stops=None, color_default='grey', @@ -635,6 +639,10 @@ def __init__(self, :param vector_join_property: property to aid in determining color for styling vector lines :param data_join_property: property to join json data to vector features :param label_property: property to use for marker label + :param label_size: size of label text + :param label_color: color of label text + :param label_halo_color: color of label text halo + :param label_halo_width: width of label text halo :param color_property: property to determine line color :param color_stops: property to determine line color :param color_default: property to determine default line color if match lookup fails @@ -661,6 +669,10 @@ def __init__(self, self.template = 'linestring' self.label_property = label_property + self.label_color = label_color + self.label_size = label_size + self.label_halo_color = label_halo_color + self.label_halo_width = label_halo_width self.color_property = color_property self.color_stops = color_stops self.color_default = color_default @@ -730,6 +742,10 @@ def add_unique_template_variables(self, options): widthProperty=self.line_width_property, widthType=self.line_width_function_type, defaultWidth=self.line_width_default, + labelColor=self.label_color, + labelSize=self.label_size, + labelHaloColor=self.label_halo_color, + labelHaloWidth=self.label_halo_width )) # vector-based linestring map variables From 9bf8311ddaed4b158605e104f8e388ad31739fe4 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Wed, 25 Apr 2018 16:32:45 -0700 Subject: [PATCH 16/16] Add linestring viz docs --- docs-markdown/viz.md | 72 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs-markdown/viz.md b/docs-markdown/viz.md index 25133b5..5422dbe 100644 --- a/docs-markdown/viz.md +++ b/docs-markdown/viz.md @@ -458,4 +458,74 @@ viz.show() ![RasterTilesViz](https://user-images.githubusercontent.com/10407788/37537676-b055a108-2924-11e8-94cb-ad3203b736af.jpg) -[Complete example](https://github.com/mapbox/mapboxgl-jupyter/blob/master/examples/rastertile-viz-types-example.ipynb) \ No newline at end of file +[Complete example](https://github.com/mapbox/mapboxgl-jupyter/blob/master/examples/rastertile-viz-types-example.ipynb) + + +## class LinestringViz + +The `LinestringViz` object handles the creation of a vector or GeoJSON-based Linestring visualization and inherits from the `MapViz` class. + +### Params +**LinestringViz**(_data, vector_url=None, vector_layer_name=None, vector_join_property=None, data_join_property=None, label_property=None, label_size=8, label_color='#131516', label_halo_color='white', label_halo_width=1, color_property=None, color_stops=None, color_default='grey', color_function_type='interpolate', line_stroke='solid', line_width_property=None, line_width_stops=None, line_width_default=1, line_width_function_type='interpolate', *args, **kwargs_) + + +Parameter | Description | Example +--|--|-- +data | can be either GeoJSON (containing polygon features) or JSON for data-join technique with vector polygons | +vector_url | optional property to define vector linestring source | "mapbox://mapbox.mapbox-terrain-v2" +vector_layer_name | property to define target layer of vector source if using vector linestring source | "contour" +vector_join_property | property to aid in determining color for styling vector lines | "ele" +data_join_property | property of json data to use as link to vector features | "elevation" +label_property | property to use for marker label | "elevation" +label_size | size of label text | 8 +label_color | color of label text | '#131516' +label_halo_color | color of label text halo | 'white' +label_halo_width | width of label text halo | 1 +color_property | property to determine line color | "elevation" +color_stops | property to determine line color | [[0, "red"], [0.5, "blue"], [1, "green"]] +color_default | property to determine default line color if match lookup fails | "#F0F0F0" +color_function_type | property to determine type of expression used by Mapbox to assign color | "interpolate" +line_stroke | property to determine line stroke (one of solid (-), dashed (--), dotted (:), dash dot (-.)) | "solid" or "-" +line_width_property | feature property for determining line width | "elevation" +line_width_stops | property to determine line width | [[0, 1], [50000, 2], [150000, 3]] +line_width_default | property to determine default line width if match lookup fails | 1.0 +line_width_function_type | property to determine `type` used by Mapbox to assign line width | "interpolate" + +[MapViz options](https://github.com/mapbox/mapboxgl-jupyter/blob/master/docs-markdown/viz.md#params) + +### Usage +```python +import random +import os + +from mapboxgl.viz import LinestringViz +from mapboxgl.utils import create_color_stops + +# Must be a public token, starting with `pk` +token = os.getenv('MAPBOX_ACCESS_TOKEN') + +# JSON join-data object +data = [{"elevation": x, "weight": random.randint(0,100)} for x in range(0, 21000, 10)] + +viz = LinestringViz(data, + vector_url='mapbox://mapbox.mapbox-terrain-v2', + vector_layer_name='contour', + vector_join_property='ele', + data_join_property='elevation', + color_property='elevation', + color_stops=create_color_stops([0, 25, 50, 75, 100], colors='YlOrRd'), + line_stroke='-', + line_width_default=2, + opacity=0.8, + center=(-122.48, 37.83), + zoom=16, + below_layer='waterway-label' + ) +viz.show() +``` + +![LinestringViz](https://user-images.githubusercontent.com/13527707/39278071-02b6b2fc-48a6-11e8-8492-ae1f991b4b9e.png) + + +[Complete example](https://github.com/mapbox/mapboxgl-jupyter/blob/master/examples/notebooks/linestring-viz.ipynb) +