iplotx 0.5.1__py3-none-any.whl → 0.5.2.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iplotx/edge/__init__.py CHANGED
@@ -105,6 +105,10 @@ class EdgeCollection(mpl.collections.PatchCollection):
105
105
  # NOTE: This should also set the transform
106
106
  super().__init__(patches, transform=transform, *args, **kwargs)
107
107
 
108
+ # Apparenyly capstyle is lost upon collection creation
109
+ if "capstyle" in self._style:
110
+ self.set_capstyle(self._style["capstyle"])
111
+
108
112
  # This is important because it prepares the right flags for scalarmappable
109
113
  self.set_facecolor("none")
110
114
 
@@ -158,6 +162,10 @@ class EdgeCollection(mpl.collections.PatchCollection):
158
162
  **kwargs,
159
163
  )
160
164
 
165
+ # Apparently capstyle is lost upon collection creation
166
+ if "capstyle" in style:
167
+ self._subedges.set_capstyle(style["capstyle"])
168
+
161
169
  def get_children(self) -> tuple:
162
170
  children = []
163
171
  if hasattr(self, "_subedges"):
@@ -188,7 +196,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
188
196
  return self._directed
189
197
 
190
198
  @directed.setter
191
- def directed(self, value) -> None:
199
+ def directed(self, value: bool) -> None:
192
200
  """Setter for the directed property.
193
201
 
194
202
  Changing this property triggers the addition/removal of arrows from the plot.
@@ -230,7 +238,11 @@ class EdgeCollection(mpl.collections.PatchCollection):
230
238
  # NOTE: The superclass also sets stale = True
231
239
  super().update_scalarmappable()
232
240
  # Now self._edgecolors has the correct colorspace values
233
- if hasattr(self, "_arrows"):
241
+ # NOTE: The following line should include a condition on
242
+ # whether the arrows are allowing color matching to the
243
+ # edges. For now, we assume that if the edges are colormapped
244
+ # we would want the arrows to be as well.
245
+ if hasattr(self, "_arrows") and (self._A is not None):
234
246
  self._arrows.set_colors(self.get_edgecolor())
235
247
 
236
248
  def get_labels(self) -> Optional[LabelCollection]:
@@ -337,6 +349,10 @@ class EdgeCollection(mpl.collections.PatchCollection):
337
349
  tension = 0
338
350
  ports = None
339
351
 
352
+ # Scale padding by dpi
353
+ dpi = self.figure.dpi if hasattr(self, "figure") else 72.0
354
+ padding = dpi / 72.0 * edge_stylei.pop("padding", 0)
355
+
340
356
  # False is a synonym for "none"
341
357
  waypoints = edge_stylei.get("waypoints", "none")
342
358
  if waypoints is False or waypoints is np.False_:
@@ -348,9 +364,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
348
364
  if waypoints != "none":
349
365
  ports = edge_stylei.get("ports", (None, None))
350
366
 
351
- if not isinstance(waypoints, str):
352
- __import__("ipdb").set_trace()
353
-
354
367
  # Compute actual edge path
355
368
  path, angles = _compute_edge_path(
356
369
  vcoord_data,
@@ -362,6 +375,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
362
375
  waypoints=waypoints,
363
376
  ports=ports,
364
377
  layout_coordinate_system=self._vertex_collection.get_layout_coordinate_system(),
378
+ padding=padding,
365
379
  )
366
380
 
367
381
  offset = edge_stylei.get("offset", 0)
@@ -375,7 +389,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
375
389
  offset = offset * vrot
376
390
  offset = np.asarray(offset, dtype=float)
377
391
  # Scale by dpi
378
- dpi = self.figure.dpi if hasattr(self, "figure") else 72.0
379
392
  offset *= dpi / 72.0
380
393
  if (offset != 0).any():
381
394
  path.vertices[:] = trans_inv(trans(path.vertices) + offset)
@@ -702,6 +715,7 @@ def make_stub_patch(**kwargs):
702
715
  "cmap",
703
716
  "norm",
704
717
  "split",
718
+ "padding",
705
719
  ]
706
720
  for prop in forbidden_props:
707
721
  if prop in kwargs:
iplotx/edge/arrow.py CHANGED
@@ -220,6 +220,16 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
220
220
  codes=[getattr(mpl.path.Path, x) for x in codes],
221
221
  closed=False,
222
222
  )
223
+ elif marker == "<":
224
+ kwargs["facecolor"] = "none"
225
+ if "color" in kwargs:
226
+ kwargs["edgecolor"] = kwargs.pop("color")
227
+ codes = ["MOVETO", "LINETO", "LINETO"]
228
+ path = mpl.path.Path(
229
+ np.array([[height, width * 0.5], [0, 0], [height, -width * 0.5]]),
230
+ codes=[getattr(mpl.path.Path, x) for x in codes],
231
+ closed=False,
232
+ )
223
233
  elif marker == ">>":
224
234
  if "color" in kwargs:
225
235
  kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
@@ -272,13 +282,80 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
272
282
  codes=[getattr(mpl.path.Path, x) for x in codes],
273
283
  closed=False,
274
284
  )
285
+ elif marker == "(":
286
+ kwargs["facecolor"] = "none"
287
+ if "color" in kwargs:
288
+ kwargs["edgecolor"] = kwargs.pop("color")
289
+ codes = ["MOVETO", "CURVE3", "CURVE3"]
290
+ path = mpl.path.Path(
291
+ np.array(
292
+ [
293
+ [height * 0.5, width * 0.5],
294
+ [-height * 0.5, 0],
295
+ [height * 0.5, -width * 0.5],
296
+ ]
297
+ ),
298
+ codes=[getattr(mpl.path.Path, x) for x in codes],
299
+ closed=False,
300
+ )
301
+ elif marker == "]":
302
+ kwargs["facecolor"] = "none"
303
+ if "color" in kwargs:
304
+ kwargs["edgecolor"] = kwargs.pop("color")
305
+ codes = ["MOVETO", "LINETO", "LINETO", "LINETO"]
306
+ path = mpl.path.Path(
307
+ np.array(
308
+ [
309
+ [-height, width * 0.5],
310
+ [0, width * 0.5],
311
+ [0, -width * 0.5],
312
+ [-height, -width * 0.5],
313
+ ]
314
+ ),
315
+ codes=[getattr(mpl.path.Path, x) for x in codes],
316
+ closed=False,
317
+ )
318
+ elif marker == "[":
319
+ kwargs["facecolor"] = "none"
320
+ if "color" in kwargs:
321
+ kwargs["edgecolor"] = kwargs.pop("color")
322
+ codes = ["MOVETO", "LINETO", "LINETO", "LINETO"]
323
+ path = mpl.path.Path(
324
+ np.array(
325
+ [
326
+ [height, width * 0.5],
327
+ [0, width * 0.5],
328
+ [0, -width * 0.5],
329
+ [height, -width * 0.5],
330
+ ]
331
+ ),
332
+ codes=[getattr(mpl.path.Path, x) for x in codes],
333
+ closed=False,
334
+ )
275
335
  elif marker == "|":
276
336
  kwargs["facecolor"] = "none"
277
337
  if "color" in kwargs:
278
338
  kwargs["edgecolor"] = kwargs.pop("color")
279
339
  codes = ["MOVETO", "LINETO"]
280
340
  path = mpl.path.Path(
281
- np.array([[-height, width * 0.5], [-height, -width * 0.5]]),
341
+ np.array([[0, width * 0.5], [0, -width * 0.5]]),
342
+ codes=[getattr(mpl.path.Path, x) for x in codes],
343
+ closed=False,
344
+ )
345
+ elif marker in ("x", "X"):
346
+ kwargs["facecolor"] = "none"
347
+ if "color" in kwargs:
348
+ kwargs["edgecolor"] = kwargs.pop("color")
349
+ codes = ["MOVETO", "LINETO", "MOVETO", "LINETO"]
350
+ path = mpl.path.Path(
351
+ np.array(
352
+ [
353
+ [height * 0.5, width * 0.5],
354
+ [-height * 0.5, -width * 0.5],
355
+ [-height * 0.5, width * 0.5],
356
+ [height * 0.5, -width * 0.5],
357
+ ]
358
+ ),
282
359
  codes=[getattr(mpl.path.Path, x) for x in codes],
283
360
  closed=False,
284
361
  )
@@ -370,4 +447,5 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
370
447
  path,
371
448
  **kwargs,
372
449
  )
450
+
373
451
  return patch, size_max
iplotx/edge/geometry.py CHANGED
@@ -2,7 +2,10 @@
2
2
  Support module with geometry- and path-related functions for edges.
3
3
  """
