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)
+
diff --git a/examples/notebooks/linestring-viz.ipynb b/examples/notebooks/linestring-viz.ipynb
new file mode 100644
index 0000000..44660d0
--- /dev/null
+++ b/examples/notebooks/linestring-viz.ipynb
@@ -0,0 +1,179 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Mapboxgl Python Library for location data visualizaiton\n",
+ "\n",
+ "https://github.com/mapbox/mapboxgl-jupyter\n",
+ "\n",
+ "\n",
+ "# Linestring Visualizations"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random\n",
+ "import os\n",
+ "\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, \"weight\": 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\": {\"sample\": 50, \"weight\": 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\": {\"sample\": 500, \"weight\": 2},\n",
+ " \"geometry\": {\n",
+ " \"type\": \"LineString\",\n",
+ " \"coordinates\": [\n",
+ " [-122.4833858013153, 37.929607404976734],\n",
+ " [-122.4830961227417, 37.83]\n",
+ " ]\n",
+ " }\n",
+ " }, {\n",
+ " \"type\": \"Feature\",\n",
+ " \"properties\": {\"sample\": 5000, \"weight\": 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",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## GeoJSON Test Linestring Source"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [],
+ "source": [
+ "# make viz with GeoJSON source\n",
+ "viz = LinestringViz(geojson, \n",
+ " color_property='sample',\n",
+ " color_stops=create_color_stops([0, 50, 100, 500, 1500], colors='Blues'),\n",
+ " line_stroke='--',\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",
+ " below_layer='waterway-label'\n",
+ " )\n",
+ "viz.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Vector Linestring Source (Topography)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "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=create_color_stops([0, 25, 50, 75, 100], colors='YlOrRd'),\n",
+ " line_stroke='-',\n",
+ " line_width_default=2,\n",
+ " opacity=0.8,\n",
+ " center=(-122.48, 37.83),\n",
+ " zoom=16,\n",
+ " below_layer='waterway-label'\n",
+ " )\n",
+ "viz.show()"
+ ]
+ }
+ ],
+ "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
+}
diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html
new file mode 100644
index 0000000..10b5b04
--- /dev/null
+++ b/mapboxgl/templates/linestring.html
@@ -0,0 +1,132 @@
+{% extends "base.html" %}
+
+
+{% block extra_css %}
+
+{% endblock extra_css %}
+
+{% block legend %}
+
+ var legend = document.getElementById('legend');
+
+ {% 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 %}
+
+{% 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 %}
+ {% 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-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": "{{ labelHaloColor }}",
+ "text-halo-width": generatePropertyExpression('interpolate', 'zoom', [[0,{{ labelHaloWidth }}], [18,5* {{ labelHaloWidth }}]]),
+ "text-color": "{{ labelColor }}"
+ }
+ }, "{{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..9b099f0
--- /dev/null
+++ b/mapboxgl/templates/vector_linestring.html
@@ -0,0 +1,123 @@
+{% extends "linestring.html" %}
+
+{% block linestring %}
+
+ // extract JSON property used for data-driven styling to add to popup
+ {% if joinData %}
+
+ let joinData = {{ joinData }};
+ var popUpKeys = {},
+ lineWidthPopUpKeys = {};
+
+ // Create filter for layers from join data
+ let layerFilter = ['in', "{{ vectorJoinDataProperty }}"]
+
+ joinData.forEach(function(row, index) {
+
+ {% if colorProperty %}
+ popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"];
+ {% endif %}
+
+ {% if widthProperty %}
+ {% if colorProperty != widthProperty %}
+ lineWidthPopUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ widthProperty }}"];
+ {% endif %}
+ {% endif %}
+
+ 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": {
+ "type": "categorical",
+ "property": "{{ vectorJoinDataProperty }}",
+ "stops": {{ vectorColorStops }},
+ "default": "{{ defaultColor }}"
+ },
+ "line-width": {
+ "type": "categorical",
+ "property": "{{ vectorJoinDataProperty }}",
+ "stops": {{ vectorWidthStops }},
+ "default": {{ defaultWidth }}
+ },
+
+ "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": "{{ labelHaloColor }}",
+ "text-halo-width": generatePropertyExpression('interpolate', 'zoom', [[0,{{ labelHaloWidth }}], [18,5* {{ labelHaloWidth }}]]),
+ "text-color": "{{ labelColor }}"
+ },
+ 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
+ {% 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)
+ .setHTML(popup_html)
+ .addTo(map);
+ });
+
+{% endblock linestring_popup %}
diff --git a/mapboxgl/utils.py b/mapboxgl/utils.py
index 73bdcad..a6f2019 100644
--- a/mapboxgl/utils.py
+++ b/mapboxgl/utils.py
@@ -250,6 +250,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 2b28d90..d5b8ba1 100644
--- a/mapboxgl/viz.py
+++ b/mapboxgl/viz.py
@@ -8,7 +8,7 @@
from mapboxgl.errors import TokenError
from mapboxgl.utils import color_map, height_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'
@@ -604,3 +604,171 @@ 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,
+ 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):
+ """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 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 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
+ :param color_function_type: property to determine `type` used by Mapbox to assign color
+ :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)
+
+ 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.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
+ self.color_function_type = color_function_type
+ self.line_stroke = line_stroke
+ 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"""
+ 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 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 = 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])
+
+ return vector_stops
+
+ def add_unique_template_variables(self, options):
+ """Update map template variables specific to linestring 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 linestring maps
+ options.update(dict(
+ colorStops=self.color_stops,
+ colorProperty=self.color_property,
+ colorType=self.color_function_type,
+ defaultColor=self.color_default,
+ lineColor=self.color_default,
+ lineDashArray=self.line_dash_array,
+ lineStroke=self.line_stroke,
+ widthStops=self.line_width_stops,
+ 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
+ if self.vector_source:
+ options.update(dict(
+ vectorUrl=self.vector_url,
+ vectorLayer=self.vector_layer_name,
+ 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()))
+
+ 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),
+ ))
+
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 6f4ca41..aa86dbf 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
@@ -221,6 +252,40 @@ def test_display_vector_extruded_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, "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, 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()
+ 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 687bb1d..fb876ca 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -8,13 +8,14 @@
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()
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,6 +160,36 @@ 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
+
+
def test_create_numeric_stops():
"""Create numeric stops from custom breaks"""
domain = [7678.214347826088, 5793.63142857143, 1200]
@@ -187,4 +224,4 @@ def test_height_map_default():
def test_height_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 height_map(50.0, stops, 42) == 5000.0
\ No newline at end of file
+ assert height_map(50.0, stops, 42) == 5000.0