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.
- phylustrator-0.1.0/LICENSE +21 -0
- phylustrator-0.1.0/PKG-INFO +132 -0
- phylustrator-0.1.0/README.md +93 -0
- phylustrator-0.1.0/pyproject.toml +73 -0
- phylustrator-0.1.0/setup.cfg +4 -0
- phylustrator-0.1.0/src/phylustrator/__init__.py +29 -0
- phylustrator-0.1.0/src/phylustrator/__main__.py +5 -0
- phylustrator-0.1.0/src/phylustrator/cli.py +85 -0
- phylustrator-0.1.0/src/phylustrator/color.py +92 -0
- phylustrator-0.1.0/src/phylustrator/compose.py +61 -0
- phylustrator-0.1.0/src/phylustrator/genomes/__init__.py +23 -0
- phylustrator-0.1.0/src/phylustrator/genomes/figure.py +107 -0
- phylustrator-0.1.0/src/phylustrator/genomes/genome.py +41 -0
- phylustrator-0.1.0/src/phylustrator/genomes/io.py +56 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layers/__init__.py +8 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layers/genes.py +35 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layers/guides.py +78 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layers/highlight.py +38 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layers/synteny.py +56 -0
- phylustrator-0.1.0/src/phylustrator/genomes/layout.py +130 -0
- phylustrator-0.1.0/src/phylustrator/genomes/matrix.py +32 -0
- phylustrator-0.1.0/src/phylustrator/genomes/panels.py +208 -0
- phylustrator-0.1.0/src/phylustrator/genomes/track.py +59 -0
- phylustrator-0.1.0/src/phylustrator/render.py +229 -0
- phylustrator-0.1.0/src/phylustrator/style.py +29 -0
- phylustrator-0.1.0/src/phylustrator/trees/__init__.py +23 -0
- phylustrator-0.1.0/src/phylustrator/trees/figure.py +122 -0
- phylustrator-0.1.0/src/phylustrator/trees/io.py +148 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/__init__.py +30 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/clades.py +35 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/coloring.py +143 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/events.py +96 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/guides.py +149 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/labels.py +49 -0
- phylustrator-0.1.0/src/phylustrator/trees/layers/tracks.py +36 -0
- phylustrator-0.1.0/src/phylustrator/trees/layout.py +136 -0
- phylustrator-0.1.0/src/phylustrator/trees/skeleton.py +90 -0
- phylustrator-0.1.0/src/phylustrator/trees/tree.py +90 -0
- phylustrator-0.1.0/src/phylustrator/zombi.py +145 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/PKG-INFO +132 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/SOURCES.txt +49 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/dependency_links.txt +1 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/entry_points.txt +2 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/requires.txt +13 -0
- phylustrator-0.1.0/src/phylustrator.egg-info/top_level.txt +1 -0
- phylustrator-0.1.0/tests/test_cli.py +27 -0
- phylustrator-0.1.0/tests/test_figure.py +60 -0
- phylustrator-0.1.0/tests/test_genomes.py +88 -0
- phylustrator-0.1.0/tests/test_io.py +66 -0
- phylustrator-0.1.0/tests/test_layout.py +80 -0
- 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
|
+

|
|
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
|
+

|
|
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,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,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
|
+
]
|