From 6b8daf18f82d0b8f7192ddae5b391bec47cdb001 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Fri, 31 May 2024 07:39:10 +0500 Subject: [PATCH 1/8] Update #1: Fixing mpl_draw() for multigraphs --- rustworkx/visualization/matplotlib.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 7f1437112..40fd8d8e0 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -754,6 +754,16 @@ def _connectionstyle(posA, posB, *args, **kwargs): else: line_width = width + # radius of edges + + reverse_edge = np.concatenate(([dst], [src])) + for edge in edge_pos: #the loop can be optimized + if bool(np.sum(np.all(np.equal(edge, reverse_edge)))): + rad = 0.25 + break + else: + rad = 0 + arrow = mpl.patches.FancyArrowPatch( (x1, y1), (x2, y2), @@ -763,7 +773,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): mutation_scale=mutation_scale, color=arrow_color, linewidth=line_width, - connectionstyle=_connectionstyle, + connectionstyle=connectionstyle + f", rad = {rad}", linestyle=style, zorder=1, ) # arrows go behind nodes @@ -1001,6 +1011,12 @@ def draw_edge_labels( x1 * label_pos + x2 * (1.0 - label_pos), y1 * label_pos + y2 * (1.0 - label_pos), ) + if (n2, n1) in labels.keys(): #loop + x += 0.05 * label_pos + if n2 > n1: + y -= 0.25 + else: + y += 0.25 if rotate: # in degrees From 01a119f796ba0cbb9df0e01633dcec266bf53a54 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Sat, 1 Jun 2024 00:53:43 +0500 Subject: [PATCH 2/8] Update matplotlib.py for formatting --- rustworkx/visualization/matplotlib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 40fd8d8e0..13526c46a 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -755,9 +755,9 @@ def _connectionstyle(posA, posB, *args, **kwargs): line_width = width # radius of edges - - reverse_edge = np.concatenate(([dst], [src])) - for edge in edge_pos: #the loop can be optimized + + reverse_edge = np.concatenate(([dst], [src])) + for edge in edge_pos: # the loop can be optimized if bool(np.sum(np.all(np.equal(edge, reverse_edge)))): rad = 0.25 break @@ -1011,7 +1011,7 @@ def draw_edge_labels( x1 * label_pos + x2 * (1.0 - label_pos), y1 * label_pos + y2 * (1.0 - label_pos), ) - if (n2, n1) in labels.keys(): #loop + if (n2, n1) in labels.keys(): # loop x += 0.05 * label_pos if n2 > n1: y -= 0.25 From cd32f0c0567404a32002c62aa9289bdf67584878 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Mon, 3 Jun 2024 06:39:33 +0500 Subject: [PATCH 3/8] Update rustworkx/visualization/matplotlib.py Co-authored-by: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> --- rustworkx/visualization/matplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 13526c46a..8e300cc83 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -773,7 +773,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): mutation_scale=mutation_scale, color=arrow_color, linewidth=line_width, - connectionstyle=connectionstyle + f", rad = {rad}", + connectionstyle=f"{connectionstyle}, rad = {rad}", linestyle=style, zorder=1, ) # arrows go behind nodes From ae60f409391c2056cdf994003ba3417796b0c39e Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Tue, 4 Jun 2024 17:34:56 +0500 Subject: [PATCH 4/8] Update matplotlib.py to remove the loop --- rustworkx/visualization/matplotlib.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 8e300cc83..5c09dc21b 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -755,14 +755,13 @@ def _connectionstyle(posA, posB, *args, **kwargs): line_width = width # radius of edges - - reverse_edge = np.concatenate(([dst], [src])) - for edge in edge_pos: # the loop can be optimized - if bool(np.sum(np.all(np.equal(edge, reverse_edge)))): - rad = 0.25 - break - else: - rad = 0 + reverse_edge = [dst, src] + if ( + len(np.where(np.all(edge_pos == reverse_edge, axis=(1, 2)))[0]) != 0 + ): # if reverse edge is in `edge_pos` + rad = 0.25 + else: + rad = 0.0 arrow = mpl.patches.FancyArrowPatch( (x1, y1), @@ -773,7 +772,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): mutation_scale=mutation_scale, color=arrow_color, linewidth=line_width, - connectionstyle=f"{connectionstyle}, rad = {rad}", + connectionstyle=connectionstyle + f", rad = {rad}", linestyle=style, zorder=1, ) # arrows go behind nodes @@ -1012,11 +1011,11 @@ def draw_edge_labels( y1 * label_pos + y2 * (1.0 - label_pos), ) if (n2, n1) in labels.keys(): # loop - x += 0.05 * label_pos + dy = np.abs(y2 - y1) if n2 > n1: - y -= 0.25 + y -= 0.25 * dy else: - y += 0.25 + y += 0.25 * dy if rotate: # in degrees From b895d05540ae1247656305337c3779fd9e306ad4 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Tue, 4 Jun 2024 17:55:41 +0500 Subject: [PATCH 5/8] Add releasenotes --- .../fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml diff --git a/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml b/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml new file mode 100644 index 000000000..2d15346b8 --- /dev/null +++ b/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fixes the plots of multigraphs using the `mpl_draw()` function. Refer to + `#12345 ` for more + details. +other: + - | + The radius of the edges of self-loops in multigraphs is set to `0.25`. + The edge labels are offset accordingly. From ef4e5b3d0d5ba0bb63eba872c9dee5e7eb52a040 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Sun, 9 Jun 2024 01:14:22 +0500 Subject: [PATCH 6/8] Reformat connectionstyle string in rustworkx/visualization/matplotlib.py Co-authored-by: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> --- rustworkx/visualization/matplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 5c09dc21b..3034cf9d9 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -772,7 +772,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): mutation_scale=mutation_scale, color=arrow_color, linewidth=line_width, - connectionstyle=connectionstyle + f", rad = {rad}", + connectionstyle=f"{connectionstyle}, rad = {rad}", linestyle=style, zorder=1, ) # arrows go behind nodes From 7b4bdbd40535e079b29970299b38d2e2f099a459 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Sun, 9 Jun 2024 01:20:03 +0500 Subject: [PATCH 7/8] Fixes #774 --- ...l-draw-digraph-plots-aecf86738ab9b0db.yaml | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml b/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml index 2d15346b8..926e36044 100644 --- a/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml +++ b/releasenotes/notes/fix-mpl-draw-digraph-plots-aecf86738ab9b0db.yaml @@ -1,10 +1,29 @@ --- fixes: - | - Fixes the plots of multigraphs using the `mpl_draw()` function. Refer to - `#12345 ` for more - details. -other: + Fixed the plots of multigraphs using :func:`.mpl_draw`. Previously, parallel edges of + multigraphs were plotted on top of each other, with overlapping arrows and labels. + The radius of parallel edges of the multigraph was fixed to be `0.25` for + `connectionstyle` supporting this argument in :func:`.draw_edges`. The edge lables + were offset to `0.25` in :func:`.draw_edge_labels` to align with their respective + edges. This fix can be tested using the following code: + + .. jupyter-execute:: + + import rustworkx + from rustworkx.visualization import mpl_draw + + graph = rustworkx.PyDiGraph() + graph.add_node('A') + graph.add_node('B') + graph.add_node('C') + + graph.add_edge(1, 0, 2) + graph.add_edge(0, 1, 3) + graph.add_edge(1, 2, 4) + + mpl_draw(graph, with_labels=True, labels=str, edge_labels=str, alpha=0.5) + - | - The radius of the edges of self-loops in multigraphs is set to `0.25`. - The edge labels are offset accordingly. + Refer to `#774 ` for more + details. From d70183ef1955a47e63a0a02425a4a12e56f48e11 Mon Sep 17 00:00:00 2001 From: Mahnoor Fatima Date: Sun, 9 Jun 2024 03:40:08 +0500 Subject: [PATCH 8/8] Optimize edge search by using sets --- rustworkx/visualization/matplotlib.py | 101 +++++++++++++------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/rustworkx/visualization/matplotlib.py b/rustworkx/visualization/matplotlib.py index 3034cf9d9..559756a13 100644 --- a/rustworkx/visualization/matplotlib.py +++ b/rustworkx/visualization/matplotlib.py @@ -636,7 +636,9 @@ def draw_edges( edge_color = "k" # set edge positions - edge_pos = np.asarray([(pos[e[0]], pos[e[1]]) for e in edge_list]) + edge_pos = set() + for e in edge_list: + edge_pos.add((tuple(pos[e[0]]), tuple(pos[e[1]]))) # Check if edge_color is an array of floats and map to edge_cmap. # This is the only case handled differently from matplotlib @@ -670,58 +672,17 @@ def to_marker_edge(marker_size, marker): arrow_collection = [] mutation_scale = arrow_size # scale factor of arrow head - # compute view - mirustworkx = np.amin(np.ravel(edge_pos[:, :, 0])) - maxx = np.amax(np.ravel(edge_pos[:, :, 0])) - miny = np.amin(np.ravel(edge_pos[:, :, 1])) - maxy = np.amax(np.ravel(edge_pos[:, :, 1])) - w = maxx - mirustworkx - h = maxy - miny - base_connectionstyle = mpl.patches.ConnectionStyle(connectionstyle) # Fallback for self-loop scale. Left outside of _connectionstyle so it is # only computed once max_nodesize = np.array(node_size).max() - def _connectionstyle(posA, posB, *args, **kwargs): - # check if we need to do a self-loop - if np.all(posA == posB): - # Self-loops are scaled by view extent, except in cases the extent - # is 0, e.g. for a single node. In this case, fall back to scaling - # by the maximum node size - selfloop_ht = 0.005 * max_nodesize if h == 0 else h - # this is called with _screen space_ values so covert back - # to data space - data_loc = ax.transData.inverted().transform(posA) - v_shift = 0.1 * selfloop_ht - h_shift = v_shift * 0.5 - # put the top of the loop first so arrow is not hidden by node - path = [ - # 1 - data_loc + np.asarray([0, v_shift]), - # 4 4 4 - data_loc + np.asarray([h_shift, v_shift]), - data_loc + np.asarray([h_shift, 0]), - data_loc, - # 4 4 4 - data_loc + np.asarray([-h_shift, 0]), - data_loc + np.asarray([-h_shift, v_shift]), - data_loc + np.asarray([0, v_shift]), - ] - - ret = mpl.path.Path(ax.transData.transform(path), [1, 4, 4, 4, 4, 4, 4]) - # if not, fall back to the user specified behavior - else: - ret = base_connectionstyle(posA, posB, *args, **kwargs) - - return ret - # FancyArrowPatch doesn't handle color strings arrow_colors = mpl.colors.colorConverter.to_rgba_array(edge_color, alpha) - for i, (src, dst) in enumerate(edge_pos): - x1, y1 = src - x2, y2 = dst + for i, edge in enumerate(edge_pos): + x1, y1 = edge[0][0], edge[0][1] + x2, y2 = edge[1][0], edge[1][1] shrink_source = 0 # space from source to tail shrink_target = 0 # space from head to target if np.iterable(node_size): # many node sizes @@ -755,10 +716,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): line_width = width # radius of edges - reverse_edge = [dst, src] - if ( - len(np.where(np.all(edge_pos == reverse_edge, axis=(1, 2)))[0]) != 0 - ): # if reverse edge is in `edge_pos` + if tuple(reversed(edge)) in edge_pos: rad = 0.25 else: rad = 0.0 @@ -772,7 +730,7 @@ def _connectionstyle(posA, posB, *args, **kwargs): mutation_scale=mutation_scale, color=arrow_color, linewidth=line_width, - connectionstyle=f"{connectionstyle}, rad = {rad}", + connectionstyle=connectionstyle + f", rad = {rad}", linestyle=style, zorder=1, ) # arrows go behind nodes @@ -780,6 +738,49 @@ def _connectionstyle(posA, posB, *args, **kwargs): arrow_collection.append(arrow) ax.add_patch(arrow) + edge_pos = np.asarray(tuple(edge_pos)) + + # compute view + mirustworkx = np.amin(np.ravel(edge_pos[:, :, 0])) + maxx = np.amax(np.ravel(edge_pos[:, :, 0])) + miny = np.amin(np.ravel(edge_pos[:, :, 1])) + maxy = np.amax(np.ravel(edge_pos[:, :, 1])) + w = maxx - mirustworkx + h = maxy - miny + + def _connectionstyle(posA, posB, *args, **kwargs): + # check if we need to do a self-loop + if np.all(posA == posB): + # Self-loops are scaled by view extent, except in cases the extent + # is 0, e.g. for a single node. In this case, fall back to scaling + # by the maximum node size + selfloop_ht = 0.005 * max_nodesize if h == 0 else h + # this is called with _screen space_ values so covert back + # to data space + data_loc = ax.transData.inverted().transform(posA) + v_shift = 0.1 * selfloop_ht + h_shift = v_shift * 0.5 + # put the top of the loop first so arrow is not hidden by node + path = [ + # 1 + data_loc + np.asarray([0, v_shift]), + # 4 4 4 + data_loc + np.asarray([h_shift, v_shift]), + data_loc + np.asarray([h_shift, 0]), + data_loc, + # 4 4 4 + data_loc + np.asarray([-h_shift, 0]), + data_loc + np.asarray([-h_shift, v_shift]), + data_loc + np.asarray([0, v_shift]), + ] + + ret = mpl.path.Path(ax.transData.transform(path), [1, 4, 4, 4, 4, 4, 4]) + # if not, fall back to the user specified behavior + else: + ret = base_connectionstyle(posA, posB, *args, **kwargs) + + return ret + # update view padx, pady = 0.05 * w, 0.05 * h corners = (mirustworkx - padx, miny - pady), (maxx + padx, maxy + pady)