4
4
 
5
- from typing import Optional
5
+ from typing import (
6
+ Optional,
7
+ Sequence,
8
+ )
6
9
  from math import atan2, tan, pi
7
10
  import numpy as np
8
11
  import matplotlib as mpl
@@ -61,45 +64,51 @@ def _compute_loops_per_angle(nloops, angles):
61
64
  ]
62
65
 
63
66
 
64
- def _get_shorter_edge_coords(vpath, vsize, theta):
67
+ def _get_shorter_edge_coords(vpath, vsize, theta, padding=0):
65
68
  # Bound theta from -pi to pi (why is that not guaranteed?)
66
69
  theta = (theta + pi) % (2 * pi) - pi
67
70
 
68
71
  # Size zero vertices need no shortening
69
72
  if vsize == 0:
70
- return np.array([0, 0])
71
-
72
- for i in range(len(vpath)):
73
- v1 = vpath.vertices[i]
74
- v2 = vpath.vertices[(i + 1) % len(vpath)]
75
- theta1 = atan2(*((v1)[::-1]))
76
- theta2 = atan2(*((v2)[::-1]))
77
-
78
- # atan2 ranges ]-3.14, 3.14]
79
- # so it can be that theta1 is -3 and theta2 is +3
80
- # therefore we need two separate cases, one that cuts at pi and one at 0
81
- cond1 = theta1 <= theta <= theta2
82
- cond2 = (
83
- (theta1 + 2 * pi) % (2 * pi)
84
- <= (theta + 2 * pi) % (2 * pi)
85
- <= (theta2 + 2 * pi) % (2 * pi)
86
- )
87
- if cond1 or cond2:
88
- break
73
+ ve = np.array([0, 0])
89
74
  else:
