phylustrator 0.3.0__tar.gz → 0.3.1__tar.gz

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.
Files changed (54) hide show
  1. {phylustrator-0.3.0/src/phylustrator.egg-info → phylustrator-0.3.1}/PKG-INFO +1 -1
  2. {phylustrator-0.3.0 → phylustrator-0.3.1}/pyproject.toml +1 -1
  3. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/guides.py +25 -5
  4. {phylustrator-0.3.0 → phylustrator-0.3.1/src/phylustrator.egg-info}/PKG-INFO +1 -1
  5. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_figure.py +58 -0
  6. {phylustrator-0.3.0 → phylustrator-0.3.1}/LICENSE +0 -0
  7. {phylustrator-0.3.0 → phylustrator-0.3.1}/README.md +0 -0
  8. {phylustrator-0.3.0 → phylustrator-0.3.1}/setup.cfg +0 -0
  9. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/__init__.py +0 -0
  10. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/__main__.py +0 -0
  11. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/cli.py +0 -0
  12. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/color.py +0 -0
  13. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/compose.py +0 -0
  14. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/__init__.py +0 -0
  15. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/figure.py +0 -0
  16. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/genome.py +0 -0
  17. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/io.py +0 -0
  18. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layers/__init__.py +0 -0
  19. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layers/genes.py +0 -0
  20. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layers/guides.py +0 -0
  21. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layers/highlight.py +0 -0
  22. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layers/synteny.py +0 -0
  23. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/layout.py +0 -0
  24. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/matrix.py +0 -0
  25. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/panels.py +0 -0
  26. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/genomes/track.py +0 -0
  27. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/render.py +0 -0
  28. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/style.py +0 -0
  29. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/__init__.py +0 -0
  30. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/figure.py +0 -0
  31. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/io.py +0 -0
  32. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/__init__.py +0 -0
  33. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/clades.py +0 -0
  34. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/coloring.py +0 -0
  35. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/events.py +0 -0
  36. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/labels.py +0 -0
  37. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layers/tracks.py +0 -0
  38. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/layout.py +0 -0
  39. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/panels.py +0 -0
  40. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/skeleton.py +0 -0
  41. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/trees/tree.py +0 -0
  42. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator/zombi.py +0 -0
  43. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator.egg-info/SOURCES.txt +0 -0
  44. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator.egg-info/dependency_links.txt +0 -0
  45. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator.egg-info/entry_points.txt +0 -0
  46. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator.egg-info/requires.txt +0 -0
  47. {phylustrator-0.3.0 → phylustrator-0.3.1}/src/phylustrator.egg-info/top_level.txt +0 -0
  48. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_below.py +0 -0
  49. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_cli.py +0 -0
  50. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_genomes.py +0 -0
  51. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_io.py +0 -0
  52. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_layout.py +0 -0
  53. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_tree.py +0 -0
  54. {phylustrator-0.3.0 → phylustrator-0.3.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phylustrator
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A composable plotter for evolutionary figures: phylogenetic trees, genomes, synteny and alignments
5
5
  Author-email: "Adrian A. Davin" <aaredav@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "phylustrator"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "A composable plotter for evolutionary figures: phylogenetic trees, genomes, synteny and alignments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -100,15 +100,35 @@ def legend(title: str = "", *, swatch: float | None = None, size: float | None =
100
100
 
101
101
  def time_marker(*times, color: str = "#444444", width: float = 1.5, dash: bool = True,
102
102
  label: str | None = None, label_size: float | None = None):
103
- """Vertical reference line(s) crossing the tree at the given distance/time value(s) — e.g. to mark
104
- a mass-extinction moment or a rate shift. Rectangular only (distance maps to x). ``label`` (if
105
- given) is written above the first line."""
103
+ """Reference marker(s) at the given distance/time value(s) — e.g. a mass-extinction moment or a
104
+ rate shift. Rectangular: a vertical line crossing the tree. Radial: a circle at that distance
105
+ from the centre, which is what a depth threshold looks like on a tree too big to read as a
106
+ rectangle. The unrooted layout places branches by angle and has no distance-from-origin axis, so
107
+ it raises rather than drawing nothing. ``label`` (if given) is written above the first marker.
108
+
109
+ Times come on the stem-inclusive scale of the rectangular layout, the same scale
110
+ ``branch_events`` takes. The radial layout starts at the crown, so the stem comes off there."""
106
111
 
107
112
  def layer(canvas, tree, layout, style):
108
- if layout.kind != "rectangular":
113
+ ls = label_size if label_size is not None else style.font_size
114
+ if layout.kind == "unrooted":
115
+ raise ValueError("time_marker needs a layout with a distance axis, and the unrooted "
116
+ "layout has none: it places branches by angle, so a distance from the "
117
+ "origin is not a place on it. Use rectangular or radial.")
118
+ if layout.kind == "radial":
119
+ # the radial layout drops the stem and starts at the crown, so shift the times by it —
120
+ # branch_events shifts the same way, for the same reason
121
+ stem = float(tree.root.length or 0.0)
122
+ for i, t in enumerate(times):
123
+ r = t - stem
124
+ if r < 0:
125
+ raise ValueError(f"time_marker at {t:g} falls inside the root stem, which the "
126
+ f"radial layout does not draw: its centre is the crown, at {stem:g}")
127
+ canvas.data_ring(r, color, width, dash=dash)
128
+ if label and i == 0:
129
+ canvas.text(0.0, -r, label, dy=-8, anchor="middle", color=color, size=ls)
109
130
  return
110
131
  y0, y1 = layout.ylim
111
- ls = label_size if label_size is not None else style.font_size
112
132
  for i, t in enumerate(times):
113
133
  canvas.line(t, y0, t, y1, color, width, dash=dash)
114
134
  if label and i == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phylustrator
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A composable plotter for evolutionary figures: phylogenetic trees, genomes, synteny and alignments
5
5
  Author-email: "Adrian A. Davin" <aaredav@gmail.com>
6
6
  License: MIT
@@ -1,5 +1,6 @@
1
1
  """Figure: the skeleton renders to SVG, and the stem shows up as one extra branch."""
2
2
 
3
+ import math
3
4
  import re
4
5
 
5
6
  import pytest
@@ -15,6 +16,7 @@ from phylustrator.trees import (
15
16
  note,
16
17
  plot,
17
18
  rubberband,
19
+ time_marker,
18
20
  )
19
21
 
20
22
 
@@ -231,3 +233,59 @@ def test_rubberband_survives_a_lopsided_tree_and_stays_smooth():
231
233
  + rubberband(pops, palette=palette, gap=8.0, smooth=smooth)).as_svg()
