Source code for superblockify.plot

"""Plotting functions."""

from os import path

import networkx as nx
import osmnx as ox
from matplotlib import patches
from matplotlib import pyplot as plt

from .attribute import determine_minmax_val, new_edge_attribute_by_function
from .config import logger

plt.set_loglevel("info")


[docs] def paint_streets(graph, cmap="hsv", **pg_kwargs): """Plot a graph with (cyclic) colormap related to edge direction. Color will be chosen based on edge bearing, cyclic in 90 degree. Function is a wrapper around `osmnx.plot_graph`. Parameters ---------- graph : networkx.Graph Input graph cmap : string, optional name of a matplotlib colormap pg_kwargs keyword arguments to pass to `osmnx.plot_graph`. Returns ------- fig, ax : tuple matplotlib figure, axis Raises ------ ValueError If no bearing attribute is found in the graph. Examples -------- _See example in `scripts/TestingNotebooks/20221122-painting_grids.py`._ """ # Check for bearing attribute. if "bearing" not in nx.get_edge_attributes(graph, "bearing"): logger.warning( "No edge attribute `bearing` found. " "Use `osmnx.add_edge_bearings` to add them on the unprojected graph." ) # Write attribute where bearings are baked down modulo 90 degrees. new_edge_attribute_by_function( graph, lambda bear: bear % 90, "bearing", "bearing_90" ) return plot_by_attribute( graph, edge_attr="bearing_90", edge_attr_types="numerical", edge_cmap=cmap, **pg_kwargs, )
[docs] def plot_by_attribute( graph, edge_attr=None, edge_attr_types="numerical", edge_cmap="hsv", edge_linewidth=1, edge_minmax_val=None, node_attr=None, node_attr_types="numerical", node_cmap="hsv", node_size=15, node_minmax_val=None, edge_color=None, node_color=None, **pg_kwargs, ): """Plot a graph based on an edge attribute and colormap. Color will be chosen based on the specified edge attribute passed to a colormap. Function is a direct wrapper around `osmnx.plot_graph`. Parameters ---------- graph : networkx.MultiDiGraph Input graph edge_attr : string, optional Graph's edge attribute to select colors by edge_attr_types : string, optional Type of the edge attribute to be plotted, can be 'numerical' or 'categorical' edge_cmap : string or matplotlib.colors.ListedColormap, optional Name of a matplotlib colormap to use for the edge colors or a colormap object edge_linewidth : float, optional Width of the edges' lines edge_minmax_val : tuple, optional Tuple of (min, max) values of the edge attribute to be plotted (default: min and max of edge attr) node_attr : string, optional Graph's node attribute to select colors by node_attr_types : string, optional Type of the node attribute to be plotted, can be 'numerical' or 'categorical' node_cmap : string or matplotlib.colors.ListedColormap, optional Name of a matplotlib colormap to use for the node colors or a colormap object node_size : int, optional Size of the nodes node_minmax_val : tuple, optional Tuple of (min, max) values of the node attribute to be plotted (default: min and max of node attr) pg_kwargs Keyword arguments to pass to `osmnx.plot_graph`. edge_color : string, optional Do not pass this attribute if `edge_attr` is set, as it is set by the edge attribute and colormap. node_color : string, optional Do not pass this attribute if `node_attr` is set, as it is set by the node attribute and colormap. Raises ------ ValueError If `edge_color`/`node_color` is set while `edge_attr`/`node_attr` is set. ValueError If `edge_linewidth` and `node_size` both <= 0, otherwise the plot will be empty. ValueError If `edge_attr` and `node_attr` are both None. Returns ------- fig, axe : tuple matplotlib figure, axis Notes ----- At least one of `edge_attr` or `node_attr` must be set. """ # pylint: disable=too-many-arguments, too-many-locals # Check if edge_attr and node_attr are both None if edge_attr is None and node_attr is None: raise ValueError("At least one of `edge_attr` or `node_attr` must be set.") # If edge_attr/node_attr is set it cannot be in pg_kwargs, check respectively if edge_attr and edge_color is not None: raise ValueError( f"The `edge_color` attribute was set to {edge_color}, it will be " f"overwritten by the colors determined with the bearings and colormap." ) if node_attr and node_color is not None: raise ValueError( f"The `node_color` attribute was set to {node_color}, it will be " f"overwritten by the colors determined with the bearings and colormap." ) e_c, n_c = None, None if edge_attr: # Choose the color for each edge based on the edge's attribute value, # if `None`, set to black. # Make list of edge colors, order is the same as in graph.edges() e_c = list( make_edge_color_list( graph, attr=edge_attr, cmap=( plt.get_cmap(edge_cmap) if isinstance(edge_cmap, str) else edge_cmap ), attr_types=edge_attr_types, minmax_val=edge_minmax_val, none_color=(0, 0, 0, 1), # black ) ) # Print list of unique colors in the colormap, with a set comprehension logger.debug( "Unique colors in the edge colormap %s (len %s): %s", edge_cmap, len(e_c), {tuple(c) for c in e_c}, ) if node_attr: # Choose the color for each node based on the node's attribute value, # if `None`, set to black. # Make list of node colors, order is the same as in graph.nodes() n_c = list( make_node_color_list( graph, attr=node_attr, cmap=( plt.get_cmap(node_cmap) if isinstance(node_cmap, str) else node_cmap ), attr_types=node_attr_types, minmax_val=node_minmax_val, none_color=(0, 0, 0, 0), # transparent ) ) # Print list of unique colors in the colormap, with a set comprehension # logger.debug( # "Unique colors in the node colormap %s (len %s): %s", # node_cmap, # len(n_c), # {tuple(c) for c in n_c}, # ) # If only edge_attr is set if e_c and not n_c: # Plot graph with osmnx's function, pass further attributes return ox.plot_graph( graph, node_size=node_size, edge_color=e_c, node_color=node_color if node_color else (0, 0, 0, 0), edge_linewidth=edge_linewidth, bgcolor=(0, 0, 0, 0), show=False, **pg_kwargs, ) # If only node_attr is set if n_c and not e_c: # Plot graph with osmnx's function, pass further attributes return ox.plot_graph( graph, node_size=node_size, edge_color=edge_color if edge_color else (0, 0, 0, 0), node_color=n_c, edge_linewidth=edge_linewidth, bgcolor=(0, 0, 0, 0), show=False, **pg_kwargs, ) # If both edge_attr and node_attr are set return ox.plot_graph( graph, node_size=node_size, edge_color=e_c, node_color=n_c, edge_linewidth=edge_linewidth, bgcolor=(0, 0, 0, 0), show=False, **pg_kwargs, )
[docs] def make_edge_color_list( graph, attr, cmap, attr_types="numerical", minmax_val=None, none_color=(0.5, 0.5, 0.5, 1), ): """Make a list of edge colors based on an edge attribute and colormap. Color will be chosen based on the specified edge attribute passed to a colormap. Parameters ---------- graph : networkx.MultiDiGraph Input graph attr : string Graph's edge attribute to select colors by attr_types : string, optional Type of the edge attribute to be plotted, can be 'numerical' or 'categorical' cmap : matplotlib.colors.Colormap Colormap to use for the edge colors minmax_val : tuple, optional If `attr_types` is 'numerical', tuple of (min, max) values of the attribute to be plotted (default: min and max of attr) none_color : tuple, optional Color to use for edges with `None` attribute value Returns ------- list List of edge colors, order is the same as in graph.edges() """ return make_color_list( graph, attr, cmap, obj_type="edge", attr_types=attr_types, minmax_val=minmax_val, none_color=none_color, )
[docs] def make_node_color_list( graph, attr, cmap, attr_types="numerical", minmax_val=None, none_color=(0.5, 0.5, 0.5, 1), ): """Make a list of node colors based on a node attribute and colormap. Color will be chosen based on the specified node attribute passed to a colormap. Parameters ---------- graph : networkx.MultiDiGraph Input graph attr : string Graph's node attribute to select colors by attr_types : string, optional Type of the node attribute to be plotted, can be 'numerical' or 'categorical' cmap : matplotlib.colors.Colormap Colormap to use for the node colors minmax_val : tuple, optional If `attr_types` is 'numerical', tuple of (min, max) values of the attribute to be plotted (default: min and max of attr) none_color : tuple, optional Color to use for nodes with `None` attribute value Raises ------ ValueError If `attr_types` is not 'numerical' or 'categorical' ValueError If `attr_types` is 'categorical' and `minmax_val` is not None Returns ------- list List of node colors, order is the same as in graph.nodes() """ return make_color_list( graph, attr, cmap, obj_type="node", attr_types=attr_types, minmax_val=minmax_val, none_color=none_color, )
[docs] def make_color_list( graph, attr, cmap, obj_type="edge", attr_types="numerical", minmax_val=None, none_color=(0.5, 0.5, 0.5, 1), ): """Make a list of colors based on an attribute and colormap. Color will be chosen based on the specified attribute passed to a colormap. Parameters ---------- graph : networkx.MultiDiGraph Input graph attr : string Graph's attribute to select colors by cmap : matplotlib.colors.Colormap Colormap to use for the colors obj_type : string, optional Type of the object to take the attribute from, can be 'edge' or 'node' attr_types : string, optional Type of the attribute to be plotted, can be 'numerical' or 'categorical' minmax_val : tuple, optional If `attr_types` is 'numerical', tuple of (min, max) values of the attribute to be plotted (default: min and max of attr) none_color : tuple, optional Color to use for objects with `None` attribute value Raises ------ ValueError If `attr_types` is not 'numerical' or 'categorical' ValueError If `attr_types` is 'categorical' and `minmax_val` is not None ValueError If `obj_type` is not "edge" or "node" Returns ------- list List of colors, order is the same as in graph.nodes() or graph.edges() """ if attr_types == "categorical" and minmax_val is not None: raise ValueError( f"The `minmax_val` attribute was set to {minmax_val}, " f"it should be None." ) if obj_type not in ["edge", "node"]: raise ValueError( f"The `obj_type` attribute was set to {obj_type}, " f"it should be either 'edge' or 'node'." ) if attr_types == "numerical": minmax_val = determine_minmax_val(graph, minmax_val, attr, attr_type=obj_type) if obj_type == "edge": return [ ( cmap((attr_val - minmax_val[0]) / (minmax_val[1] - minmax_val[0])) if attr_val is not None else none_color ) for u, v, k, attr_val in graph.edges(keys=True, data=attr) ] # obj_type == "node" return [ ( cmap((attr_val - minmax_val[0]) / (minmax_val[1] - minmax_val[0])) if attr_val is not None else none_color ) for u, attr_val in graph.nodes(data=attr) ] if attr_types == "categorical": # Enumerate through the unique values of the attribute # and assign a color to each value, `None` will be assigned to `none_color`. unique_vals = ( set(attr_val for u, v, k, attr_val in graph.edges(keys=True, data=attr)) if obj_type == "edge" else set(attr_val for u, attr_val in graph.nodes(data=attr)) ) # To sort the values, remove None from the set, sort the values, # and add None back to the set. unique_vals.discard(None) unique_vals = list(unique_vals) try: unique_vals = sorted(unique_vals) except TypeError: # If the values are not sortable, just leave them as they are. logger.debug( "The values of the attribute %s are not sortable, " "the order of the colors in the colormap will be random.", attr, ) finally: unique_vals.append(None) if obj_type == "edge": return [ ( cmap(unique_vals.index(attr_val) / (len(unique_vals) - 1)) if attr_val is not None else none_color ) for u, v, k, attr_val in graph.edges(keys=True, data=attr) ] # obj_type == "node" return [ ( cmap(unique_vals.index(attr_val) / (len(unique_vals) - 1)) if attr_val is not None else none_color ) for u, attr_val in graph.nodes(data=attr) ] # If attr_types is not 'numerical' or 'categorical', raise an error raise ValueError( f"The `attr_types` attribute was set to {attr_types}, " f"it should be 'numerical' or 'categorical'." )
[docs] def plot_component_size( graph, attr, component_size, component_values, size_measure_label, ignore=None, title=None, cmap="hsv", minmax_val=None, num_component_log_scale=True, show_legend=None, xticks=None, **kwargs, ): # pylint: disable=too-many-locals """Plot the distribution of component sizes for each partition value. x-axis: values of the partition y-axis: size of the component (e.g. number of edges, nodes or length) color: value of the partition Parameters ---------- graph : networkx.MultiDiGraph Input graph attr : string Graph's attribute to select colormap min and max values by if `minmax_val` is incomplete component_size : list Number of edges in each component component_values : list Value of the partition for each component size_measure_label : str Label of the size measure (e.g. "Number of edges", "Number of nodes", "Length (m)") ignore : list, optional List of values to ignore, plot in gray. If None, no values are ignored. title : str, optional Title of the plot cmap : string, optional Name of a matplotlib colormap minmax_val : tuple, optional Tuple of (min, max) values of the attribute to be plotted (default: min and max of attr) num_component_log_scale : bool, optional If True, the y-axis is plotted on a log scale show_legend : bool, optional If True, the legend is shown. If None, the legend is shown if the unique values of the partition are less than 23. xticks : list, optional List of xticks kwargs Keyword arguments to pass to `matplotlib.pyplot.plot`. Returns ------- fig, axe : tuple matplotlib figure, axis """ fig, axe = plt.subplots() # Choose color of each value minmax_val = determine_minmax_val(graph, minmax_val, attr) colormap = plt.get_cmap(cmap) # Plot logger.debug("Plotting component/partition sizes for %s.", title) # Labelling axe.set_xlabel(attr) axe.set_ylabel(size_measure_label) if title is not None: axe.set_title(f"Component size of {title}") # Scaling and grid if num_component_log_scale: axe.set_yscale("log") axe.grid(True) plt.xticks(xticks) # Make legend with unique colors sorted_unique_values = sorted(set(component_values)) # Show legend if `show_legend` is True, not when it is False, # and if it is None, only if the number of unique values is less than 23 if show_legend or (show_legend is None and len(sorted_unique_values) < 23): sorted_unique_colors = [ colormap((v - minmax_val[0]) / (minmax_val[1] - minmax_val[0])) for v in sorted_unique_values ] # Place legend on the outside right without cutting off the plot axe.legend( handles=[ patches.Patch( color=sorted_unique_colors[i], label=sorted_unique_values[i] ) for i in range(len(sorted_unique_values)) ], fontsize="small", bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0, ) plt.tight_layout() # Scatter plot axe.scatter( component_values, component_size, c=( [ ( colormap((v - minmax_val[0]) / (minmax_val[1] - minmax_val[0])) if i is False else "gray" ) for v, i in zip(component_values, ignore) ] if ignore is not None else [ colormap((v - minmax_val[0]) / (minmax_val[1] - minmax_val[0])) for v in component_values ] ), alpha=0.5, zorder=2, **kwargs, ) return fig, axe
[docs] def plot_road_type_for(graph, included_types, name, **plt_kwargs): """Plot the distribution of road types for the given graph. Find possible road types from the graph's edges, or at https://wiki.openstreetmap.org/wiki/Key:highway. Parameters ---------- graph : networkx.MultiDiGraph Input graph included_types : list List of osm highway keys to be highlighted, all other edges have another color name : str Title of the plot plt_kwargs Keyword arguments to pass to `matplotlib.pyplot.plot`. Returns ------- fig, axe : tuple matplotlib figure, axis """ # Write to 'searched_road_type' attribute 1 if edge is in included_types, # 0 otherwise # graph.edges[edge]["highway"] could be list or string for edge in graph.edges: if ( isinstance(graph.edges[edge]["highway"], list) and any( road_type in included_types for road_type in graph.edges[edge]["highway"] ) ) or ( isinstance(graph.edges[edge]["highway"], str) and graph.edges[edge]["highway"] in included_types ): graph.edges[edge]["residential"] = 1 else: graph.edges[edge]["residential"] = 0 # Plot logger.debug("Plotting residential/other edges for %s.", name) # Use plot_by_attribute to plot the distribution of residential edges return plot_by_attribute( graph, edge_attr="residential", edge_attr_types="numerical", edge_cmap="cool", edge_minmax_val=(0, 1), **plt_kwargs, )
[docs] def save_plot(results_dir, fig, filename, **sa_kwargs): """Save the plot `fig` to file. Saved in the results_dir/filename. Parameters ---------- results_dir : str Directory to save to. fig : matplotlib.figure.Figure Figure to save. filename : str Filename to save to. sa_kwargs Keyword arguments to pass to `matplotlib.pyplot.savefig`. """ filename = path.join(results_dir, filename) # Log saving logger.debug( "Saving plot (%s) to %s", fig.axes[0].get_title(), filename, ) # Check if al axes are compatible with tight_layout # if there are more than one axes if len(fig.axes) > 1: for axe in fig.axes: # Also axe.get_subplotspec() might be None if axe.get_subplotspec() is None: logger.debug( "Not using tight_layout because one of the axes " "has no subplotspec." ) break else: fig.tight_layout() else: fig.tight_layout() # Save fig.savefig(filename, **sa_kwargs)