Source code for easygraph.functions.drawing.utils

from itertools import chain
from typing import List
from typing import Optional
from typing import Tuple

import matplotlib
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.axes import Axes
from matplotlib.collections import LineCollection
from matplotlib.collections import PatchCollection
from matplotlib.patches import Circle
from matplotlib.patches import PathPatch
from matplotlib.path import Path
from scipy.spatial import ConvexHull

from .geometry import common_tangent_radian
from .geometry import polar_position
from .geometry import rad_2_deg
from .geometry import radian_from_atan
from .geometry import vlen


# from fa2 import ForceAtlas2
# import bezier
# import numpy as np
# from easygraph import to_networkx
# from easygraph.utils.exception import EasyGraphError
# import easygraph as eg


[docs] def safe_div(a: np.ndarray, b: np.ndarray, jitter_scale: float = 0.000001): mask = b == 0 b[mask] = 1 inv_b = 1.0 / b res = a * inv_b if mask.sum() > 0: res[mask.repeat(2, 2)] = np.random.randn(mask.sum() * 2) * jitter_scale return res
[docs] def init_pos(num_v: int, center: Tuple[float, float] = (0, 0), scale: float = 1.0): return (np.random.rand(num_v, 2) * 2 - 1) * scale + center
[docs] def draw_line_edge( ax: Axes, v_coor: np.array, v_size: list, e_list: List[Tuple[int, int]], show_arrow: bool, e_color: list, e_line_width: list, ): arrow_head_width = ( [0.015 * w for w in e_line_width] if show_arrow else [0] * len(e_list) ) for eidx, e in enumerate(e_list): start_pos = v_coor[e[0]] end_pos = v_coor[e[1]] dir = end_pos - start_pos dir = dir / np.linalg.norm(dir) start_pos = start_pos + dir * v_size[e[0]] end_pos = end_pos - dir * v_size[e[1]] x, y = start_pos[0], start_pos[1] dx, dy = end_pos[0] - x, end_pos[1] - y ax.arrow( x, y, dx, dy, head_width=arrow_head_width[eidx], color=e_color[eidx], linewidth=e_line_width[eidx], length_includes_head=True, )
[docs] def draw_circle_edge( ax: Axes, v_coor: List[Tuple[float, float]], v_size: list, e_list: List[Tuple[int, int]], e_color: list, e_fill_color: list, e_line_width: list, ): n_v = len(v_coor) line_paths, arc_paths, vertices = hull_layout(n_v, e_list, v_coor, v_size) for eidx, lines in enumerate(line_paths): pathdata = [] for line in lines: if len(line) == 0: continue start_pos, end_pos = line pathdata.append((Path.MOVETO, start_pos.tolist())) pathdata.append((Path.LINETO, end_pos.tolist())) if len(list(zip(*pathdata))) == 0: continue codes, verts = zip(*pathdata) path = Path(verts, codes) ax.add_patch( PathPatch( path, linewidth=e_line_width[eidx], facecolor=e_fill_color[eidx], edgecolor=e_color[eidx], ) ) for eidx, arcs in enumerate(arc_paths): for arc in arcs: center, theta1, theta2, radius = arc x, y = center[0], center[1] patcjes_arc = matplotlib.patches.Arc( (x, y), 2 * radius, 2 * radius, theta1=theta1, theta2=theta2, color=e_color[eidx], linewidth=e_line_width[eidx], # edgecolor=e_color[eidx], edgecolor=e_color[eidx], facecolor=e_fill_color[eidx], ) ax.add_patch( matplotlib.patches.Arc( (x, y), 2 * radius, 2 * radius, theta1=theta1, theta2=theta2, color=e_color[eidx], linewidth=e_line_width[eidx], # edgecolor=e_color[eidx], edgecolor=e_color[eidx], facecolor=e_fill_color[eidx], ) )
[docs] def edge_list_to_incidence_matrix(num_v: int, e_list: List[tuple]) -> np.ndarray: v_idx = list(chain(*e_list)) e_idx = [[idx] * len(e) for idx, e in enumerate(e_list)] e_idx = list(chain(*e_idx)) H = np.zeros((num_v, len(e_list))) H[v_idx, e_idx] = 1 return H
[docs] def draw_vertex( ax: Axes, v_coor: List[Tuple[float, float]], v_label: Optional[List[str]], font_size: int, font_family: str, v_size: list, v_color: list, edgecolors, v_line_width: list, ): patches = [] n = v_coor.shape[0] if v_label is None: v_label = [""] * n for coor, label, size, width in zip(v_coor.tolist(), v_label, v_size, v_line_width): circle = Circle(coor, size) circle.lineWidth = width # circle.label = label if label != "": x, y = coor[0], coor[1] offset = 0, -1.3 * size x += offset[0] y += offset[1] ax.text( x, y, label, fontsize=font_size, fontfamily=font_family, ha="center", va="top", ) patches.append(circle) edgecolors = "black" if edgecolors == None else edgecolors p = PatchCollection(patches, facecolors=v_color, edgecolors=edgecolors) ax.add_collection(p)
[docs] def hull_layout(n_v, e_list, pos, v_size, radius_increment=0.3): line_paths = [None] * len(e_list) arc_paths = [None] * len(e_list) polygons_vertices_index = [] vertices_radius = np.array(v_size) vertices_increased_radius = vertices_radius * radius_increment vertices_radius += vertices_increased_radius e_degree = [len(e) for e in e_list] e_idxs = np.argsort(np.array(e_degree)) # for edge in e_list: for e_idx in e_idxs: edge = list(e_list[e_idx]) line_path_for_e = [] arc_path_for_e = [] if len(edge) == 1: arc_path_for_e.append([pos[edge[0]], 0, 360, vertices_radius[edge[0]]]) vertices_radius[edge] += vertices_increased_radius[edge] line_paths[e_idx] = line_path_for_e arc_paths[e_idx] = arc_path_for_e continue pos_in_edge = pos[edge] if len(edge) == 2: vertices_index = np.array((0, 1), dtype=np.int64) else: hull = ConvexHull(pos_in_edge) vertices_index = hull.vertices n_vertices = vertices_index.shape[0] vertices_index = np.append(vertices_index, vertices_index[0]) # close the loop thetas = [] for i in range(n_vertices): # line i1 = edge[vertices_index[i]] i2 = edge[vertices_index[i + 1]] r1 = vertices_radius[i1] r2 = vertices_radius[i2] p1 = pos[i1] p2 = pos[i2] dp = p2 - p1 dp_len = vlen(dp) beta = radian_from_atan(dp[0], dp[1]) alpha = common_tangent_radian(r1, r2, dp_len) theta = beta - alpha start_point = polar_position(r1, theta, p1) end_point = polar_position(r2, theta, p2) line_path_for_e.append((start_point, end_point)) thetas.append(theta) for i in range(n_vertices): # arcs theta_1 = thetas[i - 1] theta_2 = thetas[i] arc_center = pos[edge[vertices_index[i]]] radius = vertices_radius[edge[vertices_index[i]]] theta_1, theta_2 = rad_2_deg(theta_1), rad_2_deg(theta_2) arc_path_for_e.append((arc_center, theta_1, theta_2, radius)) vertices_radius[edge] += vertices_increased_radius[edge] polygons_vertices_index.append(vertices_index.copy()) # line_paths.append(line_path_for_e) # arc_paths.append(arc_path_for_e) line_paths[e_idx] = line_path_for_e arc_paths[e_idx] = arc_path_for_e return line_paths, arc_paths, polygons_vertices_index
[docs] def apply_alpha(colors, alpha, elem_list, cmap=None, vmin=None, vmax=None): """Apply an alpha (or list of alphas) to the colors provided. Parameters ---------- colors : color string or array of floats (default='r') Color of element. Can be a single color format string, or a sequence of colors with the same length as nodelist. If numeric values are specified they will be mapped to colors using the cmap and vmin,vmax parameters. See matplotlib.scatter for more details. alpha : float or array of floats Alpha values for elements. This can be a single alpha value, in which case it will be applied to all the elements of color. Otherwise, if it is an array, the elements of alpha will be applied to the colors in order (cycling through alpha multiple times if necessary). elem_list : array of networkx objects The list of elements which are being colored. These could be nodes, edges or labels. cmap : matplotlib colormap Color map for use if colors is a list of floats corresponding to points on a color mapping. vmin, vmax : float Minimum and maximum values for normalizing colors if a colormap is used Returns ------- rgba_colors : numpy ndarray Array containing RGBA format values for each of the node colours. """ from itertools import cycle from itertools import islice from numbers import Number import matplotlib as mpl import matplotlib.cm # call as mpl.cm import matplotlib.colors # call as mpl.colors import numpy as np # If we have been provided with a list of numbers as long as elem_list, # apply the color mapping. if len(colors) == len(elem_list) and isinstance(colors[0], Number): mapper = mpl.cm.ScalarMappable(cmap=cmap) mapper.set_clim(vmin, vmax) rgba_colors = mapper.to_rgba(colors) # Otherwise, convert colors to matplotlib's RGB using the colorConverter # object. These are converted to numpy ndarrays to be consistent with the # to_rgba method of ScalarMappable. else: try: rgba_colors = np.array([mpl.colors.colorConverter.to_rgba(colors)]) except ValueError: rgba_colors = np.array( [mpl.colors.colorConverter.to_rgba(color) for color in colors] ) # Set the final column of the rgba_colors to have the relevant alpha values try: # If alpha is longer than the number of colors, resize to the number of # elements. Also, if rgba_colors.size (the number of elements of # rgba_colors) is the same as the number of elements, resize the array, # to avoid it being interpreted as a colormap by scatter() if len(alpha) > len(rgba_colors) or rgba_colors.size == len(elem_list): rgba_colors = np.resize(rgba_colors, (len(elem_list), 4)) rgba_colors[1:, 0] = rgba_colors[0, 0] rgba_colors[1:, 1] = rgba_colors[0, 1] rgba_colors[1:, 2] = rgba_colors[0, 2] rgba_colors[:, 3] = list(islice(cycle(alpha), len(rgba_colors))) except TypeError: rgba_colors[:, -1] = alpha return rgba_colors
# def draw_easygraph_nodes( # G, # pos, # nodelist=None, # node_size=300, # node_color="#1f78b4", # node_shape="o", # alpha=None, # cmap=None, # vmin=None, # vmax=None, # ax=None, # linewidths=None, # edgecolors=None, # label=None, # margins=None, # ): # """Draw the nodes of the graph G. # This draws only the nodes of the graph G. # Parameters # ---------- # G : graph # A easygraph graph # pos : dictionary # A dictionary with nodes as keys and positions as values. # Positions should be sequences of length 2. # ax : Matplotlib Axes object, optional # Draw the graph in the specified Matplotlib axes. # nodelist : list (default list(G)) # Draw only specified nodes # node_size : scalar or array (default=300) # Size of nodes. If an array it must be the same length as nodelist. # node_color : color or array of colors (default='#1f78b4') # Node color. Can be a single color or a sequence of colors with the same # length as nodelist. Color can be string or rgb (or rgba) tuple of # floats from 0-1. If numeric values are specified they will be # mapped to colors using the cmap and vmin,vmax parameters. See # matplotlib.scatter for more details. # node_shape : string (default='o') # The shape of the node. Specification is as matplotlib.scatter # marker, one of 'so^>v<dph8'. # alpha : float or array of floats (default=None) # The node transparency. This can be a single alpha value, # in which case it will be applied to all the nodes of color. Otherwise, # if it is an array, the elements of alpha will be applied to the colors # in order (cycling through alpha multiple times if necessary). # cmap : Matplotlib colormap (default=None) # Colormap for mapping intensities of nodes # vmin,vmax : floats or None (default=None) # Minimum and maximum for node colormap scaling # linewidths : [None | scalar | sequence] (default=1.0) # Line width of symbol border # edgecolors : [None | scalar | sequence] (default = node_color) # Colors of node borders # label : [None | string] # Label for legend # margins : float or 2-tuple, optional # Sets the padding for axis autoscaling. Increase margin to prevent # clipping for nodes that are near the edges of an image. Values should # be in the range ``[0, 1]``. See :meth:`matplotlib.axes.Axes.margins` # for details. The default is `None`, which uses the Matplotlib default. # Returns # ------- # matplotlib.collections.PathCollection # `PathCollection` of the nodes. # Examples # -------- # >>> from easygraph.datasets import get_graph_karateclub # >>> import easygraph as eg # >>> G = get_graph_karateclub() # >>> nodes = eg.draw_easygraph_nodes(G, pos=eg.circular_position(G)) # """ # from collections.abc import Iterable # import matplotlib as mpl # import matplotlib.collections # call as mpl.collections # import matplotlib.pyplot as plt # import numpy as np # if ax is None: # ax = plt.gca() # if nodelist is None: # nodelist = list(G) # if len(nodelist) == 0: # empty nodelist, no drawing # return mpl.collections.PathCollection(None) # try: # xy = np.asarray([pos[v] for v in nodelist]) # except KeyError as err: # raise EasyGraphError(f"Node {err} has no position.") from err # if isinstance(alpha, Iterable): # node_color = apply_alpha(node_color, alpha, nodelist, cmap, vmin, vmax) # alpha = None # node_collection = ax.scatter( # xy[:, 0], # xy[:, 1], # s=node_size, # c=node_color, # marker=node_shape, # cmap=cmap, # vmin=vmin, # vmax=vmax, # alpha=alpha, # linewidths=linewidths, # edgecolors=edgecolors, # label=label, # ) # ax.tick_params( # axis="both", # which="both", # bottom=False, # left=False, # labelbottom=False, # labelleft=False, # ) # if margins is not None: # if isinstance(margins, Iterable): # ax.margins(*margins) # else: # ax.margins(margins) # node_collection.set_zorder(2) # return node_collection # def draw_curved_edges(G, pos, dist_ratio=0.2, bezier_precision=20, polarity='random'): # # Get nodes into np array # edges = np.array(G.edges()) # l = edges.shape[0] # if polarity == 'random': # # Random polarity of curve # rnd = np.where(np.random.randint(2, size=l)==0, -1, 1) # else: # # Create a fixed (hashed) polarity column in the case we use fixed polarity # # This is useful, e.g., for animations # rnd = np.where(np.mod(np.vectorize(hash)(edges[:,0])+np.vectorize(hash)(edges[:,1]),2)==0,-1,1) # # Coordinates (x,y) of both nodes for each edge # # e.g., https://stackoverflow.com/questions/16992713/translate-every-element-in-numpy-array-according-to-key # # Note the np.vectorize method doesn't work for all node position dictionaries for some reason # u, inv = np.unique(edges, return_inverse = True) # coords = np.array([pos[x] for x in u])[inv].reshape([edges.shape[0], 2, edges.shape[1]]) # coords_node1 = coords[:,0,:] # coords_node2 = coords[:,1,:] # # Swap node1/node2 allocations to make sure the directionality works correctly # should_swap = coords_node1[:,0] > coords_node2[:,0] # coords_node1[should_swap], coords_node2[should_swap] = coords_node2[should_swap], coords_node1[should_swap] # # Distance for control points # dist = dist_ratio * np.sqrt(np.sum((coords_node1-coords_node2)**2, axis=1)) # # Gradients of line connecting node & perpendicular # m1 = (coords_node2[:,1]-coords_node1[:,1])/(coords_node2[:,0]-coords_node1[:,0]) # m2 = -1/m1 # # Temporary points along the line which connects two nodes # # e.g., https://math.stackexchange.com/questions/656500/given-a-point-slope-and-a-distance-along-that-slope-easily-find-a-second-p # t1 = dist/np.sqrt(1+m1**2) # v1 = np.array([np.ones(l),m1]) # coords_node1_displace = coords_node1 + (v1*t1).T # coords_node2_displace = coords_node2 - (v1*t1).T # # Control points, same distance but along perpendicular line # # rnd gives the 'polarity' to determine which side of the line the curve should arc # t2 = dist/np.sqrt(1+m2**2) # v2 = np.array([np.ones(len(edges)),m2]) # coords_node1_ctrl = coords_node1_displace + (rnd*v2*t2).T # coords_node2_ctrl = coords_node2_displace + (rnd*v2*t2).T # # Combine all these four (x,y) columns into a 'node matrix' # node_matrix = np.array([coords_node1, coords_node1_ctrl, coords_node2_ctrl, coords_node2]) # # Create the Bezier curves and store them in a list # curveplots = [] # for i in range(l): # nodes = node_matrix[:,i,:].T # curveplots.append(bezier.Curve(nodes, degree=3).evaluate_multi(np.linspace(0,1,bezier_precision)).T) # # Return an array of these curves # curves = np.array(curveplots) # return curves # def draw_curved_graph(G, colors, ax): # #G = to_networkx(G) # # layout # pos = eg.spring_layout(G, iterations=50) # eg.draw_networkx_nodes(G, pos, ax=ax, node_size=200, node_color=colors[0], alpha=0.5) # # 绘制标签 # eg.draw_networkx_labels(G, pos, ax=ax, font_size=8, font_family='Arial', font_color='black') # # Produce the curves # curves = draw_curved_edges(G, pos) # lc = LineCollection(curves, color=colors[1], alpha=0.4) # # 添加连线 # ax.add_collection(lc) # # 设置坐标轴参数 # ax.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False) # plt.savefig('Figure.pdf') # plt.show()