90
- raise ValueError("Angle for patch not found")
75
+ for i in range(len(vpath)):
76
+ v1 = vpath.vertices[i]
77
+ v2 = vpath.vertices[(i + 1) % len(vpath)]
78
+ theta1 = atan2(*((v1)[::-1]))
79
+ theta2 = atan2(*((v2)[::-1]))
80
+
81
+ # atan2 ranges ]-3.14, 3.14]
82
+ # so it can be that theta1 is -3 and theta2 is +3
83
+ # therefore we need two separate cases, one that cuts at pi and one at 0
84
+ cond1 = theta1 <= theta <= theta2
85
+ cond2 = (
86
+ (theta1 + 2 * pi) % (2 * pi)
87
+ <= (theta + 2 * pi) % (2 * pi)
88
+ <= (theta2 + 2 * pi) % (2 * pi)
89
+ )
90
+ if cond1 or cond2:
91
+ break
92
+ else:
93
+ raise ValueError("Angle for patch not found")
91
94
 
92
- # The edge meets the patch of the vertex on the v1-v2 size,
93
- # at angle theta from the center
94
- mtheta = tan(theta)
95
- if v2[0] == v1[0]:
96
- xe = v1[0]
97
- else:
98
- m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
99
- xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
100
- ye = mtheta * xe
101
- ve = np.array([xe, ye])
102
- return ve * vsize
95
+ # The edge meets the patch of the vertex on the v1-v2 size,
96
+ # at angle theta from the center
97
+ mtheta = tan(theta)
98
+ if v2[0] == v1[0]:
99
+ xe = v1[0]
100
+ else:
101
+ m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
102
+ xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
103
+ ye = mtheta * xe
104
+ ve = np.array([xe, ye])
105
+
106
+ ve = ve * vsize
107
+
108
+ # Padding (assuming dpi scaling is already applied to the padding)
109
+ ve += padding * np.array([np.cos(theta), np.sin(theta)])
110
+
111
+ return ve
103
112
 
104
113
 
105
114
  def _fix_parallel_edges_straight(
@@ -136,11 +145,12 @@ def _compute_loop_path(
136
145
  angle2,
137
146
  trans_inv,
138
147
  looptension,
148
+ padding=0,
139
149
  ):
140
150
  # Shorten at starting angle
141
- start = _get_shorter_edge_coords(vpath, vsize, angle1) + vcoord_fig
151
+ start = _get_shorter_edge_coords(vpath, vsize, angle1, padding) + vcoord_fig
142
152
  # Shorten at end angle
143
- end = _get_shorter_edge_coords(vpath, vsize, angle2) + vcoord_fig
153
+ end = _get_shorter_edge_coords(vpath, vsize, angle2, padding) + vcoord_fig
144
154
 
145
155
  aux1 = (start - vcoord_fig) * looptension + vcoord_fig
146
156
  aux2 = (end - vcoord_fig) * looptension + vcoord_fig
@@ -172,6 +182,7 @@ def _compute_edge_path_straight(
172
182
  trans,
173
183
  trans_inv,
174
184
  layout_coordinate_system: str = "cartesian",
185
+ padding: float = 0,
175
186
  **kwargs,
176
187
  ):
177
188
  if layout_coordinate_system not in ("cartesian", "polar"):
@@ -200,11 +211,11 @@ def _compute_edge_path_straight(
200
211
  theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
201
212
 
202
213
  # Shorten at starting vertex
203
- vs = _get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta) + vcoord_fig[0]
214
+ vs = _get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta, padding) + vcoord_fig[0]
204
215
  points.append(vs)
