From 03db8aaca10a181db40390bfdcd4d01ae60d6323 Mon Sep 17 00:00:00 2001 From: akacarlyann Date: Sat, 14 Apr 2018 18:55:13 -0700 Subject: [PATCH] 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])