iplotx 0.4.0__py3-none-any.whl → 0.5.1__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/tree.py CHANGED
@@ -14,6 +14,7 @@ from .style import (
14
14
  context,
15
15
  get_style,
16
16
  rotate_style,
17
+ merge_styles,
17
18
  )
18
19
  from .utils.matplotlib import (
19
20
  _stale_wrapper,
@@ -63,9 +64,7 @@ class TreeArtist(mpl.artist.Artist):
63
64
  tree,
64
65
  layout: Optional[str] = "horizontal",
65
66
  directed: bool | str = False,
66
- vertex_labels: Optional[
67
- bool | list[str] | dict[Hashable, str] | pd.Series
68
- ] = None,
67
+ vertex_labels: Optional[bool | list[str] | dict[Hashable, str] | pd.Series] = None,
69
68
  edge_labels: Optional[Sequence | dict[Hashable, str] | pd.Series] = None,
70
69
  leaf_labels: Optional[Sequence | dict[Hashable, str]] | pd.Series = None,
71
70
  transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
@@ -193,9 +192,7 @@ class TreeArtist(mpl.artist.Artist):
193
192
 
194
193
  def get_layout(self, kind="vertex"):
195
194
  """Get vertex or edge layout."""
196
- layout_columns = [
197
- f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
198
- ]
195
+ layout_columns = [f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])]
199
196
 
200
197
  if kind == "vertex":
201
198
  layout = self._ipx_internal_data["vertex_df"][layout_columns]
@@ -264,9 +261,7 @@ class TreeArtist(mpl.artist.Artist):
264
261
 
265
262
  def get_leaf_vertices(self) -> Optional[VertexCollection]:
266
263
  """Get leaf VertexCollection artist."""
267
- if hasattr(self, "_leaf_vertices"):
268
- return self._leaf_vertices
269
- return None
264
+ return self._leaf_vertices
270
265
 
271
266
  def get_leaf_edges(self) -> Optional[LeafEdgeCollection]:
272
267
  """Get LeafEdgeCollection artist if present."""
@@ -284,12 +279,11 @@ class TreeArtist(mpl.artist.Artist):
284
279
 
285
280
  def get_leaf_labels(self) -> Optional[LabelCollection]:
286
281
  """Get the leaf label artist if present."""
287
- if hasattr(self, "_leaf_vertices"):
288
- return self._leaf_vertices.get_labels()
289
- return None
282
+ return self._leaf_vertices.get_labels()
290
283
 
291
284
  def get_leaf_edge_labels(self) -> Optional[LabelCollection]:
292
285
  """Get the leaf edge label artist if present."""
286
+ # TODO: leaf edge labels are basically unsupported as of now
293
287
  if hasattr(self, "_leaf_edges"):
294
288
  return self._leaf_edges.get_labels()
295
289
  return None
@@ -312,8 +306,6 @@ class TreeArtist(mpl.artist.Artist):
312
306
  """Add edges from the leaf to the max leaf depth."""
313
307
  # If there are no leaves, no leaf labels, or leaves are not deep,
314
308
  # skip leaf edges
315
- if not hasattr(self, "_leaf_vertices"):
316
- return
317
309
  leaf_style = get_style(".leaf", {})
318
310
  if ("deep" not in leaf_style) and self.get_leaf_labels() is None:
319
311
  return
@@ -397,16 +389,14 @@ class TreeArtist(mpl.artist.Artist):
397
389
  if user_leaf_style.get("deep", True):
398
390
  if layout_name == "radial":
399
391
  leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
400
- elif layout_name == "horizontal":
401
- if orientation == "right":
402
- leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
403
- else:
404
- leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].min()
405
- elif layout_name == "vertical":
406
- if orientation == "descending":
407
- leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].min()
408
- else:
409
- leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].max()
392
+ elif (layout_name, orientation) == ("horizontal", "right"):
393
+ leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
394
+ elif (layout_name, orientation) == ("horizontal", "left"):
395
+ leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].min()
396
+ elif (layout_name, orientation) == ("vertical", "descending"):
397
+ leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].min()
398
+ elif (layout_name, orientation) == ("vertical", "ascending"):
399
+ leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].max()
410
400
  else:
411
401
  raise ValueError(
412
402
  f"Layout and orientation not supported: {layout_name}, {orientation}."
@@ -471,7 +461,24 @@ class TreeArtist(mpl.artist.Artist):
471
461
  """Add cascade patches."""
472
462
  # NOTE: If leaf labels are present and the cascades are requested to wrap around them,
473
463
  # we have to compute the max extend of the cascades from the leaf labels.
474
- maxdepth = None
464
+ layout = self.get_layout()
465
+ layout_name = self._ipx_internal_data["layout_name"]
466
+ orientation = self._ipx_internal_data["orientation"]
467
+ maxdepth = 1e-10
468
+ if layout_name == "horizontal":
469
+ if orientation == "right":
470
+ maxdepth = layout.values[:, 0].max()
471
+ else:
472
+ maxdepth = layout.values[:, 0].min()
473
+ elif layout_name == "vertical":
474
+ if orientation == "descending":
475
+ maxdepth = layout.values[:, 1].min()
476
+ else:
477
+ maxdepth = layout.values[:, 1].max()
478
+ elif layout_name == "radial":
479
+ # layout values are: r, theta
480
+ maxdepth = layout.values[:, 0].max()
481
+
475
482
  style_cascade = get_style(".cascade")
476
483
  extend_to_labels = style_cascade.get("extend", False) == "leaf_labels"
477
484
  has_leaf_labels = self.get_leaf_labels() is not None
@@ -483,9 +490,9 @@ class TreeArtist(mpl.artist.Artist):
483
490
 
484
491
  self._cascades = CascadeCollection(
485
492
  tree=self.tree,
486
- layout=self.get_layout(),
487
- layout_name=self._ipx_internal_data["layout_name"],
488
- orientation=self._ipx_internal_data["orientation"],
493
+ layout=layout,
494
+ layout_name=layout_name,
495
+ orientation=orientation,
489
496
  style=style_cascade,
490
497
  provider=data_providers["tree"][self._ipx_internal_data["tree_library"]],
491
498
  transform=self.get_offset_transform(),
@@ -496,9 +503,7 @@ class TreeArtist(mpl.artist.Artist):
496
503
  layout_name = self.get_layout_name()
497
504
  if layout_name == "radial":
498
505
  maxdepth = 0
499
- bboxes = self.get_leaf_labels().get_datalims_children(
500
- self.get_offset_transform()
501
- )
506
+ bboxes = self.get_leaf_labels().get_datalims_children(self.get_offset_transform())
502
507
  for bbox in bboxes:
503
508
  r1 = np.linalg.norm([bbox.xmax, bbox.ymax])
504
509
  r2 = np.linalg.norm([bbox.xmax, bbox.ymin])
@@ -541,9 +546,7 @@ class TreeArtist(mpl.artist.Artist):
541
546
  else:
542
547
  cmap_fun = None
543
548
 
544
- edge_df = self._ipx_internal_data["edge_df"].set_index(
545
- ["_ipx_source", "_ipx_target"]
546
- )
549
+ edge_df = self._ipx_internal_data["edge_df"].set_index(["_ipx_source", "_ipx_target"])
547
550
 
548
551
  if "cmap" in edge_style:
549
552
  colorarray = []
@@ -551,7 +554,7 @@ class TreeArtist(mpl.artist.Artist):
551
554
  adjacent_vertex_ids = []
552
555
  waypoints = []
553
556
  for i, (vid1, vid2) in enumerate(edge_df.index):
554
- edge_stylei = rotate_style(edge_style, index=i, key=(vid1, vid2))
557
+ edge_stylei = rotate_style(edge_style, index=i, key=vid2)
555
558
 
556
559
  # FIXME:: Improve this logic. We have three layers of priority:
557
560
  # 1. Explicitely set in the style of "plot"
@@ -573,6 +576,8 @@ class TreeArtist(mpl.artist.Artist):
573
576
 
574
577
  # Tree layout determines waypoints
575
578
  waypointsi = edge_stylei.pop("waypoints", None)
579
+ if isinstance(waypointsi, (bool, np.bool)):
580
+ waypointsi = ["none", None][int(waypointsi)]
576
581
  if waypointsi is None:
577
582
  layout_name = self._ipx_internal_data["layout_name"]
578
583
  if layout_name == "horizontal":
@@ -582,7 +587,9 @@ class TreeArtist(mpl.artist.Artist):
582
587
  elif layout_name == "radial":
583
588
  waypointsi = "r0a1"
584
589
  else:
585
- waypointsi = "none"
590
+ raise ValueError(
591
+ f"Layout not supported: {layout_name}. ",
592
+ )
586
593
  waypoints.append(waypointsi)
587
594
 
588
595
  # These are not the actual edges drawn, only stubs to establish
@@ -630,6 +637,7 @@ class TreeArtist(mpl.artist.Artist):
630
637
  self,
631
638
  nodes: Sequence[Hashable],
632
639
  style: Optional[dict[str, Any] | Sequence[str | dict[str, Any]]] = None,
640
+ **kwargs,
633
641
  ) -> None:
634
642
  """Style a subtree of the tree.
635
643
 
@@ -639,21 +647,26 @@ class TreeArtist(mpl.artist.Artist):
639
647
  style: Style or sequence of styles to apply to the subtree. Each style can
640
648
  be either a string, referring to an internal `iplotx` style, or a dictionary
641
649
  with custom styling elements.
650
+ kwargs: Additional flat style elements. If both style and kwargs are provided,
651
+ kwargs is applied last.
642
652
  """
653
+ styles = []
654
+ if isinstance(style, (str, dict)):
655
+ styles = [style]
656
+ elif style is not None:
657
+ styles = list(style)
658
+ style = merge_styles(styles + [kwargs])
659
+
643
660
  provider = data_providers["tree"][self._ipx_internal_data["tree_library"]]
644
661
 
645
662
  # Get last (deepest) common ancestor of the requested nodes
646
663
  root = provider(self.tree).get_lca(nodes)
647
664
 
648
665
  # Populate a DataFrame with the array of properties to update
649
- vertex_idx = {
650
- node: i for i, node in enumerate(self._ipx_internal_data["vertex_df"].index)
651
- }
666
+ vertex_idx = {node: i for i, node in enumerate(self._ipx_internal_data["vertex_df"].index)}
652
667
  edge_idx = {
653
668
  node: i
654
- for i, node in enumerate(
655
- self._ipx_internal_data["edge_df"]["_ipx_target"].values
656
- )
669
+ for i, node in enumerate(self._ipx_internal_data["edge_df"]["_ipx_target"].values)
657
670
  }
658
671
  vertex_props = {}
659
672
  edge_props = {}
iplotx/typing.py CHANGED
@@ -32,12 +32,14 @@ LayoutType = Union[
32
32
  Sequence[Sequence[float]],
33
33
  np.ndarray,
34
34
  pd.DataFrame,
35
+ dict[Hashable, Sequence[float] | tuple[float, float]],
35
36
  # igraph.Layout,
36
37
  ]
37
38
  GroupingType = Union[
38
39
  Sequence[set],
39
40
  Sequence[int],
40
41
  Sequence[str],
42
+ dict[str, set],
41
43
  # igraph.clustering.Clustering,
42
44
  # igraph.clustering.VertexClustering,
43
45
  # igraph.clustering.Cover,
iplotx/utils/geometry.py CHANGED
@@ -1,5 +1,9 @@
1
+ from typing import (
2
+ Sequence,
3
+ )
1
4
  from math import atan2
2
5
  import numpy as np
6
+ import matplotlib as mpl
3
7
 
4
8
 
5
9
  # See also this link for the general answer (using scipy to compute coefficients):
@@ -13,28 +17,7 @@ def _evaluate_squared_bezier(points, t):
13
17
  def _evaluate_cubic_bezier(points, t):
14
18
  """Evaluate a cubic Bezier curve at t."""
15
19
  p0, p1, p2, p3 = points
16
- return (
17
- (1 - t) ** 3 * p0
18
- + 3 * (1 - t) ** 2 * t * p1
19
- + 3 * (1 - t) * t**2 * p2
20
- + t**3 * p3
21
- )
22
-
23
-
24
- def _evaluate_cubic_bezier_derivative(points, t):
25
- """Evaluate the derivative of a cubic Bezier curve at t."""
26
- p0, p1, p2, p3 = points
27
- # (dx / dt, dy / dt) is the parametric gradient
28
- # to get the angle from this, one can just atanh(dy/dt, dx/dt)
29
- # This is equivalent to computing the actual bezier curve
30
- # at low t, of course, which is the geometric interpretation
31
- # (obviously, division by t is irrelenant)
32
- return (
33
- 3 * p0 * (1 - t) ** 2
34
- + 3 * p1 * (1 - t) * (-3 * t + 1)
35
- + 3 * p2 * t * (2 - 3 * t)
36
- + 3 * p3 * t**2
37
- )
20
+ return (1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t**2 * p2 + t**3 * p3
38
21
 
39
22
 
40
23
  def convex_hull(points):
@@ -90,9 +73,7 @@ def _convex_hull_Graham_scan(points):
90
73
  pivot_idx = miny_idx[points[miny_idx, 0].argmin()]
91
74
 
92
75
  # Compute angles against that pivot, ensuring the pivot itself last
93
- angles = np.arctan2(
94
- points[:, 1] - points[pivot_idx, 1], points[:, 0] - points[pivot_idx, 0]
95
- )
76
+ angles = np.arctan2(points[:, 1] - points[pivot_idx, 1], points[:, 0] - points[pivot_idx, 0])
96
77
  angles[pivot_idx] = np.inf
97
78
 
98
79
  # Sort points by angle
@@ -169,22 +150,36 @@ def _convex_hull_Graham_scan(points):
169
150
 
170
151
 
171
152
  def _compute_group_path_with_vertex_padding(
172
- hull,
173
- points,
174
- transform,
175
- vertexpadding=10,
176
- points_per_curve=30,
153
+ hull: np.ndarray | Sequence[int],
154
+ points: np.ndarray,
155
+ transform: mpl.transforms.Transform,
156
+ vertexpadding: int = 10,
177
157
  # TODO: check how dpi affects this
178
- dpi=72.0,
179
- ):
158
+ dpi: float = 72.0,
159
+ ) -> np.ndarray:
180
160
  """Offset path for a group based on vertex padding.
181
161
 
182
- At the input, the structure is [v1, v1, v1, ..., vn, vn, vn, v1]
183
-
184
- # NOTE: this would look better as a cubic Bezier, but ok for now.
162
+ Parameters:
163
+ hull: The coordinates (not indices!) of the convex hull.
164
+ points: This is the np.ndarray where the coordinates will be written to (output).
165
+ The length is some integer ppc * len(hull) + 1 because for each vertex, this
166
+ function wraps around it using a certain fixed ppc number of points, plus the
167
+ final point for CLOSEPOLY.
168
+ transform: The transform of the hull points.
169
+ vertexpadding: The padding to apply to the vertices, in figure coordinates.
170
+ dpi (WIP): The dpi of the figure renderer.
171
+
172
+ Returns:
173
+ None. The output is written to the `points` array in place. This ensures that the
174
+ length of this array is unchanged, which is important to ensure that the vertices
175
+ and SVG codes are in sync.
185
176
  """
186
- # Short form
187
- ppc = points_per_curve
177
+ if len(hull) == 0:
178
+ return
179
+
180
+ # Short form for point per curve
181
+ ppc = (len(points) - 1) // len(hull)
182
+ assert len(points) % ppc == 1
188
183
 
189
184
  # No padding, set degenerate path
190
185
  if vertexpadding == 0:
@@ -196,11 +191,9 @@ def _compute_group_path_with_vertex_padding(
196
191
  # Transform into figure coordinates
197
192
  trans = transform.transform
198
193
  trans_inv = transform.inverted().transform
199
- points = trans(points)
200
194
 
201
195
  # Singleton: draw a circle around it
202
196
  if len(hull) == 1:
203
-
204
197
  # NOTE: linspace is double inclusive, which covers CLOSEPOLY
205
198
  thetas = np.linspace(
206
199
  -np.pi,
@@ -213,7 +206,6 @@ def _compute_group_path_with_vertex_padding(
213
206
 
214
207
  # Doublet: draw two semicircles
215
208
  if len(hull) == 2:
216
-
217
209
  # Unit vector connecting the two points
218
210
  dv = trans(hull[0]) - trans(hull[1])
219
211
  dv = dv / np.sqrt((dv**2).sum())
@@ -127,32 +127,35 @@ def _get_label_width_height(text, hpadding=18, vpadding=12, dpi=72.0, **kwargs):
127
127
 
128
128
  def _compute_mid_coord_and_rot(path, trans):
129
129
  """Compute mid point of an edge, straight or curved."""
130
- # Distinguish between straight and curved paths
130
+ # Straight path
131
131
  if path.codes[-1] == mpl.path.Path.LINETO:
132
132
  coord = path.vertices.mean(axis=0)
133
- vtr = trans(path.vertices)
134
- rot = atan2(
135
- vtr[-1, 1] - vtr[0, 1],
136
- vtr[-1, 0] - vtr[0, 0],
137
- )
133
+ v1 = path.vertices[0]
134
+ v2 = path.vertices[-1]
138
135
 
139
136
  # Cubic Bezier
140
137
  elif path.codes[-1] == mpl.path.Path.CURVE4:
141
138
  coord = _evaluate_cubic_bezier(path.vertices, 0.5)
142
- # TODO:
143
- rot = 0
139
+ v1 = _evaluate_cubic_bezier(path.vertices, 0.475)
140
+ v2 = _evaluate_cubic_bezier(path.vertices, 0.525)
144
141
 
145
142
  # Square Bezier
146
143
  elif path.codes[-1] == mpl.path.Path.CURVE3:
147
144
  coord = _evaluate_squared_bezier(path.vertices, 0.5)
148
- # TODO:
149
- rot = 0
145
+ v1 = _evaluate_squared_bezier(path.vertices, 0.475)
146
+ v2 = _evaluate_squared_bezier(path.vertices, 0.525)
150
147
 
151
148
  else:
152
149
  raise ValueError(
153
150
  "Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
154
151
  )
155
152
 
153
+ v1 = trans(v1)
154
+ v2 = trans(v2)
155
+ rot = atan2(
156
+ v2[1] - v1[1],
157
+ v2[0] - v1[0],
158
+ )
156
159
  return coord, rot
157
160
 
158
161
 
iplotx/utils/style.py CHANGED
@@ -1,9 +1,14 @@
1
1
  import copy
2
+ from collections import defaultdict
2
3
 
3
4
 
4
5
  def copy_with_deep_values(style):
5
6
  """Make a deep copy of the style dict but do not create copies of the keys."""
6
- newdict = {}
7
+ # Defaultdict should be respected
8
+ if hasattr(style, "default_factory"):
9
+ newdict = defaultdict(lambda: style.default_factory())
10
+ else:
11
+ newdict = {}
7
12
  for key, value in style.items():
8
13
  if isinstance(value, dict):
9
14
  newdict[key] = copy_with_deep_values(value)
iplotx/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  iplotx version information module.
3
3
  """
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.5.1"
iplotx/vertex.py CHANGED
@@ -55,7 +55,7 @@ class VertexCollection(PatchCollection):
55
55
  *args,
56
56
  layout_coordinate_system: str = "cartesian",
57
57
  style: Optional[dict[str, Any]] = None,
58
- labels: Optional[Sequence[str]] = None,
58
+ labels: Optional[Sequence[str] | pd.Series] = None,
59
59
  **kwargs,
60
60
  ):
61
61
  """Initialise the VertexCollection.
@@ -69,10 +69,13 @@ class VertexCollection(PatchCollection):
69
69
 
70
70
  self._index = layout.index
71
71
  self._style = style
72
- self._labels = labels
73
72
  self._layout = layout
74
73
  self._layout_coordinate_system = layout_coordinate_system
75
74
 
75
+ if (labels is not None) and (not isinstance(labels, pd.Series)):
76
+ labels = pd.Series(labels, index=self._layout.index)
77
+ self._labels = labels
78
+
76
79
  # Create patches from structured data
77
80
  patches, sizes, kwargs2 = self._init_vertex_patches()
78
81
 
@@ -91,6 +94,10 @@ class VertexCollection(PatchCollection):
91
94
  if self._labels is not None:
92
95
  self._compute_label_collection()
93
96
 
97
+ def __len__(self):
98
+ """Return the number of vertices in the collection."""
99
+ return len(self.get_paths())
100
+
94
101
  def get_children(self) -> tuple[mpl.artist.Artist]:
95
102
  """Get the children artists.
96
103
 
@@ -227,9 +234,7 @@ class VertexCollection(PatchCollection):
227
234
 
228
235
  if style.get("size", 20) == "label":
229
236
  if self._labels is None:
230
- warnings.warn(
231
- "No labels found, cannot resize vertices based on labels."
232
- )
237
+ warnings.warn("No labels found, cannot resize vertices based on labels.")
233
238
  style["size"] = get_style("default.vertex")["size"]
234
239
 
235
240
  if "cmap" in style:
@@ -265,9 +270,7 @@ class VertexCollection(PatchCollection):
265
270
  transform = self.get_offset_transform()
266
271
 
267
272
  style = (
268
- copy_with_deep_values(self._style.get("label", None))
269
- if self._style is not None
270
- else {}
273
+ copy_with_deep_values(self._style.get("label", None)) if self._style is not None else {}
271
274
  )
272
275
  forbidden_props = ["hpadding", "vpadding"]
273
276
  for prop in forbidden_props:
@@ -380,9 +383,7 @@ def make_patch(
380
383
  elif marker in ("s", "square", "r", "rectangle"):
381
384
  art = Rectangle((-size[0] / 2, -size[1] / 2), size[0], size[1], **kwargs)
382
385
  elif marker in ("^", "triangle"):
383
- art = RegularPolygon(
384
- (0, 0), numVertices=3, radius=size[0] / np.sqrt(2), **kwargs
385
- )
386
+ art = RegularPolygon((0, 0), numVertices=3, radius=size[0] / np.sqrt(2), **kwargs)
386
387
  elif marker in ("v", "triangle_down"):
387
388
  art = RegularPolygon(
388
389
  (0, 0),
@@ -408,21 +409,13 @@ def make_patch(
408
409
  **kwargs,
409
410
  )
410
411
  elif marker in ("d", "diamond"):
411
- art = RegularPolygon(
412
- (0, 0), numVertices=4, radius=size[0] / np.sqrt(2), **kwargs
413
- )
412
+ art = RegularPolygon((0, 0), numVertices=4, radius=size[0] / np.sqrt(2), **kwargs)
414
413
  elif marker in ("p", "pentagon"):
415
- art = RegularPolygon(
416
- (0, 0), numVertices=5, radius=size[0] / np.sqrt(2), **kwargs
417
- )
414
+ art = RegularPolygon((0, 0), numVertices=5, radius=size[0] / np.sqrt(2), **kwargs)
418
415
  elif marker in ("h", "hexagon"):
419
- art = RegularPolygon(
420
- (0, 0), numVertices=6, radius=size[0] / np.sqrt(2), **kwargs
421
- )
416
+ art = RegularPolygon((0, 0), numVertices=6, radius=size[0] / np.sqrt(2), **kwargs)
422
417
  elif marker in ("8", "octagon"):
423
- art = RegularPolygon(
424
- (0, 0), numVertices=8, radius=size[0] / np.sqrt(2), **kwargs
425
- )
418
+ art = RegularPolygon((0, 0), numVertices=8, radius=size[0] / np.sqrt(2), **kwargs)
426
419
  elif marker in ("e", "ellipse"):
427
420
  art = Ellipse((0, 0), size[0], size[1], **kwargs)
428
421
  elif marker in ("*", "star"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iplotx
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -39,12 +39,29 @@ Description-Content-Type: text/markdown
39
39
  ![Github Actions](https://github.com/fabilab/iplotx/actions/workflows/test.yml/badge.svg)
40
40
  ![PyPI - Version](https://img.shields.io/pypi/v/iplotx)
41
41
  ![RTD](https://readthedocs.org/projects/iplotx/badge/?version=latest)
42
+ [![Coverage Status](https://coveralls.io/repos/github/fabilab/iplotx/badge.svg?branch=main)](https://coveralls.io/github/fabilab/iplotx?branch=main)
42
43
  ![pylint](assets/pylint.svg)
43
44
 
44
45
  # iplotx
45
- Plotting networks from igraph and networkx.
46
+ [![Banner](docs/source/_static/banner.png)](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
46
47
 
47
- **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.
48
65
 
49
66
  ## Installation
50
67
  ```bash
@@ -71,19 +88,20 @@ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documen
71
88
  ## Gallery
72
89
  See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
73
90
 
74
- ## Roadmap
75
- - Plot networks from igraph and networkx interchangeably, using matplotlib as a backend. ✅
76
- - Support interactive plotting, e.g. zooming and panning after the plot is created. ✅
77
- - Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.).
78
- - 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.). ✅
79
97
  - Efficient plotting of large graphs using matplotlib's collection functionality. ✅
80
- - Support editing plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
81
- - Support animations, e.g. showing the evolution of a network over time. ✅
82
- - Support mouse interaction, e.g. hovering over or clicking on nodes and edges to get information about them. ✅
83
- - Support trees from special libraries such as ete3, biopython, etc. This will need a dedicated function and layouting. ✅
84
- - Support uni- and bi-directional communication between graph object and plot object.🏗️
85
-
86
- **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.🏗️
87
105
 
88
106
  ## Authors
89
107
  Fabio Zanini (https://fabilab.org)
@@ -0,0 +1,38 @@
1
+ iplotx/__init__.py,sha256=MKb9UCXKgDHHkeATuJWxYdM-AotfBo2fbWy-Rkbn9Is,509
2
+ iplotx/artists.py,sha256=Bpn6NS8S_B_E4OW88JYW6aEu2bIuIQJmbs2paTmBAoY,522
3
+ iplotx/cascades.py,sha256=OPqF7Huls-HFmDA5MCF6DEZlUeRVaXsbQcHBoKAgNJs,8182
4
+ iplotx/groups.py,sha256=_9KdIiTAi1kXtd2mDywgBJCbqoRq2z-5fzOPf76Wgb8,6287
5
+ iplotx/label.py,sha256=i107wE-9kC_MVWsgWeYG6sRy_ZmyvITNm2laIij9SR0,8761
6
+ iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
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
10
+ iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
11
+ iplotx/version.py,sha256=dsB8xhRODrwa4OXOSbePgoZzy41j_DudRu80QRgFkpw,66
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
16
+ iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
17
+ iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
18
+ iplotx/ingest/__init__.py,sha256=tsXDoa7Rs6Y1ulWtjCcUsO4tQIigeQ6ZMiU2PQDyhwQ,4751
19
+ iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
20
+ iplotx/ingest/typing.py,sha256=pi-mn4ULkFjTo_fFdJPUjTHrWzbny4MNgoMylN4mNKM,13940
21
+ iplotx/ingest/providers/network/igraph.py,sha256=8dWeaQ_ZNdltC098V2YeLXsGdJHQnBa6shF1GAfl0Zg,2973
22
+ iplotx/ingest/providers/network/networkx.py,sha256=FIXMI3hXU1WtAzPVlQZcz47b-4V2omeHttnNTgS2gQw,4328
23
+ iplotx/ingest/providers/network/simple.py,sha256=yKILiE3-ZhBUGSs7eYuhV8tQDyueCosbbgovZZYpSPQ,3664
24
+ iplotx/ingest/providers/tree/biopython.py,sha256=4N_54cVyHHPcASJZGr6pHKE2p5R3i8Cm307SLlSLHLA,1480
25
+ iplotx/ingest/providers/tree/cogent3.py,sha256=JmELbDK7LyybiJzFNbmeqZ4ySJoDajvFfJebpNfFKWo,1073
26
+ iplotx/ingest/providers/tree/ete4.py,sha256=D7usSq0MOjzrk3EoLi834IlaDGwv7_qG6Qt0ptfKqfI,928
27
+ iplotx/ingest/providers/tree/simple.py,sha256=vOAlQbkm2HdlBTQab6s7mjAnLibVmeNOfc6y6UpBqzw,2533
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
32
+ iplotx/utils/geometry.py,sha256=UH2gAcM5rYW7ADnJEm7HIJTpPF4UOm8P3vjSVCOGjqM,9192
33
+ iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
34
+ iplotx/utils/matplotlib.py,sha256=KpkuwXuSqpYbPVKbrlP8u_1Ry5gy5q60ZBrFiR-do_Q,5284
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,,