iplotx 0.5.0__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,46 +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
- print(m12, mtheta)
100
- xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
101
- ye = mtheta * xe
102
- ve = np.array([xe, ye])
103
- 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
104
112
 
105
113
 
106
114
  def _fix_parallel_edges_straight(
@@ -137,11 +145,12 @@ def _compute_loop_path(
137
145
  angle2,
138
146
  trans_inv,
139
147
  looptension,
148
+ padding=0,
140
149
  ):
141
150
  # Shorten at starting angle
142
- start = _get_shorter_edge_coords(vpath, vsize, angle1) + vcoord_fig
151
+ start = _get_shorter_edge_coords(vpath, vsize, angle1, padding) + vcoord_fig
143
152
  # Shorten at end angle
144
- end = _get_shorter_edge_coords(vpath, vsize, angle2) + vcoord_fig
153
+ end = _get_shorter_edge_coords(vpath, vsize, angle2, padding) + vcoord_fig
145
154
 
146
155
  aux1 = (start - vcoord_fig) * looptension + vcoord_fig
147
156
  aux2 = (end - vcoord_fig) * looptension + vcoord_fig
@@ -173,6 +182,7 @@ def _compute_edge_path_straight(
173
182
  trans,
174
183
  trans_inv,
175
184
  layout_coordinate_system: str = "cartesian",
185
+ padding: float = 0,
176
186
  **kwargs,
177
187
  ):
178
188
  if layout_coordinate_system not in ("cartesian", "polar"):
@@ -199,14 +209,13 @@ def _compute_edge_path_straight(
199
209
 
200
210
  # Angle of the straight line
201
211
  theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
202
- print(vcoord_data_cart, vcoord_fig, theta)
203
212
 
204
213
  # Shorten at starting vertex
205
- 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]
206
215
  points.append(vs)
207
216
 
208
217
  # Shorten at end vertex
209
- 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]
210
219
  points.append(ve)
211
220
 
212
221
  codes = ["MOVETO", "LINETO"]
@@ -228,9 +237,41 @@ def _compute_edge_path_waypoints(
228
237
  layout_coordinate_system: str = "cartesian",
229
238
  points_per_curve: int = 30,
230
239
  ports: Pair[Optional[str]] = (None, None),
240
+ padding: float = 0,
231
241
  **kwargs,
232
242
  ):
233
- 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"):
234
275
  assert layout_coordinate_system == "cartesian"
235
276
 
236
277
  # Coordinates in figure (default) coords
@@ -252,7 +293,8 @@ def _compute_edge_path_waypoints(
252
293
 
253
294
  # Shorten at vertex border
254
295
  vshorts[i] = (
255
- _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]
256
298
  )
257
299
 
258
300
  # Shorten waypoints to keep the angles right
@@ -300,7 +342,9 @@ def _compute_edge_path_waypoints(
300
342
  theta = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
301
343
 
302
344
  # Shorten at vertex border
303
- 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
+ )
304
348
  thetas.append(theta)
305
349
  vshorts.append(vshort)
306
350
 
@@ -347,6 +391,7 @@ def _compute_edge_path_curved(
347
391
  trans,
348
392
  trans_inv,
349
393
  ports: Pair[Optional[str]] = (None, None),
394
+ padding: float = 0,
350
395
  ):
