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()