Source code for driada.network.drawing

from ..utils.plot import create_default_figure
from .matrix_utils import get_laplacian, get_norm_laplacian
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from itertools import combinations
from matplotlib import cm
import matplotlib as mpl
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize as color_normalize


[docs] def draw_degree_distr(net, mode=None, cumulative=0, survival=1, log_log=0, ax=None, **kwargs): """Draw the degree distribution of a network. Visualizes the degree distribution as a histogram or line plot, with options for cumulative distributions and log-log scaling. Production-quality styling is applied automatically via make_beautiful(). Parameters ---------- net : Network Network object to analyze. mode : {'all', 'in', 'out'} or None, optional Which degree type to plot. If None and graph is directed, plots all three types. Default is None. cumulative : int, optional If 1, plot cumulative distribution. Default is 0. survival : int, optional If 1 and cumulative=1, plot survival function (1-CDF). Default is 1. log_log : int, optional If 1, use log-log scale. Default is 0. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new figure. **kwargs Additional styling parameters passed to make_beautiful(). Common options: - spine_width : float (default 3) - tick_width : float (default 3) - tick_labelsize : int (default 20) - label_size : int (default 24) - title_size : int (default 24) - legend_fontsize : int (default 16) - legend_loc : str (default "upper right") Returns ------- ax : matplotlib.axes.Axes The styled axes object. Notes ----- For directed graphs, shows in-degree, out-degree, and total degree distributions in different colors unless mode is specified. Log-log plots are useful for identifying power-law distributions. In log-log mode, the last element is excluded to avoid log(0). Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> # Create a simple directed network with a triangle >>> edges = [(0, 1), (1, 2), (2, 0)] >>> graph = nx.DiGraph(edges) >>> net = Network(graph=graph) >>> # Draw degree distribution (will show uniform degree of 2) >>> draw_degree_distr(net) # doctest: +SKIP >>> # For undirected network >>> graph_undir = nx.Graph(edges) >>> net_undir = Network(graph=graph_undir) >>> draw_degree_distr(net_undir, log_log=1) # doctest: +SKIP""" from driada.utils.plot import make_beautiful if not net.directed: mode = "all" # Create figure only if ax not provided if ax is None: fig, ax = create_default_figure(figsize=(10, 8)) ax.set_title("Degree distribution") if mode is not None: distr = net.get_degree_distr(mode=mode) if cumulative: if survival: distr = 1 - np.cumsum(distr) else: distr = np.cumsum(distr) if log_log: (degree,) = ax.loglog(distr[:-1], linewidth=2, c="k", label="degree") else: (degree,) = ax.plot(distr, linewidth=2, c="k", label="degree") ax.legend(handles=[degree], fontsize=16) else: distr = net.get_degree_distr(mode="all") outdistr = net.get_degree_distr(mode="out") indistr = net.get_degree_distr(mode="in") distrlist = [distr, outdistr, indistr] if cumulative: if survival: distrlist = [1 - np.cumsum(d) for d in distrlist] else: distrlist = [np.cumsum(d) for d in distrlist] if log_log: (degree,) = ax.loglog(distrlist[0][:-1], linewidth=2, c="k", label="degree") (outdegree,) = ax.loglog(distrlist[1][:-1], linewidth=2, c="b", label="outdegree") (indegree,) = ax.loglog(distrlist[2][:-1], linewidth=2, c="r", label="indegree") else: (degree,) = ax.plot(distrlist[0], linewidth=2, c="k", label="degree") (outdegree,) = ax.plot(distrlist[1], linewidth=2, c="b", label="outdegree") (indegree,) = ax.plot(distrlist[2], linewidth=2, c="r", label="indegree") ax.legend(handles=[degree, outdegree, indegree], fontsize=16) # Default styling parameters style_defaults = { 'spine_width': 3, 'tick_width': 3, 'tick_labelsize': 20, 'label_size': 24, 'title_size': 24, 'legend_fontsize': 16, 'legend_loc': "upper right", 'legend_frameon': True, } # Merge user kwargs with defaults (user kwargs override defaults) style_params = {**style_defaults, **kwargs} # Always apply production-quality styling ax = make_beautiful(ax, **style_params) return ax
[docs] def draw_spectrum(net, mode="adj", ax=None, colors=None, cmap="plasma", nbins=None, **kwargs): """Visualize the eigenvalue spectrum of a network matrix. For directed graphs, plots eigenvalues in the complex plane. For undirected graphs, shows a histogram of real eigenvalues. Production-quality styling is applied automatically via make_beautiful(). Parameters ---------- net : Network Network object to analyze. mode : {'adj', 'lap', 'nlap'}, optional Matrix type: 'adj' for adjacency, 'lap' for Laplacian, 'nlap' for normalized Laplacian. Default is 'adj'. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new figure. Default is None. colors : array-like, optional Colors for scatter plot points (directed graphs only). Default is None. cmap : str, optional Colormap name. Default is 'plasma'. nbins : int, optional Number of histogram bins (undirected graphs only). If None, uses Sturges' rule: ceil(log2(n)) + 1. **kwargs Additional styling parameters passed to make_beautiful(). See draw_degree_distr() docstring for common options. Returns ------- ax : matplotlib.axes.Axes The styled axes object. Notes ----- The spectrum provides insights into network properties: - Largest eigenvalue relates to network connectivity - Spectral gap indicates mixing time and robustness - Complex eigenvalues (directed) indicate cyclic structure Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> # Create a small cycle graph >>> graph = nx.cycle_graph(5) >>> net = Network(graph=graph) >>> # Draw adjacency matrix spectrum >>> draw_spectrum(net, mode='adj') # doctest: +SKIP >>> # Draw Laplacian spectrum - eigenvalues should be non-negative >>> draw_spectrum(net, mode='lap') # doctest: +SKIP""" from driada.utils.plot import make_beautiful spectrum = net.get_spectrum(mode) data = np.array(sorted(list(set(spectrum)), key=np.abs)) if ax is None: fig, ax = create_default_figure(figsize=(12, 10)) if net.directed: ax.scatter(data.real, data.imag, cmap=cmap, c=colors) ax.set_xlabel("Real part") ax.set_ylabel("Imaginary part") else: if nbins is None: # Use Sturges' rule: nbins = ceil(log2(n)) + 1 nbins = int(np.ceil(np.log2(len(spectrum)))) + 1 ax.hist(data.real, bins=nbins, edgecolor='black', linewidth=0.5, alpha=0.8) ax.set_xlabel("Eigenvalue") ax.set_ylabel("Count") # Default styling parameters style_defaults = { 'spine_width': 3, 'tick_width': 3, 'tick_labelsize': 20, 'label_size': 24, 'title_size': 24, 'legend_fontsize': 16, } # Merge user kwargs with defaults style_params = {**style_defaults, **kwargs} # Always apply production-quality styling ax = make_beautiful(ax, **style_params) return ax
[docs] def get_vector_coloring(vec, cmap="plasma"): """Create color mapping for vector values. Maps numerical values to colors using matplotlib colormap, normalizing to [0, 1] range. Parameters ---------- vec : array-like Vector of numerical values to map to colors. cmap : str, optional Matplotlib colormap name. Default is 'plasma'. Returns ------- numpy.ndarray Array of RGBA color values, shape (n, 4). Raises ------ ValueError If all values in vec are identical (no variation to map). Examples -------- >>> import numpy as np >>> values = [0.1, 0.5, 0.9, 0.3] >>> colors = get_vector_coloring(values, cmap='viridis') >>> colors.shape (4, 4) >>> # Colors are RGBA values in range [0, 1] >>> assert np.all(colors >= 0) and np.all(colors <= 1) >>> # Test with uniform values - should raise error >>> try: ... get_vector_coloring([1.0, 1.0, 1.0]) ... except ValueError as e: ... print("Error:", str(e)) Error: All values in vector are identical, cannot create color mapping""" cmap = plt.get_cmap(cmap) vec = np.array(vec).ravel() vec_min = np.min(vec) vec_max = np.max(vec) if vec_max == vec_min: raise ValueError("All values in vector are identical, cannot create color mapping") colors = cmap((vec - vec_min) / (vec_max - vec_min)) return colors
[docs] def draw_eigenvectors( net, left_ind, right_ind, mode="adj", nodesize=None, cmap="plasma", draw_edges=True, edge_options={}, ): """Draw network nodes colored by eigenvector components. Visualizes a network with nodes colored according to the values of eigenvectors in the specified index range. Creates a grid of subplots, one for each eigenvector from left_ind to right_ind (inclusive). Parameters ---------- net : Network Network object to visualize. left_ind : int Starting index of eigenvectors to visualize (inclusive). right_ind : int Ending index of eigenvectors to visualize (inclusive). mode : str, optional Matrix mode for eigendecomposition. Options: 'adj', 'lap', 'nlap'. Default is 'adj'. nodesize : float or None, optional Size of nodes in the visualization. If None, uses default size. Default is None. cmap : str or matplotlib colormap, optional Colormap for coloring nodes. Default is 'plasma'. draw_edges : bool, optional Whether to draw edges. Default is True. edge_options : dict, optional Additional options for edge drawing (passed to networkx). Default is empty dict. Returns ------- matplotlib.figure.Figure Figure containing the eigenvector visualization. Notes ----- The function creates a grid of subplots arranged to fit all requested eigenvectors. Each subplot shows the network with nodes colored by the corresponding eigenvector's components. The subplot title shows the eigenvector index and its eigenvalue. Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> # Create a graph with interesting spectral properties >>> graph = nx.cycle_graph(8) >>> net = Network(graph=graph) >>> # Visualize first 4 eigenvectors of adjacency matrix >>> draw_eigenvectors(net, 0, 3, mode='adj') # doctest: +SKIP >>> # Visualize Laplacian eigenvectors (Fiedler vector is at index 1) >>> draw_eigenvectors(net, 1, 2, mode='lap') # doctest: +SKIP """ spectrum = net.get_spectrum(mode) eigenvectors = net.get_eigenvectors(mode) vecs = eigenvectors[:, left_ind : right_ind + 1] # vecs = np.abs(net.eigenvectors[:, left_ind: right_ind+1]) eigvals = np.real(spectrum[left_ind : right_ind + 1]) npics = vecs.shape[1] pics_in_a_row = int(np.ceil(np.sqrt(npics))) pics_in_a_col = int(np.ceil(1.0 * npics / pics_in_a_row)) fig, axs = plt.subplots( nrows=pics_in_a_col, ncols=pics_in_a_row, figsize=(8 * pics_in_a_row, 8 * pics_in_a_col), ) for ax in axs.ravel(): ax.set_axis_off() plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, hspace=0.2, wspace=0.1) if net.pos is None: pos = nx.layout.spring_layout(net.graph) # pos = nx.drawing.layout.circular_layout(net.graph) else: pos = net.pos if nodesize is None: nodesize = np.sqrt(net.scaled_outdeg) * 500 + 50 for i in range(npics): vec = vecs[:, i] ax = fig.add_subplot(pics_in_a_col, pics_in_a_row, i + 1) text = "eigenvector " + str(i + 1) + " lambda " + str(np.round(eigvals[i], 3)) ax.set_title(text) ncolors = get_vector_coloring(vec) options = { "node_color": ncolors, "node_size": nodesize, "cmap": mpl.colormaps[cmap], } nx.draw_networkx_nodes(net.graph, pos, ax=ax, **options) if draw_edges: nx.draw_networkx_edges(net.graph, pos, **edge_options) # pc, = mpl.collections.PatchCollection(nodes, cmap = options['cmap']) # pc.set_array(edge_colors) cmappable = ScalarMappable(color_normalize(0, 1), cmap=cmap) fig.colorbar(cmappable, ax=ax) ax.set_axis_off() plt.tight_layout() plt.show() return fig
[docs] def draw_net(net, colors=None, nodesize=None, ax=None): """Visualize a network graph with customizable node properties. Parameters ---------- net : Network Network object to visualize. colors : array-like, optional Node colors. Can be a single color or array of values to be mapped to colors. Default is None. nodesize : array-like, optional Node sizes. If None, sizes are based on node out-degree. Default is None. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new figure. Default is None. Returns ------- None Displays the network visualization. Notes ----- If no node positions are stored in the network, uses spring layout for automatic positioning. Node sizes by default are proportional to the square root of scaled out-degree. Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> import numpy as np >>> # Create a small network with a path >>> edges = [(0, 1), (1, 2)] >>> graph = nx.Graph(edges) >>> net = Network(graph=graph, create_nx_graph=True) >>> # Draw network with default settings >>> draw_net(net) # doctest: +SKIP >>> # Draw with custom colors based on node index >>> colors = [0, 0.5, 1] # Three nodes with gradient colors >>> draw_net(net, colors=colors) # doctest: +SKIP""" if ax is None: fig, ax = plt.subplots(figsize=(16, 12)) if net.pos is None: print("Node positions not found, auto layout was constructed") pos = nx.layout.spring_layout(net.graph) # pos = nx.drawing.layout.circular_layout(net.graph) else: pos = net.pos if nodesize is None: nodesize = np.sqrt(net.scaled_outdeg) * 100 + 10 node_options = { "node_size": nodesize, "cmap": cm.get_cmap("Spectral"), } edge_options = {} nx.draw_networkx_nodes(net.graph, pos, node_color=colors, ax=ax, **node_options) nx.draw_networkx_edges(net.graph, pos, ax=ax, **edge_options) plt.show()
[docs] def show_mat(net, dtype=None, mode="adj", ax=None): """Display a network matrix as a heatmap. Parameters ---------- net : Network Network object containing the matrix. dtype : numpy.dtype, optional Data type to cast matrix to before display. Useful for binary visualization. Default is None. mode : str, optional Matrix type to display: - 'adj': adjacency matrix - 'lap'/'lap_out': Laplacian matrix - 'nlap': normalized Laplacian Default is 'adj'. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new figure. Default is None. Returns ------- None Displays the matrix visualization. Notes ----- If the requested matrix hasn't been computed yet, it will be generated on demand. Sparse matrices are converted to dense for visualization. Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> import numpy as np >>> # Create a small cycle graph >>> edges = [(0, 1), (1, 2), (2, 0)] >>> graph = nx.Graph(edges) >>> net = Network(graph=graph) >>> # Show adjacency matrix as binary (shows connections) >>> show_mat(net, dtype=bool) # doctest: +SKIP >>> # Show Laplacian matrix (degree matrix minus adjacency) >>> show_mat(net, mode='lap') # doctest: +SKIP >>> # For a directed graph >>> digraph = nx.DiGraph(edges) >>> net_dir = Network(graph=digraph) >>> show_mat(net_dir, mode='adj') # doctest: +SKIP""" mat = getattr(net, mode) if mat is None: if mode in ["lap", "lap_out"]: mat = get_laplacian(net.adj) elif mode == "nlap": mat = get_norm_laplacian(net.adj) if ax is None: fig, ax = plt.subplots(figsize=(10, 10)) # Convert to dense array if sparse if hasattr(mat, "toarray"): mat_dense = mat.toarray() else: mat_dense = np.asarray(mat) if dtype is not None: ax.matshow(mat_dense.astype(dtype)) else: ax.matshow(mat_dense)
[docs] def plot_lem_embedding(net, ndim, colors=None): """Plot Laplacian Eigenmaps embedding of the network. Computes a low-dimensional embedding using Laplacian Eigenmaps and visualizes nodes in the embedded space. Parameters ---------- net : Network Network object to embed. ndim : int Number of dimensions for embedding (2 or 3). colors : array-like, optional Node colors for visualization. Default is None. Returns ------- None Displays the embedding plot. Notes ----- Laplacian Eigenmaps use the eigenvectors of the graph Laplacian corresponding to the smallest non-zero eigenvalues to embed nodes in a low-dimensional space while preserving local structure. Examples -------- >>> import matplotlib >>> matplotlib.use('Agg') # Use non-interactive backend for testing >>> from driada.network import Network >>> import networkx as nx >>> # Create a small network that can be well-embedded >>> graph = nx.cycle_graph(6) >>> net = Network(graph=graph) >>> # Create 2D Laplacian Eigenmaps embedding >>> plot_lem_embedding(net, ndim=2) # doctest: +SKIP >>> # Create 3D embedding for more complex visualization >>> graph_3d = nx.complete_graph(5) >>> net_3d = Network(graph=graph_3d) >>> plot_lem_embedding(net_3d, ndim=3) # doctest: +SKIP""" if net.lem_emb is None: net.construct_lem_embedding(ndim) if colors is None: colors = range(net.lem_emb.shape[1]) psize = 10 # Handle both sparse and dense arrays if hasattr(net.lem_emb, "toarray"): data = net.lem_emb.toarray() else: data = net.lem_emb pairs = list(combinations(np.arange(ndim), 2)) npics = len(pairs) pics_in_a_row = int(np.ceil(np.sqrt(npics))) pics_in_a_col = int(np.ceil(1.0 * npics / pics_in_a_row)) fig = plt.figure(figsize=(20, 20)) fig.suptitle("Projections") for i in range(len(pairs)): ax = fig.add_subplot(pics_in_a_row, pics_in_a_col, i + 1) i1, i2 = pairs[i] scatter = ax.scatter(data[i1, :], data[i2, :], c=colors, s=psize) ax.legend(*scatter.legend_elements(), loc="upper left", title="Classes") ax.text( min(data[i1, :]), min(data[i2, :]), "axes " + str(i1 + 1) + " vs." + str(i2 + 1), bbox={"facecolor": "red", "alpha": 0.5, "pad": 10}, )
# ax.add_artist(legend)