205
216
 
206
217
  # Shorten at end vertex
207
- ve = _get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi) + vcoord_fig[1]
218
+ ve = _get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi, padding) + vcoord_fig[1]
208
219
  points.append(ve)
209
220
 
210
221
  codes = ["MOVETO", "LINETO"]
@@ -226,9 +237,41 @@ def _compute_edge_path_waypoints(
226
237
  layout_coordinate_system: str = "cartesian",
227
238
  points_per_curve: int = 30,
228
239
  ports: Pair[Optional[str]] = (None, None),
240
+ padding: float = 0,
229
241
  **kwargs,
230
242
  ):
231
- if waypoints in ("x0y1", "y0x1"):
243
+ if not isinstance(waypoints, str):
244
+ # Only cartesian coordinates supported for numerical waypoints for now
245
+ assert layout_coordinate_system == "cartesian"
246
+
247
+ waypoints = trans(np.array(waypoints, ndmin=2))
248
+
249
+ # Coordinates in figure (default) coords
250
+ vcoord_fig = trans(vcoord_data)
251
+
252
+ # Angles of the straight lines
253
+ thetas = [None, None]
254
+ vshorts = [None, None]
255
+ for i in range(2):
256
+ # This picks always the first waypoint for i == 0,
257
+ # the last waypoint for i == 1. They might be the same.
258
+ waypoint = waypoints[-i]
259
+ if ports[i] is None:
260
+ thetas[i] = atan2(*((waypoint - vcoord_fig[i])[::-1]))
261
+ else:
262
+ thetas[i] = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
263
+
264
+ # Shorten at vertex border
265
+ vshorts[i] = (
266
+ _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i], padding)
267
+ + vcoord_fig[i]
268
+ )
269
+
270
+ points = [vshorts[0]] + list(waypoints) + [vshorts[1]]
271
+ codes = ["MOVETO"] + ["LINETO"] * len(waypoints) + ["LINETO"]
272
+ angles = tuple(thetas)
273
+
274
+ elif waypoints in ("x0y1", "y0x1"):
232
275
  assert layout_coordinate_system == "cartesian"
233
276
 
234
277
  # Coordinates in figure (default) coords
@@ -250,7 +293,8 @@ def _compute_edge_path_waypoints(
250
293
 
251
294
  # Shorten at vertex border
252
295
  vshorts[i] = (
253
- _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i]) + vcoord_fig[i]
296
+ _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i], padding)
297
+ + vcoord_fig[i]
254
298
  )
255
299
 
256
300
  # Shorten waypoints to keep the angles right
@@ -298,7 +342,9 @@ def _compute_edge_path_waypoints(
298
342
  theta = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
299
343
 
300
344
  # Shorten at vertex border
301
- vshort = _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], theta) + vcoord_fig[i]
345
+ vshort = (
346
+ _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], theta, padding) + vcoord_fig[i]
347
+ )
302
348
  thetas.append(theta)
303
349
  vshorts.append(vshort)
304
350
 
@@ -345,6 +391,7 @@ def _compute_edge_path_curved(
345
391
  trans,
346
392
  trans_inv,
347
393
  ports: Pair[Optional[str]] = (None, None),
394
+ padding: float = 0,
348
395
  ):
