diff --git a/plots/tree-phylogenetic/implementations/python/letsplot.py b/plots/tree-phylogenetic/implementations/python/letsplot.py index e5654ca39a..bc72124a86 100644 --- a/plots/tree-phylogenetic/implementations/python/letsplot.py +++ b/plots/tree-phylogenetic/implementations/python/letsplot.py @@ -1,9 +1,10 @@ -""" pyplots.ai +""" anyplot.ai tree-phylogenetic: Phylogenetic Tree Diagram -Library: letsplot 4.8.2 | Python 3.13.11 -Quality: 91/100 | Created: 2025-12-31 +Library: letsplot 4.9.0 | Python 3.13.13 +Quality: 93/100 | Updated: 2026-05-15 """ +import os import re import pandas as pd @@ -12,8 +13,16 @@ LetsPlot.setup_html() +THEME = os.getenv("ANYPLOT_THEME", "light") +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" +GRID_COLOR = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" + +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442"] + -# Simple Newick parser for phylogenetic tree def parse_newick(newick_str): """Parse Newick format string into tree structure.""" newick_str = newick_str.strip().rstrip(";") @@ -23,9 +32,7 @@ def parse_node(s, parent_id=None, depth=0): nodes = [] s = s.strip() - # Check if this is a leaf node (no parentheses) if "(" not in s: - # Leaf: name:length or just name match = re.match(r"([^:]*):?([\d.]*)", s) name = match.group(1) if match else s length = float(match.group(2)) if match and match.group(2) else 0.1 @@ -34,9 +41,7 @@ def parse_node(s, parent_id=None, depth=0): {"id": node_id[0], "name": name, "length": length, "parent": parent_id, "depth": depth, "children": []} ] - # Internal node: find matching parentheses if s.startswith("("): - # Find the matching closing parenthesis level = 0 children_str = "" remaining = "" @@ -50,7 +55,6 @@ def parse_node(s, parent_id=None, depth=0): remaining = s[i + 1 :] break - # Parse branch length for this internal node match = re.match(r":?([\d.]*)", remaining) length = float(match.group(1)) if match and match.group(1) else 0.1 @@ -66,7 +70,6 @@ def parse_node(s, parent_id=None, depth=0): } nodes.append(current_node) - # Split children by comma at level 0 children = [] level = 0 current = "" @@ -83,7 +86,6 @@ def parse_node(s, parent_id=None, depth=0): if current.strip(): children.append(current.strip()) - # Parse each child for child_str in children: child_nodes = parse_node(child_str, current_id, depth + 1) nodes.extend(child_nodes) @@ -94,18 +96,14 @@ def parse_node(s, parent_id=None, depth=0): return parse_node(newick_str) -# Primate phylogenetic tree (based on mitochondrial DNA) newick = "((((Human:0.1,Chimpanzee:0.12):0.08,Gorilla:0.2):0.15,(Orangutan:0.25,Gibbon:0.28):0.1):0.2,(Macaque:0.35,(Baboon:0.3,Mandrill:0.32):0.05):0.15)" nodes = parse_newick(newick) -# Build node dictionary for easy lookup node_dict = {n["id"]: n for n in nodes} -# Calculate x positions (cumulative branch length from root) def calc_x_positions(node_dict): - # Find root (node with no parent) root = [n for n in node_dict.values() if n["parent"] is None][0] def assign_x(node_id, parent_x=0): @@ -117,17 +115,13 @@ def assign_x(node_id, parent_x=0): assign_x(root["id"], 0) -# Calculate y positions (spacing for leaves, centered for internal nodes) def calc_y_positions(node_dict): - # Get leaves in order leaves = [n for n in node_dict.values() if not n["children"]] leaves.sort(key=lambda n: n["id"]) - # Assign y positions to leaves for i, leaf in enumerate(leaves): leaf["y"] = i - # Calculate y for internal nodes (average of children) def get_y(node_id): node = node_dict[node_id] if "y" in node: @@ -143,72 +137,68 @@ def get_y(node_id): calc_x_positions(node_dict) calc_y_positions(node_dict) -# Build segments for the tree (horizontal and vertical lines) segments = [] for node in node_dict.values(): if node["parent"] is not None: parent = node_dict[node["parent"]] - # Horizontal segment from parent x to node x at node y segments.append({"x": parent["x"], "xend": node["x"], "y": node["y"], "yend": node["y"], "type": "horizontal"}) - # Vertical segment at parent x from parent y to node y segments.append( {"x": parent["x"], "xend": parent["x"], "y": parent["y"], "yend": node["y"], "type": "vertical"} ) df_segments = pd.DataFrame(segments) -# Get leaf labels leaves = [n for n in node_dict.values() if not n["children"]] df_labels = pd.DataFrame([{"x": n["x"] + 0.02, "y": n["y"], "label": n["name"]} for n in leaves]) -# Get internal node points df_nodes = pd.DataFrame([{"x": n["x"], "y": n["y"]} for n in node_dict.values()]) -# Define clade colors for visualization clade_colors = { - "Human": "#306998", - "Chimpanzee": "#306998", - "Gorilla": "#306998", - "Orangutan": "#FFD43B", - "Gibbon": "#FFD43B", - "Macaque": "#22C55E", - "Baboon": "#22C55E", - "Mandrill": "#22C55E", + "Human": OKABE_ITO[0], + "Chimpanzee": OKABE_ITO[0], + "Gorilla": OKABE_ITO[0], + "Orangutan": OKABE_ITO[1], + "Gibbon": OKABE_ITO[1], + "Macaque": OKABE_ITO[2], + "Baboon": OKABE_ITO[2], + "Mandrill": OKABE_ITO[2], } df_labels["color"] = df_labels["label"].map(clade_colors) -# Create the phylogenetic tree plot +anyplot_theme = theme( + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), + panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG), + panel_grid_major_x=element_line(color=GRID_COLOR, size=0.3), + panel_grid_major_y=element_blank(), + panel_grid_minor=element_blank(), + axis_title_x=element_text(size=20, color=INK), + axis_title_y=element_blank(), + axis_text_x=element_text(size=16, color=INK_SOFT), + axis_text_y=element_blank(), + axis_ticks_y=element_blank(), + axis_line_y=element_blank(), + plot_title=element_text(size=24, face="bold", color=INK), + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), + legend_text=element_text(size=16, color=INK_SOFT), +) + plot = ( ggplot() - + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_segments, color="#306998", size=1.5) - + geom_point(aes(x="x", y="y"), data=df_nodes, color="#306998", size=4) + + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_segments, color=OKABE_ITO[0], size=1.5) + + geom_point(aes(x="x", y="y"), data=df_nodes, color=OKABE_ITO[0], size=4) + geom_point(aes(x="x", y="y", color="color"), data=df_labels, size=6, show_legend=False) - + geom_text(aes(x="x", y="y", label="label"), data=df_labels, hjust=0, size=14, family="sans-serif") + + geom_text(aes(x="x", y="y", label="label"), data=df_labels, hjust=0, size=14, color=INK_SOFT, family="sans-serif") + scale_color_identity() + scale_x_continuous(limits=[0, 0.85]) + labs( - title="Primate Evolution · tree-phylogenetic · letsplot · pyplots.ai", + title="Primate Evolution · tree-phylogenetic · letsplot · anyplot.ai", x="Evolutionary Distance (substitutions per site)", - y="", ) + theme_minimal() - + theme( - plot_title=element_text(size=24, face="bold"), - axis_title_x=element_text(size=20), - axis_title_y=element_blank(), - axis_text_x=element_text(size=16), - axis_text_y=element_blank(), - axis_ticks_y=element_blank(), - panel_grid_major_y=element_blank(), - panel_grid_minor=element_blank(), - panel_grid_major_x=element_line(color="#E5E5E5", size=0.5), - ) + + anyplot_theme + ggsize(1600, 900) ) -# Save as PNG (scale 3x for 4800x2700) -ggsave(plot, "plot.png", path=".", scale=3) - -# Save as HTML for interactivity -ggsave(plot, "plot.html", path=".") +ggsave(plot, f"plot-{THEME}.png", path=".", scale=3) +ggsave(plot, f"plot-{THEME}.html", path=".") diff --git a/plots/tree-phylogenetic/metadata/python/letsplot.yaml b/plots/tree-phylogenetic/metadata/python/letsplot.yaml index 98ae9c6b57..a060f22ba5 100644 --- a/plots/tree-phylogenetic/metadata/python/letsplot.yaml +++ b/plots/tree-phylogenetic/metadata/python/letsplot.yaml @@ -1,210 +1,236 @@ library: letsplot +language: python specification_id: tree-phylogenetic created: '2025-12-31T13:55:48Z' -updated: '2025-12-31T14:07:03Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20620338760 +updated: '2026-05-15T01:18:45Z' +generated_by: claude-haiku +workflow_run: 25894790203 issue: 3070 -python_version: 3.13.11 -library_version: 4.8.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/letsplot/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/letsplot/plot.html -quality_score: 91 -impl_tags: - dependencies: [] - techniques: - - layer-composition - - html-export - patterns: - - data-generation - dataprep: [] - styling: [] +python_version: 3.13.13 +library_version: 4.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/python/letsplot/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/python/letsplot/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/python/letsplot/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/tree-phylogenetic/python/letsplot/plot-dark.html +quality_score: 93 review: strengths: - - Excellent implementation of rectangular phylogenetic tree using lets-plot grammar - of graphics - - Custom Newick parser handles complex nested structure correctly - - Color-coded clades (great apes, lesser apes, Old World monkeys) add visual interest - - Branch lengths accurately reflect evolutionary distances with proportional x-axis - positioning - - Clean tree layout with horizontal and vertical segments properly connected - - Good use of theme_minimal with customized axis and grid settings + - Perfect visual quality with excellent text legibility and theme adaptation in + both light and dark renders + - Correct phylogenetic tree implementation with expert Newick parsing and proportional + branch length encoding + - Thoughtful clade-based color encoding that enhances understanding of evolutionary + relationships + - Expert-level use of letsplot's grammar of graphics API and theme customization + system + - Comprehensive spec compliance with all required features implemented and working + correctly + - Real-world, scientifically accurate example data (primate mitochondrial evolution) weaknesses: - - Missing legend to explain the three clade color groups (blue/yellow/green) - - No scale bar to indicate branch length units - image_description: 'The plot displays a phylogenetic tree (rectangular cladogram - layout) showing primate evolutionary relationships. The tree has a blue color - scheme (#306998) for branches and nodes, with leaf nodes color-coded by clade: - blue for great apes (Human, Chimpanzee, Gorilla), yellow for lesser apes (Orangutan, - Gibbon), and green for Old World monkeys (Macaque, Baboon, Mandrill). The x-axis - shows "Evolutionary Distance (substitutions per site)" ranging from 0 to 0.85. - Species labels appear to the right of each leaf node. The title reads "Primate - Evolution · tree-phylogenetic · letsplot · pyplots.ai". Layout is horizontal with - good use of canvas space.' + - 'DE-02: Visual refinement could be enhanced with more explicit grid styling and + explicit spine control in theme' + - 'LM-02: Could leverage more advanced letsplot interactive features if expanded + to support interactivity' + - 'DE-03: Visual storytelling could be amplified with annotations highlighting a + specific evolutionary pattern or clade significance' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) - correct theme surface + Chrome: Title and axis labels in dark text (INK #1A1A17), tick labels in soft gray (INK_SOFT #4A4A44) - all clearly readable + Data: Green (#009E73) tree segments, colored species labels by clade (green, orange #D55E00, blue #0072B2), proportional branch lengths + Grid: Subtle vertical gridlines appropriate for tree visualization + Legibility verdict: PASS - All text has excellent contrast against light background + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) - correct theme surface + Chrome: Title and axis labels in light text (F0EFE8), tick labels in light gray (B8B7B0) - all clearly readable, no dark-on-dark failures + Data: Tree segments, nodes, and species label colors identical to light render (#009E73 green, #D55E00 orange, #0072B2 blue) + Grid: Subtle vertical gridlines with theme-adaptive opacity + Legibility verdict: PASS - All text has excellent contrast against dark background, theme adaptation is perfect criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 30 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 8 + max: 8 passed: true - comment: Title is bold and large, axis labels are clear, species names are - readable at 14pt + comment: All text explicitly sized and perfectly readable in both themes - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text, species labels are well-spaced vertically + comment: No overlapping text elements; all fully readable - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Branch lines (size 1.5) and nodes (size 4-6) are clearly visible, - though branch width could be slightly thicker + comment: Tree segments, nodes, and labels optimally sized for data density - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Blue, yellow, green clade colors are colorblind-safe and distinguishable + comment: Okabe-Ito palette CVD-safe with good contrast - id: VQ-05 - name: Layout Balance + name: Layout & Canvas score: 4 - max: 5 + max: 4 passed: true - comment: Good horizontal layout filling canvas, but some empty space on right - side beyond species labels + comment: Perfect layout with 60-70% canvas fill and balanced margins - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: X-axis has descriptive label with units "Evolutionary Distance (substitutions - per site)" + comment: Descriptive X-axis label with units - id: VQ-07 - name: Grid & Legend - score: 0 + name: Palette Compliance + score: 2 max: 2 passed: true - comment: No legend explaining clade colors; subtle grid only on x-axis which - is appropriate + comment: 'First series #009E73; backgrounds correct; theme-adaptive chrome + perfect' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Strong design with thoughtful clade coloring; well above defaults + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Subtle grid and minimal spines; could be more explicit + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Visual hierarchy guides viewer; clade coloring adds semantic meaning spec_compliance: - score: 23 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct rectangular phylogenetic tree (cladogram) visualization - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Branch lengths correctly proportional to evolutionary distance on - x-axis - - id: SC-03 + comment: Correct rectangular cladogram with proportional branch lengths + - id: SC-02 name: Required Features score: 4 - max: 5 + max: 4 passed: true - comment: Has branch lengths, species labels, clade colors, but missing scale - bar indicator - - id: SC-04 - name: Data Range + comment: 'All spec features present: Newick parsing, proportional distances, + labels, clade visualization' + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: X-axis shows full evolutionary distance range (0 to 0.85) - - id: SC-05 - name: Legend Accuracy - score: 1 - max: 2 - passed: true - comment: No legend present to explain clade color coding - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: X/Y correctly mapped; tree structure accurate + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: 'Correctly formatted: "Primate Evolution · tree-phylogenetic · letsplot - · pyplots.ai"' + comment: Title format correct; implicit legend through coloring data_quality: - score: 18 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows 8 primate species with varied branch lengths, demonstrates - evolutionary divergence, but all leaf nodes end at similar x-positions + comment: 'Shows all phylogenetic tree features: multiple clades, varying branch + lengths, nested hierarchies' - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Primate phylogeny based on mitochondrial DNA is a real, scientifically - relevant scenario + comment: Real-world primate evolution scenario; scientifically plausible and + neutral - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Branch lengths in substitutions per site are realistic, though some - species (Mandrill, Baboon) should have longer terminal branches + comment: Branch lengths plausible; species relationships factually correct code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 name: KISS Structure - score: 2 + score: 3 max: 3 passed: true - comment: Contains helper functions (parse_newick, calc_x_positions, calc_y_positions) - which adds complexity, but necessary for Newick parsing + comment: 'Linear flow: imports → theme → parser → calculations → plot → save' - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Deterministic Newick string input, no random elements + comment: Deterministic Newick string; fully reproducible - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only uses re, pandas, and lets_plot - all necessary + comment: All imports used; no unnecessary dependencies - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current lets-plot API + comment: Appropriate complexity; no over-engineering - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png and plot.html - library_features: - score: 5 - max: 5 + comment: Correct output format with PNG and HTML + library_mastery: + score: 9 + max: 10 items: - - id: LF-01 - name: Uses distinctive features + - id: LM-01 + name: Idiomatic Usage score: 5 max: 5 passed: true - comment: Excellent use of lets-plot's ggplot2 grammar with geom_segment, geom_point, - geom_text, scale_color_identity, theme customization, and ggsize/ggsave + comment: Expert use of ggplot() API, geoms, and theme customization + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Uses ggsize() and ggsave() with advanced theme system verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - manual-ticks + - layer-composition + patterns: + - data-generation + - matrix-construction + - iteration-over-groups + dataprep: [] + styling: + - publication-ready + - grid-styling