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)
Esempio n. 4
0
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)
Esempio n. 5
0
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)