phylustrator 0.1.0__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 (51) hide show
  1. phylustrator-0.1.0/LICENSE +21 -0
  2. phylustrator-0.1.0/PKG-INFO +132 -0
  3. phylustrator-0.1.0/README.md +93 -0
  4. phylustrator-0.1.0/pyproject.toml +73 -0
  5. phylustrator-0.1.0/setup.cfg +4 -0
  6. phylustrator-0.1.0/src/phylustrator/__init__.py +29 -0
  7. phylustrator-0.1.0/src/phylustrator/__main__.py +5 -0
  8. phylustrator-0.1.0/src/phylustrator/cli.py +85 -0
  9. phylustrator-0.1.0/src/phylustrator/color.py +92 -0
  10. phylustrator-0.1.0/src/phylustrator/compose.py +61 -0
  11. phylustrator-0.1.0/src/phylustrator/genomes/__init__.py +23 -0
  12. phylustrator-0.1.0/src/phylustrator/genomes/figure.py +107 -0
  13. phylustrator-0.1.0/src/phylustrator/genomes/genome.py +41 -0
  14. phylustrator-0.1.0/src/phylustrator/genomes/io.py +56 -0
  15. phylustrator-0.1.0/src/phylustrator/genomes/layers/__init__.py +8 -0
  16. phylustrator-0.1.0/src/phylustrator/genomes/layers/genes.py +35 -0
  17. phylustrator-0.1.0/src/phylustrator/genomes/layers/guides.py +78 -0
  18. phylustrator-0.1.0/src/phylustrator/genomes/layers/highlight.py +38 -0
  19. phylustrator-0.1.0/src/phylustrator/genomes/layers/synteny.py +56 -0
  20. phylustrator-0.1.0/src/phylustrator/genomes/layout.py +130 -0
  21. phylustrator-0.1.0/src/phylustrator/genomes/matrix.py +32 -0
  22. phylustrator-0.1.0/src/phylustrator/genomes/panels.py +208 -0
  23. phylustrator-0.1.0/src/phylustrator/genomes/track.py +59 -0
  24. phylustrator-0.1.0/src/phylustrator/render.py +229 -0
  25. phylustrator-0.1.0/src/phylustrator/style.py +29 -0
  26. phylustrator-0.1.0/src/phylustrator/trees/__init__.py +23 -0
  27. phylustrator-0.1.0/src/phylustrator/trees/figure.py +122 -0
  28. phylustrator-0.1.0/src/phylustrator/trees/io.py +148 -0
  29. phylustrator-0.1.0/src/phylustrator/trees/layers/__init__.py +30 -0
  30. phylustrator-0.1.0/src/phylustrator/trees/layers/clades.py +35 -0
  31. phylustrator-0.1.0/src/phylustrator/trees/layers/coloring.py +143 -0
  32. phylustrator-0.1.0/src/phylustrator/trees/layers/events.py +96 -0
  33. phylustrator-0.1.0/src/phylustrator/trees/layers/guides.py +149 -0
  34. phylustrator-0.1.0/src/phylustrator/trees/layers/labels.py +49 -0
  35. phylustrator-0.1.0/src/phylustrator/trees/layers/tracks.py +36 -0
  36. phylustrator-0.1.0/src/phylustrator/trees/layout.py +136 -0
  37. phylustrator-0.1.0/src/phylustrator/trees/skeleton.py +90 -0
  38. phylustrator-0.1.0/src/phylustrator/trees/tree.py +90 -0
  39. phylustrator-0.1.0/src/phylustrator/zombi.py +145 -0
  40. phylustrator-0.1.0/src/phylustrator.egg-info/PKG-INFO +132 -0
  41. phylustrator-0.1.0/src/phylustrator.egg-info/SOURCES.txt +49 -0
  42. phylustrator-0.1.0/src/phylustrator.egg-info/dependency_links.txt +1 -0
  43. phylustrator-0.1.0/src/phylustrator.egg-info/entry_points.txt +2 -0
  44. phylustrator-0.1.0/src/phylustrator.egg-info/requires.txt +13 -0
  45. phylustrator-0.1.0/src/phylustrator.egg-info/top_level.txt +1 -0
  46. phylustrator-0.1.0/tests/test_cli.py +27 -0
  47. phylustrator-0.1.0/tests/test_figure.py +60 -0
  48. phylustrator-0.1.0/tests/test_genomes.py +88 -0
  49. phylustrator-0.1.0/tests/test_io.py +66 -0
  50. phylustrator-0.1.0/tests/test_layout.py +80 -0
  51. phylustrator-0.1.0/tests/test_tree.py +31 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Adrián A. Davín
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: phylustrator
3
+ Version: 0.1.0
4
+ Summary: A composable plotter for evolutionary figures: phylogenetic trees, genomes, synteny and alignments
5
+ Author-email: "Adrian A. Davin" <aaredav@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AADavin/Phylustrator
8
+ Project-URL: Documentation, https://github.com/AADavin/Phylustrator#readme
9
+ Project-URL: Repository, https://github.com/AADavin/Phylustrator
10
+ Project-URL: Issues, https://github.com/AADavin/Phylustrator/issues
11
+ Project-URL: Changelog, https://github.com/AADavin/Phylustrator/blob/main/CHANGELOG.md
12
+ Keywords: phylogenetics,visualization,tree,biology,evolution,svg
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: drawsvg
28
+ Provides-Extra: export
29
+ Requires-Dist: cairosvg>=2.5; extra == "export"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.3; extra == "dev"
34
+ Requires-Dist: mypy>=1.0; extra == "dev"
35
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
36
+ Requires-Dist: ete3; extra == "dev"
37
+ Requires-Dist: cairosvg>=2.5; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # Phylustrator
41
+
42
+ A small, composable plotter for evolutionary figures. Two domains share one grammar: **`ph.trees`**
43
+ plots phylogenetic trees and **`ph.genomes`** plots genomes, synteny and alignments. Start a figure,
44
+ add layers with `+`, and save it to SVG, PDF, or PNG.
45
+
46
+ ![A 100-tip tree with branches coloured by a Brownian-motion trait](docs/img/tree.png)
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install git+https://github.com/AADavin/Phylustrator
52
+ ```
53
+
54
+ SVG output needs nothing else; for PDF/PNG also install `cairosvg` (`pip install cairosvg`).
55
+
56
+ ## Trees
57
+
58
+ ```python
59
+ import phylustrator as ph
60
+
61
+ tree = ph.trees.loads("((((Human:6,Chimp:6)a:2,Gorilla:8)b:3,Orang:11)c:5,Gibbon:16)root;")
62
+ brain = {"Human": 1350, "Chimp": 400, "Gorilla": 500, "Orang": 400, "Gibbon": 100,
63
+ "a": 650, "b": 560, "c": 500, "root": 470}
64
+
65
+ (ph.trees.plot(tree)
66
+ + ph.trees.color_branches(brain)
67
+ + ph.trees.tip_labels()
68
+ + ph.trees.colorbar("brain size (cc)")
69
+ + ph.trees.time_axis("million years")).save("tree.png")
70
+ ```
71
+
72
+ That is the whole idea: `plot(tree)` starts a figure and each `+ layer` adds one decoration.
73
+
74
+ - **Layouts** — `rectangular` (default), `radial`, `unrooted`.
75
+ - **Layers** — `color_branches`, `color_history`, `tip_labels`, `node_labels`, `tip_track`,
76
+ `branch_events`, `colorbar`, `legend`, `time_axis`, `time_marker`, `scale_bar`, `note`,
77
+ `highlight_clade`.
78
+
79
+ ## Genomes
80
+
81
+ The same grammar, for genome maps. Plot a genome as a line or a ring, colour genes by family or
82
+ strand, link two genomes with synteny ribbons, or set a copy-number heatmap / alignment beside a tree.
83
+
84
+ ```python
85
+ import phylustrator as ph
86
+
87
+ G = ph.genomes.read_gff("genome.gff") # {name: Genome}
88
+ genome = next(iter(G.values()))
89
+ (ph.genomes.plot(genome, layout="circular", coordinates="nucleotide")
90
+ + ph.genomes.genes(by="strand")
91
+ + ph.genomes.position_axis()).save("ring.png")
92
+ ```
93
+
94
+ - **Layouts** — `linear`, `circular`, and `stack` (one genome per row, for synteny).
95
+ - **Layers** — `genes`, `synteny`, `highlight`, `position_axis`.
96
+ - **Panels** — `heatmap`, `alignment`, placed next to a tree with `ph.beside(tree, panel)`.
97
+
98
+ ## ZOMBI2 I/O
99
+
100
+ `ph.zombi` reads the output of the [ZOMBI2](https://github.com/AADavin/zombi2) genome-evolution
101
+ simulator into the data models above — kept in one clearly-separated layer so the core stays
102
+ format-agnostic:
103
+
104
+ ```python
105
+ import phylustrator as ph
106
+
107
+ G = ph.zombi.read_genomes("run/genomes") # {lineage: Genome}
108
+ M = ph.zombi.read_profiles("run") # family x genome copy-number Matrix
109
+ aln = ph.zombi.read_alignment("run", family=0) # Alignment keyed by genome
110
+ tree = ph.zombi.read_species_tree("run") # a Tree
111
+ ```
112
+
113
+ ## Command line
114
+
115
+ `phyl` is a one-shot tree viewer — hand it a Newick file:
116
+
117
+ ```bash
118
+ phyl tree.nwk # render to a temporary PDF and open it
119
+ phyl tree.nwk -o fig.svg # save instead (format from the extension: .svg / .pdf / .png)
120
+ phyl tree.nwk --radial --no-labels
121
+ ```
122
+
123
+ Flags: `--layout {rectangular,radial,unrooted}` (or `--radial` / `--unrooted`), `--no-labels`,
124
+ `--node-labels`, `--no-stem`, `--no-open`. Colouring and everything else live in the Python API.
125
+
126
+ ## Dependencies
127
+
128
+ Only `drawsvg` (plus `cairosvg` for PDF/PNG). No `ete3`, no `matplotlib`.
129
+
130
+ ## License
131
+
132
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,93 @@
1
+ # Phylustrator
2
+
3
+ A small, composable plotter for evolutionary figures. Two domains share one grammar: **`ph.trees`**
4
+ plots phylogenetic trees and **`ph.genomes`** plots genomes, synteny and alignments. Start a figure,
5
+ add layers with `+`, and save it to SVG, PDF, or PNG.
6
+
7
+ ![A 100-tip tree with branches coloured by a Brownian-motion trait](docs/img/tree.png)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install git+https://github.com/AADavin/Phylustrator
13
+ ```
14
+
15
+ SVG output needs nothing else; for PDF/PNG also install `cairosvg` (`pip install cairosvg`).
16
+
17
+ ## Trees
18
+
19
+ ```python
20
+ import phylustrator as ph
21
+
22
+ tree = ph.trees.loads("((((Human:6,Chimp:6)a:2,Gorilla:8)b:3,Orang:11)c:5,Gibbon:16)root;")
23
+ brain = {"Human": 1350, "Chimp": 400, "Gorilla": 500, "Orang": 400, "Gibbon": 100,
24
+ "a": 650, "b": 560, "c": 500, "root": 470}
25
+
26
+ (ph.trees.plot(tree)
27
+ + ph.trees.color_branches(brain)
28
+ + ph.trees.tip_labels()
29
+ + ph.trees.colorbar("brain size (cc)")
30
+ + ph.trees.time_axis("million years")).save("tree.png")
31
+ ```
32
+
33
+ That is the whole idea: `plot(tree)` starts a figure and each `+ layer` adds one decoration.
34
+
35
+ - **Layouts** — `rectangular` (default), `radial`, `unrooted`.
36
+ - **Layers** — `color_branches`, `color_history`, `tip_labels`, `node_labels`, `tip_track`,
37
+ `branch_events`, `colorbar`, `legend`, `time_axis`, `time_marker`, `scale_bar`, `note`,
38
+ `highlight_clade`.
39
+
40
+ ## Genomes
41
+
42
+ The same grammar, for genome maps. Plot a genome as a line or a ring, colour genes by family or
43
+ strand, link two genomes with synteny ribbons, or set a copy-number heatmap / alignment beside a tree.
44
+
45
+ ```python
46
+ import phylustrator as ph
47
+
48
+ G = ph.genomes.read_gff("genome.gff") # {name: Genome}
49
+ genome = next(iter(G.values()))
50
+ (ph.genomes.plot(genome, layout="circular", coordinates="nucleotide")
51
+ + ph.genomes.genes(by="strand")
52
+ + ph.genomes.position_axis()).save("ring.png")
53
+ ```
54
+
55
+ - **Layouts** — `linear`, `circular`, and `stack` (one genome per row, for synteny).
56
+ - **Layers** — `genes`, `synteny`, `highlight`, `position_axis`.
57
+ - **Panels** — `heatmap`, `alignment`, placed next to a tree with `ph.beside(tree, panel)`.
58
+
59
+ ## ZOMBI2 I/O
60
+
61
+ `ph.zombi` reads the output of the [ZOMBI2](https://github.com/AADavin/zombi2) genome-evolution
62
+ simulator into the data models above — kept in one clearly-separated layer so the core stays
63
+ format-agnostic:
64
+
65
+ ```python
66
+ import phylustrator as ph
67
+
68
+ G = ph.zombi.read_genomes("run/genomes") # {lineage: Genome}
69
+ M = ph.zombi.read_profiles("run") # family x genome copy-number Matrix
70
+ aln = ph.zombi.read_alignment("run", family=0) # Alignment keyed by genome
71
+ tree = ph.zombi.read_species_tree("run") # a Tree
72
+ ```
73
+
74
+ ## Command line
75
+
76
+ `phyl` is a one-shot tree viewer — hand it a Newick file:
77
+
78
+ ```bash
79
+ phyl tree.nwk # render to a temporary PDF and open it
80
+ phyl tree.nwk -o fig.svg # save instead (format from the extension: .svg / .pdf / .png)
81
+ phyl tree.nwk --radial --no-labels
82
+ ```
83
+
84
+ Flags: `--layout {rectangular,radial,unrooted}` (or `--radial` / `--unrooted`), `--no-labels`,
85
+ `--node-labels`, `--no-stem`, `--no-open`. Colouring and everything else live in the Python API.
86
+
87
+ ## Dependencies
88
+
89
+ Only `drawsvg` (plus `cairosvg` for PDF/PNG). No `ete3`, no `matplotlib`.
90
+
91
+ ## License
92
+
93
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "phylustrator"
7
+ version = "0.1.0"
8
+ description = "A composable plotter for evolutionary figures: phylogenetic trees, genomes, synteny and alignments"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Adrian A. Davin", email = "aaredav@gmail.com"},
14
+ ]
15
+ keywords = ["phylogenetics", "visualization", "tree", "biology", "evolution", "svg"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
26
+ "Topic :: Scientific/Engineering :: Visualization",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "drawsvg",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/AADavin/Phylustrator"
35
+ Documentation = "https://github.com/AADavin/Phylustrator#readme"
36
+ Repository = "https://github.com/AADavin/Phylustrator"
37
+ Issues = "https://github.com/AADavin/Phylustrator/issues"
38
+ Changelog = "https://github.com/AADavin/Phylustrator/blob/main/CHANGELOG.md"
39
+
40
+ [project.optional-dependencies]
41
+ export = ["cairosvg>=2.5"]
42
+ dev = [
43
+ "pytest>=7.0",
44
+ "pytest-cov>=4.0",
45
+ "ruff>=0.3",
46
+ "mypy>=1.0",
47
+ "pre-commit>=3.0",
48
+ "ete3", # test-only: the differential oracle for the Newick parser
49
+ "cairosvg>=2.5",
50
+ ]
51
+
52
+ [project.scripts]
53
+ phyl = "phylustrator.cli:main"
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ python_files = ["test_*.py", "tests_*.py"]
58
+ addopts = "-v --tb=short"
59
+
60
+ [tool.mypy]
61
+ python_version = "3.10"
62
+ warn_return_any = true
63
+ warn_unused_configs = true
64
+ ignore_missing_imports = true
65
+
66
+ [tool.ruff]
67
+ target-version = "py310"
68
+ line-length = 120
69
+ src = ["src"]
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E", "F", "W", "I"]
73
+ ignore = ["E501"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ """Phylustrator — a lean, composable plotter for phylogenetics: **trees** and **genomes**.
2
+
3
+ Two domains, one grammar (``plot(x) + layer + …``), one shared drawing backend:
4
+
5
+ import phylustrator as ph
6
+
7
+ # trees
8
+ tree = ph.trees.loads("((A:1,B:1)C:2,D:3)R;")
9
+ (ph.trees.plot(tree) + ph.trees.color_branches(vals) + ph.trees.time_axis()).save("tree.pdf")
10
+
11
+ # genomes
12
+ G = ph.zombi.read_genomes("run")
13
+ (ph.genomes.plot(G["n12"], layout="circular") + ph.genomes.genes(by="family")).save("ring.png")
14
+
15
+ # bridge: a matrix beside a tree
16
+ ph.beside(ph.trees.plot(tree) + ph.trees.tip_labels(), ph.genomes.heatmap(ph.zombi.read_profiles("run")))
17
+
18
+ ``ph.zombi`` is the only ZOMBI2-format-aware module; ``trees`` / ``genomes`` are general.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from . import genomes, trees, zombi
24
+ from .compose import Composite, beside
25
+ from .style import Style
26
+
27
+ __version__ = "0.1.0"
28
+
29
+ __all__ = ["trees", "genomes", "zombi", "beside", "Composite", "Style", "__version__"]
@@ -0,0 +1,5 @@
1
+ """``python -m phylustrator`` → the ``phyl`` command-line viewer."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,85 @@
1
+ """``phyl`` — a one-shot tree viewer.
2
+
3
+ phyl tree.nwk # render to a temporary PDF and open it
4
+ phyl tree.nwk -o fig.svg # save instead (format from the extension)
5
+ phyl tree.nwk --radial --no-labels
6
+
7
+ Deliberately tiny: it reads a Newick file, draws it, and shows or saves it. Anything richer
8
+ (colouring, tracks, custom styles) lives in the Python API.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ from . import __version__
21
+ from .trees import node_labels, plot, read, scale_bar, tip_labels, time_axis
22
+
23
+
24
+ def _parser() -> argparse.ArgumentParser:
25
+ p = argparse.ArgumentParser(prog="phyl", description="Draw a phylogenetic tree from a Newick file.")
26
+ p.add_argument("tree", help="Newick tree file")
27
+ p.add_argument("-o", "--output", metavar="FILE",
28
+ help="save here (.svg/.pdf/.png); default: a temporary PDF, opened")
29
+ p.add_argument("--layout", choices=["rectangular", "radial", "unrooted"], default="rectangular")
30
+ p.add_argument("--radial", action="store_const", const="radial", dest="layout",
31
+ help="shortcut for --layout radial")
32
+ p.add_argument("--unrooted", action="store_const", const="unrooted", dest="layout",
33
+ help="shortcut for --layout unrooted")
34
+ p.add_argument("--no-labels", action="store_true", help="hide tip labels")
35
+ p.add_argument("--node-labels", action="store_true", help="also label internal nodes")
36
+ p.add_argument("--no-stem", action="store_true", help="start at the crown (hide the root stem)")
37
+ p.add_argument("--no-open", action="store_true", help="do not open the temporary file")
38
+ p.add_argument("-V", "--version", action="version", version=f"phyl {__version__}")
39
+ return p
40
+
41
+
42
+ def _open(path: Path) -> None:
43
+ try:
44
+ if sys.platform == "darwin":
45
+ subprocess.run(["open", str(path)], check=False)
46
+ elif sys.platform.startswith("win"):
47
+ os.startfile(str(path)) # type: ignore[attr-defined] # noqa: S606
48
+ else:
49
+ subprocess.run(["xdg-open", str(path)], check=False)
50
+ except OSError:
51
+ pass # opening is a convenience; the path is printed regardless
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> int:
55
+ parser = _parser()
56
+ args = parser.parse_args(argv)
57
+
58
+ try:
59
+ tree = read(args.tree)
60
+ except (FileNotFoundError, ValueError) as exc:
61
+ parser.error(str(exc))
62
+
63
+ figure = plot(tree, layout=args.layout, stem=not args.no_stem)
64
+ if not args.no_labels:
65
+ figure = figure + tip_labels()
66
+ if args.node_labels:
67
+ figure = figure + node_labels()
68
+ figure = figure + (time_axis() if args.layout == "rectangular" else scale_bar())
69
+
70
+ if args.output:
71
+ out = figure.save(args.output)
72
+ print(out)
73
+ return 0
74
+
75
+ # No -o: a genuinely throwaway file in the system temp dir.
76
+ tmp = Path(tempfile.mkdtemp(prefix="phyl_")) / "tree.pdf"
77
+ out = figure.save(tmp) # may return a .svg sibling if cairosvg is absent
78
+ print(out)
79
+ if not args.no_open:
80
+ _open(out)
81
+ return 0
82
+
83
+
84
+ if __name__ == "__main__":
85
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ """Colour — colormaps for continuous values, palettes for categories, and normalisation.
2
+
3
+ Matplotlib-free: the viridis ramp is a small embedded lookup table sampled by interpolation, and the
4
+ categorical palette is Paul Tol's colour-blind-safe "bright" set.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from typing import Callable, Iterable
11
+
12
+ # viridis, 16 anchor colours (R, G, B in 0–255), sampled evenly from matplotlib.
13
+ _COLORMAPS: dict[str, list[tuple[int, int, int]]] = {
14
+ "viridis": [
15
+ (68, 1, 84), (72, 26, 108), (71, 47, 125), (65, 68, 135),
16
+ (57, 86, 140), (49, 104, 142), (42, 120, 142), (35, 136, 142),
17
+ (31, 152, 139), (34, 168, 132), (53, 183, 121), (84, 197, 104),
18
+ (122, 209, 81), (165, 219, 54), (210, 226, 27), (253, 231, 37),
19
+ ],
20
+ }
21
+
22
+ # Paul Tol "bright" — distinct and colour-blind-safe.
23
+ _PALETTE = ["#4477AA", "#EE6677", "#228833", "#CCBB44", "#66CCEE", "#AA3377", "#BBBBBB"]
24
+
25
+
26
+ def to_hex(rgb: tuple[float, float, float]) -> str:
27
+ return "#{:02x}{:02x}{:02x}".format(*(int(max(0, min(255, round(c)))) for c in rgb))
28
+
29
+
30
+ def colormap(name: str = "viridis") -> Callable[[float], tuple[int, int, int]]:
31
+ """Return a sampler ``t in [0, 1] -> (R, G, B)`` that interpolates the named colormap."""
32
+ anchors = _colormap_anchors(name)
33
+ n = len(anchors)
34
+
35
+ def sample(t: float) -> tuple[int, int, int]:
36
+ t = max(0.0, min(1.0, float(t)))
37
+ pos = t * (n - 1)
38
+ i = int(math.floor(pos))
39
+ if i >= n - 1:
40
+ return anchors[-1]
41
+ frac = pos - i
42
+ a, b = anchors[i], anchors[i + 1]
43
+ return tuple(a[k] + (b[k] - a[k]) * frac for k in range(3))
44
+
45
+ return sample
46
+
47
+
48
+ def colormap_hex(name: str = "viridis") -> list[str]:
49
+ """The colormap's anchor colours as hex — for a gradient bar."""
50
+ return [to_hex(rgb) for rgb in _colormap_anchors(name)]
51
+
52
+
53
+ def _colormap_anchors(name: str) -> list[tuple[int, int, int]]:
54
+ anchors = _COLORMAPS.get(name.lower())
55
+ if anchors is None:
56
+ raise ValueError(f"unknown colormap {name!r}; available: {sorted(_COLORMAPS)}")
57
+ return anchors
58
+
59
+
60
+ def palette(labels: Iterable) -> dict:
61
+ """A ``{label: hex colour}`` map over ``labels`` (sorted for stability), from the bright set."""
62
+ ordered = sorted(set(labels), key=str)
63
+ return {label: _PALETTE[i % len(_PALETTE)] for i, label in enumerate(ordered)}
64
+
65
+
66
+ def normalize(values: Iterable[float]) -> tuple[float, float, Callable[[float], float]]:
67
+ """Return ``(vmin, vmax, to_unit)`` where ``to_unit(v)`` maps ``[vmin, vmax] -> [0, 1]``."""
68
+ nums = [float(v) for v in values]
69
+ vmin, vmax = min(nums), max(nums)
70
+ span = (vmax - vmin) or 1.0
71
+ return vmin, vmax, lambda v: (float(v) - vmin) / span
72
+
73
+
74
+ def _is_number(v) -> bool:
75
+ return isinstance(v, (int, float)) and not isinstance(v, bool)
76
+
77
+
78
+ def map_values(values: dict, *, cmap: str = "viridis",
79
+ palette: dict | None = None) -> tuple[dict, dict]:
80
+ """Turn ``{key: value}`` into ``({key: hex colour}, scale)``, dispatching on the data: numbers get
81
+ the colormap (and a ``continuous`` scale for a colour bar), labels get a palette (and a
82
+ ``categorical`` scale for a legend). ``scale`` is ``None`` if there is nothing to colour."""
83
+ present = {k: v for k, v in values.items() if v is not None}
84
+ if not present:
85
+ return {}, None
86
+ if all(_is_number(v) for v in present.values()):
87
+ vmin, vmax, to_unit = normalize(present.values())
88
+ sample = colormap(cmap)
89
+ colors = {k: to_hex(sample(to_unit(v))) for k, v in present.items()}
90
+ return colors, {"kind": "continuous", "vmin": vmin, "vmax": vmax, "cmap": cmap}
91
+ pal = palette or globals()["palette"](present.values())
92
+ return {k: pal[v] for k, v in present.items()}, {"kind": "categorical", "palette": pal}
@@ -0,0 +1,61 @@
1
+ """Composition — put a Genustrator panel beside a Phylustrator tree, rows lined up with the tips.
2
+
3
+ ``beside(tree, panel)`` takes a **Phylustrator** figure (the phylogeny — Phylustrator draws it, we do
4
+ not redraw it) and a panel (``heatmap`` / ``alignment``). It asks the tree for its tip pixel positions
5
+ (``Figure.geometry``), renders the tree into the left column, and draws the panel to the right with
6
+ each row at its tip's ``y`` — so a genome's row sits exactly on its leaf.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .render import Canvas
12
+ from .style import Style
13
+
14
+
15
+ class Composite:
16
+ """The rendered composite; save like any figure."""
17
+
18
+ def __init__(self, canvas: Canvas) -> None:
19
+ self._canvas = canvas
20
+
21
+ def as_svg(self) -> str:
22
+ return self._canvas.as_svg()
23
+
24
+ def save(self, path):
25
+ return self._canvas.save(path)
26
+
27
+
28
+ def beside(tree, panel, *, width: float = 1100.0, height: float | None = None,
29
+ tree_fraction: float = 0.4, gap: float = 18.0, pad: float = 34.0,
30
+ footer: float = 0.0, background: str = "white") -> Composite:
31
+ """Render Phylustrator ``tree`` on the left and ``panel`` on the right, rows aligned to the tips.
32
+
33
+ ``tree_fraction`` is the share of the width given to the tree column (the rest, minus ``gap`` and
34
+ ``pad``, holds the panel). ``footer`` reserves blank height below the rows (for a panel's colour
35
+ key). Rows are matched to tips by label and drawn in the tree's tip order. Needs ``cairosvg``."""
36
+ try:
37
+ import cairosvg
38
+ except ImportError as exc: # pragma: no cover
39
+ raise RuntimeError("genustrator.beside needs cairosvg (pip install genustrator[export]) "
40
+ "to place the tree into the composite") from exc
41
+
42
+ n_tips = len(tree.tree.leaves)
43
+ # per-tip row height eases from ~44px (few tips) down to ~24px (many), so 25-40 rows stay sane
44
+ row_px = max(24.0, 46.0 - 0.55 * n_tips)
45
+ H = height if height is not None else max(260.0, 70.0 + row_px * n_tips) + footer
46
+ tree_w = round(width * tree_fraction)
47
+ tree_h = H - footer # tips fill the area above the footer
48
+
49
+ sized = tree.with_size(tree_w, tree_h)
50
+ geom = sized.geometry()
51
+ png = cairosvg.svg2png(bytestring=sized.as_svg().encode(),
52
+ output_width=int(tree_w * 2), output_height=int(tree_h * 2))
53
+
54
+ canvas = Canvas(Style(width=width, height=H, margin=0, background=background), (0.0, 1.0), (0.0, 1.0))
55
+ canvas.embed_png(png, 0, 0, tree_w, tree_h)
56
+
57
+ wanted = set(panel.rows)
58
+ rows = [(t.name, t.y) for t in geom.tips if t.name in wanted]
59
+ if rows:
60
+ panel.draw(canvas, tree_w + gap, width - pad, rows, canvas.style)
61
+ return Composite(canvas)
@@ -0,0 +1,23 @@
1
+ """The **genomes** domain — plot genomes, synteny and alignments. Same grammar as ``trees``:
2
+ ``phylustrator.genomes.plot(genome) + layer + …``
3
+
4
+ import phylustrator as ph
5
+ G = ph.zombi.read_genomes("run") # or ph.genomes.read_gff("genome.gff")
6
+ (ph.genomes.plot(G["n12"], layout="circular") + ph.genomes.genes(by="family")).save("ring.png")
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .figure import Figure, StackFigure, plot, stack
12
+ from .genome import Chromosome, Gene, Genome
13
+ from .io import read_gff
14
+ from .layers import genes, highlight, position_axis, synteny
15
+ from .matrix import Alignment, Matrix
16
+ from .panels import alignment, heatmap, states
17
+
18
+ __all__ = [
19
+ "Gene", "Chromosome", "Genome", "read_gff",
20
+ "plot", "stack", "Figure", "StackFigure",
21
+ "genes", "synteny", "highlight", "position_axis",
22
+ "Matrix", "Alignment", "heatmap", "alignment", "states",
23
+ ]