351
396
  """Shorten the edge path along a cubic Bezier between the vertex centres.
352
397
 
@@ -399,7 +444,9 @@ def _compute_edge_path_curved(
399
444
  thetas = [None, None]
400
445
  for i in range(2):
401
446
  thetas[i] = atan2(*((auxs[i] - vcoord_fig[i])[::-1]))
402
- 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
+ )
403
450
 
404
451
  path = {
405
452
  "vertices": [
@@ -424,7 +471,7 @@ def _compute_edge_path_curved(
424
471
  def _compute_edge_path(
425
472
  *args,
426
473
  tension: float = 0,
427
- waypoints: str = "none",
474
+ waypoints: str | tuple[float, float] | Sequence[tuple[float, float]] | np.ndarray = "none",
428
475
  ports: Pair[Optional[str]] = (None, None),
429
476
  layout_coordinate_system: str = "cartesian",
430
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.0"
5
+ __version__ = "0.5.2-dev"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iplotx
3
- Version: 0.5.0
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
@@ -43,9 +43,25 @@ Description-Content-Type: text/markdown
43
43
  ![pylint](assets/pylint.svg)
44
44
 
45
45
  # iplotx
46
- Plotting networks from igraph and networkx.
46
+ [![Banner](docs/source/_static/banner.png)](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
47
47
 
48
- **NOTE**: This is currently beta quality software. The API and functionality are settling in and might break occasionally.
48
+ Visualise networks and trees in Python, with style.
49
+
50
+ Supports:
51
+ - **networks**:
52
+ - [networkx](https://networkx.org/)
53
+ - [igraph](igraph.readthedocs.io/)
54
+ - [minimal network data structure](https://iplotx.readthedocs.io/en/latest/gallery/plot_simplenetworkdataprovider.html#sphx-glr-gallery-plot-simplenetworkdataprovider-py) (for educational purposes)
55
+ - **trees**:
56
+ - [ETE4](https://etetoolkit.github.io/ete/)
57
+ - [cogent3](https://cogent3.org/)
58
+ - [Biopython](https://biopython.org/)
59
+ - [scikit-bio](https://scikit.bio)
60
+ - [minimal tree data structure](https://iplotx.readthedocs.io/en/latest/gallery/tree/plot_simpletreedataprovider.html#sphx-glr-gallery-tree-plot-simpletreedataprovider-py) (for educational purposes)
61
+
62
+ In addition to the above, *any* network or tree analysis library can register an [entry point](https://iplotx.readthedocs.io/en/latest/providers.html#creating-a-custom-data-provider) to gain compatibility with `iplotx` with no intervention from our side.
63
+
64
+ **NOTE**: This is currently late beta quality software. The API and functionality might break rarely.
49
65
 
50
66
  ## Installation
51
67
  ```bash
@@ -72,19 +88,20 @@ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documen
72
88
  ## Gallery
73
89
  See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
74
90
 
75
- ## Roadmap
76
- - Plot networks from igraph and networkx interchangeably, using matplotlib as a backend. ✅
77
- - Support interactive plotting, e.g. zooming and panning after the plot is created. ✅
78
- - Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.).
79
- - Support flexible yet easy styling. ✅
91
+ ## Features
92
+ - Plot networks from multiple libraries including networkx and igraph, using matplotlib as a backend. ✅
93
+ - Plot trees from multiple libraries such as cogent3, ETE4, skbio, and biopython. ✅
94
+ - Flexible yet easy styling, including an internal library of styles
95
+ - Interactive plotting, e.g. zooming and panning after the plot is created. ✅
96
+ - Store the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.). ✅
80
97
  - Efficient plotting of large graphs using matplotlib's collection functionality. ✅
81
- - Support editing plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
82
- - Support animations, e.g. showing the evolution of a network over time. ✅
83
- - Support mouse interaction, e.g. hovering over or clicking on nodes and edges to get information about them. ✅
84
- - Support trees from special libraries such as ete3, biopython, etc. This will need a dedicated function and layouting. ✅
85
- - Support uni- and bi-directional communication between graph object and plot object.🏗️
86
-
87
- **NOTE:** The last item can probably be achieved already by using `matplotlib`'s existing callback functionality. It is currently untested, but if you manage to get it to work on your graph let me know and I'll add it to the examples (with credit).
98
+ - Edit plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
99
+ - Animations, e.g. showing the evolution of a network over time. ✅
100
+ - Mouse and keyboard interaction, e.g. hovering over nodes/edges to get information about them. ✅
101
+ - Node clustering and covers, e.g. showing communities in a network. ✅
102
+ - Choice of tree layouts and orientations.
103
+ - Tree-specific options: cascades, subtree styling, split edges, etc. ✅
104
+ - (WIP) Support uni- and bi-directional communication between graph object and plot object.🏗️
88
105
 
89
106
  ## Authors
90
107
  Fabio Zanini (https://fabilab.org)
@@ -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=jzr3_UVrKVs0fo73K5UioUFwKYv5i4rA--sHkaSaQkw,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=-WLdI-BVB12-Zcq1Jc4LqDFoViuOfljTax-YHI1N7cc,13359
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.0.dist-info/METADATA,sha256=QPPJUkwVArD550-tLmUpq11Qaczb7M9xgma8bDHijVc,3958
37
- iplotx-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- iplotx-0.5.0.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,,