11from math import atan2 , tan , cos , pi , sin
2+ from collections import defaultdict
23import numpy as np
34import matplotlib as mpl
45
@@ -74,6 +75,9 @@ def _compute_paths(self, transform=None):
7475 loop_vertex_dict [v1 ]["indices" ].append (i )
7576
7677 # 2. Make paths for non-loop edges
78+ # NOTE: keep track of parallel edges to offset them
79+ parallel_edges = defaultdict (list )
80+
7781 # Get actual coordinates of the vertex border
7882 paths = []
7983 for i , (v1 , v2 ) in enumerate (vids ):
@@ -119,6 +123,25 @@ def _compute_paths(self, transform=None):
119123
120124 # Add the path for this non-loop edge
121125 paths .append (path )
126+ # FIXME: curved parallel edges depend on the direction of curvature...!
127+ parallel_edges [(v1 , v2 )].append (i )
128+
129+ # Fix parallel edges
130+ if not self ._style .get ("curved" , False ):
131+ for (v1 , v2 ), indices in parallel_edges .items ():
132+ nparallel = len (indices )
133+ indices_inv = parallel_edges [(v2 , v1 )]
134+ nparallel_inv = len (indices_inv )
135+ ntot = len (indices ) + len (indices_inv )
136+ if ntot > 1 :
137+ self ._fix_parallel_edges_straight (
138+ paths ,
139+ indices ,
140+ indices_inv ,
141+ trans ,
142+ trans_inv ,
143+ offset = self ._style .get ("offset" , 3 ),
144+ )
122145
123146 # 3. Deal with loops at the end
124147 for vid , ldict in loop_vertex_dict .items ():
@@ -155,6 +178,37 @@ def _compute_paths(self, transform=None):
155178
156179 return paths
157180
181+ def _fix_parallel_edges_straight (
182+ self ,
183+ paths ,
184+ indices ,
185+ indices_inv ,
186+ trans ,
187+ trans_inv ,
188+ offset = 3 ,
189+ ):
190+ """Offset parallel edges along the same path."""
191+ ntot = len (indices ) + len (indices_inv )
192+
193+ # This is straight so two vertices anyway
194+ # NOTE: all paths will be the same, which is why we need to offset them
195+ vs , ve = trans (paths [indices [0 ]].vertices )
196+
197+ # Move orthogonal to the line
198+ if vs [1 ] == ve [1 ]:
199+ fracs = np .array ([0 , 1 ]) * (2 * int (ve [0 ] > vs [0 ]) - 1 )
200+ else :
201+ m_orth = - (ve [0 ] - vs [0 ]) / (ve [1 ] - vs [1 ])
202+ fracs = np .array ([1 , m_orth ]) / np .sqrt (1 + m_orth ** 2 )
203+ fracs *= 2 * int (ve [1 ] > vs [1 ]) - 1
204+
205+ # NOTE: for now treat all the same
206+ for i , idx in enumerate (indices + indices_inv ):
207+ # Offset the path
208+ paths [idx ].vertices = trans_inv (
209+ trans (paths [idx ].vertices ) + fracs * offset * (i - ntot / 2 )
210+ )
211+
158212 def _compute_loop_path (
159213 self ,
160214 vcoord_fig ,
@@ -243,10 +297,11 @@ def _shorten_path_undirected_curved(
243297
244298 # Move orthogonal to the line
245299 if vs [1 ] == ve [1 ]:
246- fracs = np .array ([0 , 1 ])
300+ fracs = np .array ([0 , 1 ]) * ( 2 * int ( ve [ 0 ] > vs [ 0 ]) - 1 )
247301 else :
248302 m_orth = - (ve [0 ] - vs [0 ]) / (ve [1 ] - vs [1 ])
249303 fracs = np .array ([1 , m_orth ]) / np .sqrt (1 + m_orth ** 2 )
304+ fracs *= 2 * int (ve [1 ] > vs [1 ]) - 1
250305
251306 aux1 += 0.1 * fracs * tension * edge_straight_length
252307 aux2 += 0.1 * fracs * tension * edge_straight_length
@@ -268,9 +323,6 @@ def _shorten_path_undirected_curved(
268323 path .vertices = trans_inv (path .vertices )
269324 return path
270325
271- def _extract_ends_and_angles (self , which = "both" ):
272- """Extract the start and/or end angles of the paths to compute arrows."""
273-
274326 def draw (self , renderer ):
275327 if self ._vertex_paths is not None :
276328 self ._paths = self ._compute_paths ()
@@ -296,8 +348,13 @@ def make_stub_patch(**kwargs):
296348 kwargs ["facecolor" ] = "none"
297349
298350 # Forget specific properties that are not supported here
299- kwargs .pop ("curved" )
300- kwargs .pop ("tension" )
351+ forbidden_props = [
352+ "curved" ,
353+ "tension" ,
354+ "offset" ,
355+ ]
356+ for prop in forbidden_props :
357+ kwargs .pop (prop )
301358
302359 # NOTE: the path is overwritten later anyway, so no reason to spend any time here
303360 art = mpl .patches .PathPatch (
0 commit comments