232
234
  assert svg.count("<path") > 0
233
235
  assert "#207080" in svg and "#c77bad" in svg
236
+
237
+
238
+ # --- time_marker across the layouts (issue #8) ------------------------------------------------
239
+
240
+ _MARKER_TREE = "((((A:1,B:1)F:1,C:2)G:1,D:3)H:1,E:4)R;"
241
+
242
+
243
+ def _circle(svg):
244
+ """The (cx, cy, r) of the one circle in the figure — the radial marker."""
245
+ found = re.findall(r'<circle cx="([-\d.]+)" cy="([-\d.]+)" r="([-\d.]+)"', svg)
246
+ assert len(found) == 1, f"expected one circle, found {len(found)}"
247
+ return tuple(float(v) for v in found[0])
248
+
249
+
250
+ def test_time_marker_draws_in_rectangular_and_radial():
251
+ """It rendered nothing at all in radial, and said nothing about it."""
252
+ tree = loads(_MARKER_TREE)
253
+ for layout in ("rectangular", "radial"):
254
+ svg = (plot(tree, layout=layout) + time_marker(2.0, color="#D55E00", width=4)).as_svg()
255
+ assert "#D55E00" in svg, f"the marker is missing from the {layout} layout"
256
+
257
+
258
+ def test_the_radial_marker_is_a_circle_through_the_nodes_at_that_distance():
259
+ tree = loads(_MARKER_TREE)
260
+ fig = plot(tree, layout="radial") + time_marker(2.0, color="#D55E00")
261
+ cx, cy, r = _circle(fig.as_svg())
262
+ at_two = [n for n in fig.geometry().nodes if n.name == "G"][0] # G sits at distance 2
263
+ assert math.hypot(at_two.x - cx, at_two.y - cy) == pytest.approx(r)
264
+ assert r > 0
265
+
266
+
267
+ def test_the_radial_marker_takes_the_stem_off_the_time():
268
+ """The radial layout starts at the crown, as branch_events already assumed for its events."""
269
+ tree = loads("((((A:1,B:1)F:1,C:2)G:1,D:3)H:1,E:4)R:1.5;")
270
+ fig = plot(tree, layout="radial") + time_marker(3.5) # 2.0 past the crown
271
+ cx, cy, r = _circle(fig.as_svg())
272
+ at_two = [n for n in fig.geometry().nodes if n.name == "G"][0]
273
+ assert math.hypot(at_two.x - cx, at_two.y - cy) == pytest.approx(r)
274
+
275
+
276
+ def test_the_radial_marker_keeps_its_dash():
277
+ tree = loads(_MARKER_TREE)
278
+ assert "stroke-dasharray" in (plot(tree, layout="radial") + time_marker(2.0)).as_svg()
279
+ assert "stroke-dasharray" not in (plot(tree, layout="radial")
280
+ + time_marker(2.0, dash=False)).as_svg()
281
+
282
+
283
+ def test_time_marker_refuses_the_unrooted_layout():
284
+ """A distance from the origin is not a place on an equal-angle layout. Say so, do not skip."""
285
+ with pytest.raises(ValueError, match="unrooted"):
286
+ (plot(loads(_MARKER_TREE), layout="unrooted") + time_marker(2.0)).as_svg()
287
+
288
+
289
+ def test_time_marker_refuses_a_time_the_radial_layout_cannot_show():
290
+ with pytest.raises(ValueError, match="stem"):
291
+ (plot(loads("((A:1,B:1)F:1,C:2)R:1.5;"), layout="radial") + time_marker(0.5)).as_svg()
File without changes
File without changes
File without changes