349
396
  """Shorten the edge path along a cubic Bezier between the vertex centres.
350
397
 
@@ -397,7 +444,9 @@ def _compute_edge_path_curved(
397
444
  thetas = [None, None]
398
445
  for i in range(2):
399
446
  thetas[i] = atan2(*((auxs[i] - vcoord_fig[i])[::-1]))
400
- vs[i] = _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i]) + vcoord_fig[i]
447
+ vs[i] = (
448
+ _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i], padding) + vcoord_fig[i]
449
+ )
401
450
 
402
451
  path = {
403
452
  "vertices": [
@@ -422,7 +471,7 @@ def _compute_edge_path_curved(
422
471
  def _compute_edge_path(
423
472
  *args,
424
473
  tension: float = 0,
425
- waypoints: str = "none",
474
+ waypoints: str | tuple[float, float] | Sequence[tuple[float, float]] | np.ndarray = "none",
426
475
  ports: Pair[Optional[str]] = (None, None),
427
476
  layout_coordinate_system: str = "cartesian",
428
477
  **kwargs,
@@ -79,6 +79,10 @@ class SimpleNetworkDataProvider(NetworkDataProvider):
79
79
  edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
80
80
  del tmp
81
81
 
82
+ # Edge labels
83
+ if edge_labels is not None:
84
+ edge_df["label"] = edge_labels
85
+
82
86
  network_data = {
83
87
  "vertex_df": vertex_df,
84
88
  "edge_df": edge_df,
@@ -21,6 +21,7 @@ class SimpleTree:
21
21
 
22
22
  children: Sequence[Self] = []
23
23
  branch_length: float = 1
24
+ name: str = ""
24
25
 
25
26
  @classmethod
26
27
  def from_dict(cls, data: dict) -> Self:
@@ -35,6 +36,7 @@ class SimpleTree:
35
36
  """
36
37
  tree = cls()
37
38
  tree.branch_length = data.get("branch_length", 1)
39
+ tree.name = data.get("name", "")
38
40
  tree.children = [cls.from_dict(child) for child in data.get("children", [])]
39
41
  return tree
40
42
 
iplotx/ingest/typing.py CHANGED
@@ -254,12 +254,12 @@ class TreeDataProvider(Protocol):
254
254
  self,
255
255
  layout: str | LayoutType,
256
256
  layout_style: Optional[dict[str, int | float | str]] = None,
257
- directed: bool | str = False,
257
+ directed: bool = False,
258
258
  vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
259
259
  edge_labels: Optional[Sequence[str] | dict] = None,
260
260
  leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
261
261
  ) -> TreeData:
262
- """Create tree data object for iplotx from ete4.core.tre.Tree classes.
262
+ """Create tree data object for iplotx from any tree provider.
263
263
 
264
264
  NOTE: This function needs NOT be implemented by individual providers.
