diff --git a/examples/notebooks/multi-layer-example.ipynb b/examples/notebooks/multi-layer-example.ipynb new file mode 100644 index 0000000..1d1980b --- /dev/null +++ b/examples/notebooks/multi-layer-example.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import pandas as pd\n", + "\n", + "from mapboxgl.map import Map\n", + "from mapboxgl.layers import CircleLayer, GraduatedCircleLayer\n", + "from mapboxgl.utils import *\n", + "\n", + "\n", + "# Must be a public token, starting with `pk`\n", + "token = os.getenv('MAPBOX_ACCESS_TOKEN')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load data from sample csv\n", + "data_url = 'https://raw.githubusercontent.com/mapbox/mapboxgl-jupyter/master/examples/data/points.csv'\n", + "df = pd.read_csv(data_url).round(3)\n", + "\n", + "df_to_geojson(df, \n", + " filename='../data/healthcare_points.geojson',\n", + " properties=['Avg Medicare Payments', 'Avg Covered Charges', 'date'], \n", + " lat='lat', \n", + " lon='lon', \n", + " precision=3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "measure_color = 'Avg Covered Charges'\n", + "color_breaks = [round(df[measure_color].quantile(q=x*0.1), 2) for x in range(2, 10)]\n", + "color_stops = create_color_stops(color_breaks, colors='Blues')\n", + "\n", + "# Generate radius breaks from data domain and circle-radius range\n", + "measure_radius = 'Avg Medicare Payments'\n", + "radius_breaks = [round(df[measure_radius].quantile(q=x*0.1), 2) for x in range(2, 10)]\n", + "radius_stops = create_radius_stops(radius_breaks, 0.5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Just show a map of the data\n", + "mv = Map(\n", + " access_token=token,\n", + " opacity=0.8,\n", + " center=(-96, 37.8),\n", + " zoom=3)\n", + "\n", + "viz = CircleLayer(\n", + " '../data/healthcare_points.geojson', \n", + " access_token=token, \n", + " radius=2)\n", + "\n", + "mv.add_layer(viz)\n", + "\n", + "mv.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Just show a map of the data\n", + "mv = Map(\n", + " access_token=token,\n", + " opacity=0.8,\n", + " center=(-96, 37.8),\n", + " zoom=3)\n", + "\n", + "viz = CircleLayer(\n", + " '../data/healthcare_points.geojson', \n", + " access_token=token, \n", + " radius=2)\n", + "\n", + "mv.add_layer(viz)\n", + "\n", + "viz2 = GraduatedCircleLayer(\n", + " '../data/healthcare_points.geojson', \n", + " access_token=token,\n", + " color_property=\"Avg Covered Charges\",\n", + " color_stops=color_stops,\n", + " radius_property=\"Avg Medicare Payments\",\n", + " radius_stops=radius_stops,\n", + " stroke_color='black',\n", + " stroke_width=0.5,\n", + " opacity=0.5,\n", + " below_layer='waterway-label')\n", + "\n", + "mv.add_layer(viz2)\n", + "\n", + "mv.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mv = Map(\n", + " access_token=token,\n", + " opacity=0.8,\n", + " center=(-96, 37.8),\n", + " zoom=3)\n", + "\n", + "mv.add_layer(viz2)\n", + "mv.add_layer(viz)\n", + "\n", + "mv.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.5.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/mapboxgl/__init__.py b/mapboxgl/__init__.py index 0efa1e2..79c4079 100644 --- a/mapboxgl/__init__.py +++ b/mapboxgl/__init__.py @@ -1,4 +1,44 @@ -from .viz import CircleViz, GraduatedCircleViz, HeatmapViz, ClusteredCircleViz, ImageViz, RasterTilesViz, ChoroplethViz, LinestringViz +from .viz import ( + CircleViz, + GraduatedCircleViz, + HeatmapViz, + ClusteredCircleViz, + ImageViz, + RasterTilesViz, + ChoroplethViz, + LinestringViz, +) + +from .layers import ( + CircleLayer, + GraduatedCircleLayer, + HeatmapLayer, + ClusteredCircleLayer, + ImageLayer, + RasterTilesLayer, + ChoroplethLayer, + LinestringLayer, +) + +from .map import Map __version__ = "0.9.0" -__all__ = ['CircleViz', 'GraduatedCircleViz', 'HeatmapViz', 'ClusteredCircleViz', 'ImageViz', 'RasterTilesViz', 'ChoroplethViz', 'LinestringViz'] +__all__ = [ + 'CircleViz', + 'GraduatedCircleViz', + 'HeatmapViz', + 'ClusteredCircleViz', + 'ImageViz', + 'RasterTilesViz', + 'ChoroplethViz', + 'LinestringViz', + 'CircleLayer', + 'GraduatedCircleLayer', + 'HeatmapLayer', + 'ClusteredCircleLayer', + 'ImageLayer', + 'RasterTilesLayer', + 'ChoroplethLayer', + 'LinestringLayer', + 'Map', +] \ No newline at end of file diff --git a/mapboxgl/layers.py b/mapboxgl/layers.py new file mode 100644 index 0000000..a645bfe --- /dev/null +++ b/mapboxgl/layers.py @@ -0,0 +1,745 @@ +import json + +import numpy + +from mapboxgl.utils import color_map, height_map +from mapboxgl import templates +from mapboxgl.utils import img_encode, numeric_map + + +class AbstractLayer(): + """Abstract class for layers""" + + def __init__(self, + data, + opacity=1, + below_layer='', + legend=True, + legend_layout='vertical', + legend_gradient=False, + legend_text_numeric_precision=None, + legend_key_shape='square', + min_zoom=0, + max_zoom=24, + *args, + **kwargs): + """Abstract class for layers + + :param opacity: opacity of map data layer + :param below_layer: the layer to put this one below + :param legend: boolean for whether to show legend on map + :param legend_layout: determines if horizontal or vertical legend used + :param legend_gradient: boolean to determine if legend keys are discrete or gradient + :param legend_fill: string background color for legend, default is white + :param legend_header_fill: string background color for legend header (in vertical layout), default is #eee + :param legend_text_numeric_precision: decimal precision for numeric legend values + :param legend_key_shape: shape of the legend item keys, default varies by viz type; one of square, contiguous_bar, rounded-square, circle, line + """ + self.data = data + self.opacity = opacity + self.below_layer = below_layer + self.legend = legend + self.legend_layout = legend_layout + self.legend_gradient = legend_gradient + self.legend_text_numeric_precision = legend_text_numeric_precision + self.legend_key_shape = legend_key_shape + self.min_zoom = min_zoom + self.max_zoom = max_zoom + + self.layer_id = None + self.template = None + self.label_property = None + + def create_html(self): + options = dict( + opacity=self.opacity, + belowLayer=self.below_layer, + showLegend=self.legend, + legendLayout=self.legend_layout, + legendGradient=json.dumps(self.legend_gradient), + legendNumericPrecision=json.dumps(self.legend_text_numeric_precision), + legendKeyShape=self.legend_key_shape, + layerId=self.layer_id, + template=self.template, + minZoom=self.min_zoom, + maxZoom=self.max_zoom + ) + + if self.label_property is None: + options.update(labelProperty=None) + else: + options.update(labelProperty='{' + self.label_property + '}') + + self.get_layer_specific_variables(options) + + return templates.format(self.template, **options) + + def get_layer_specific_variables(self, options): + pass + + +class CircleLayer(AbstractLayer): + """Create a circle layer""" + + def __init__(self, + data, + label_property=None, + label_size=8, + label_color='#131516', + label_halo_color='white', + label_halo_width=1, + radius=1, + color_property=None, + color_stops=None, + color_default='grey', + color_function_type='interpolate', + stroke_color='grey', + stroke_width=0.1, + legend_key_shape='circle', + *args, + **kwargs): + """Construct a Mapviz object + + :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 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 radius: radius of circle + :param stroke_color: color of circle stroke outline + :param stroke_width: with of circle stroke outline + + """ + super(CircleLayer, self).__init__(data, *args, **kwargs) + + self.template = 'circle' + 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.radius = radius + self.stroke_color = stroke_color + self.stroke_width = stroke_width + self.color_function_type = color_function_type + self.color_default = color_default + self.legend_key_shape = legend_key_shape + + def get_layer_specific_variables(self, options): + """Update map template variables specific to circle visual""" + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + colorProperty=self.color_property, + colorType=self.color_function_type, + colorStops=self.color_stops, + strokeWidth=self.stroke_width, + strokeColor=self.stroke_color, + radius=self.radius, + defaultColor=self.color_default, + labelColor=self.label_color, + labelSize=self.label_size, + labelHaloColor=self.label_halo_color, + labelHaloWidth=self.label_halo_width, + )) + + +class GraduatedCircleLayer(AbstractLayer): + """Create a graduated circle layer""" + + def __init__(self, + data, + 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', + stroke_color='grey', + stroke_width=0.1, + radius_property=None, + radius_stops=None, + radius_default=2, + radius_function_type='interpolate', + legend_key_shape='circle', + *args, + **kwargs): + """Construct a Mapviz object + + :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 radius_property: property to determine circle radius + :param radius_stops: property to determine circle radius + :param radius_default: property to determine default circle radius if match lookup fails + :param radius_function_type: property to determine `type` used by Mapbox to assign radius size + :param stroke_color: color of circle stroke outline + :param stroke_width: with of circle stroke outline + + """ + super(GraduatedCircleLayer, self).__init__(data, *args, **kwargs) + + self.template = 'graduated_circle' + 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.radius_property = radius_property + self.radius_stops = radius_stops + self.color_function_type = color_function_type + self.color_default = color_default + self.radius_function_type = radius_function_type + self.radius_default = radius_default + self.stroke_color = stroke_color + self.stroke_width = stroke_width + self.legend_key_shape = legend_key_shape + + def get_layer_specific_variables(self, options): + """Update map template variables specific to graduated circle visual""" + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + colorProperty=self.color_property, + colorStops=self.color_stops, + colorType=self.color_function_type, + radiusType=self.radius_function_type, + defaultColor=self.color_default, + defaultRadius=self.radius_default, + radiusProperty=self.radius_property, + radiusStops=self.radius_stops, + strokeWidth=self.stroke_width, + strokeColor=self.stroke_color, + labelColor=self.label_color, + labelSize=self.label_size, + labelHaloColor=self.label_halo_color, + labelHaloWidth=self.label_halo_width + )) + + +class HeatmapLayer(AbstractLayer): + """Create a heatmap layer""" + + def __init__(self, + data, + label_property=None, + weight_property=None, + weight_stops=None, + color_stops=None, + radius_stops=None, + intensity_stops=None, + *args, + **kwargs): + """Create a heatmap layer + + :param weight_property: property to determine heatmap weight. EX. "population" + :param weight_stops: stops to determine heatmap weight. EX. [[10, 0], [100, 1]] + :param color_stops: stops to determine heatmap color. EX. [[0, "red"], [0.5, "blue"], [1, "green"]] + :param radius_stops: stops to determine heatmap radius based on zoom. EX: [[0, 1], [12, 30]] + :param intensity_stops: stops to determine the heatmap intensity based on zoom. EX: [[0, 0.1], [20, 5]] + + """ + super(HeatmapLayer, self).__init__(data, *args, **kwargs) + + self.template = 'heatmap' + self.label_property = label_property + self.weight_property = weight_property + self.weight_stops = weight_stops + if color_stops: + # Make the first color stop in a heatmap have opacity 0 for good visual effect + self.color_stops = [[0.00001, 'rgba(0,0,0,0)']] + color_stops + self.radius_stops = radius_stops + self.intensity_stops = intensity_stops + + def get_layer_specific_variables(self, options): + """Update map template variables specific to heatmap visual""" + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + colorStops=self.color_stops, + radiusStops=self.radius_stops, + weightProperty=self.weight_property, + weightStops=self.weight_stops, + intensityStops=self.intensity_stops, + )) + + +class ClusteredCircleLayer(AbstractLayer): + """Create a clustered circle layer""" + + def __init__(self, + data, + label_property=None, + label_size=8, + label_color='#131516', + label_halo_color='white', + label_halo_width=1, + color_stops=None, + radius_stops=None, + cluster_radius=30, + cluster_maxzoom=14, + radius_default=2, + color_default='black', + stroke_color='grey', + stroke_width=0.1, + legend_key_shape='circle', + *args, + **kwargs): + """Construct a Mapviz object + + :param color_property: property to determine circle color + :param color_stops: property to determine circle color + :param radius_property: property to determine circle radius + :param radius_stops: property to determine circle radius + :param stroke_color: color of circle stroke outline + :param stroke_width: with of circle stroke outline + :param radius_default: radius of circles not contained in a cluster + :param color_default: color of circles not contained in a cluster + + """ + super(ClusteredCircleLayer, self).__init__(data, *args, **kwargs) + + self.template = 'clustered_circle' + 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_stops = color_stops + self.radius_stops = radius_stops + self.clusterRadius = cluster_radius + self.clusterMaxZoom = cluster_maxzoom + self.radius_default = radius_default + self.color_default = color_default + self.stroke_color = stroke_color + self.stroke_width = stroke_width + self.color_default = color_default + self.legend_key_shape = legend_key_shape + + def get_layer_specific_variables(self, options): + """Update map template variables specific to a clustered circle visual""" + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + colorStops=self.color_stops, + colorDefault=self.color_default, + radiusStops=self.radius_stops, + clusterRadius=self.clusterRadius, + clusterMaxZoom=self.clusterMaxZoom, + strokeWidth=self.stroke_width, + strokeColor=self.stroke_color, + radiusDefault=self.radius_default, + labelColor=self.label_color, + labelSize=self.label_size, + labelHaloColor=self.label_halo_color, + labelHaloWidth=self.label_halo_width + )) + + +class ChoroplethLayer(AbstractLayer): + """Create a choropleth layer""" + + 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, + height_property=None, + height_stops=None, + height_default=0.0, + height_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_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 polygon color + :param color_stops: property to determine polygon color + :param color_default: property to determine default polygon color if match lookup fails + :param color_function_type: property to determine `type` used by Mapbox to assign color + :param line_color: property to determine choropleth line color + :param line_stroke: property to determine choropleth line stroke (solid, dashed, dotted, dash dot) + :param line_width: property to determine choropleth line width + :param height_property: feature property for determining polygon height in 3D extruded choropleth map + :param height_stops: property for determining 3D extrusion height + :param height_default: default height for 3D extruded polygons + :param height_function_type: roperty to determine `type` used by Mapbox to assign height + """ + super(ChoroplethLayer, 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_choropleth' + self.vector_source = True + else: + self.template = 'choropleth' + self.vector_source = False + + 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 + self.height_property = height_property + self.height_stops = height_stops + self.height_default = height_default + self.height_function_type = height_function_type + + def generate_vector_color_map(self): + """Generate color stops array for use with match expression in mapbox template""" + 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_height_map(self): + """Generate height stops array for use with match expression in mapbox template""" + vector_stops = [] + + if self.height_function_type == 'match': + match_height = self.height_stops + + for row in self.data: + # map height to JSON feature using height_property + height = height_map(row[self.height_property], self.height_stops, self.height_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], height]) + + return vector_stops + + def get_layer_specific_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] + + # check if choropleth map should include 3-D extrusion + self.extrude = all([bool(self.height_property), bool(self.height_stops)]) + + # common variables for vector and geojson-based choropleths + options.update(dict( + colorStops=self.color_stops, + 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, + extrudeChoropleth=self.extrude + )) + if self.extrude: + options.update(dict( + heightType=self.height_function_type, + heightProperty=self.height_property, + heightStops=self.height_stops, + defaultHeight=self.height_default, + )) + + # vector-based choropleth map variables + if self.vector_source: + options.update(dict( + vectorUrl=self.vector_url, + vectorLayer=self.vector_layer_name, + vectorColorStops=self.generate_vector_color_map(), + vectorJoinDataProperty=self.vector_join_property, + joinData=json.dumps(self.data, ensure_ascii=False), + dataJoinProperty=self.data_join_property, + )) + if self.extrude: + options.update(dict( + vectorHeightStops=self.generate_vector_height_map(), + )) + + # geojson-based choropleth map variables + else: + options.update(dict( + geojson_data=json.dumps(self.data, ensure_ascii=False), + )) + + +class ImageLayer(AbstractLayer): + """Create a image viz""" + + def __init__(self, + image, + coordinates, + legend=False, + *args, + **kwargs): + """Construct a Mapviz object + + :param coordinates: property to determine image coordinates (UL, UR, LR, LL). + EX. [[-80.425, 46.437], [-71.516, 46.437], [-71.516, 37.936], [-80.425, 37.936]] + :param image: url, local path or a numpy ndarray + :param legend: default setting is to hide heatmap legend + + """ + super(ImageLayer, self).__init__(None, *args, **kwargs) + + if type(image) is numpy.ndarray: + image = img_encode(image) + + self.template = 'image' + self.image = image + self.coordinates = coordinates + + def get_layer_specific_variables(self, options): + """Update map template variables specific to image visual""" + options.update(dict( + image=self.image, + coordinates=self.coordinates)) + + +class RasterTilesLayer(AbstractLayer): + """Create a rastertiles map""" + + def __init__(self, + tiles_url, + tiles_size=256, + tiles_bounds=None, + tiles_minzoom=0, + tiles_maxzoom=22, + legend=False, + *args, + **kwargs): + """Construct a Mapviz object + + :param tiles_url: property to determine tiles url endpoint + :param tiles_size: property to determine displayed tiles size + :param tiles_bounds: property to determine the tiles endpoint bounds + :param tiles_minzoom: property to determine the tiles endpoint min zoom + :param tiles_max: property to determine the tiles endpoint max zoom + :param legend: default setting is to hide heatmap legend + + """ + super(RasterTilesLayer, self).__init__(None, *args, **kwargs) + + self.template = 'raster' + self.tiles_url = tiles_url + self.tiles_size = tiles_size + self.tiles_bounds = tiles_bounds + self.tiles_minzoom = tiles_minzoom + self.tiles_maxzoom = tiles_maxzoom + + def get_layer_specific_variables(self, options): + """Update map template variables specific to a raster visual""" + options.update(dict( + tiles_url=self.tiles_url, + tiles_size=self.tiles_size, + tiles_minzoom=self.tiles_minzoom, + tiles_maxzoom=self.tiles_maxzoom, + tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) + + +class LinestringLayer(AbstractLayer): + """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', + legend_key_shape='line', + *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(LinestringLayer, 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 + self.legend_key_shape = legend_key_shape + + 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 get_layer_specific_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/mapboxgl/map.py b/mapboxgl/map.py new file mode 100644 index 0000000..cf394e6 --- /dev/null +++ b/mapboxgl/map.py @@ -0,0 +1,165 @@ +import codecs +import json +import os + +from IPython.core.display import HTML, display + +from mapboxgl.errors import TokenError +from mapboxgl import templates + + +GL_JS_VERSION = 'v0.49.0' + + +class Map(object): + + def __init__(self, + access_token=None, + center=(0, 0), + opacity=1, + div_id='map', + height='500px', + style='mapbox://styles/mapbox/light-v9?optimize=true', + width='100%', + zoom=0, + min_zoom=0, + max_zoom=24, + pitch=0, + bearing=0, + box_zoom_on=True, + double_click_zoom_on=True, + scroll_zoom_on=True, + touch_zoom_on=True, + legend_fill='white', + legend_header_fill='white', + legend_text_color='#6e6e6e', + legend_title_halo_color='white', + legend_key_borders_on=True + ): + """Construct a MapViz object + + :param access_token: Mapbox GL JS access token. + :param center: map center point + :param style: url to mapbox style or stylesheet as a Python dictionary in JSON format + :param div_id: The HTML div id of the map container in the viz + :param width: The CSS width of the HTML div id in % or pixels. + :param height: The CSS height of the HTML map div in % or pixels. + :param zoom: starting zoom level for map + :param opacity: opacity of map data layer + :param pitch: starting pitch (in degrees) for map + :param bearing: starting bearing (in degrees) for map + :param box_zoom_on: boolean indicating if map can be zoomed to a region by dragging a bounding box + :param double_click_zoom_on: boolean indicating if map can be zoomed with double-click + :param scroll_zoom_on: boolean indicating if map can be zoomed with the scroll wheel + :param touch_zoom_on: boolean indicating if map can be zoomed with two-finger touch gestures + :param legend_fill: string background color for legend, default is white + :param legend_header_fill: string background color for legend header (in vertical layout), default is #eee + :param legend_text_color: string color for legend text default is #6e6e6e + :param legend_title_halo_color: color of legend title text halo + :param legend_key_borders_on: boolean for whether to show/hide legend key borders + + """ + if access_token is None: + access_token = os.environ.get('MAPBOX_ACCESS_TOKEN', '') + if access_token.startswith('sk'): + raise TokenError('Mapbox access token must be public (pk), not secret (sk). ' \ + 'Please sign up at https://www.mapbox.com/signup/ to get a public token. ' \ + 'If you already have an account, you can retreive your token at https://www.mapbox.com/account/.') + self.access_token = access_token + self.template = 'map' + self.div_id = div_id + self.width = width + self.height = height + self.style = style + self.center = center + self.zoom = zoom + self.opacity = opacity + self.label_property = None + self.min_zoom = min_zoom + self.max_zoom = max_zoom + self.pitch = pitch + self.bearing = bearing + self.box_zoom_on = box_zoom_on + self.double_click_zoom_on = double_click_zoom_on + self.scroll_zoom_on = scroll_zoom_on + self.touch_zoom_on = touch_zoom_on + self.legend_fill = legend_fill + self.legend_header_fill = legend_header_fill + self.legend_text_color = legend_text_color, + self.legend_title_halo_color = legend_title_halo_color + self.legend_key_borders_on = legend_key_borders_on + self.layer_id_counter = 0 + self.layers = [] + + def as_iframe(self, html_data): + """Build the HTML representation for the mapviz.""" + + srcdoc = html_data.replace('"', "'") + return (''.format( + div_id=self.div_id, + srcdoc=srcdoc, + width=self.width, + height=self.height)) + + def show(self, **kwargs): + # Load the HTML iframe + html = self.create_html(**kwargs) + map_html = self.as_iframe(html) + + # Display the iframe in the current jupyter notebook view + display(HTML(map_html)) + + def create_html(self, filename=None): + """Create a circle visual from a geojson data source""" + if isinstance(self.style, str): + style = "'{}'".format(self.style) + else: + style = self.style + options = dict( + gl_js_version=GL_JS_VERSION, + accessToken=self.access_token, + div_id=self.div_id, + style=style, + center=list(self.center), + zoom=self.zoom, + opacity=self.opacity, + minzoom=self.min_zoom, + maxzoom=self.max_zoom, + pitch=self.pitch, + bearing=self.bearing, + boxZoomOn=json.dumps(self.box_zoom_on), + doubleClickZoomOn=json.dumps(self.double_click_zoom_on), + scrollZoomOn=json.dumps(self.scroll_zoom_on), + touchZoomOn=json.dumps(self.touch_zoom_on), + legendFill=self.legend_fill, + legendHeaderFill=self.legend_header_fill, + legendTextColor=self.legend_text_color, + legendTitleHaloColor=self.legend_title_halo_color, + legendKeyBordersOn=json.dumps(self.legend_key_borders_on) + ) + + if self.label_property is None: + options.update(labelProperty=None) + else: + options.update(labelProperty='{' + self.label_property + '}') + + html = [] + html.append(templates.format(self.template, **options)) + for layer in self.layers: + html.append(layer.create_html()) + if filename: + with codecs.open(filename, "w", "utf-8-sig") as f: + f.write("\n".join(html)) + return None + else: + return "\n".join(html) + + def add_layer(self, layer): + self.layers.append(layer) + layer.layer_id = self.layer_id_counter + self.layer_id_counter +=1 + + def remove_layer(self, layer): + self.layers.remove(layer) + layer.show_legend = False diff --git a/mapboxgl/templates/base.html b/mapboxgl/templates/base.html deleted file mode 100644 index 2f664f7..0000000 --- a/mapboxgl/templates/base.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "main.html" %} - -{% block javascript %} - - mapboxgl.accessToken = '{{ accessToken }}'; - - var map = new mapboxgl.Map({ - container: 'map', - attributionControl: false, - style: {{ style }}, - center: {{ center }}, - zoom: {{ zoom }}, - pitch: {{ pitch }}, - bearing: {{ bearing }}, - scrollZoom: {{ scrollZoomOn|safe }}, - touchZoom: {{ touchZoomOn|safe }}, - doubleClickZoom: {{ doubleClickZoomOn|safe }}, - boxZoom: {{ boxZoomOn|safe }}, - transformRequest: (url, resourceType) => { - if ( url.slice(0,22) == 'https://api.mapbox.com' || - url.slice(0,26) == 'https://a.tiles.mapbox.com' || - url.slice(0,26) == 'https://b.tiles.mapbox.com' || - url.slice(0,26) == 'https://c.tiles.mapbox.com' || - url.slice(0,26) == 'https://d.tiles.mapbox.com') { - //Add Mapboxgl-Jupyter Plugin identifier for Mapbox API traffic - return { - url: [url.slice(0, url.indexOf("?")+1), "pluginName=PythonMapboxgl&", url.slice(url.indexOf("?")+1)].join('') - } - } - else { - //Do not transform URL for non Mapbox GET requests - return {url: url} - } - }, - }); - - {% block attribution %} - - map.addControl(new mapboxgl.AttributionControl({ compact: true })); - - {% endblock attribution %} - - {% block navigation %} - - map.addControl(new mapboxgl.NavigationControl()); - - {% endblock navigation %} - - {% block legend %}{% endblock legend %} - - {% block map %}{% endblock map %} - -{% endblock %} diff --git a/mapboxgl/templates/choropleth.html b/mapboxgl/templates/choropleth.html index 59fb005..9719264 100644 --- a/mapboxgl/templates/choropleth.html +++ b/mapboxgl/templates/choropleth.html @@ -1,99 +1,44 @@ -{% extends "base.html" %} +{% extends "layer.html" %} {% block extra_css %} - + .gradient.bordered, .legend-key.bordered { border: solid {{ lineColor }} {{ lineWidth }}px; } {% endblock extra_css %} -{% block legend %} - +{% block extra_javascript %} {% if showLegend %} {% if extrudeChoropleth %} {% if colorStops and colorProperty and heightProperty %} {% if colorProperty != heightProperty and extrudeChoropleth %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ heightProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ heightProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% else %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% endif %} {% else %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% endif %} -{% endblock legend %} - -{% block map %} - map.on('style.load', function() { {% block choropleth %} // Add geojson data source - map.addSource("data", { + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, "buffer": 1, "maxzoom": 14 }); - // Add data layer - map.addLayer({ - "id": "choropleth-fill", - "source": "data", - "type": "fill", - "paint": { - "fill-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), - "fill-opacity": {{ opacity }} - } - }, "{{ belowLayer }}" ); - - // Add border layer - map.addLayer({ - "id": "choropleth-line", - "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": "choropleth-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 }}" ); - // Optional extrusion layer {% if extrudeChoropleth %} map.addLayer({ - id: "choropleth-extrusion", - type: "fill-extrusion", - source: "data", + id: "choropleth-extrusion{{ layerId }}", + source: "data{{ layerId }}", + type: "fill-extrusion", paint: { "fill-extrusion-opacity": {{ opacity }}, "fill-extrusion-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), @@ -101,51 +46,139 @@ } }, "{{ belowLayer }}"); + {% else %} + + // Add data layer + map.addLayer({ + "id": "choropleth-fill{{ layerId }}", + "source": "data{{ layerId }}", + "type": "fill", + "paint": { + "fill-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), + "fill-opacity": {{ opacity }} + } + }, "{{ belowLayer }}" ); + + // Add border layer + map.addLayer({ + "id": "choropleth-line{{ layerId }}", + "source": "data{{ layerId }}", + "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": "choropleth-label{{ layerId }}", + "source": "data{{ layerId }}", + "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 }}" ); + {% endif %} {% endblock choropleth %} - // Create a popup - var popup = new mapboxgl.Popup({ - closeButton: false, - closeOnClick: false - }); - {% block choropleth_popup %} - // Show the popup on mouseover - map.on('mousemove', 'choropleth-fill', 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); - }); + {% if extrudeChoropleth %} - {% endblock choropleth_popup %} + // Create a popup + var popup{{ layerId }} = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false + }); - map.on('mouseleave', 'choropleth-fill', function() { - map.getCanvas().style.cursor = ''; - popup.remove(); - }); - - // Fly to on click - map.on('click', 'choropleth-fill', function(e) { - map.flyTo({ - center: e.lngLat, - zoom: map.getZoom() + 1 + // Show the popup on mouseover + map.on('mousemove', "choropleth-extrusion{{ layerId }}", 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{{ layerId }}.setLngLat(e.lngLat) + .setHTML(popup_html) + .addTo(map); + }); + 9 + map.on('mouseleave', "choropleth-extrusion{{ layerId }}", function() { + map.getCanvas().style.cursor = ''; + popup{{ layerId }}.remove(); }); - }); - }); + // Fly to on click + map.on('click', "choropleth-extrusion{{ layerId }}", function(e) { + map.flyTo({ + center: e.lngLat, + zoom: map.getZoom() + 1 + }); + }); -{% endblock map %} + {% else %} + + // Show the popup on mouseover + map.on('mousemove', "choropleth-fill{{ layerId }}", 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{{ layerId }}.setLngLat(e.lngLat) + .setHTML(popup_html) + .addTo(map); + }); + + + map.on('mouseleave', "choropleth-fill{{ layerId }}", function() { + map.getCanvas().style.cursor = ''; + popup{{ layerId }}.remove(); + }); + + // Fly to on click + map.on('click', "choropleth-fill{{ layerId }}", function(e) { + map.flyTo({ + center: e.lngLat, + zoom: map.getZoom() + 1 + }); + }); + + {% endif %} + + {% endblock choropleth_popup %} + + + + }); +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/circle.html b/mapboxgl/templates/circle.html index c9be63f..5004e9b 100644 --- a/mapboxgl/templates/circle.html +++ b/mapboxgl/templates/circle.html @@ -1,27 +1,20 @@ -{% extends "base.html" %} +{% extends "layer.html" %} {% block extra_css %} - {% endblock extra_css %} -{% block legend %} - +{% block extra_javascript %} {% if showLegend %} {% if colorStops and colorProperty %} - calcColorLegend({{ colorStops }} , "{{ colorProperty }}"); + calcColorLegend({{ colorStops }} , "{{ colorProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% endif %} - -{% endblock legend %} - -{% block map %} map.on('style.load', function() { - - map.addSource("data", { + + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, "buffer": 1, @@ -29,11 +22,11 @@ }); map.addLayer({ - "id": "label", - "source": "data", + "id": "label{{ layerId }}", + "source": "data{{ layerId }}", "type": "symbol", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "layout": { {% if labelProperty %} "text-field": "{{ labelProperty }}", @@ -49,11 +42,11 @@ }, "{{belowLayer}}" ) map.addLayer({ - "id": "circle", - "source": "data", + "id": "circle{{ layerId }}", + "source": "data{{ layerId }}", "type": "circle", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "paint": { {% if colorProperty %} "circle-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}" ), @@ -66,16 +59,16 @@ "circle-opacity" : {{ opacity }}, "circle-stroke-opacity" : {{ opacity }} } - }, "label"); + }, "label{{ layerId }}"); // Create a popup - var popup = new mapboxgl.Popup({ + var popup{{ layerId }} = new mapboxgl.Popup({ closeButton: false, closeOnClick: false }); // Show the popup on mouseover - map.on('mousemove', 'circle', function(e) { + map.on('mousemove', "circle{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; @@ -87,23 +80,22 @@ } popup_html += '' - popup.setLngLat(e.features[0].geometry.coordinates) + popup{{ layerId }}.setLngLat(e.features[0].geometry.coordinates) .setHTML(popup_html) .addTo(map); }); - map.on('mouseleave', 'circle', function() { + map.on('mouseleave', "circle{{ layerId }}", function() { map.getCanvas().style.cursor = ''; - popup.remove(); + popup{{ layerId }}.remove(); }); // Fly to on click - map.on('click', 'circle', function(e) { + map.on('click', "circle{{ layerId }}", function(e) { map.easeTo({ center: e.features[0].geometry.coordinates, zoom: map.getZoom() + 1 }); }); }); - -{% endblock map %} +{% endblock extra_javascript %} \ No newline at end of file diff --git a/mapboxgl/templates/clustered_circle.html b/mapboxgl/templates/clustered_circle.html index 5a6717e..364c17f 100644 --- a/mapboxgl/templates/clustered_circle.html +++ b/mapboxgl/templates/clustered_circle.html @@ -1,25 +1,19 @@ -{% extends "base.html" %} +{% extends "layer.html" %} {% block extra_css %} - {% endblock extra_css %} -{% block legend %} +{% block extra_javascript %} {% if showLegend %} - calcColorLegend({{ colorStops }}, "Point Density"); + calcColorLegend({{ colorStops }}, "Point Density", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} -{% endblock legend %} - -{% block map %} - map.on('style.load', function() { - - map.addSource("data", { + + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, //data from dataframe output to geojson "buffer": 0, @@ -30,11 +24,11 @@ }); map.addLayer({ - "id": "label", - "source": "data", + "id": "label{{ layerId }}", + "source": "data{{ layerId }}", "type": "symbol", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "layout": { "text-field": "{point_count_abbreviated}", "text-size" : generateInterpolateExpression('zoom', [[0, {{ labelSize }}],[22, 3* {{ labelSize }}]] ), @@ -47,11 +41,11 @@ }, "{{belowLayer}}" ) map.addLayer({ - "id": "circle-cluster", - "source": "data", + "id": "circle-cluster{{ layerId }}", + "source": "data{{ layerId }}", "type": "circle", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "filter": ["has", "point_count"], "paint": { "circle-color": generateInterpolateExpression( "point_count", {{ colorStops }} ), @@ -61,14 +55,14 @@ "circle-opacity" : {{ opacity }}, "circle-stroke-opacity" : {{ opacity }} } - }, "label"); + }, "label{{ layerId }}"); map.addLayer({ - "id": "circle-unclustered", - "source": "data", + "id": "circle-unclustered{{ layerId }}", + "source": "data{{ layerId }}", "type": "circle", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "filter": ["!has", "point_count"], "paint": { "circle-color": "{{ colorDefault }}", @@ -78,16 +72,16 @@ "circle-opacity" : {{ opacity }}, "circle-stroke-opacity" : {{ opacity }} } - }, "circle-cluster"); + }, "circle-cluster{{ layerId }}"); // Create a popup - var popup = new mapboxgl.Popup({ + var popup{{ layerId }} = new mapboxgl.Popup({ closeButton: false, closeOnClick: false }); // Show the popup on mouseover - map.on('mousemove', 'circle-unclustered', function(e) { + map.on('mousemove', "circle-unclustered{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; let popup_html = '
  • Location: ' + f.geometry.coordinates[0].toPrecision(6) + @@ -99,12 +93,12 @@ popup_html += '
  • ' - popup.setLngLat(e.features[0].geometry.coordinates) + popup{{ layerId }}.setLngLat(e.features[0].geometry.coordinates) .setHTML(popup_html) .addTo(map); }); - map.on('mousemove', 'circle-cluster', function(e) { + map.on('mousemove', "circle-cluster{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; let popup_html = '
  • Location: ' + f.geometry.coordinates[0].toPrecision(6) + @@ -114,30 +108,30 @@ popup_html += '
  • ' - popup.setLngLat(e.features[0].geometry.coordinates) + popup{{ layerId }}.setLngLat(e.features[0].geometry.coordinates) .setHTML(popup_html) .addTo(map); }); - map.on('mouseleave', 'circle-unclustered', function() { + map.on('mouseleave', "circle-unclustered{{ layerId }}", function() { map.getCanvas().style.cursor = ''; - popup.remove(); + popup{{ layerId }}.remove(); }); - map.on('mouseleave', 'circle-cluster', function() { + map.on('mouseleave', "circle-cluster{{ layerId }}", function() { map.getCanvas().style.cursor = ''; - popup.remove(); + popup{{ layerId }}.remove(); }); - map.on('click', 'circle-unclustered', function(e) { + map.on('click', "circle-unclustered{{ layerId }}", function(e) { map.easeTo({ center: e.features[0].geometry.coordinates, zoom: map.getZoom() + 1 }); }); - map.on('click', 'circle-cluster', function(e) { + map.on('click', "circle-cluster{{ layerId }}", function(e) { map.easeTo({ center: e.features[0].geometry.coordinates, zoom: map.getZoom() + 1 @@ -145,4 +139,4 @@ }); }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/graduated_circle.html b/mapboxgl/templates/graduated_circle.html index b95cb40..4d44324 100644 --- a/mapboxgl/templates/graduated_circle.html +++ b/mapboxgl/templates/graduated_circle.html @@ -1,27 +1,21 @@ -{% extends "base.html" %} +{% extends "layer.html" %} {% block extra_css %} - {% endblock extra_css %} -{% block legend %} +{% block extra_javascript %} {% if showLegend %} {% if colorStops and colorProperty and radiusProperty %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ radiusProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ radiusProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% endif %} -{% endblock legend %} - -{% block map %} - map.on('style.load', function() { - map.addSource("data", { + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, //data from dataframe output to geojson "buffer": 0, @@ -29,11 +23,11 @@ }); map.addLayer({ - "id": "label", - "source": "data", + "id": "label{{ layerId }}", + "source": "data{{ layerId }}", "type": "symbol", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "layout": { {% if labelProperty %} "text-field": "{{ labelProperty }}", @@ -49,11 +43,11 @@ }, "{{belowLayer}}" ) map.addLayer({ - "id": "circle", - "source": "data", + "id": "circle{{ layerId }}", + "source": "data{{ layerId }}", "type": "circle", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "paint": { {% if colorProperty %} "circle-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}" ), @@ -70,16 +64,16 @@ "circle-opacity" : {{ opacity }}, "circle-stroke-opacity" : {{ opacity }} } - }, "label"); + }, "label{{ layerId }}"); // Create a popup - var popup = new mapboxgl.Popup({ + var popup{{ layerId }} = new mapboxgl.Popup({ closeButton: false, closeOnClick: false }); // Show the popup on mouseover - map.on('mousemove', 'circle', function(e) { + map.on('mousemove', "circle{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; let popup_html = '
  • Location: ' + f.geometry.coordinates[0].toPrecision(6) + @@ -91,18 +85,18 @@ popup_html += '
  • ' - popup.setLngLat(e.features[0].geometry.coordinates) + popup{{ layerId }}.setLngLat(e.features[0].geometry.coordinates) .setHTML(popup_html) .addTo(map); }); - map.on('mouseleave', 'circle', function() { + map.on('mouseleave', "circle{{ layerId }}", function() { map.getCanvas().style.cursor = ''; - popup.remove(); + popup{{ layerId }}.remove(); }); // Fly to on click - map.on('click', 'circle', function(e) { + map.on('click', "circle{{ layerId }}", function(e) { map.easeTo({ center: e.features[0].geometry.coordinates, zoom: map.getZoom() + 1 @@ -110,4 +104,4 @@ }); }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/heatmap.html b/mapboxgl/templates/heatmap.html index 2d3dd90..86838a2 100644 --- a/mapboxgl/templates/heatmap.html +++ b/mapboxgl/templates/heatmap.html @@ -1,12 +1,10 @@ -{% extends "base.html" %} +{% extends "layer.html" %} -{% block legend %}{% endblock legend %} - -{% block map %} +{% block extra_javascript %} map.on('style.load', function() { - map.addSource("data", { + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, //data from dataframe output to geojson "buffer": 0, @@ -14,11 +12,11 @@ }); map.addLayer({ - "id": "circle", - "source": "data", + "id": "circle{{ layerId }}", + "source": "data{{ layerId }}", "type": "heatmap", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, + "maxzoom": {{ maxZoom }}, + "minzoom": {{ minZoom }}, "paint": { {% if radiusStops %} "heatmap-radius": generatePropertyExpression('interpolate', 'zoom', {{ radiusStops }}), @@ -38,4 +36,4 @@ }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/image.html b/mapboxgl/templates/image.html index 194f356..e4fffeb 100644 --- a/mapboxgl/templates/image.html +++ b/mapboxgl/templates/image.html @@ -1,23 +1,21 @@ -{% extends "base.html" %} +{% extends "layer.html" %} -{% block legend %}{% endblock legend %} - -{% block map %} +{% block extra_javascript %} map.on('style.load', function() { - map.addSource("image", { + map.addSource("data{{ "image{{ layerId }}" }}", { "type": "image", "url": "{{ image }}", // url or base64 encoded image "coordinates": {{ coordinates }} }); map.addLayer({ - "id": 'image', + "id": "image{{ layerId }}", "type": "raster", - "source": "image" + "source": "data{{ "image{{ layerId }}" }}" }, "{{belowLayer}}"); }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/layer.html b/mapboxgl/templates/layer.html new file mode 100644 index 0000000..0c30e63 --- /dev/null +++ b/mapboxgl/templates/layer.html @@ -0,0 +1,14 @@ + + + + + diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index 760b273..0be674a 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -1,29 +1,25 @@ -{% extends "base.html" %} +{% extends "layer.html" %} -{% block legend %} +{% block extra_javascript %} {% if showLegend %} {% if colorStops and colorProperty and widthProperty %} {% if colorProperty != widthProperty %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ widthProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ widthProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% else %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% elif colorStops and colorProperty %} - calcColorLegend({{ colorStops }}, "{{ colorProperty }}"); + calcColorLegend({{ colorStops }}, "{{ colorProperty }}", {{ layerId }}, "{{legendKeyShape}}", {{legendGradient}}, {{legendNumericPrecision}}, "{{legendLayout}}" ); {% endif %} {% endif %} -{% endblock legend %} - -{% block map %} - map.on('style.load', function() { {% block linestring %} // Add geojson data source - map.addSource("data", { + map.addSource("data{{ layerId }}", { "type": "geojson", "data": {{ geojson_data }}, "buffer": 1, @@ -32,8 +28,8 @@ // Add data layer map.addLayer({ - "id": "linestring", - "source": "data", + "id": "linestring{{ layerId }}", + "source": "data{{ layerId }}", "type": "line", "layout": { "line-join": "round", @@ -59,8 +55,8 @@ // Add label layer map.addLayer({ - "id": "linestring-label", - "source": "data", + "id": "label{{ layerId }}", + "source": "data{{ layerId }}"", "type": "symbol", "layout": { {% if labelProperty %} @@ -77,17 +73,17 @@ }, "{{belowLayer}}" ); {% endblock linestring %} + + {% block linestring_popup %} // Create a popup - var popup = new mapboxgl.Popup({ + var popup{{ layerId }} = new mapboxgl.Popup({ closeButton: false, closeOnClick: false }); - - {% block linestring_popup %} // Show the popup on mouseover - map.on('mousemove', 'linestring', function(e) { + map.on('mousemove', "linestring{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; @@ -98,20 +94,20 @@ } popup_html += '' - popup.setLngLat(e.lngLat) + popup{{ layerId }}.setLngLat(e.lngLat) .setHTML(popup_html) .addTo(map); }); {% endblock linestring_popup %} - map.on('mouseleave', 'linestring', function() { + map.on('mouseleave', "linestring{{ layerId }}", function() { map.getCanvas().style.cursor = ''; - popup.remove(); + popup{{ layerId }}.remove(); }); // Fly to on click - map.on('click', 'linestring', function(e) { + map.on('click', "linestring{{ layerId }}", function(e) { map.flyTo({ center: e.lngLat, zoom: map.getZoom() + 1 @@ -120,4 +116,4 @@ }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/main.html b/mapboxgl/templates/main.html deleted file mode 100644 index dd240d9..0000000 --- a/mapboxgl/templates/main.html +++ /dev/null @@ -1,256 +0,0 @@ - - - -mapboxgl-jupyter viz - - - - - - -{% block extra_css %}{% endblock extra_css %} - - - -
    - - - - - - - - diff --git a/mapboxgl/templates/map.html b/mapboxgl/templates/map.html index 5f5c340..d9b56ba 100644 --- a/mapboxgl/templates/map.html +++ b/mapboxgl/templates/map.html @@ -1,29 +1,302 @@ -{% extends "base.html" %} - -{% block legend %}{% endblock legend %} - -{% block map %} - - map.on('style.load', function() { - - // Add data source - map.addSource("data", { - "type": "geojson", - "data": {{ geojson_data }}, - "buffer": 1, - "maxzoom": 14 - }); - - // Add data layer - map.addLayer({ - "id": "circle", - "source": "data", - "type": "circle", - "maxzoom": {{ maxzoom }}, - "minzoom": {{ minzoom }}, - "paint": { "circle-radius": 1 } - }); + + + +mapboxgl-jupyter viz + + + + + + + + +
    + +
    + + + + + + + + diff --git a/mapboxgl/templates/raster.html b/mapboxgl/templates/raster.html index 1561ed1..670c308 100644 --- a/mapboxgl/templates/raster.html +++ b/mapboxgl/templates/raster.html @@ -1,11 +1,8 @@ -{% extends "base.html" %} +{% extends "layer.html" %} -{% block legend %}{% endblock legend %} - -{% block map %} +{% block extra_javascript %} map.on('style.load', function() { - console.log('Yoooooo'); let params = { "type": "raster", "tiles": [ @@ -17,14 +14,14 @@ } if ({{ tiles_bounds }} !== undefined) params.bounds = {{ tiles_bounds }}; - map.addSource("raster-tiles", params); + map.addSource("raster-tiles{{ "rater-layer{{ layerId }}" }}", params); map.addLayer({ - "id": "raster-tiles", + "id": "rater-layer{{ layerId }}", "type": "raster", - "source": "raster-tiles" + "source": "raster-tiles{{ "rater-layer{{ layerId }}" }}", }, "{{belowLayer}}" ); }); -{% endblock map %} +{% endblock extra_javascript %} diff --git a/mapboxgl/templates/vector_choropleth.html b/mapboxgl/templates/vector_choropleth.html index 139b913..d22b119 100644 --- a/mapboxgl/templates/vector_choropleth.html +++ b/mapboxgl/templates/vector_choropleth.html @@ -27,16 +27,16 @@ {% endif %} // Add vector data source - map.addSource("vector-data", { + map.addSource("data{{ layerId }}", { type: "vector", url: "{{ vectorUrl }}", }); // Add layer from the vector tile source with data-driven style map.addLayer({ - "id": "choropleth-fill", + "id": "choropleth-fill{{ layerId }}", "type": "fill", - "source": "vector-data", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", "paint": { "fill-color": { @@ -52,8 +52,8 @@ // Add border layer map.addLayer({ - "id": "choropleth-line", - "source": "vector-data", + "id": "choropleth-line{{ layerId }}", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", "type": "line", "layout": { @@ -73,8 +73,8 @@ // Add label layer map.addLayer({ - "id": "choropleth-label", - "source": "vector-data", + "id": "choropleth-label{{ layerId }}", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", "type": "symbol", "layout": { @@ -95,9 +95,9 @@ {% if extrudeChoropleth %} map.addLayer({ - id: "choropleth-extrusion", + id: "choropleth-extrusion{{ layerId }}", type: "fill-extrusion", - "source": "vector-data", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", paint: { "fill-extrusion-opacity": {{ opacity }}, @@ -123,8 +123,14 @@ {% block choropleth_popup %} + // Create a popup + var popup{{ layerId }} = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false + }); + // Show the popup on mouseover - map.on('mousemove', 'choropleth-fill', function(e) { + map.on('mousemove', "choropleth-fill{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; @@ -144,7 +150,7 @@ {% endif %} popup_html += '' - popup.setLngLat(e.lngLat) + popup{{ layerId }}.setLngLat(e.lngLat) .setHTML(popup_html) .addTo(map); }); diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 9b099f0..4777c8c 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -30,15 +30,15 @@ {% endif %} // Add vector data source - map.addSource("vector-data", { + map.addSource("data{{ layerId }}", { type: "vector", url: "{{ vectorUrl }}", }); // Add data layer from the vector tile source with data-driven style map.addLayer({ - "id": "linestring", - "source": "vector-data", + "id": "linestring{{ layerId }}", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", "type": "line", "layout": { @@ -69,8 +69,8 @@ // Add label layer map.addLayer({ - "id": "linestring-label", - "source": "vector-data", + "id": "label{{ layerId }}", + "source": "data{{ layerId }}", "source-layer": "{{ vectorLayer }}", "type": "symbol", "layout": { @@ -92,8 +92,14 @@ {% block linestring_popup %} + // Create a popup + var popup{{ layerId }} = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false + }); + // Show the popup on mouseover - map.on('mousemove', 'linestring', function(e) { + map.on('mousemove', "linestring{{ layerId }}", function(e) { map.getCanvas().style.cursor = 'pointer'; let f = e.features[0]; @@ -115,7 +121,7 @@ {% endif %} popup_html += '' - popup.setLngLat(e.lngLat) + popup{{ layerId }}.setLngLat(e.lngLat) .setHTML(popup_html) .addTo(map); }); diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 87d6418..8fca800 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -1,20 +1,17 @@ -import codecs -import json -import os +from .map import Map +from .layers import ( + CircleLayer, + GraduatedCircleLayer, + HeatmapLayer, + ClusteredCircleLayer, + ImageLayer, + RasterTilesLayer, + ChoroplethLayer, + LinestringLayer, +) from IPython.core.display import HTML, display -import numpy - -from mapboxgl.errors import TokenError -from mapboxgl.utils import color_map, height_map -from mapboxgl import templates -from mapboxgl.utils import img_encode, numeric_map - - -GL_JS_VERSION = 'v0.49.0' - - class MapViz(object): def __init__(self, @@ -28,8 +25,6 @@ def __init__(self, style='mapbox://styles/mapbox/light-v9?optimize=true', width='100%', zoom=0, - min_zoom=0, - max_zoom=24, pitch=0, bearing=0, box_zoom_on=True, @@ -77,116 +72,51 @@ def __init__(self, :param legend_key_borders_on: boolean for whether to show/hide legend key borders """ - if access_token is None: - access_token = os.environ.get('MAPBOX_ACCESS_TOKEN', '') - if access_token.startswith('sk'): - raise TokenError('Mapbox access token must be public (pk), not secret (sk). ' \ - 'Please sign up at https://www.mapbox.com/signup/ to get a public token. ' \ - 'If you already have an account, you can retreive your token at https://www.mapbox.com/account/.') - self.access_token = access_token - self.template = 'map' - self.data = data - self.div_id = div_id - self.width = width - self.height = height - self.style = style - self.center = center - self.zoom = zoom - self.below_layer = below_layer - self.opacity = opacity - self.label_property = None - self.min_zoom = min_zoom - self.max_zoom = max_zoom - self.pitch = pitch - self.bearing = bearing - self.box_zoom_on = box_zoom_on - self.double_click_zoom_on = double_click_zoom_on - self.scroll_zoom_on = scroll_zoom_on - self.touch_zoom_on = touch_zoom_on - self.legend = legend - self.legend_layout = legend_layout - self.legend_style = legend_style - self.legend_gradient = legend_gradient - self.legend_fill = legend_fill - self.legend_header_fill = legend_header_fill - self.legend_text_color = legend_text_color - self.legend_text_numeric_precision = legend_text_numeric_precision - self.legend_title_halo_color = legend_title_halo_color - self.legend_key_shape = legend_key_shape - self.legend_key_borders_on = legend_key_borders_on + self.__dict__['map'] = Map( + access_token=access_token, + center=center, + opacity=opacity, + div_id=div_id, + height=height, + style=style, + width=width, + zoom=zoom, + pitch=pitch, + bearing=bearing, + box_zoom_on=box_zoom_on, + double_click_zoom_on=double_click_zoom_on, + scroll_zoom_on=scroll_zoom_on, + touch_zoom_on=touch_zoom_on, + legend_fill=legend_fill, + legend_header_fill=legend_header_fill, + legend_text_color=legend_text_color, + legend_title_halo_color=legend_title_halo_color, + legend_key_borders_on=legend_key_borders_on + ) + + self.__dict__['layer'] = None + + def __setattr__(self, name, value): + if hasattr(self.map, name): + self.map.__dict__[name] = value + elif hasattr(self.layer, name): + self.layer.__dict__[name] = value + def as_iframe(self, html_data): """Build the HTML representation for the mapviz.""" - - srcdoc = html_data.replace('"', "'") - return (''.format( - div_id=self.div_id, - srcdoc=srcdoc, - width=self.width, - height=self.height)) + return self.map.as_iframe(html_data) def show(self, **kwargs): # Load the HTML iframe - html = self.create_html(**kwargs) - map_html = self.as_iframe(html) - - # Display the iframe in the current jupyter notebook view - display(HTML(map_html)) + self.map.show(**kwargs) def add_unique_template_variables(self, options): pass def create_html(self, filename=None): """Create a circle visual from a geojson data source""" - if isinstance(self.style, str): - style = "'{}'".format(self.style) - else: - style = self.style - options = dict( - gl_js_version=GL_JS_VERSION, - accessToken=self.access_token, - div_id=self.div_id, - style=style, - center=list(self.center), - zoom=self.zoom, - geojson_data=json.dumps(self.data, ensure_ascii=False), - belowLayer=self.below_layer, - opacity=self.opacity, - minzoom=self.min_zoom, - maxzoom=self.max_zoom, - pitch=self.pitch, - bearing=self.bearing, - boxZoomOn=json.dumps(self.box_zoom_on), - doubleClickZoomOn=json.dumps(self.double_click_zoom_on), - scrollZoomOn=json.dumps(self.scroll_zoom_on), - touchZoomOn=json.dumps(self.touch_zoom_on), - showLegend=self.legend, - legendLayout=self.legend_layout, - legendStyle=self.legend_style, # reserve name for custom CSS? - legendGradient=json.dumps(self.legend_gradient), - legendFill=self.legend_fill, - legendHeaderFill=self.legend_header_fill, - legendTextColor=self.legend_text_color, - legendNumericPrecision=json.dumps(self.legend_text_numeric_precision), - legendTitleHaloColor=self.legend_title_halo_color, - legendKeyShape=self.legend_key_shape, - legendKeyBordersOn=json.dumps(self.legend_key_borders_on)) - - if self.label_property is None: - options.update(labelProperty=None) - else: - options.update(labelProperty='{' + self.label_property + '}') - - self.add_unique_template_variables(options) - - if filename: - html = templates.format(self.template, **options) - with codecs.open(filename, "w", "utf-8-sig") as f: - f.write(html) - return None - else: - return templates.format(self.template, **options) + return self.map.create_html(filename) class CircleViz(MapViz): @@ -207,6 +137,8 @@ def __init__(self, stroke_color='grey', stroke_width=0.1, legend_key_shape='circle', + min_zoom=0, + max_zoom=24, *args, **kwargs): """Construct a Mapviz object @@ -227,37 +159,28 @@ def __init__(self, """ super(CircleViz, self).__init__(data, *args, **kwargs) - self.template = 'circle' - 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.radius = radius - self.stroke_color = stroke_color - self.stroke_width = stroke_width - self.color_function_type = color_function_type - self.color_default = color_default - self.legend_key_shape = legend_key_shape - - def add_unique_template_variables(self, options): - """Update map template variables specific to circle visual""" - options.update(dict( - geojson_data=json.dumps(self.data, ensure_ascii=False), - colorProperty=self.color_property, - colorType=self.color_function_type, - colorStops=self.color_stops, - strokeWidth=self.stroke_width, - strokeColor=self.stroke_color, - radius=self.radius, - defaultColor=self.color_default, - labelColor=self.label_color, - labelSize=self.label_size, - labelHaloColor=self.label_halo_color, - labelHaloWidth=self.label_halo_width, - )) + self.__dict__['layer'] = CircleLayer( + data=data, + label_property=label_property, + label_color=label_color, + label_size=label_size, + label_halo_color=label_halo_color, + label_halo_width=label_halo_width, + color_property=color_property, + color_stops=color_stops, + radius=radius, + stroke_color=stroke_color, + stroke_width=stroke_width, + color_function_type=color_function_type, + color_default=color_default, + legend_key_shape=legend_key_shape, + min_zoom=min_zoom, + max_zoom=max_zoom, + *args, + **kwargs + ) + + self.map.add_layer(self.layer) class GraduatedCircleViz(MapViz): @@ -300,42 +223,29 @@ def __init__(self, """ super(GraduatedCircleViz, self).__init__(data, *args, **kwargs) - self.template = 'graduated_circle' - 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.radius_property = radius_property - self.radius_stops = radius_stops - self.color_function_type = color_function_type - self.color_default = color_default - self.radius_function_type = radius_function_type - self.radius_default = radius_default - self.stroke_color = stroke_color - self.stroke_width = stroke_width - self.legend_key_shape = legend_key_shape - - def add_unique_template_variables(self, options): - """Update map template variables specific to graduated circle visual""" - options.update(dict( - colorProperty=self.color_property, - colorStops=self.color_stops, - colorType=self.color_function_type, - radiusType=self.radius_function_type, - defaultColor=self.color_default, - defaultRadius=self.radius_default, - radiusProperty=self.radius_property, - radiusStops=self.radius_stops, - strokeWidth=self.stroke_width, - strokeColor=self.stroke_color, - labelColor=self.label_color, - labelSize=self.label_size, - labelHaloColor=self.label_halo_color, - labelHaloWidth=self.label_halo_width - )) + self.__dict__['layer'] = GraduatedCircleLayer( + data=data, + label_property=label_property, + label_color=label_color, + label_size=label_size, + label_halo_color=label_halo_color, + label_halo_width=label_halo_width, + color_property=color_property, + color_stops=color_stops, + radius_property=radius_property, + radius_stops=radius_stops, + color_function_type=color_function_type, + color_default=color_default, + radius_function_type=radius_function_type, + radius_default=radius_default, + stroke_color=stroke_color, + stroke_width=stroke_width, + legend_key_shape=legend_key_shape, + *args, + **kwargs + ) + + self.map.add_layer(self.layer) class HeatmapViz(MapViz): @@ -343,6 +253,7 @@ class HeatmapViz(MapViz): def __init__(self, data, + label_property=None, weight_property=None, weight_stops=None, color_stops=None, @@ -361,24 +272,19 @@ def __init__(self, """ super(HeatmapViz, self).__init__(data, *args, **kwargs) - self.template = 'heatmap' - self.weight_property = weight_property - self.weight_stops = weight_stops - if color_stops: - # Make the first color stop in a heatmap have opacity 0 for good visual effect - self.color_stops = [[0.00001, 'rgba(0,0,0,0)']] + color_stops - self.radius_stops = radius_stops - self.intensity_stops = intensity_stops + self.__dict__['layer'] = HeatmapLayer( + data=data, + label_property=label_property, + weight_property=weight_property, + weight_stops=weight_stops, + color_stops=color_stops, + radius_stops=radius_stops, + intensity_stops=intensity_stops, + *args, + **kwargs + ) - def add_unique_template_variables(self, options): - """Update map template variables specific to heatmap visual""" - options.update(dict( - colorStops=self.color_stops, - radiusStops=self.radius_stops, - weightProperty=self.weight_property, - weightStops=self.weight_stops, - intensityStops=self.intensity_stops, - )) + self.map.add_layer(self.layer) class ClusteredCircleViz(MapViz): @@ -415,38 +321,26 @@ def __init__(self, """ super(ClusteredCircleViz, self).__init__(data, *args, **kwargs) - self.template = 'clustered_circle' - 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_stops = color_stops - self.radius_stops = radius_stops - self.clusterRadius = cluster_radius - self.clusterMaxZoom = cluster_maxzoom - self.radius_default = radius_default - self.color_default = color_default - self.stroke_color = stroke_color - self.stroke_width = stroke_width - self.color_default = color_default - self.legend_key_shape = legend_key_shape - - def add_unique_template_variables(self, options): - """Update map template variables specific to a clustered circle visual""" - options.update(dict( - colorStops=self.color_stops, - colorDefault=self.color_default, - radiusStops=self.radius_stops, - clusterRadius=self.clusterRadius, - clusterMaxZoom=self.clusterMaxZoom, - strokeWidth=self.stroke_width, - strokeColor=self.stroke_color, - radiusDefault=self.radius_default, - labelColor=self.label_color, - labelSize=self.label_size, - labelHaloColor=self.label_halo_color, - labelHaloWidth=self.label_halo_width - )) + self.__dict__['layer'] = ClusteredCircleLayer( + data=data, + label_color=label_color, + label_size=label_size, + label_halo_color=label_halo_color, + label_halo_width=label_halo_width, + color_stops=color_stops, + radius_stops=radius_stops, + cluster_radius=cluster_radius, + cluster_maxzoom=cluster_maxzoom, + radius_default=radius_default, + color_default=color_default, + stroke_color=stroke_color, + stroke_width=stroke_width, + legend_key_shape=legend_key_shape, + *args, + **kwargs + ) + + self.map.add_layer(self.layer) class ChoroplethViz(MapViz): @@ -466,7 +360,7 @@ def __init__(self, line_color='white', line_stroke='solid', line_width=1, - height_property=None, + height_property=None, height_stops=None, height_default=0.0, height_function_type='interpolate', @@ -495,122 +389,39 @@ def __init__(self, """ super(ChoroplethViz, 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_choropleth' - self.vector_source = True - else: - self.vector_source = False - self.template = 'choropleth' - - 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 - self.height_property = height_property - self.height_stops = height_stops - self.height_default = height_default - self.height_function_type = height_function_type - self.legend_key_shape = legend_key_shape + + self.__dict__['layer'] = ChoroplethLayer( + data=data, + vector_url=vector_url, + vector_layer_name=vector_layer_name, + vector_join_property=vector_join_property, + data_join_property=data_join_property, # vector only + label_property=label_property, + color_property=color_property, + color_stops=color_stops, + color_default=color_default, + color_function_type=color_function_type, + line_color=line_color, + line_stroke=line_stroke, + line_width=line_width, + height_property=height_property, + height_stops=height_stops, + height_default=height_default, + height_function_type=height_function_type, + legend_key_shape=legend_key_shape, + *args, + **kwargs + ) + + self.map.add_layer(self.layer) 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 + return self.layer.generate_vector_color_map() def generate_vector_height_map(self): """Generate height stops array for use with match expression in mapbox template""" - vector_stops = [] - - if self.height_function_type == 'match': - match_height = self.height_stops - - for row in self.data: - - # map height to JSON feature using height_property - height = height_map(row[self.height_property], self.height_stops, self.height_default) - - # link to vector feature using data_join_property (from JSON object) - vector_stops.append([row[self.data_join_property], height]) - - return vector_stops - - def add_unique_template_variables(self, options): - """Update map template variables specific to heatmap visual""" - - # 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] - - # check if choropleth map should include 3-D extrusion - self.extrude = all([bool(self.height_property), bool(self.height_stops)]) - - # common variables for vector and geojson-based choropleths - options.update(dict( - colorStops=self.color_stops, - 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, - extrudeChoropleth=self.extrude, - )) - if self.extrude: - options.update(dict( - heightType=self.height_function_type, - heightProperty=self.height_property, - heightStops=self.height_stops, - defaultHeight=self.height_default, - )) - - # vector-based choropleth map variables - if self.vector_source: - options.update(dict( - vectorUrl=self.vector_url, - vectorLayer=self.vector_layer_name, - vectorColorStops=self.generate_vector_color_map(), - vectorJoinDataProperty=self.vector_join_property, - joinData=json.dumps(self.data, ensure_ascii=False), - dataJoinProperty=self.data_join_property, - )) - if self.extrude: - options.update(dict( - vectorHeightStops=self.generate_vector_height_map(), - )) - - # geojson-based choropleth map variables - else: - options.update(dict( - geojson_data=json.dumps(self.data, ensure_ascii=False), - )) + return self.layer.generate_vector_height_map() class ImageViz(MapViz): @@ -632,18 +443,12 @@ def __init__(self, """ super(ImageViz, self).__init__(None, *args, **kwargs) - if type(image) is numpy.ndarray: - image = img_encode(image) - - self.template = 'image' - self.image = image - self.coordinates = coordinates + self.__dict__['layer'] = ImageLayer( + image=image, + coordinates=coordinates, + ) - def add_unique_template_variables(self, options): - """Update map template variables specific to image visual""" - options.update(dict( - image=self.image, - coordinates=self.coordinates)) + self.map.add_layer(self.layer) class RasterTilesViz(MapViz): @@ -670,21 +475,15 @@ def __init__(self, """ super(RasterTilesViz, self).__init__(None, *args, **kwargs) - self.template = 'raster' - self.tiles_url = tiles_url - self.tiles_size = tiles_size - self.tiles_bounds = tiles_bounds - self.tiles_minzoom = tiles_minzoom - self.tiles_maxzoom = tiles_maxzoom + self.__dict__['layer'] = RasterTilesLayer( + tiles_url=tiles_url, + tiles_size=tiles_size, + tiles_bounds=tiles_bounds, + tiles_minzoom=tiles_minzoom, + tiles_maxzoom=tiles_maxzoom + ) - def add_unique_template_variables(self, options): - """Update map template variables specific to a raster visual""" - options.update(dict( - tiles_url=self.tiles_url, - tiles_size=self.tiles_size, - tiles_minzoom=self.tiles_minzoom, - tiles_maxzoom=self.tiles_maxzoom, - tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) + self.map.add_layer(self.layer) class LinestringViz(MapViz): @@ -737,121 +536,36 @@ def __init__(self, """ 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 - self.legend_key_shape = legend_key_shape + + self.__dict__['layer'] = LinestringLayer( + data=data, + vector_url=vector_url, + vector_layer_name=vector_layer_name, + vector_join_property=vector_join_property, + data_join_property=data_join_property, + label_property=label_property, + label_size=label_size, + label_color=label_color, + label_halo_color=label_halo_color, + label_halo_width=label_halo_width, + color_property=color_property, + color_stops=color_stops, + color_default=color_default, + color_function_type=color_function_type, + line_stroke=line_stroke, + line_width_property=line_width_property, + line_width_stops=line_width_stops, + line_width_default=line_width_default, + line_width_function_type=line_width_function_type, + legend_key_shape=legend_key_shape, + ) + + self.map.add_layer(self.layer) 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 + return self.layer.generate_vector_color_map() 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), - )) - + return self.layer.generate_vector_width_map()