Skip to content

Commit

Permalink
Refine linestring iz with data-driven styles for line width; update v…
Browse files Browse the repository at this point in the history
…iz arguments; refine default color and line width handling
  • Loading branch information
akacarlyann committed Apr 15, 2018
1 parent d6014e1 commit 3d6b878
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 33 deletions.
29 changes: 24 additions & 5 deletions mapboxgl/templates/linestring.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@
<!-- update legend item key style -->
{% block extra_css %}
<style type='text/css'>
.legend div span { border-radius: 20%; }
.legend div span { border-radius: 20%; height: 4px; margin-bottom: 3px; }
</style>
{% 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 %}

Expand Down Expand Up @@ -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 }}" );
Expand Down
37 changes: 31 additions & 6 deletions mapboxgl/templates/vector_linestring.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"]);
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -86,7 +103,15 @@
}

// Add property from joined data to vector's feature popup
popup_html += '<li><b> ' + "{{ colorProperty }}".toUpperCase() + '</b>: ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + ' </li>'
{% if colorProperty %}
popup_html += '<li><b> ' + "{{ colorProperty }}".toUpperCase() + '</b>: ' + popUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + ' </li>'
{% endif %}

{% if widthProperty %}
{% if colorProperty != widthProperty %}
popup_html += '<li><b> ' + "{{ widthProperty }}".toUpperCase() + '</b>: ' + lineWidthPopUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + ' </li>'
{% endif %}
{% endif %}

popup_html += '</div>'
popup.setLngLat(e.lngLat)
Expand Down
137 changes: 115 additions & 22 deletions mapboxgl/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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"""

Expand All @@ -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)
Expand All @@ -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"""
Expand All @@ -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", "--"]:
Expand All @@ -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),
Expand Down

0 comments on commit 3d6b878

Please sign in to comment.