265
265
  """
@@ -314,15 +314,17 @@ class TreeDataProvider(Protocol):
314
314
  edge_data = {"_ipx_source": [], "_ipx_target": []}
315
315
  for node in self.preorder():
316
316
  for child in self.get_children(node):
317
- if directed == "parent":
318
- edge_data["_ipx_source"].append(child)
319
- edge_data["_ipx_target"].append(node)
320
- else:
321
- edge_data["_ipx_source"].append(node)
322
- edge_data["_ipx_target"].append(child)
317
+ edge_data["_ipx_source"].append(node)
318
+ edge_data["_ipx_target"].append(child)
323
319
  edge_df = pd.DataFrame(edge_data)
324
320
  tree_data["edge_df"] = edge_df
325
321
 
322
+ # Add edge labels
323
+ # NOTE: Partial support only for now, only lists
324
+ if edge_labels is not None:
325
+ # Cycling sequence
326
+ edge_df["label"] = [edge_labels[i % len(edge_labels)] for i in range(len(edge_df))]
327
+
326
328
  # Add branch support
327
329
  if hasattr(self, "get_support"):
328
330
  support = self.get_support()
iplotx/label.py CHANGED
@@ -77,6 +77,12 @@ class LabelCollection(mpl.artist.Artist):
77
77
  child.set_figure(fig)
78
78
  self._update_offsets(dpi=fig.dpi)
79
79
 
80
+ def get_texts(self):
81
+ """Get the texts of the labels."""
82
+ return [child.get_text() for child in self.get_children()]
83
+
84
+ get_text = get_texts
85
+
80
86
  def _get_margins_with_dpi(self, dpi: float = 72.0) -> np.ndarray:
81
87
  return self._margins * dpi / 72.0
82
88
 
iplotx/plotting.py CHANGED
@@ -139,8 +139,7 @@ def tree(
139
139
  Parameters:
140
140
  tree: The tree to plot. Can be a BioPython.Phylo.Tree object.
141
141
  layout: The layout to use for plotting.
142
- directed: If False, donot draw arrows. If True or "child", draw arrows from parent to child
143
- node. If "parent", draw arrows the other way around.
142
+ directed: If False, do not draw arrows.
144
143
  show_support: If True, show the support values for the nodes (assumed to be from 0 to 100,
145
144
  rounded to nearest integer). If both this parameter and vertex_labels are set,
146
145
  show_support takes precedence and hides the vertex labels.
iplotx/style/__init__.py CHANGED
@@ -60,7 +60,7 @@ def get_style(name: str = "", *args) -> dict[str, Any]:
60
60
  style = style[namei]
61
61
  # NOTE: if asking for a nonexistent, non-leaf style
62
62
  # give the benefit of the doubt and set an empty dict
63
- # which will not fail unless the uder tries to enter it
63
+ # which will not fail unless the user tries to enter it
64
64
  elif namei not in style_leaves:
65
65
  style = {}
66
66
  elif len(args) > 0:
iplotx/style/leaf_info.py CHANGED
@@ -22,6 +22,7 @@ rotating_leaves = (
22
22
  "vpadding",
23
23
  "hmargin",
24
24
  "vmargin",
25
+ "padding",
25
26
  "ports",
26
27
  "width",
27
28
  "height",
@@ -38,6 +39,7 @@ nonrotating_leaves = (
38
39
  "deep",
39
40
  "angular",
40
41
  "curved",
42
+ "capstyle",
41
43
  )
42
44
 
43
45
  # Union of all style leaves (rotating and nonrotating)
iplotx/style/library.py CHANGED
@@ -58,6 +58,28 @@ style_library = {
58
58
  },
59
59
  }
60
60
  },
61
+ # Feedback, for regulatory networks
62
+ "feedback": {
63
+ "edge": {
64
+ "linewidth": 4,
65
+ "padding": 10,
66
+ "arrow": {
67
+ "marker": ")>",
68
+ "width": 20,
69
+ "height": 28,
70
+ },
71
+ },
72
+ "vertex": {
73
+ "size": 35,
74
+ "color": None,
75
+ "facecolor": "white",
76
+ "edgecolor": "black",
77
+ "linewidth": 4,
78
+ "label": {
79
+ "color": "black",
80
+ },
81
+ },
82
+ },
61
83
  # Tree style, with zero-size vertices
62
84
  "tree": {
63
85
  "vertex": {
iplotx/tree.py CHANGED
@@ -63,7 +63,7 @@ class TreeArtist(mpl.artist.Artist):
63
63
  self,
64
64
  tree,
65
65
  layout: Optional[str] = "horizontal",
66
- directed: bool | str = False,
66
+ directed: bool = False,
67
67
  vertex_labels: Optional[bool | list[str] | dict[Hashable, str] | pd.Series] = None,
68
68
  edge_labels: Optional[Sequence | dict[Hashable, str] | pd.Series] = None,
69
69
  leaf_labels: Optional[Sequence | dict[Hashable, str]] | pd.Series = None,
@@ -76,8 +76,7 @@ class TreeArtist(mpl.artist.Artist):
76
76
  Parameters:
77
77
  tree: The tree to plot.
78
78
  layout: The layout to use for the tree. Can be "horizontal", "vertical", or "radial".
79
- directed: Whether the tree is directed. Can be a boolean or a string with the
80
- following choices: "parent" or "child".
79
+ directed: Whether the tree is directed. Must be a boolean.
81
80
  vertex_labels: Labels for the vertices. Can be a list, dictionary, or pandas Series.
82
81
  edge_labels: Labels for the edges. Can be a sequence of strings.
83
82
  leaf_labels: Labels for the leaves. Can be a sequence of strings or a pandas Series.
@@ -294,7 +293,7 @@ class TreeArtist(mpl.artist.Artist):
294
293
  layout=self.get_layout(),
295
294
  layout_coordinate_system=self._ipx_internal_data.get(
296
295
  "layout_coordinate_system",
297
- "catesian",
296
+ "cartesian",
298
297
  ),
299
298
  style=get_style(".vertex"),
300
299
  labels=self._get_label_series("vertex"),
@@ -586,10 +585,9 @@ class TreeArtist(mpl.artist.Artist):
586
585
  waypointsi = "y0x1"
587
586
  elif layout_name == "radial":
588
587
  waypointsi = "r0a1"
589
- else:
590
- raise ValueError(
591
- f"Layout not supported: {layout_name}. ",
592
- )
588
+ # NOTE: no need to catch the default case, it's caught
589
+ # when making the layout already. We should *never* be
590
+ # in an "else" case here.
593
591
  waypoints.append(waypointsi)
594
592
 
595
593
  # These are not the actual edges drawn, only stubs to establish
@@ -612,7 +610,6 @@ class TreeArtist(mpl.artist.Artist):
612
610
  else:
613
611
  edge_style["waypoints"] = waypoints
614
612
 
615
- # NOTE: Trees are directed is their "directed" property is True, "child", or "parent"
616
613
  self._edges = EdgeCollection(
617
614
  edgepatches,
618
615
  vertex_ids=adjacent_vertex_ids,
@@ -90,6 +90,9 @@ def _get_label_width_height(text, hpadding=18, vpadding=12, dpi=72.0, **kwargs):
90
90
  very accurate. Yet, it is often good enough and easier to implement than a careful
91
91
  orchestration of Figure.draw_without_rendering.
92
92
  """
