def show_map(selected_stats, year): control = WidgetControl(widget=districtbox, position='topright', min_width = 250, max_width=500) # load selected stats into choro_data_all choro_data_all, unit = choro_data_complete[selected_stats], units[selected_stats] # for geo plot extract chosen year and assign to choro_data choro_data = choro_data_all[choro_data_all['year']==year] choro_data = dict(choro_data.drop(columns=['year', 'name']).to_dict('split')['data']) # initialize bar chart with Frankfurt vs Offenbach update_figure('06412', selected_stats, choro_data_all, year) update_figure('06413', selected_stats, choro_data_all, year) # initialize districtbox loading_name, loading_values = id_to_name['06413'], choro_data['06413'] districtbox.value = f'<center><p><b>{loading_name}</b>:</p> {loading_values:g} {unit} {norm_unit}</center>' # set y-axis label fig.update_layout(yaxis_title=f'{stat_dict[selected_stats]} [{unit} {norm_unit}]', yaxis={'range':[0,max(choro_data_all[selected_stats])]}) # define chropleth layer for basic geo plotting layer = Choropleth(geo_data=geo_data,choro_data=choro_data,colormap=cm, style={'fillOpacity': 0.65, 'dashArray': '0, 0', 'weight':1}) # define GeoJSON layer for click and hover event interactions geo_json = GeoJSON(data=geo_data, style={'opacity': 0, 'dashArray': '9', 'fillOpacity': .0, 'weight': 1}, hover_style={'color': 'blue', 'dashArray': '0', 'fillOpacity': 0.7}) # on hover, the districtbox is updated to show properties of the hovered district def update_districtbox(feature, **kwargs): feature['value'] = choro_data[feature['id']] districtbox.value = f'<center><p><b>{id_to_name[feature["id"]]}</b>:</p> {feature["value"]:g} {unit} {norm_unit}</center>' # this function is called upon a click events and triggers figure update with the arguments passed from the map def update_fig_on_click(feature, **kwags): update_figure(feature['id'], selected_stats, choro_data_all, year) geo_json.on_hover(update_districtbox) geo_json.on_click(update_fig_on_click) # add layers and controls; set layout parameters m = Map(basemap=basemaps.OpenStreetMap.Mapnik, center=(50.5,9), zoom=8) m.add_layer(layer) m.add_layer(geo_json) m.add_control(control) m.layout.width = '40%' m.layout.height = '700px' # custom made legend using min/max normalization min_value, max_value = min(choro_data.values()), max(choro_data.values()) legend = LegendControl( {f"{min_value:g} {unit} {norm_unit}": cm(0), #hier f"{min_value+0.5*(max_value-min_value):g} {unit} {norm_unit}": cm(.5), f"{max_value:g} {unit} {norm_unit}": cm(1)}, name= f"{stat_dict[selected_stats]} ({year})", position="bottomleft") m.add_control(legend) return HBox([m, fig], layout=Layout(width='85%'))
def _save_features(self): """save the features as layers on the map""" # remove any sub aoi layer layers_2_keep = [ "CartoDB.DarkMatter", "restoration layer", self.aoi_model.name ] [ self.m.remove_layer(l) for l in self.m.layers if l.name not in layers_2_keep ] # save the drawn features draw_features = self.draw_features # remove the shapes from the dc # as a side effect the draw_features member will be emptied self.m.dc.clear() # reset the draw_features # I'm sure the the AOI folder exists because the recipe was already saved there self.draw_features = draw_features features_file = (cp.result_dir / self.aoi_model.name / f"features_{self.question_model.recipe_name}.geojson") with features_file.open("w") as f: json.dump(draw_features, f) # set up the colors using the tab10 matplotlib colormap self.colors = [ to_hex(plt.cm.tab10(i)) for i in range(len(self.draw_features["features"])) ] # create a layer for each aoi for feat, color in zip(self.draw_features["features"], self.colors): name = feat["properties"]["name"] style = {**cp.aoi_style, "color": color, "fillColor": color} hover_style = {**style, "fillOpacity": 0.4, "weight": 2} layer = GeoJSON(data=feat, style=style, hover_style=hover_style, name=name) layer.on_hover(self._display_name) self.m.add_layer(layer) return self
def map_shapefile(gdf, attribute, continuous=False, cmap='viridis', basemap=basemaps.Esri.WorldImagery, default_zoom=None, hover_col=True, **style_kwargs): """ Plots a geopandas GeoDataFrame over an interactive ipyleaflet basemap, with features coloured based on attribute column values. Optionally, can be set up to print selected data from features in the GeoDataFrame. Last modified: February 2020 Parameters ---------- gdf : geopandas.GeoDataFrame A GeoDataFrame containing the spatial features to be plotted over the basemap. attribute: string, required An required string giving the name of any column in the GeoDataFrame you wish to have coloured on the choropleth. continuous: boolean, optional Whether to plot data as a categorical or continuous variable. Defaults to remapping the attribute which is suitable for categorical data. For continuous data set `continuous` to True. cmap : string, optional A string giving the name of a `matplotlib.cm` colormap that will be used to style the features in the GeoDataFrame. Features will be coloured based on the selected attribute. Defaults to the 'viridis' colormap. basemap : ipyleaflet.basemaps object, optional An optional `ipyleaflet.basemaps` object used as the basemap for the interactive plot. Defaults to `basemaps.Esri.WorldImagery`. default_zoom : int, optional An optional integer giving a default zoom level for the interactive ipyleaflet plot. Defaults to None, which infers the zoom level from the extent of the data. hover_col : boolean or str, optional If True (the default), the function will print values from the GeoDataFrame's `attribute` column above the interactive map when a user hovers over the features in the map. Alternatively, a custom shapefile field can be specified by supplying a string giving the name of the field to print. Set to False to prevent any attributes from being printed. **style_kwargs : Optional keyword arguments to pass to the `style` paramemter of the `ipyleaflet.Choropleth` function. This can be used to control the appearance of the shapefile, for example 'stroke' and 'weight' (controlling line width), 'fillOpacity' (polygon transparency) and 'dashArray' (whether to plot lines/outlines with dashes). For more information: https://ipyleaflet.readthedocs.io/en/latest/api_reference/choropleth.html """ def on_hover(event, id, properties): with dbg: text = properties.get(hover_col, '???') lbl.value = f'{hover_col}: {text}' # Verify that attribute exists in shapefile if attribute not in gdf.columns: raise ValueError(f"The `attribute` {attribute} does not exist " f"in the geopandas.GeoDataFrame. " f"Valid attributes include {gdf.columns.values}.") # If hover_col is True, use 'attribute' as the default hover attribute. # Otherwise, hover_col will use the supplied attribute field name if hover_col and (hover_col is True): hover_col = attribute # If a custom string if supplied to hover_col, check this exists elif hover_col and (type(hover_col) == str): if hover_col not in gdf.columns: raise ValueError(f"The `hover_col` field {hover_col} does " f"not exist in the geopandas.GeoDataFrame. " f"Valid attributes include " f"{gdf.columns.values}.") # Convert to WGS 84 and GeoJSON format gdf_wgs84 = gdf.to_crs(epsg=4326) data_geojson = gdf_wgs84.__geo_interface__ # If continuous is False, remap categorical classes for visualisation if not continuous: # Zip classes data together to make a dictionary classes_uni = list(gdf[attribute].unique()) classes_clean = list(range(0, len(classes_uni))) classes_dict = dict(zip(classes_uni, classes_clean)) # Get values to colour by as a list classes = gdf[attribute].map(classes_dict).tolist() # If continuous is True then do not remap else: # Get values to colour by as a list classes = gdf[attribute].tolist() # Create the dictionary to colour map by keys = gdf.index id_class_dict = dict(zip(keys.astype(str), classes)) # Get centroid to focus map on lon1, lat1, lon2, lat2 = gdf_wgs84.total_bounds lon = (lon1 + lon2) / 2 lat = (lat1 + lat2) / 2 if default_zoom is None: # Calculate default zoom from latitude of features default_zoom = _degree_to_zoom_level(lat1, lat2, margin=-0.5) # Plot map m = Map(center=(lat, lon), zoom=default_zoom, basemap=basemap, layout=dict(width='800px', height='600px')) # Define default plotting parameters for the choropleth map. # The nested dict structure sets default values which can be # overwritten/customised by `choropleth_kwargs` values style_kwargs = dict({'fillOpacity': 0.8}, **style_kwargs) # Get `branca.colormap` object from matplotlib string cm_cmap = cm.get_cmap(cmap, 30) colormap = branca.colormap.LinearColormap([cm_cmap(i) for i in np.linspace(0, 1, 30)]) # Create the choropleth choropleth = Choropleth(geo_data=data_geojson, choro_data=id_class_dict, colormap=colormap, style=style_kwargs) # If the vector data contains line features, they will not be # be coloured by default. To resolve this, we need to manually copy # across the 'fillColor' attribute to the 'color' attribute for each # feature, then plot the data as a GeoJSON layer rather than the # choropleth layer that we use for polygon data. linefeatures = any(x in ['LineString', 'MultiLineString'] for x in gdf.geometry.type.values) if linefeatures: # Copy colour from fill to line edge colour for i in keys: choropleth.data['features'][i]['properties']['style']['color'] = \ choropleth.data['features'][i]['properties']['style']['fillColor'] # Add GeoJSON layer to map feature_layer = GeoJSON(data=choropleth.data, style=style_kwargs) m.add_layer(feature_layer) else: # Add Choropleth layer to map m.add_layer(choropleth) # If a column is specified by `hover_col`, print data from the # hovered feature above the map if hover_col and not linefeatures: # Use cholopleth object if data is polygon lbl = ipywidgets.Label() dbg = ipywidgets.Output() choropleth.on_hover(on_hover) display(lbl) else: lbl = ipywidgets.Label() dbg = ipywidgets.Output() feature_layer.on_hover(on_hover) display(lbl) # Display the map display(m)
class TileGridTool: """A tool for adding a dynamic Mercator tile grid to a map. The grid is recalculated/adapted dynamically to the visible part of the map only as the user zooms and pans over it. The grid level is limited from 0 to the current map zoom level plus 4, or there would be too many cells and the grid would be too dense to see anything else. """ def __init__( self, a_map: Map, description: str = "Mercator", position: str = "topright", ): """Instantiate a tile grid tool and place it on a map. """ self._max_zoom_delta = 4 self.tile_id = "" self.level = int(a_map.zoom) style = {"color": "#888888", "weight": 1, "fillOpacity": 0} hover_style = {"weight": 3, "fillOpacity": 0.1} self.gj = GeoJSON(data=geojson.Feature(), name=description, style=style, hover_style=hover_style) min, max = 0, int(a_map.zoom) + self._max_zoom_delta self.slider = IntSlider(description=description, min=min, max=max, value=self.level) self.ht = HTML(f"ID: {self.tile_id} Map zoom: {int(a_map.zoom)}") self.close_btn = Button( icon="times", button_style="info", tooltip="Close the widget", layout=Layout(width="32px"), ) self.widget = HBox([self.slider, self.ht, self.close_btn]) def hover(event, feature, **kwargs): if event == "mouseover": self.tile_id = feature["id"] self.ht.value = f"{self.tile_id} Map zoom: {int(a_map.zoom)}" def slider_moved(event): if event["type"] == "change" and event["name"] == "value": self.level = event["new"] # Ipyleaflet buglet(?): This name is updated in the GeoJSON layer, # but not in the LayersControl! self.gj.name = f"Mercator" # level {self.level}" self.tile_id = "" map_interacted({ "type": "change", "name": "bounds", "owner": a_map }) self.slider.observe(slider_moved) def map_interacted(event): if event["type"] == "change" and event["name"] == "bounds": self.ht.value = f"{self.tile_id}, Map zoom: {int(a_map.zoom)}" self.slider.max = int(a_map.zoom) + self._max_zoom_delta m = event["owner"] ((south, west), (north, east)) = m.bounds b_poly = list(m.bounds_polygon) b_poly += [tuple(b_poly[0])] # m += Polyline(locations=b_poly) # Attention in the order of west, south, east, north! tiles = mercantile.tiles(west, south, east, north, zooms=self.level) features = [mercantile.feature(t) for t in tiles] self.gj.data = geojson.FeatureCollection(features=features) # Ipyleaflet buglet(?): This name is updated in the GeoJSON layer, # but not in the LayersControl! self.gj.name = f"Mercator" # level {self.level}" self.gj.on_hover(hover) def close_click(change): self.widget.children = [] self.widget.close() self.close_btn.on_click(close_click) a_map += self.gj a_map.observe(map_interacted) map_interacted({"type": "change", "name": "bounds", "owner": a_map}) self.widget_control = WidgetControl(widget=self.widget, position=position) a_map.add_control(self.widget_control)
def map_shapefile(gdf, weight=2, colormap=mpl.cm.YlOrRd, basemap=basemaps.Esri.WorldImagery, default_zoom=None, hover_col=None, hover_prefix=''): """ Plots a geopandas GeoDataFrame over an interactive ipyleaflet basemap. Optionally, can be set up to print selected data from features in the GeoDataFrame. Last modified: October 2019 Parameters ---------- gdf : geopandas.GeoDataFrame A GeoDataFrame containing the spatial features to be plotted over the basemap weight : float or int, optional An optional numeric value giving the weight that line features will be plotted as. Defaults to 2; larger numbers = thicker colormap : matplotlib.cm, optional An optional matplotlib.cm colormap used to style the features in the GeoDataFrame. Features will be coloured by the order they appear in the GeoDataFrame. Defaults to the `YlOrRd` colormap. basemap : ipyleaflet.basemaps object, optional An optional ipyleaflet.basemaps object used as the basemap for the interactive plot. Defaults to `basemaps.Esri.WorldImagery` default_zoom : int, optional An optional integer giving a default zoom level for the interactive ipyleaflet plot. Defaults to 13 hover_col : str, optional An optional string giving the name of any column in the GeoDataFrame you wish to have data from printed above the interactive map when a user hovers over the features in the map. Defaults to None which will not print any data. """ def n_colors(n, colormap=colormap): data = np.linspace(0.0, 1.0, n) c = [mpl.colors.rgb2hex(d[0:3]) for d in colormap(data)] return c def data_to_colors(data, colormap=colormap): c = [ mpl.colors.rgb2hex(d[0:3]) for d in colormap(mpl.colors.Normalize()(data)) ] return c def on_hover(event, id, properties): with dbg: text = properties.get(hover_col, '???') lbl.value = f'{hover_col}: {text}' # print(properties) # Convert to WGS 84 and GeoJSON format gdf_wgs84 = gdf.to_crs(epsg=4326) data = gdf_wgs84.__geo_interface__ # For each feature in dataset, append colour values n_features = len(data['features']) colors = n_colors(n_features) for feature, color in zip(data['features'], colors): feature['properties']['style'] = { 'color': color, 'weight': weight, 'fillColor': color, 'fillOpacity': 1.0 } # Get centroid to focus map on lon1, lat1, lon2, lat2 = gdf_wgs84.total_bounds lon = (lon1 + lon2) / 2 lat = (lat1 + lat2) / 2 if default_zoom is None: # Calculate default zoom from latitude of features default_zoom = _degree_to_zoom_level(lat1, lat2, margin=-0.5) # Plot map m = Map(center=(lat, lon), zoom=default_zoom, basemap=basemap, layout=dict(width='800px', height='600px')) # Add GeoJSON layer to map feature_layer = GeoJSON(data=data) m.add_layer(feature_layer) # If a column is specified by `hover_col`, print data from the # hovered feature above the map if hover_col: lbl = ipywidgets.Label() dbg = ipywidgets.Output() feature_layer.on_hover(on_hover) display(lbl) # Display the map display(m)