|
| 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") |
0 commit comments