93
+ if len(text) == 0:
94
+ return (0, 0)
95
+
93
96
  if "fontsize" in kwargs:
94
97
  kwargs["size"] = kwargs.pop("fontsize")
95
98
  forbidden_props = [
iplotx/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  iplotx version information module.
3
3
  """
4
4
 
5
- __version__ = "0.5.1"
5
+ __version__ = "0.5.2-dev"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iplotx
3
- Version: 0.5.1
3
+ Version: 0.5.2.dev0
4
4
  Summary: Plot networkx from igraph and networkx.
5
5
  Project-URL: Homepage, https://github.com/fabilab/iplotx
6
6
  Project-URL: Documentation, https://readthedocs.org/iplotx
@@ -2,37 +2,37 @@ iplotx/__init__.py,sha256=MKb9UCXKgDHHkeATuJWxYdM-AotfBo2fbWy-Rkbn9Is,509
2
2
  iplotx/artists.py,sha256=Bpn6NS8S_B_E4OW88JYW6aEu2bIuIQJmbs2paTmBAoY,522
3
3
  iplotx/cascades.py,sha256=OPqF7Huls-HFmDA5MCF6DEZlUeRVaXsbQcHBoKAgNJs,8182
4
4
  iplotx/groups.py,sha256=_9KdIiTAi1kXtd2mDywgBJCbqoRq2z-5fzOPf76Wgb8,6287
5
- iplotx/label.py,sha256=i107wE-9kC_MVWsgWeYG6sRy_ZmyvITNm2laIij9SR0,8761
5
+ iplotx/label.py,sha256=6am3a0ejcW_bWEXSOODE1Ke3AyCU1lJ45RfnXNbHAQw,8923
6
6
  iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
7
7
  iplotx/network.py,sha256=SlmDgc4tbCfvO08QWk-jUXrUfaz6S3xoXQVg6rP1910,11345
8
- iplotx/plotting.py,sha256=RZj-E_2R8AbXoJmxr_qAC-g_nOudqep-TDSIV4QB9BM,7408
9
- iplotx/tree.py,sha256=iILQRKUZzcDKIiwI1LheSuixi5y_3PAQrz61vdwi6DU,27448
8
+ iplotx/plotting.py,sha256=yACxkD6unKc5eDsAp7ZabRCAwLEXBowSMESX2oGNBDU,7291
9
+ iplotx/tree.py,sha256=S_9tf8Mixv9P5dq616tjxuxdDYRmUXLNAcSXTxEgm_I,27310
10
10
  iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
11
- iplotx/version.py,sha256=dsB8xhRODrwa4OXOSbePgoZzy41j_DudRu80QRgFkpw,66
11
+ iplotx/version.py,sha256=qs-N8bGi6RQubKakAGIJAWbOPniPVtj7B0gtM0s9tTc,70
12
12
  iplotx/vertex.py,sha256=OjDIkJCNU-IhZUVeZTSzGwTlHLrxu27lUThiUuEb6Qs,14497
13
- iplotx/edge/__init__.py,sha256=0w-BDZpVyR4qM908PM5DzlNVXwwfxAeDNyHNXPWPgcc,26237
14
- iplotx/edge/arrow.py,sha256=y8xMZY1eR5BXBmkX0_aDIn-3CeqaL6jwGGLw-ndUf50,12867
15
- iplotx/edge/geometry.py,sha256=g9_z7nwlQhQm9Tvj2tme9dGboxkN-4jeUUg02gU-vOk,13285
13
+ iplotx/edge/__init__.py,sha256=VkAsuxphQa-co79MZWzWErkRAkp97CwB20ozPEnpvrM,26888
14
+ iplotx/edge/arrow.py,sha256=C4XoHGCYou1z2alz5Q2VhdaWYEzgebtEF70zVYY_frk,15533
15
+ iplotx/edge/geometry.py,sha256=tiaF4PzvsNBoROrEgcCsw0YdxxZr3oBxF4ord_k4ThA,15069
16
16
  iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
17
17
  iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
18
18
  iplotx/ingest/__init__.py,sha256=tsXDoa7Rs6Y1ulWtjCcUsO4tQIigeQ6ZMiU2PQDyhwQ,4751
19
19
  iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
20
- iplotx/ingest/typing.py,sha256=pi-mn4ULkFjTo_fFdJPUjTHrWzbny4MNgoMylN4mNKM,13940
20
+ iplotx/ingest/typing.py,sha256=hVEcAREjFFFbAWsxRkQuvpy1B4L7JEv_NRVVmrEbUVk,13984
21
21
  iplotx/ingest/providers/network/igraph.py,sha256=8dWeaQ_ZNdltC098V2YeLXsGdJHQnBa6shF1GAfl0Zg,2973
22
22
  iplotx/ingest/providers/network/networkx.py,sha256=FIXMI3hXU1WtAzPVlQZcz47b-4V2omeHttnNTgS2gQw,4328
23
- iplotx/ingest/providers/network/simple.py,sha256=yKILiE3-ZhBUGSs7eYuhV8tQDyueCosbbgovZZYpSPQ,3664
23
+ iplotx/ingest/providers/network/simple.py,sha256=e_aHhiHhN9DrMoNrt7tEMPURXGhQ1TYRPzsxDEptUlc,3766
24
24
  iplotx/ingest/providers/tree/biopython.py,sha256=4N_54cVyHHPcASJZGr6pHKE2p5R3i8Cm307SLlSLHLA,1480
25
25
  iplotx/ingest/providers/tree/cogent3.py,sha256=JmELbDK7LyybiJzFNbmeqZ4ySJoDajvFfJebpNfFKWo,1073
26
26
  iplotx/ingest/providers/tree/ete4.py,sha256=D7usSq0MOjzrk3EoLi834IlaDGwv7_qG6Qt0ptfKqfI,928
27
- iplotx/ingest/providers/tree/simple.py,sha256=vOAlQbkm2HdlBTQab6s7mjAnLibVmeNOfc6y6UpBqzw,2533
27
+ iplotx/ingest/providers/tree/simple.py,sha256=aV9wGqBomJ5klM_aJQeuL_Q_J1pLCv6AFN98BPDiKUw,2593
28
28
  iplotx/ingest/providers/tree/skbio.py,sha256=O1KUr8tYi28pZ3VVjapgO4Uj-YpMuix3GhOH5je8Lv4,822
29
- iplotx/style/__init__.py,sha256=4K6EtAKOFth3zS_jdaDCvOEMeZxIgnMM_rtpH_G74io,12253
30
- iplotx/style/leaf_info.py,sha256=2XckYhvE3FvNYUaQj_CY2HwtYfZA8FUQ7uBXe_ukaWU,938
31
- iplotx/style/library.py,sha256=yryxQUSHMIwGgeS0Iq1BediVRRaFguJcjhXMj_vsHo8,8007
29
+ iplotx/style/__init__.py,sha256=XMkQZ1U63wVNo98Zo5uJAn-uQgW2OTZABAizJqiuB3s,12253
30
+ iplotx/style/leaf_info.py,sha256=JoX1cPjRM_k3f93jzUPQ3gPlVP4wY_n032nOVhrgelU,969
31
+ iplotx/style/library.py,sha256=wO-eeY3EZfAl0v21aX9f5_MiZhHuL2kGsBYA3uJkIGs,8535
32
32
  iplotx/utils/geometry.py,sha256=UH2gAcM5rYW7ADnJEm7HIJTpPF4UOm8P3vjSVCOGjqM,9192
33
33
  iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
34
- iplotx/utils/matplotlib.py,sha256=KpkuwXuSqpYbPVKbrlP8u_1Ry5gy5q60ZBrFiR-do_Q,5284
34
+ iplotx/utils/matplotlib.py,sha256=TutVJ1dEWYgX_-CY6MvdhRvWYqxpByGb3TKrSByYPNM,5330
35
35
  iplotx/utils/style.py,sha256=wMWxJykxBD-JmcN8-rSKlWcV6pMfwKgR4EzSpk_NX8k,547
36
- iplotx-0.5.1.dist-info/METADATA,sha256=P1TOV3nMA2ihSTdEFQLNdRztBrKUmpVcIpxNFYPcOQE,4889
37
- iplotx-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- iplotx-0.5.1.dist-info/RECORD,,
36
+ iplotx-0.5.2.dev0.dist-info/METADATA,sha256=D_Wrepyok3RsInEki56AiTbAM63Zn4DyvfcFy8v4J0E,4894
37
+ iplotx-0.5.2.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ iplotx-0.5.2.dev0.dist-info/RECORD,,