Skip to content

Commit 5fad741

Browse files
committed
Parallel edge offset for straight edges
1 parent ef497a2 commit 5fad741

File tree

2 files changed

+64
-6
lines changed

2 files changed

+64
-6
lines changed

iplotx/edge/undirected.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from math import atan2, tan, cos, pi, sin
2+
from collections import defaultdict
23
import numpy as np
34
import 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(

iplotx/styles.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def _update(style: dict, current: dict):
9696

9797
def reset():
9898
"""Reset to default style."""
99+
global current
99100
current = deepcopy(styles["default"])
100101

101102

0 commit comments

Comments
 (0)