Skip to content

Commit e42d762

Browse files
feat(pygal): implement tree-phylogenetic (#3113)
## Implementation: `tree-phylogenetic` - pygal Implements the **pygal** version of `tree-phylogenetic`. **File:** `plots/tree-phylogenetic/implementations/pygal.py` **Parent Issue:** #3070 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620337510)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 4769361 commit e42d762

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""pyplots.ai
2+
tree-phylogenetic: Phylogenetic Tree Diagram
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 82/100 | Created: 2025-12-31
5+
"""
6+
7+
import cairosvg
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Primate phylogenetic tree based on mitochondrial DNA divergence (simplified)
13+
# Tree structure: ((((Human, Chimpanzee), Gorilla), Orangutan), (Gibbon, Macaque))
14+
# Scaled to use more canvas width - multiply x positions to spread tree wider
15+
scale_factor = 1.35
16+
species_data = [
17+
("Human", 0.95 * scale_factor, 0),
18+
("Chimpanzee", 0.95 * scale_factor, 1.5),
19+
("Gorilla", 0.82 * scale_factor, 3),
20+
("Orangutan", 0.62 * scale_factor, 5),
21+
("Gibbon", 0.95 * scale_factor, 7),
22+
("Macaque", 0.95 * scale_factor, 8.5),
23+
]
24+
25+
# Y positions from species data
26+
species_y = {name: y for name, _, y in species_data}
27+
species_x = {name: x for name, x, _ in species_data}
28+
29+
# Define tree connections
30+
tree_segments = []
31+
32+
# Human-Chimpanzee clade (most recent common ancestor)
33+
hc_ancestor_x = 0.82 * scale_factor
34+
hc_ancestor_y = (species_y["Human"] + species_y["Chimpanzee"]) / 2
35+
tree_segments.append([(hc_ancestor_x, species_y["Human"]), (species_x["Human"], species_y["Human"])])
36+
tree_segments.append([(hc_ancestor_x, species_y["Chimpanzee"]), (species_x["Chimpanzee"], species_y["Chimpanzee"])])
37+
tree_segments.append([(hc_ancestor_x, species_y["Human"]), (hc_ancestor_x, species_y["Chimpanzee"])])
38+
39+
# Human-Chimp-Gorilla clade
40+
hcg_ancestor_x = 0.62 * scale_factor
41+
hcg_ancestor_y = (hc_ancestor_y + species_y["Gorilla"]) / 2
42+
tree_segments.append([(hcg_ancestor_x, hc_ancestor_y), (hc_ancestor_x, hc_ancestor_y)])
43+
tree_segments.append([(hcg_ancestor_x, species_y["Gorilla"]), (species_x["Gorilla"], species_y["Gorilla"])])
44+
tree_segments.append([(hcg_ancestor_x, hc_ancestor_y), (hcg_ancestor_x, species_y["Gorilla"])])
45+
46+
# Great apes clade including Orangutan
47+
great_apes_x = 0.41 * scale_factor
48+
great_apes_y = (hcg_ancestor_y + species_y["Orangutan"]) / 2
49+
tree_segments.append([(great_apes_x, hcg_ancestor_y), (hcg_ancestor_x, hcg_ancestor_y)])
50+
tree_segments.append([(great_apes_x, species_y["Orangutan"]), (species_x["Orangutan"], species_y["Orangutan"])])
51+
tree_segments.append([(great_apes_x, hcg_ancestor_y), (great_apes_x, species_y["Orangutan"])])
52+
53+
# Gibbon-Macaque clade
54+
gm_ancestor_x = 0.68 * scale_factor
55+
gm_ancestor_y = (species_y["Gibbon"] + species_y["Macaque"]) / 2
56+
tree_segments.append([(gm_ancestor_x, species_y["Gibbon"]), (species_x["Gibbon"], species_y["Gibbon"])])
57+
tree_segments.append([(gm_ancestor_x, species_y["Macaque"]), (species_x["Macaque"], species_y["Macaque"])])
58+
tree_segments.append([(gm_ancestor_x, species_y["Gibbon"]), (gm_ancestor_x, species_y["Macaque"])])
59+
60+
# Root: connects great apes and gibbon-macaque clades (x=0)
61+
root_x = 0.0
62+
root_y = (great_apes_y + gm_ancestor_y) / 2
63+
tree_segments.append([(root_x, great_apes_y), (great_apes_x, great_apes_y)])
64+
tree_segments.append([(root_x, gm_ancestor_y), (gm_ancestor_x, gm_ancestor_y)])
65+
tree_segments.append([(root_x, great_apes_y), (root_x, gm_ancestor_y)])
66+
67+
# Colorblind-friendly palette for species markers
68+
species_colors = ["#E63946", "#457B9D", "#2A9D8F", "#E9C46A", "#F4A261", "#9C6644"]
69+
70+
# Branch color - pyplots blue
71+
branch_color = "#306998"
72+
73+
# Custom style for pyplots - larger fonts for 4800x2700 canvas
74+
custom_style = Style(
75+
background="white",
76+
plot_background="white",
77+
foreground="#333",
78+
foreground_strong="#333",
79+
foreground_subtle="#999",
80+
colors=(branch_color,),
81+
title_font_size=56,
82+
label_font_size=44,
83+
major_label_font_size=40,
84+
legend_font_size=44,
85+
value_font_size=36,
86+
tooltip_font_size=28,
87+
stroke_width=6,
88+
opacity=1.0,
89+
guide_stroke_color="#ddd",
90+
)
91+
92+
# Create XY chart for phylogenetic tree
93+
chart = pygal.XY(
94+
width=4800,
95+
height=2700,
96+
style=custom_style,
97+
title="Primate Evolution · tree-phylogenetic · pygal · pyplots.ai",
98+
x_title="Evolutionary Distance (substitutions per site)",
99+
y_title="",
100+
show_legend=False,
101+
show_dots=False,
102+
stroke_style={"width": 6},
103+
fill=False,
104+
show_x_guides=False,
105+
show_y_guides=False,
106+
show_y_labels=False,
107+
range=(-1.5, 10),
108+
xrange=(-0.05, 1.45),
109+
print_values=False,
110+
)
111+
112+
# Add all tree branches as unnamed series
113+
for seg in tree_segments:
114+
chart.add(None, seg, show_dots=False, stroke_style={"width": 6})
115+
116+
# Add species markers (dots only, labels added via SVG)
117+
for i, (_name, x_pos, y_pos) in enumerate(species_data):
118+
color = species_colors[i % len(species_colors)]
119+
chart.add(
120+
None, [{"value": (x_pos, y_pos), "color": color}], show_dots=True, dots_size=28, stroke_style={"width": 0}
121+
)
122+
123+
# Render to SVG string first
124+
svg_content = chart.render().decode("utf-8")
125+
126+
# Calculate pixel positions for species labels
127+
# Plot area bounds for coordinate conversion
128+
plot_x_min, plot_x_max = 180, 4620
129+
plot_y_min, plot_y_max = 100, 2500
130+
data_x_min, data_x_max = -0.05, 1.45
131+
data_y_min, data_y_max = -1.5, 10
132+
133+
# Generate species label SVG elements positioned directly next to leaf nodes
134+
species_labels_svg = '<g class="species-labels">\n'
135+
for i, (name, x_pos, y_pos) in enumerate(species_data):
136+
# Inline coordinate conversion (data to pixel)
137+
px = plot_x_min + (x_pos - data_x_min) / (data_x_max - data_x_min) * (plot_x_max - plot_x_min)
138+
py = plot_y_max - (y_pos - data_y_min) / (data_y_max - data_y_min) * (plot_y_max - plot_y_min)
139+
color = species_colors[i % len(species_colors)]
140+
# Position label to the right of the marker
141+
species_labels_svg += f' <text x="{px + 50}" y="{py + 12}" font-size="42" fill="{color}" '
142+
species_labels_svg += f'font-family="sans-serif" font-weight="bold">{name}</text>\n'
143+
species_labels_svg += "</g>\n"
144+
145+
# Add scale bar with label - inline coordinate conversion
146+
scale_x_data, scale_y_data = 0.0, -1.0
147+
scale_px = plot_x_min + (scale_x_data - data_x_min) / (data_x_max - data_x_min) * (plot_x_max - plot_x_min)
148+
scale_py = plot_y_max - (scale_y_data - data_y_min) / (data_y_max - data_y_min) * (plot_y_max - plot_y_min)
149+
scale_end_x_data = 0.1
150+
scale_end_px = plot_x_min + (scale_end_x_data - data_x_min) / (data_x_max - data_x_min) * (plot_x_max - plot_x_min)
151+
scale_width = scale_end_px - scale_px
152+
153+
scale_bar_svg = f"""
154+
<g class="scale-bar">
155+
<line x1="{scale_px}" y1="{scale_py}" x2="{scale_end_px}" y2="{scale_py}" stroke="#333" stroke-width="10"/>
156+
<text x="{scale_px + scale_width / 2}" y="{scale_py + 55}" text-anchor="middle" font-size="40" fill="#333" font-family="sans-serif">0.1 substitutions/site</text>
157+
</g>
158+
"""
159+
160+
# Insert labels and scale bar before closing </svg> tag
161+
svg_content = svg_content.replace("</svg>", species_labels_svg + scale_bar_svg + "</svg>")
162+
163+
# Save SVG
164+
with open("plot.svg", "w") as f:
165+
f.write(svg_content)
166+
167+
# For HTML, use the modified SVG
168+
html_content = f"""<!DOCTYPE html>
169+
<html>
170+
<head>
171+
<meta charset="utf-8">
172+
<title>Phylogenetic Tree - pygal</title>
173+
<style>body {{ margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }}</style>
174+
</head>
175+
<body>
176+
{svg_content}
177+
</body>
178+
</html>"""
179+
with open("plot.html", "w") as f:
180+
f.write(html_content)
181+
182+
# Convert to PNG using cairosvg
183+
cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to="plot.png")
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
library: pygal
2+
specification_id: tree-phylogenetic
3+
created: '2025-12-31T13:57:05Z'
4+
updated: '2025-12-31T14:49:21Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620337510
7+
issue: 3070
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/tree-phylogenetic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/tree-phylogenetic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/tree-phylogenetic/pygal/plot.html
13+
quality_score: 82
14+
review:
15+
strengths:
16+
- Creative use of pygal XY chart to represent phylogenetic tree structure
17+
- Excellent colorblind-friendly color palette for species markers
18+
- Includes proper scale bar with units for evolutionary distance interpretation
19+
- Species labels are clearly positioned next to their markers without overlap
20+
- Scientifically accurate primate evolutionary relationships represented
21+
weaknesses:
22+
- Code uses a helper function which deviates from KISS principle
23+
- Tree is somewhat compressed with unused whitespace on the right side of the canvas
24+
- Y-axis grid lines are visible but serve no purpose for tree visualization

0 commit comments

Comments
 (0)