From 550134f7f9d43d8f5525873447ad63e85c79962a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:26:20 +0000 Subject: [PATCH 1/8] feat(altair): implement root-locus-basic --- .../implementations/altair.py | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 plots/root-locus-basic/implementations/altair.py diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py new file mode 100644 index 0000000000..37253a7f1f --- /dev/null +++ b/plots/root-locus-basic/implementations/altair.py @@ -0,0 +1,300 @@ +"""pyplots.ai +root-locus-basic: Root Locus Plot for Control Systems +Library: altair | Python 3.13 +Quality: pending | Created: 2026-03-20 +""" + +import altair as alt +import numpy as np +import pandas as pd + + +# Data - Root locus for G(s) = 1 / (s(s+1)(s+2)) +# Open-loop poles at s = 0, -1, -2; no zeros +# Characteristic equation: s³ + 3s² + 2s + K = 0 +den_coeffs = [1.0, 3.0, 2.0, 0.0] + +gains = np.concatenate( + [ + np.linspace(0.001, 0.3, 100), + np.linspace(0.3, 0.5, 100), + np.linspace(0.5, 2, 200), + np.linspace(2, 6, 150), + np.linspace(6, 20, 150), + np.linspace(20, 80, 100), + ] +) +n_roots = 3 +all_roots = np.zeros((len(gains), n_roots), dtype=complex) + +for i, k in enumerate(gains): + poly = np.array(den_coeffs, dtype=float) + poly[-1] += k + all_roots[i] = np.roots(poly) + +# Sort roots into continuous branches via nearest-neighbor matching +all_roots[0] = np.sort(all_roots[0].real) +for i in range(1, len(gains)): + prev = all_roots[i - 1] + curr = all_roots[i] + used = [False] * n_roots + order = np.zeros(n_roots, dtype=int) + for j in range(n_roots): + best_m, best_d = -1, np.inf + for m in range(n_roots): + if not used[m]: + d = abs(prev[j] - curr[m]) + if d < best_d: + best_d = d + best_m = m + used[best_m] = True + order[j] = best_m + all_roots[i] = curr[order] + +# Build branch dataframe +rows = [] +for b in range(n_roots): + for i in range(len(gains)): + rows.append( + { + "real": float(all_roots[i, b].real), + "imaginary": float(all_roots[i, b].imag), + "gain": float(gains[i]), + "branch": f"Branch {b + 1}", + "idx": i, + } + ) + +locus_df = pd.DataFrame(rows) + +# Open-loop poles +poles_df = pd.DataFrame( + {"real": [0.0, -1.0, -2.0], "imaginary": [0.0, 0.0, 0.0], "label": ["Pole (s=0)", "Pole (s=−1)", "Pole (s=−2)"]} +) + +# Imaginary axis crossing: ω = √2, K = 6 +omega_cross = np.sqrt(2) +crossing_df = pd.DataFrame( + {"real": [0.0, 0.0], "imaginary": [omega_cross, -omega_cross], "label": ["jω = j√2 (K=6)", "jω = −j√2 (K=6)"]} +) + +# Breakaway point: d/ds[s(s+1)(s+2)] = 3s²+6s+2 = 0 → s ≈ -0.423 +breakaway_df = pd.DataFrame({"bx": [(-6 + np.sqrt(12)) / 6], "by": [0.0], "label": ["Breakaway"]}) + +# Damping ratio lines (ζ = 0.2, 0.4, 0.6, 0.8) +damping_rows = [] +for zeta in [0.2, 0.4, 0.6, 0.8]: + angle = np.pi - np.arccos(zeta) + r_max = 7.0 + for side, sign in [("upper", 1), ("lower", -1)]: + seg_name = f"ζ={zeta}_{side}" + damping_rows.append({"dx": 0.0, "dy": 0.0, "seg": seg_name, "ord": 0}) + damping_rows.append( + {"dx": r_max * np.cos(angle), "dy": sign * r_max * np.sin(angle), "seg": seg_name, "ord": 1} + ) + +damping_df = pd.DataFrame(damping_rows) + +# Natural frequency arcs (ωn = 1, 2, 3, 4) in left half-plane +wn_rows = [] +for wn in [1.0, 2.0, 3.0, 4.0]: + theta = np.linspace(np.pi / 2, 3 * np.pi / 2, 60) + for j, t in enumerate(theta): + wn_rows.append({"wx": wn * np.cos(t), "wy": wn * np.sin(t), "wn": f"ωn={wn}", "ord": j}) + +wn_df = pd.DataFrame(wn_rows) + +# Real axis segments: (-1, 0) and (-∞, -2) +real_axis_rows = [] +for rx0, rx1, seg in [(-1.0, 0.0, "seg1"), (-7.0, -2.0, "seg2")]: + real_axis_rows.append({"rx": rx0, "ry": 0.0, "seg": seg, "ord": 0}) + real_axis_rows.append({"rx": rx1, "ry": 0.0, "seg": seg, "ord": 1}) + +real_axis_df = pd.DataFrame(real_axis_rows) + +# Arrow direction indicators along complex branches +arrows = [] +for b in range(n_roots): + for idx in [400, 600]: + if idx + 5 < len(gains): + r0 = all_roots[idx, b] + if abs(r0.imag) > 0.5: + arrows.append( + { + "ax": float(r0.real), + "ay": float(r0.imag), + "branch": f"Branch {b + 1}", + "shape": "triangle-up" if r0.imag > 0 else "triangle-down", + } + ) + +arrow_df = pd.DataFrame(arrows) if arrows else pd.DataFrame({"ax": [], "ay": [], "branch": [], "shape": []}) + +# Scales +x_scale = alt.Scale(domain=[-7.0, 2.5], nice=False) +y_scale = alt.Scale(domain=[-6.0, 6.0], nice=False) + +# Layer: Damping ratio lines +damping_layer = ( + alt.Chart(damping_df) + .mark_line(strokeWidth=1, strokeDash=[6, 4], color="#d0d0d0") + .encode( + x=alt.X("dx:Q", scale=x_scale, axis=None), + y=alt.Y("dy:Q", scale=y_scale, axis=None), + detail="seg:N", + order="ord:Q", + ) +) + +# Layer: Natural frequency arcs +wn_layer = ( + alt.Chart(wn_df) + .mark_line(strokeWidth=1, strokeDash=[4, 4], color="#d0d0d0") + .encode(x=alt.X("wx:Q", scale=x_scale), y=alt.Y("wy:Q", scale=y_scale), detail="wn:N", order="ord:Q") +) + +# Layer: Real axis segments +real_axis_layer = ( + alt.Chart(real_axis_df) + .mark_line(strokeWidth=4, color="#306998", opacity=0.3) + .encode(x=alt.X("rx:Q", scale=x_scale), y=alt.Y("ry:Q", scale=y_scale), detail="seg:N", order="ord:Q") +) + +# Layer: Locus branches +branch_palette = ["#306998", "#e07b39", "#2ca02c"] +locus_layer = ( + alt.Chart(locus_df) + .mark_line(strokeWidth=2.5, opacity=0.9) + .encode( + x=alt.X( + "real:Q", scale=x_scale, title="Real Axis", axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=False) + ), + y=alt.Y( + "imaginary:Q", + scale=y_scale, + title="Imaginary Axis", + axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=False), + ), + color=alt.Color( + "branch:N", + scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), + legend=alt.Legend( + title="Branch", + titleFontSize=18, + labelFontSize=16, + symbolSize=200, + symbolStrokeWidth=3, + orient="top-right", + offset=10, + ), + ), + order="idx:Q", + tooltip=[ + alt.Tooltip("branch:N", title="Branch"), + alt.Tooltip("real:Q", title="Real", format=".3f"), + alt.Tooltip("imaginary:Q", title="Imaginary", format=".3f"), + alt.Tooltip("gain:Q", title="Gain K", format=".2f"), + ], + ) +) + +# Layer: Open-loop poles (× markers) +poles_layer = ( + alt.Chart(poles_df) + .mark_point(shape="cross", size=400, strokeWidth=3.5, color="#d62728", filled=False) + .encode( + x=alt.X("real:Q", scale=x_scale), + y=alt.Y("imaginary:Q", scale=y_scale), + tooltip=[alt.Tooltip("label:N", title=""), alt.Tooltip("real:Q", title="Real")], + ) +) + +# Layer: Imaginary axis crossings +crossing_layer = ( + alt.Chart(crossing_df) + .mark_point(shape="diamond", size=350, strokeWidth=2.5, color="#d62728", filled=True) + .encode( + x=alt.X("real:Q", scale=x_scale), + y=alt.Y("imaginary:Q", scale=y_scale), + tooltip=[alt.Tooltip("label:N", title="Crossing")], + ) +) + +# Layer: Crossing labels +crossing_text = ( + alt.Chart(crossing_df) + .mark_text(fontSize=14, fontWeight="bold", color="#d62728", align="left", dx=16) + .encode(x=alt.X("real:Q", scale=x_scale), y=alt.Y("imaginary:Q", scale=y_scale), text="label:N") +) + +# Layer: Breakaway point +breakaway_layer = ( + alt.Chart(breakaway_df) + .mark_point(shape="square", size=200, color="#555555", filled=True, opacity=0.7) + .encode( + x=alt.X("bx:Q", scale=x_scale), y=alt.Y("by:Q", scale=y_scale), tooltip=[alt.Tooltip("label:N", title="Point")] + ) +) + +# Layer: Arrow direction indicators +arrow_up = arrow_df[arrow_df["ay"] > 0] if len(arrow_df) > 0 else arrow_df +arrow_down = arrow_df[arrow_df["ay"] <= 0] if len(arrow_df) > 0 else arrow_df + +arrow_up_layer = ( + alt.Chart(arrow_up) + .mark_point(shape="triangle-up", size=220, filled=True, opacity=0.85) + .encode( + x=alt.X("ax:Q", scale=x_scale), + y=alt.Y("ay:Q", scale=y_scale), + color=alt.Color( + "branch:N", scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), legend=None + ), + ) +) + +arrow_down_layer = ( + alt.Chart(arrow_down) + .mark_point(shape="triangle-down", size=220, filled=True, opacity=0.85) + .encode( + x=alt.X("ax:Q", scale=x_scale), + y=alt.Y("ay:Q", scale=y_scale), + color=alt.Color( + "branch:N", scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), legend=None + ), + ) +) + +# Compose +chart = ( + ( + damping_layer + + wn_layer + + real_axis_layer + + locus_layer + + poles_layer + + crossing_layer + + crossing_text + + breakaway_layer + + arrow_up_layer + + arrow_down_layer + ) + .properties( + width=1400, + height=1100, + title=alt.Title( + "root-locus-basic · altair · pyplots.ai", + fontSize=28, + color="#222222", + subtitle="G(s) = 1 / s(s+1)(s+2) — Closed-Loop Pole Trajectories vs Gain K", + subtitleFontSize=17, + subtitleColor="#777777", + subtitlePadding=6, + ), + ) + .configure_view(strokeWidth=0) + .interactive() +) + +# Save +chart.save("plot.png", scale_factor=3.0) +chart.save("plot.html") From ecfedce3eba71c051ec7d693f758ff435a9201ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:26:28 +0000 Subject: [PATCH 2/8] chore(altair): add metadata for root-locus-basic --- plots/root-locus-basic/metadata/altair.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 plots/root-locus-basic/metadata/altair.yaml diff --git a/plots/root-locus-basic/metadata/altair.yaml b/plots/root-locus-basic/metadata/altair.yaml new file mode 100644 index 0000000000..4500dc199c --- /dev/null +++ b/plots/root-locus-basic/metadata/altair.yaml @@ -0,0 +1,19 @@ +# Per-library metadata for altair implementation of root-locus-basic +# Auto-generated by impl-generate.yml + +library: altair +specification_id: root-locus-basic +created: '2026-03-20T21:26:28Z' +updated: '2026-03-20T21:26:28Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 23362981953 +issue: 4414 +python_version: 3.14.3 +library_version: 6.0.0 +preview_url: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.html +quality_score: null +review: + strengths: [] + weaknesses: [] From 4ec7c9b8807e2b672512618371f7ecda40b196a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:31:10 +0000 Subject: [PATCH 3/8] chore(altair): update quality score 81 and review feedback for root-locus-basic --- .../implementations/altair.py | 6 +- plots/root-locus-basic/metadata/altair.yaml | 233 +++++++++++++++++- 2 files changed, 229 insertions(+), 10 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index 37253a7f1f..f4af2d069a 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,7 +1,7 @@ -"""pyplots.ai +""" pyplots.ai root-locus-basic: Root Locus Plot for Control Systems -Library: altair | Python 3.13 -Quality: pending | Created: 2026-03-20 +Library: altair 6.0.0 | Python 3.14.3 +Quality: 81/100 | Created: 2026-03-20 """ import altair as alt diff --git a/plots/root-locus-basic/metadata/altair.yaml b/plots/root-locus-basic/metadata/altair.yaml index 4500dc199c..642a87db1b 100644 --- a/plots/root-locus-basic/metadata/altair.yaml +++ b/plots/root-locus-basic/metadata/altair.yaml @@ -1,10 +1,7 @@ -# Per-library metadata for altair implementation of root-locus-basic -# Auto-generated by impl-generate.yml - library: altair specification_id: root-locus-basic created: '2026-03-20T21:26:28Z' -updated: '2026-03-20T21:26:28Z' +updated: '2026-03-20T21:31:10Z' generated_by: claude-opus-4-5-20251101 workflow_run: 23362981953 issue: 4414 @@ -13,7 +10,229 @@ library_version: 6.0.0 preview_url: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.html -quality_score: null +quality_score: 81 review: - strengths: [] - weaknesses: [] + strengths: + - 'Comprehensive root locus features: all major spec elements implemented (poles, + branches, real-axis segments, crossings, breakaway, damping/frequency grid, direction + arrows)' + - Smart branch-sorting algorithm using nearest-neighbor matching ensures continuous + curves + - Rich interactive features with detailed tooltips showing branch, real/imaginary + parts, and gain K + - Clean, well-organized code with appropriate data computation + - Good data storytelling with labeled stability crossings and gain values + weaknesses: + - Axis scaling is not equal, distorting geometric relationships (circles appear + as ellipses, angles are incorrect) + - Damping ratio and natural frequency guide lines are nearly invisible (#d0d0d0 + on white) + - Layout has significant empty space on the left side; domain could be tightened + - Axis labels lack standard control systems notation + image_description: The plot shows a root locus diagram for G(s) = 1/[s(s+1)(s+2)] + with three color-coded branches. Branch 1 (blue) extends along the negative real + axis to the left. Branches 2 (orange) and 3 (green) curve into the upper and lower + half-planes respectively, forming complex conjugate trajectories. Red cross markers + indicate open-loop poles at s=0, s=-1, and s=-2 on the real axis. Red filled diamond + markers with labels identify the imaginary axis crossings at jw = +/-j*sqrt(2) + (K=6). A small gray square marks the breakaway point near s=-0.42. Very faint + dashed gray lines show constant damping ratio lines radiating from the origin + and natural frequency semicircular arcs. Semi-transparent thick blue segments + indicate the real-axis portions of the root locus. Small colored triangle arrows + on the complex branches indicate the direction of increasing gain. The title reads + "root-locus-basic - altair - pyplots.ai" with subtitle about the transfer function. + A legend in the top-right identifies the three branches. The domain spans [-7, + 2.5] x [-6, 6] with content concentrated in the center-right area. + criteria_checklist: + visual_quality: + score: 23 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: Font sizes explicitly set throughout (title 28, axis 18/22, legend + 16/18). Crossing labels at 14pt slightly small but legible. + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: Mostly clean. Some mild crowding near origin where poles, breakaway, + and branch convergence cluster. + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Branch lines and markers clearly visible. Damping/frequency guide + lines (#d0d0d0) nearly invisible on white. + - id: VQ-04 + name: Color Accessibility + score: 3 + max: 4 + passed: true + comment: Blue/orange/green generally distinguishable. Green branch could be + confused with blue by deuteranope viewers. + - id: VQ-05 + name: Layout & Canvas + score: 2 + max: 4 + passed: false + comment: Content concentrated in center-right. Domain extends far left with + mostly empty space and faint grid lines. + - id: VQ-06 + name: Axis Labels & Title + score: 1 + max: 2 + passed: false + comment: Descriptive labels but lack standard control notation (sigma, jomega). + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Custom palette, clear typography hierarchy, red accents. Above defaults + but not publication-ready. + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: View stroke removed, domain-appropriate guide lines instead of generic + grid. Guide lines too faint. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Labeled stability crossings with gain values, direction arrows, breakaway + point. Good narrative. + spec_compliance: + score: 14 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct root locus plot showing pole trajectories in complex plane. + - id: SC-02 + name: Required Features + score: 3 + max: 4 + passed: true + comment: Most features present. Missing equal axis scaling required by spec + for geometric preservation. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Real/imaginary correctly mapped to x/y axes. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format correct. Legend clearly identifies all three branches. + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows branches, breakaway, crossings, real-axis segments, direction + arrows. Could annotate asymptotes. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Classic textbook transfer function G(s)=1/[s(s+1)(s+2)]. Neutral, + educational. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Mathematically correct values verified analytically. + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Linear script: imports, data computation, layer construction, composition, + save.' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Fully deterministic polynomial root-finding. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All three imports (altair, numpy, pandas) are used. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Well-structured with smart nearest-neighbor branch sorting. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png with scale_factor=3.0 plus HTML export. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: 'Good declarative Altair patterns: layered composition, encoding + types, detail/order channels.' + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Leverages interactive zoom/pan, rich tooltips, HTML export, declarative + detail/order channels. + verdict: REJECTED +impl_tags: + dependencies: [] + techniques: + - layer-composition + - annotations + - hover-tooltips + - html-export + patterns: + - data-generation + - iteration-over-groups + - matrix-construction + dataprep: [] + styling: + - alpha-blending + - grid-styling From 160e9b4ec6d803c6adc5dfdac0060b0711df885c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:46:52 +0000 Subject: [PATCH 4/8] chore(altair): update quality score 78 and review feedback for root-locus-basic --- .../implementations/altair.py | 2 +- plots/root-locus-basic/metadata/altair.yaml | 155 +++++++++--------- 2 files changed, 81 insertions(+), 76 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index f4af2d069a..53667f1a99 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,7 +1,7 @@ """ pyplots.ai root-locus-basic: Root Locus Plot for Control Systems Library: altair 6.0.0 | Python 3.14.3 -Quality: 81/100 | Created: 2026-03-20 +Quality: 78/100 | Created: 2026-03-20 """ import altair as alt diff --git a/plots/root-locus-basic/metadata/altair.yaml b/plots/root-locus-basic/metadata/altair.yaml index 642a87db1b..5b1f07fff7 100644 --- a/plots/root-locus-basic/metadata/altair.yaml +++ b/plots/root-locus-basic/metadata/altair.yaml @@ -1,7 +1,7 @@ library: altair specification_id: root-locus-basic created: '2026-03-20T21:26:28Z' -updated: '2026-03-20T21:31:10Z' +updated: '2026-03-20T21:46:52Z' generated_by: claude-opus-4-5-20251101 workflow_run: 23362981953 issue: 4414 @@ -10,85 +10,89 @@ library_version: 6.0.0 preview_url: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.html -quality_score: 81 +quality_score: 78 review: strengths: - - 'Comprehensive root locus features: all major spec elements implemented (poles, - branches, real-axis segments, crossings, breakaway, damping/frequency grid, direction - arrows)' - - Smart branch-sorting algorithm using nearest-neighbor matching ensures continuous - curves - - Rich interactive features with detailed tooltips showing branch, real/imaginary - parts, and gain K - - Clean, well-organized code with appropriate data computation - - Good data storytelling with labeled stability crossings and gain values + - Comprehensive spec coverage with all major root locus features (poles, branches, + crossings, breakaway, direction arrows, guide lines) + - Good use of Altair interactive features — tooltips showing gain K values are genuinely + useful for root locus analysis + - Mathematically correct data generation with proper branch sorting algorithm + - Domain-specific guide lines (damping ratios, natural frequencies) replace generic + grid effectively + - Clean visual hierarchy with red for critical points, distinct colors for branches weaknesses: - - Axis scaling is not equal, distorting geometric relationships (circles appear - as ellipses, angles are incorrect) - - Damping ratio and natural frequency guide lines are nearly invisible (#d0d0d0 - on white) - - Layout has significant empty space on the left side; domain could be tightened - - Axis labels lack standard control systems notation - image_description: The plot shows a root locus diagram for G(s) = 1/[s(s+1)(s+2)] - with three color-coded branches. Branch 1 (blue) extends along the negative real - axis to the left. Branches 2 (orange) and 3 (green) curve into the upper and lower - half-planes respectively, forming complex conjugate trajectories. Red cross markers - indicate open-loop poles at s=0, s=-1, and s=-2 on the real axis. Red filled diamond - markers with labels identify the imaginary axis crossings at jw = +/-j*sqrt(2) - (K=6). A small gray square marks the breakaway point near s=-0.42. Very faint - dashed gray lines show constant damping ratio lines radiating from the origin - and natural frequency semicircular arcs. Semi-transparent thick blue segments - indicate the real-axis portions of the root locus. Small colored triangle arrows - on the complex branches indicate the direction of increasing gain. The title reads - "root-locus-basic - altair - pyplots.ai" with subtitle about the transfer function. - A legend in the top-right identifies the three branches. The domain spans [-7, - 2.5] x [-6, 6] with content concentrated in the center-right area. + - Axis tick labels and titles are barely visible in the rendered output despite + explicit font sizes + - Canvas size (4200x3300) does not match either target format (4800x2700 or 3600x3600) + - Layout has significant wasted space with data concentrated center-right; green + branch clips at bottom edge + - Axis scaling is not equal as spec requires — x spans 9.5 units vs y spans 12 units + - Code is verbose with many separate DataFrames; branch sorting algorithm could + be more concise + image_description: 'The plot displays a root locus diagram for G(s) = 1/[s(s+1)(s+2)] + with the title "root-locus-basic · altair · pyplots.ai" and a subtitle describing + the transfer function. Three branches are shown in distinct colors: Branch 1 (blue, + #306998) extends along the negative real axis to the left, Branch 2 (orange, #e07b39) + curves upward into the upper half-plane, and Branch 3 (green, #2ca02c) curves + downward into the lower half-plane. Open-loop poles are marked with red cross + markers at s=0, s=-1, and s=-2 on the real axis. Imaginary axis crossings are + marked with red filled diamonds labeled "jω = j√2 (K=6)" and "jω = -j√2 (K=6)". + A gray filled square marks the breakaway point near s≈-0.423. Triangle direction + arrows appear along the complex branches. Light dashed gray lines show damping + ratio guidelines radiating from the origin and natural frequency arcs as semicircles + in the left half-plane. Semi-transparent blue segments highlight the real-axis + portions of the root locus between (-1, 0) and (-∞, -2). The legend appears in + the top-right corner. Axis tick labels and axis titles ("Real Axis", "Imaginary + Axis") are present but appear faint in the rendered output. The green branch extends + to the very bottom edge of the canvas.' criteria_checklist: visual_quality: - score: 23 + score: 22 max: 30 items: - id: VQ-01 name: Text Legibility - score: 7 + score: 6 max: 8 passed: true - comment: Font sizes explicitly set throughout (title 28, axis 18/22, legend - 16/18). Crossing labels at 14pt slightly small but legible. + comment: Font sizes explicitly set (title 28, axis labels 18, titles 22, legend + 16-18) but axis ticks and titles appear faint in rendered output - id: VQ-02 name: No Overlap score: 5 max: 6 passed: true - comment: Mostly clean. Some mild crowding near origin where poles, breakaway, - and branch convergence cluster. + comment: Minor overlap between breakaway marker and pole cross near s=0; crossing + labels near branch lines - id: VQ-03 name: Element Visibility score: 5 max: 6 passed: true - comment: Branch lines and markers clearly visible. Damping/frequency guide - lines (#d0d0d0) nearly invisible on white. + comment: Branch lines, poles, crossings, and arrows all visible; green branch + clips at bottom edge - id: VQ-04 name: Color Accessibility score: 3 max: 4 passed: true - comment: Blue/orange/green generally distinguishable. Green branch could be - confused with blue by deuteranope viewers. + comment: Blue/orange/green palette is colorblind-friendly; gray guide lines + (#d0d0d0) may be too faint - id: VQ-05 name: Layout & Canvas score: 2 max: 4 passed: false - comment: Content concentrated in center-right. Domain extends far left with - mostly empty space and faint grid lines. + comment: Canvas 4200x3300 doesn't match target formats; data concentrated + center-right with wasted space; green branch clipped - id: VQ-06 name: Axis Labels & Title score: 1 max: 2 passed: false - comment: Descriptive labels but lack standard control notation (sigma, jomega). + comment: Descriptive axis titles 'Real Axis' and 'Imaginary Axis' but barely + visible in render; no units (acceptable for s-plane) design_excellence: score: 13 max: 20 @@ -98,22 +102,22 @@ review: score: 5 max: 8 passed: true - comment: Custom palette, clear typography hierarchy, red accents. Above defaults - but not publication-ready. + comment: Custom palette, thoughtful marker differentiation, subtitle with + transfer function; above defaults but not publication-ready - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: View stroke removed, domain-appropriate guide lines instead of generic - grid. Guide lines too faint. + comment: View stroke removed, grid replaced with domain-specific guide lines, + legend well-configured - id: DE-03 name: Data Storytelling score: 4 max: 6 passed: true - comment: Labeled stability crossings with gain values, direction arrows, breakaway - point. Good narrative. + comment: Stability boundary crossings labeled with gain value, breakaway marked, + direction arrows, red highlighting creates hierarchy spec_compliance: score: 14 max: 15 @@ -123,28 +127,28 @@ review: score: 5 max: 5 passed: true - comment: Correct root locus plot showing pole trajectories in complex plane. + comment: Correct root locus plot showing pole trajectories in complex plane - id: SC-02 name: Required Features score: 3 max: 4 passed: true - comment: Most features present. Missing equal axis scaling required by spec - for geometric preservation. + comment: All features present except equal axis scaling and proper centering + on origin - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Real/imaginary correctly mapped to x/y axes. + comment: Real on x-axis, imaginary on y-axis, all data visible - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct. Legend clearly identifies all three branches. + comment: Title follows exact spec format; legend labels clear data_quality: - score: 14 + score: 13 max: 15 items: - id: DQ-01 @@ -152,23 +156,24 @@ review: score: 5 max: 6 passed: true - comment: Shows branches, breakaway, crossings, real-axis segments, direction - arrows. Could annotate asymptotes. + comment: 'Shows all key root locus behaviors: pole migration, breakaway, complex + plane entry, stability crossing, branches to infinity' - id: DQ-02 name: Realistic Context - score: 5 + score: 4 max: 5 passed: true - comment: Classic textbook transfer function G(s)=1/[s(s+1)(s+2)]. Neutral, - educational. + comment: Classic textbook transfer function G(s)=1/[s(s+1)(s+2)], mathematically + correct - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Mathematically correct values verified analytically. + comment: Gain range 0.001-80 covers all transitions; pole locations and crossings + mathematically verified code_quality: - score: 10 + score: 9 max: 10 items: - id: CQ-01 @@ -176,32 +181,33 @@ review: score: 3 max: 3 passed: true - comment: 'Linear script: imports, data computation, layer construction, composition, - save.' + comment: 'Linear flow: imports, data generation, layers, composition, save' - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Fully deterministic polynomial root-finding. + comment: Fully deterministic with np.roots(), no randomness - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All three imports (altair, numpy, pandas) are used. + comment: Only altair, numpy, pandas — all used - id: CQ-04 name: Code Elegance - score: 2 + score: 1 max: 2 passed: true - comment: Well-structured with smart nearest-neighbor branch sorting. + comment: Well-structured but verbose at ~300 lines; branch sorting algorithm + could be more concise - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png with scale_factor=3.0 plus HTML export. + comment: Saves plot.png with scale_factor=3.0 and plot.html; current Altair + API library_mastery: score: 7 max: 10 @@ -211,15 +217,15 @@ review: score: 4 max: 5 passed: true - comment: 'Good declarative Altair patterns: layered composition, encoding - types, detail/order channels.' + comment: Good declarative layer composition, encoding channels, tooltips with + formatting; heavy manual data prep - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Leverages interactive zoom/pan, rich tooltips, HTML export, declarative - detail/order channels. + comment: Interactive tooltips with gain K, zoom/pan, HTML export — meaningful + Altair-specific features verdict: REJECTED impl_tags: dependencies: [] @@ -234,5 +240,4 @@ impl_tags: - matrix-construction dataprep: [] styling: - - alpha-blending - grid-styling From 882009632603afe4fd9c8d00ac6ac258c3f0f303 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:54:01 +0000 Subject: [PATCH 5/8] fix(altair): address review feedback for root-locus-basic Attempt 2/3 - fixes based on AI review --- .../implementations/altair.py | 216 +++++++++--------- 1 file changed, 106 insertions(+), 110 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index 53667f1a99..91e59f3540 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,4 +1,4 @@ -""" pyplots.ai +"""pyplots.ai root-locus-basic: Root Locus Plot for Control Systems Library: altair 6.0.0 | Python 3.14.3 Quality: 78/100 | Created: 2026-03-20 @@ -16,8 +16,7 @@ gains = np.concatenate( [ - np.linspace(0.001, 0.3, 100), - np.linspace(0.3, 0.5, 100), + np.linspace(0.001, 0.5, 150), np.linspace(0.5, 2, 200), np.linspace(2, 6, 150), np.linspace(6, 20, 150), @@ -35,20 +34,15 @@ # Sort roots into continuous branches via nearest-neighbor matching all_roots[0] = np.sort(all_roots[0].real) for i in range(1, len(gains)): - prev = all_roots[i - 1] - curr = all_roots[i] - used = [False] * n_roots + prev, curr = all_roots[i - 1], all_roots[i] + dist = np.abs(prev[:, None] - curr[None, :]) order = np.zeros(n_roots, dtype=int) + used = set() for j in range(n_roots): - best_m, best_d = -1, np.inf - for m in range(n_roots): - if not used[m]: - d = abs(prev[j] - curr[m]) - if d < best_d: - best_d = d - best_m = m - used[best_m] = True - order[j] = best_m + dists = [(dist[j, m], m) for m in range(n_roots) if m not in used] + _, best = min(dists) + used.add(best) + order[j] = best all_roots[i] = curr[order] # Build branch dataframe @@ -64,7 +58,6 @@ "idx": i, } ) - locus_df = pd.DataFrame(rows) # Open-loop poles @@ -79,20 +72,16 @@ ) # Breakaway point: d/ds[s(s+1)(s+2)] = 3s²+6s+2 = 0 → s ≈ -0.423 -breakaway_df = pd.DataFrame({"bx": [(-6 + np.sqrt(12)) / 6], "by": [0.0], "label": ["Breakaway"]}) +breakaway_df = pd.DataFrame({"bx": [(-6 + np.sqrt(12)) / 6], "by": [0.0], "label": ["Breakaway (s ≈ −0.42)"]}) -# Damping ratio lines (ζ = 0.2, 0.4, 0.6, 0.8) +# Damping ratio guide lines (ζ = 0.2, 0.4, 0.6, 0.8) damping_rows = [] for zeta in [0.2, 0.4, 0.6, 0.8]: angle = np.pi - np.arccos(zeta) - r_max = 7.0 for side, sign in [("upper", 1), ("lower", -1)]: - seg_name = f"ζ={zeta}_{side}" - damping_rows.append({"dx": 0.0, "dy": 0.0, "seg": seg_name, "ord": 0}) - damping_rows.append( - {"dx": r_max * np.cos(angle), "dy": sign * r_max * np.sin(angle), "seg": seg_name, "ord": 1} - ) - + seg = f"ζ={zeta}_{side}" + damping_rows.append({"gx": 0.0, "gy": 0.0, "seg": seg, "ord": 0}) + damping_rows.append({"gx": 6.0 * np.cos(angle), "gy": sign * 6.0 * np.sin(angle), "seg": seg, "ord": 1}) damping_df = pd.DataFrame(damping_rows) # Natural frequency arcs (ωn = 1, 2, 3, 4) in left half-plane @@ -100,119 +89,131 @@ for wn in [1.0, 2.0, 3.0, 4.0]: theta = np.linspace(np.pi / 2, 3 * np.pi / 2, 60) for j, t in enumerate(theta): - wn_rows.append({"wx": wn * np.cos(t), "wy": wn * np.sin(t), "wn": f"ωn={wn}", "ord": j}) - + wn_rows.append({"gx": wn * np.cos(t), "gy": wn * np.sin(t), "wn": f"ωn={wn}", "ord": j}) wn_df = pd.DataFrame(wn_rows) # Real axis segments: (-1, 0) and (-∞, -2) -real_axis_rows = [] -for rx0, rx1, seg in [(-1.0, 0.0, "seg1"), (-7.0, -2.0, "seg2")]: - real_axis_rows.append({"rx": rx0, "ry": 0.0, "seg": seg, "ord": 0}) - real_axis_rows.append({"rx": rx1, "ry": 0.0, "seg": seg, "ord": 1}) - -real_axis_df = pd.DataFrame(real_axis_rows) +real_axis_df = pd.DataFrame( + { + "rx": [-1.0, 0.0, -6.0, -2.0], + "ry": [0.0, 0.0, 0.0, 0.0], + "seg": ["seg1", "seg1", "seg2", "seg2"], + "ord": [0, 1, 0, 1], + } +) # Arrow direction indicators along complex branches arrows = [] for b in range(n_roots): - for idx in [400, 600]: + for idx in [400, 550]: if idx + 5 < len(gains): r0 = all_roots[idx, b] - if abs(r0.imag) > 0.5: - arrows.append( - { - "ax": float(r0.real), - "ay": float(r0.imag), - "branch": f"Branch {b + 1}", - "shape": "triangle-up" if r0.imag > 0 else "triangle-down", - } - ) - -arrow_df = pd.DataFrame(arrows) if arrows else pd.DataFrame({"ax": [], "ay": [], "branch": [], "shape": []}) - -# Scales -x_scale = alt.Scale(domain=[-7.0, 2.5], nice=False) -y_scale = alt.Scale(domain=[-6.0, 6.0], nice=False) - -# Layer: Damping ratio lines -damping_layer = ( - alt.Chart(damping_df) - .mark_line(strokeWidth=1, strokeDash=[6, 4], color="#d0d0d0") - .encode( - x=alt.X("dx:Q", scale=x_scale, axis=None), - y=alt.Y("dy:Q", scale=y_scale, axis=None), - detail="seg:N", - order="ord:Q", - ) -) + if abs(r0.imag) > 0.3: + arrows.append({"ax": float(r0.real), "ay": float(r0.imag), "branch": f"Branch {b + 1}"}) +arrow_df = pd.DataFrame(arrows) if arrows else pd.DataFrame({"ax": [], "ay": [], "branch": []}) -# Layer: Natural frequency arcs -wn_layer = ( - alt.Chart(wn_df) - .mark_line(strokeWidth=1, strokeDash=[4, 4], color="#d0d0d0") - .encode(x=alt.X("wx:Q", scale=x_scale), y=alt.Y("wy:Q", scale=y_scale), detail="wn:N", order="ord:Q") -) +# Equal-scaling axes centered on origin (square canvas, equal domain = equal scaling) +axis_range = 6.0 +x_scale = alt.Scale(domain=[-axis_range, axis_range], nice=False) +y_scale = alt.Scale(domain=[-axis_range, axis_range], nice=False) -# Layer: Real axis segments -real_axis_layer = ( - alt.Chart(real_axis_df) - .mark_line(strokeWidth=4, color="#306998", opacity=0.3) - .encode(x=alt.X("rx:Q", scale=x_scale), y=alt.Y("ry:Q", scale=y_scale), detail="seg:N", order="ord:Q") -) - -# Layer: Locus branches branch_palette = ["#306998", "#e07b39", "#2ca02c"] +branch_domain = ["Branch 1", "Branch 2", "Branch 3"] + +# Layer: Locus branches — FIRST so its axis config takes effect locus_layer = ( alt.Chart(locus_df) .mark_line(strokeWidth=2.5, opacity=0.9) .encode( x=alt.X( - "real:Q", scale=x_scale, title="Real Axis", axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=False) + "real:Q", + scale=x_scale, + title="Real Axis (σ)", + axis=alt.Axis( + labelFontSize=16, + titleFontSize=20, + titleFontWeight="bold", + titleColor="#333333", + labelColor="#333333", + grid=False, + tickCount=7, + titlePadding=12, + ), ), y=alt.Y( "imaginary:Q", scale=y_scale, - title="Imaginary Axis", - axis=alt.Axis(labelFontSize=18, titleFontSize=22, grid=False), + title="Imaginary Axis (jω)", + axis=alt.Axis( + labelFontSize=16, + titleFontSize=20, + titleFontWeight="bold", + titleColor="#333333", + labelColor="#333333", + grid=False, + tickCount=7, + titlePadding=12, + ), ), color=alt.Color( "branch:N", - scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), + scale=alt.Scale(domain=branch_domain, range=branch_palette), legend=alt.Legend( title="Branch", - titleFontSize=18, - labelFontSize=16, - symbolSize=200, + titleFontSize=16, + labelFontSize=14, + symbolSize=180, symbolStrokeWidth=3, orient="top-right", - offset=10, + offset=5, ), ), order="idx:Q", tooltip=[ alt.Tooltip("branch:N", title="Branch"), - alt.Tooltip("real:Q", title="Real", format=".3f"), - alt.Tooltip("imaginary:Q", title="Imaginary", format=".3f"), + alt.Tooltip("real:Q", title="σ", format=".3f"), + alt.Tooltip("imaginary:Q", title="jω", format=".3f"), alt.Tooltip("gain:Q", title="Gain K", format=".2f"), ], ) ) +# Layer: Damping ratio lines +damping_layer = ( + alt.Chart(damping_df) + .mark_line(strokeWidth=1, strokeDash=[6, 4], color="#c0c0c0") + .encode(x=alt.X("gx:Q", scale=x_scale), y=alt.Y("gy:Q", scale=y_scale), detail="seg:N", order="ord:Q") +) + +# Layer: Natural frequency arcs +wn_layer = ( + alt.Chart(wn_df) + .mark_line(strokeWidth=1, strokeDash=[4, 4], color="#c0c0c0") + .encode(x=alt.X("gx:Q", scale=x_scale), y=alt.Y("gy:Q", scale=y_scale), detail="wn:N", order="ord:Q") +) + +# Layer: Real axis segments +real_axis_layer = ( + alt.Chart(real_axis_df) + .mark_line(strokeWidth=5, color="#306998", opacity=0.25) + .encode(x=alt.X("rx:Q", scale=x_scale), y=alt.Y("ry:Q", scale=y_scale), detail="seg:N", order="ord:Q") +) + # Layer: Open-loop poles (× markers) poles_layer = ( alt.Chart(poles_df) - .mark_point(shape="cross", size=400, strokeWidth=3.5, color="#d62728", filled=False) + .mark_point(shape="cross", size=450, strokeWidth=3.5, color="#d62728", filled=False) .encode( x=alt.X("real:Q", scale=x_scale), y=alt.Y("imaginary:Q", scale=y_scale), - tooltip=[alt.Tooltip("label:N", title=""), alt.Tooltip("real:Q", title="Real")], + tooltip=[alt.Tooltip("label:N", title=""), alt.Tooltip("real:Q", title="σ")], ) ) # Layer: Imaginary axis crossings crossing_layer = ( alt.Chart(crossing_df) - .mark_point(shape="diamond", size=350, strokeWidth=2.5, color="#d62728", filled=True) + .mark_point(shape="diamond", size=400, strokeWidth=2.5, color="#d62728", filled=True) .encode( x=alt.X("real:Q", scale=x_scale), y=alt.Y("imaginary:Q", scale=y_scale), @@ -223,54 +224,50 @@ # Layer: Crossing labels crossing_text = ( alt.Chart(crossing_df) - .mark_text(fontSize=14, fontWeight="bold", color="#d62728", align="left", dx=16) + .mark_text(fontSize=15, fontWeight="bold", color="#d62728", align="left", dx=18) .encode(x=alt.X("real:Q", scale=x_scale), y=alt.Y("imaginary:Q", scale=y_scale), text="label:N") ) # Layer: Breakaway point breakaway_layer = ( alt.Chart(breakaway_df) - .mark_point(shape="square", size=200, color="#555555", filled=True, opacity=0.7) + .mark_point(shape="square", size=220, color="#555555", filled=True, opacity=0.8) .encode( x=alt.X("bx:Q", scale=x_scale), y=alt.Y("by:Q", scale=y_scale), tooltip=[alt.Tooltip("label:N", title="Point")] ) ) # Layer: Arrow direction indicators -arrow_up = arrow_df[arrow_df["ay"] > 0] if len(arrow_df) > 0 else arrow_df -arrow_down = arrow_df[arrow_df["ay"] <= 0] if len(arrow_df) > 0 else arrow_df +arrow_up_df = arrow_df[arrow_df["ay"] > 0] if len(arrow_df) > 0 else arrow_df +arrow_down_df = arrow_df[arrow_df["ay"] <= 0] if len(arrow_df) > 0 else arrow_df arrow_up_layer = ( - alt.Chart(arrow_up) - .mark_point(shape="triangle-up", size=220, filled=True, opacity=0.85) + alt.Chart(arrow_up_df) + .mark_point(shape="triangle-up", size=250, filled=True, opacity=0.85) .encode( x=alt.X("ax:Q", scale=x_scale), y=alt.Y("ay:Q", scale=y_scale), - color=alt.Color( - "branch:N", scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), legend=None - ), + color=alt.Color("branch:N", scale=alt.Scale(domain=branch_domain, range=branch_palette), legend=None), ) ) arrow_down_layer = ( - alt.Chart(arrow_down) - .mark_point(shape="triangle-down", size=220, filled=True, opacity=0.85) + alt.Chart(arrow_down_df) + .mark_point(shape="triangle-down", size=250, filled=True, opacity=0.85) .encode( x=alt.X("ax:Q", scale=x_scale), y=alt.Y("ay:Q", scale=y_scale), - color=alt.Color( - "branch:N", scale=alt.Scale(domain=["Branch 1", "Branch 2", "Branch 3"], range=branch_palette), legend=None - ), + color=alt.Color("branch:N", scale=alt.Scale(domain=branch_domain, range=branch_palette), legend=None), ) ) -# Compose +# Compose — locus_layer first so its axis config renders chart = ( ( - damping_layer + locus_layer + + damping_layer + wn_layer + real_axis_layer - + locus_layer + poles_layer + crossing_layer + crossing_text @@ -279,22 +276,21 @@ + arrow_down_layer ) .properties( - width=1400, - height=1100, + width=1200, + height=1200, title=alt.Title( "root-locus-basic · altair · pyplots.ai", fontSize=28, color="#222222", subtitle="G(s) = 1 / s(s+1)(s+2) — Closed-Loop Pole Trajectories vs Gain K", subtitleFontSize=17, - subtitleColor="#777777", - subtitlePadding=6, + subtitleColor="#666666", + subtitlePadding=8, ), ) .configure_view(strokeWidth=0) .interactive() ) -# Save chart.save("plot.png", scale_factor=3.0) chart.save("plot.html") From b20519b15c1166448137e5690a03beb0a7649bd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 21:59:53 +0000 Subject: [PATCH 6/8] chore(altair): update quality score 87 and review feedback for root-locus-basic --- .../implementations/altair.py | 4 +- plots/root-locus-basic/metadata/altair.yaml | 166 ++++++++---------- 2 files changed, 79 insertions(+), 91 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index 91e59f3540..ce892f7072 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,7 +1,7 @@ -"""pyplots.ai +""" pyplots.ai root-locus-basic: Root Locus Plot for Control Systems Library: altair 6.0.0 | Python 3.14.3 -Quality: 78/100 | Created: 2026-03-20 +Quality: 87/100 | Created: 2026-03-20 """ import altair as alt diff --git a/plots/root-locus-basic/metadata/altair.yaml b/plots/root-locus-basic/metadata/altair.yaml index 5b1f07fff7..4b84a19c0d 100644 --- a/plots/root-locus-basic/metadata/altair.yaml +++ b/plots/root-locus-basic/metadata/altair.yaml @@ -1,7 +1,7 @@ library: altair specification_id: root-locus-basic created: '2026-03-20T21:26:28Z' -updated: '2026-03-20T21:46:52Z' +updated: '2026-03-20T21:59:52Z' generated_by: claude-opus-4-5-20251101 workflow_run: 23362981953 issue: 4414 @@ -10,116 +10,107 @@ library_version: 6.0.0 preview_url: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.html -quality_score: 78 +quality_score: 87 review: strengths: - - Comprehensive spec coverage with all major root locus features (poles, branches, - crossings, breakaway, direction arrows, guide lines) - - Good use of Altair interactive features — tooltips showing gain K values are genuinely - useful for root locus analysis - - Mathematically correct data generation with proper branch sorting algorithm - - Domain-specific guide lines (damping ratios, natural frequencies) replace generic - grid effectively - - Clean visual hierarchy with red for critical points, distinct colors for branches + - Perfect spec compliance with all required root locus features implemented correctly + - Mathematically accurate data with proper polynomial root computation + - Strong visual hierarchy with red critical points, prominent colored branches, + and subtle gray guide lines + - Effective use of Altair interactive tooltips showing gain values along locus + - Clean well-structured code with logical layer composition weaknesses: - - Axis tick labels and titles are barely visible in the rendered output despite - explicit font sizes - - Canvas size (4200x3300) does not match either target format (4800x2700 or 3600x3600) - - Layout has significant wasted space with data concentrated center-right; green - branch clips at bottom edge - - Axis scaling is not equal as spec requires — x spans 9.5 units vs y spans 12 units - - Code is verbose with many separate DataFrames; branch sorting algorithm could - be more concise - image_description: 'The plot displays a root locus diagram for G(s) = 1/[s(s+1)(s+2)] - with the title "root-locus-basic · altair · pyplots.ai" and a subtitle describing - the transfer function. Three branches are shown in distinct colors: Branch 1 (blue, - #306998) extends along the negative real axis to the left, Branch 2 (orange, #e07b39) - curves upward into the upper half-plane, and Branch 3 (green, #2ca02c) curves + - DE-01/DE-02 could be pushed further with more typographic refinement + - Axis range [-6, 6] leaves some unused canvas space in corners + - Crossing labels at 15pt slightly smaller than ideal for canvas size + image_description: 'The plot displays a root locus diagram for the transfer function + G(s) = 1/s(s+1)(s+2) on a square canvas with equal axis scaling from -6 to 6. + Three branches are shown in distinct colors: Branch 1 (blue) extends along the + negative real axis from the origin toward negative infinity; Branch 2 (orange) + curves upward from the real axis into the upper half-plane; Branch 3 (green) curves downward into the lower half-plane. Open-loop poles are marked with red cross - markers at s=0, s=-1, and s=-2 on the real axis. Imaginary axis crossings are - marked with red filled diamonds labeled "jω = j√2 (K=6)" and "jω = -j√2 (K=6)". - A gray filled square marks the breakaway point near s≈-0.423. Triangle direction - arrows appear along the complex branches. Light dashed gray lines show damping - ratio guidelines radiating from the origin and natural frequency arcs as semicircles - in the left half-plane. Semi-transparent blue segments highlight the real-axis - portions of the root locus between (-1, 0) and (-∞, -2). The legend appears in - the top-right corner. Axis tick labels and axis titles ("Real Axis", "Imaginary - Axis") are present but appear faint in the rendered output. The green branch extends - to the very bottom edge of the canvas.' + markers at s=0, s=-1, and s=-2 on the real axis. Red diamond markers indicate + imaginary axis crossings at jw = plus/minus sqrt(2) with labels showing K=6. A + dark gray square marks the breakaway point near s approximately -0.42. Light blue + thick lines highlight real axis segments between (-1, 0) and from -2 to negative + infinity. Subtle dashed gray lines show constant damping ratio guides radiating + from the origin and natural frequency semicircular arcs in the left half-plane. + Triangle-shaped direction arrows on the complex branches indicate the direction + of increasing gain. The title reads "root-locus-basic · altair · pyplots.ai" with + a subtitle describing the transfer function. A legend in the top-right identifies + the three branches.' criteria_checklist: visual_quality: - score: 22 + score: 26 max: 30 items: - id: VQ-01 name: Text Legibility - score: 6 + score: 7 max: 8 passed: true - comment: Font sizes explicitly set (title 28, axis labels 18, titles 22, legend - 16-18) but axis ticks and titles appear faint in rendered output + comment: Font sizes explicitly set (title 28pt, axis 20pt, ticks 16pt, crossing + labels 15pt). All readable. - id: VQ-02 name: No Overlap score: 5 max: 6 passed: true - comment: Minor overlap between breakaway marker and pole cross near s=0; crossing - labels near branch lines + comment: Minor visual crowding near origin but no text overlap. - id: VQ-03 name: Element Visibility score: 5 max: 6 passed: true - comment: Branch lines, poles, crossings, and arrows all visible; green branch - clips at bottom edge + comment: Branch lines, pole markers, and crossing markers well-sized. Guide + lines appropriately subtle. - id: VQ-04 name: Color Accessibility - score: 3 + score: 4 max: 4 passed: true - comment: Blue/orange/green palette is colorblind-friendly; gray guide lines - (#d0d0d0) may be too faint + comment: Blue/orange/green palette is colorblind-safe. Red for critical markers + provides good contrast. - id: VQ-05 name: Layout & Canvas - score: 2 + score: 3 max: 4 - passed: false - comment: Canvas 4200x3300 doesn't match target formats; data concentrated - center-right with wasted space; green branch clipped + passed: true + comment: Square 3600x3600 canvas appropriate. Axis range slightly generous + but needed for guide lines. - id: VQ-06 name: Axis Labels & Title - score: 1 + score: 2 max: 2 - passed: false - comment: Descriptive axis titles 'Real Axis' and 'Imaginary Axis' but barely - visible in render; no units (acceptable for s-plane) + passed: true + comment: Real Axis (σ) and Imaginary Axis (jω) with proper engineering notation. design_excellence: - score: 13 + score: 14 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 5 + score: 6 max: 8 passed: true - comment: Custom palette, thoughtful marker differentiation, subtitle with - transfer function; above defaults but not publication-ready + comment: Custom palette with intentional color hierarchy. Professional appearance + above defaults. - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: View stroke removed, grid replaced with domain-specific guide lines, - legend well-configured + comment: View border removed, grid disabled, guide lines as subtle dashed + gray. - id: DE-03 name: Data Storytelling score: 4 max: 6 passed: true - comment: Stability boundary crossings labeled with gain value, breakaway marked, - direction arrows, red highlighting creates hierarchy + comment: Visual hierarchy through color contrast, labeled crossings, direction + arrows guide the viewer. spec_compliance: - score: 14 + score: 15 max: 15 items: - id: SC-01 @@ -127,53 +118,52 @@ review: score: 5 max: 5 passed: true - comment: Correct root locus plot showing pole trajectories in complex plane + comment: Correct root locus plot with pole trajectories in complex plane. - id: SC-02 name: Required Features - score: 3 + score: 4 max: 4 passed: true - comment: All features present except equal axis scaling and proper centering - on origin + comment: 'All spec features present: poles, branches, arrows, crossings, real + axis segments, guide lines.' - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Real on x-axis, imaginary on y-axis, all data visible + comment: Real/imaginary correctly mapped to X/Y axes. - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title follows exact spec format; legend labels clear + comment: Title format correct. Legend labels match branch data. data_quality: - score: 13 + score: 15 max: 15 items: - id: DQ-01 name: Feature Coverage - score: 5 + score: 6 max: 6 passed: true - comment: 'Shows all key root locus behaviors: pole migration, breakaway, complex - plane entry, stability crossing, branches to infinity' + comment: 'Comprehensive: multiple branches, breakaway, crossings, real axis + segments, guide grid.' - id: DQ-02 name: Realistic Context - score: 4 + score: 5 max: 5 passed: true - comment: Classic textbook transfer function G(s)=1/[s(s+1)(s+2)], mathematically - correct + comment: Classic control systems transfer function. Standard textbook example. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Gain range 0.001-80 covers all transitions; pole locations and crossings - mathematically verified + comment: 'Mathematically accurate: breakaway at s≈-0.42, crossing at ω=√2 + for K=6.' code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 @@ -181,33 +171,31 @@ review: score: 3 max: 3 passed: true - comment: 'Linear flow: imports, data generation, layers, composition, save' + comment: Imports → data → chart layers → composition → save. No functions/classes. - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Fully deterministic with np.roots(), no randomness + comment: Fully deterministic polynomial root computation. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only altair, numpy, pandas — all used + comment: 'All imports used: altair, numpy, pandas.' - id: CQ-04 name: Code Elegance - score: 1 + score: 2 max: 2 passed: true - comment: Well-structured but verbose at ~300 lines; branch sorting algorithm - could be more concise + comment: Clean, well-organized. Necessary complexity for domain-specific plot. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves plot.png with scale_factor=3.0 and plot.html; current Altair - API + comment: Saves plot.png and plot.html. Current Altair API. library_mastery: score: 7 max: 10 @@ -217,15 +205,15 @@ review: score: 4 max: 5 passed: true - comment: Good declarative layer composition, encoding channels, tooltips with - formatting; heavy manual data prep + comment: 'Good declarative Altair patterns: layered composition, encoding + types, tooltips, .interactive().' - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Interactive tooltips with gain K, zoom/pan, HTML export — meaningful - Altair-specific features + comment: Interactive tooltips with gain values, HTML export, multi-layer composition + with varied marks. verdict: REJECTED impl_tags: dependencies: [] From 6e27feb231944bc86f7fdb9e12eecb23d01b6cc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 22:06:49 +0000 Subject: [PATCH 7/8] fix(altair): address review feedback for root-locus-basic Attempt 3/3 - fixes based on AI review --- .../implementations/altair.py | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index ce892f7072..89e93e67ef 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,7 +1,6 @@ -""" pyplots.ai +"""pyplots.ai root-locus-basic: Root Locus Plot for Control Systems Library: altair 6.0.0 | Python 3.14.3 -Quality: 87/100 | Created: 2026-03-20 """ import altair as alt @@ -81,9 +80,16 @@ for side, sign in [("upper", 1), ("lower", -1)]: seg = f"ζ={zeta}_{side}" damping_rows.append({"gx": 0.0, "gy": 0.0, "seg": seg, "ord": 0}) - damping_rows.append({"gx": 6.0 * np.cos(angle), "gy": sign * 6.0 * np.sin(angle), "seg": seg, "ord": 1}) + damping_rows.append({"gx": 5.0 * np.cos(angle), "gy": sign * 5.0 * np.sin(angle), "seg": seg, "ord": 1}) damping_df = pd.DataFrame(damping_rows) +# Damping ratio labels at end of guide lines +damping_label_rows = [] +for zeta in [0.4, 0.8]: + angle = np.pi - np.arccos(zeta) + damping_label_rows.append({"lx": 4.6 * np.cos(angle), "ly": 4.6 * np.sin(angle), "label": f"ζ={zeta}"}) +damping_label_df = pd.DataFrame(damping_label_rows) + # Natural frequency arcs (ωn = 1, 2, 3, 4) in left half-plane wn_rows = [] for wn in [1.0, 2.0, 3.0, 4.0]: @@ -95,7 +101,7 @@ # Real axis segments: (-1, 0) and (-∞, -2) real_axis_df = pd.DataFrame( { - "rx": [-1.0, 0.0, -6.0, -2.0], + "rx": [-1.0, 0.0, -5.0, -2.0], "ry": [0.0, 0.0, 0.0, 0.0], "seg": ["seg1", "seg1", "seg2", "seg2"], "ord": [0, 1, 0, 1], @@ -105,7 +111,7 @@ # Arrow direction indicators along complex branches arrows = [] for b in range(n_roots): - for idx in [400, 550]: + for idx in [350, 500]: if idx + 5 < len(gains): r0 = all_roots[idx, b] if abs(r0.imag) > 0.3: @@ -113,9 +119,8 @@ arrow_df = pd.DataFrame(arrows) if arrows else pd.DataFrame({"ax": [], "ay": [], "branch": []}) # Equal-scaling axes centered on origin (square canvas, equal domain = equal scaling) -axis_range = 6.0 -x_scale = alt.Scale(domain=[-axis_range, axis_range], nice=False) -y_scale = alt.Scale(domain=[-axis_range, axis_range], nice=False) +x_scale = alt.Scale(domain=[-5.0, 5.0], nice=False) +y_scale = alt.Scale(domain=[-5.0, 5.0], nice=False) branch_palette = ["#306998", "#e07b39", "#2ca02c"] branch_domain = ["Branch 1", "Branch 2", "Branch 3"] @@ -123,7 +128,7 @@ # Layer: Locus branches — FIRST so its axis config takes effect locus_layer = ( alt.Chart(locus_df) - .mark_line(strokeWidth=2.5, opacity=0.9) + .mark_line(strokeWidth=2.8, opacity=0.92) .encode( x=alt.X( "real:Q", @@ -131,13 +136,15 @@ title="Real Axis (σ)", axis=alt.Axis( labelFontSize=16, - titleFontSize=20, + titleFontSize=21, titleFontWeight="bold", - titleColor="#333333", - labelColor="#333333", + titleColor="#2a2a2a", + labelColor="#444444", grid=False, - tickCount=7, - titlePadding=12, + tickCount=6, + titlePadding=14, + domainColor="#888888", + tickColor="#888888", ), ), y=alt.Y( @@ -146,13 +153,15 @@ title="Imaginary Axis (jω)", axis=alt.Axis( labelFontSize=16, - titleFontSize=20, + titleFontSize=21, titleFontWeight="bold", - titleColor="#333333", - labelColor="#333333", + titleColor="#2a2a2a", + labelColor="#444444", grid=False, - tickCount=7, - titlePadding=12, + tickCount=6, + titlePadding=14, + domainColor="#888888", + tickColor="#888888", ), ), color=alt.Color( @@ -181,14 +190,21 @@ # Layer: Damping ratio lines damping_layer = ( alt.Chart(damping_df) - .mark_line(strokeWidth=1, strokeDash=[6, 4], color="#c0c0c0") + .mark_line(strokeWidth=0.8, strokeDash=[6, 4], color="#d0d0d0") .encode(x=alt.X("gx:Q", scale=x_scale), y=alt.Y("gy:Q", scale=y_scale), detail="seg:N", order="ord:Q") ) +# Layer: Damping ratio labels +damping_label_layer = ( + alt.Chart(damping_label_df) + .mark_text(fontSize=12, color="#aaaaaa", fontStyle="italic", align="center") + .encode(x=alt.X("lx:Q", scale=x_scale), y=alt.Y("ly:Q", scale=y_scale), text="label:N") +) + # Layer: Natural frequency arcs wn_layer = ( alt.Chart(wn_df) - .mark_line(strokeWidth=1, strokeDash=[4, 4], color="#c0c0c0") + .mark_line(strokeWidth=0.8, strokeDash=[4, 4], color="#d0d0d0") .encode(x=alt.X("gx:Q", scale=x_scale), y=alt.Y("gy:Q", scale=y_scale), detail="wn:N", order="ord:Q") ) @@ -224,7 +240,7 @@ # Layer: Crossing labels crossing_text = ( alt.Chart(crossing_df) - .mark_text(fontSize=15, fontWeight="bold", color="#d62728", align="left", dx=18) + .mark_text(fontSize=17, fontWeight="bold", color="#c5211e", align="left", dx=20, font="sans-serif") .encode(x=alt.X("real:Q", scale=x_scale), y=alt.Y("imaginary:Q", scale=y_scale), text="label:N") ) @@ -266,6 +282,7 @@ ( locus_layer + damping_layer + + damping_label_layer + wn_layer + real_axis_layer + poles_layer @@ -281,11 +298,14 @@ title=alt.Title( "root-locus-basic · altair · pyplots.ai", fontSize=28, - color="#222222", - subtitle="G(s) = 1 / s(s+1)(s+2) — Closed-Loop Pole Trajectories vs Gain K", - subtitleFontSize=17, - subtitleColor="#666666", - subtitlePadding=8, + fontWeight="bold", + color="#1a1a1a", + subtitle="G(s) = 1 / s(s+1)(s+2) · Closed-Loop Pole Trajectories vs Gain K", + subtitleFontSize=18, + subtitleColor="#555555", + subtitlePadding=10, + anchor="start", + offset=10, ), ) .configure_view(strokeWidth=0) From caa48126dc1a7b54b671a9daec08cf20118726af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 22:11:12 +0000 Subject: [PATCH 8/8] chore(altair): update quality score 86 and review feedback for root-locus-basic --- .../implementations/altair.py | 3 +- plots/root-locus-basic/metadata/altair.yaml | 129 ++++++++---------- 2 files changed, 61 insertions(+), 71 deletions(-) diff --git a/plots/root-locus-basic/implementations/altair.py b/plots/root-locus-basic/implementations/altair.py index 89e93e67ef..6904290538 100644 --- a/plots/root-locus-basic/implementations/altair.py +++ b/plots/root-locus-basic/implementations/altair.py @@ -1,6 +1,7 @@ -"""pyplots.ai +""" pyplots.ai root-locus-basic: Root Locus Plot for Control Systems Library: altair 6.0.0 | Python 3.14.3 +Quality: 86/100 | Created: 2026-03-20 """ import altair as alt diff --git a/plots/root-locus-basic/metadata/altair.yaml b/plots/root-locus-basic/metadata/altair.yaml index 4b84a19c0d..95ccf7827b 100644 --- a/plots/root-locus-basic/metadata/altair.yaml +++ b/plots/root-locus-basic/metadata/altair.yaml @@ -1,7 +1,7 @@ library: altair specification_id: root-locus-basic created: '2026-03-20T21:26:28Z' -updated: '2026-03-20T21:59:52Z' +updated: '2026-03-20T22:11:12Z' generated_by: claude-opus-4-5-20251101 workflow_run: 23362981953 issue: 4414 @@ -10,38 +10,37 @@ library_version: 6.0.0 preview_url: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.png preview_thumb: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot_thumb.png preview_html: https://storage.googleapis.com/pyplots-images/plots/root-locus-basic/altair/plot.html -quality_score: 87 +quality_score: 86 review: strengths: - - Perfect spec compliance with all required root locus features implemented correctly - - Mathematically accurate data with proper polynomial root computation - - Strong visual hierarchy with red critical points, prominent colored branches, - and subtle gray guide lines - - Effective use of Altair interactive tooltips showing gain values along locus - - Clean well-structured code with logical layer composition + - Excellent spec compliance — all required root locus features present and correctly + implemented + - Strong data storytelling with labeled crossings, breakaway point, and transfer + function in subtitle + - Clean layered architecture with well-separated concerns per chart layer + - Proper engineering notation on axes and annotations + - Deterministic, mathematically correct root computation with appropriate gain range weaknesses: - - DE-01/DE-02 could be pushed further with more typographic refinement - - Axis range [-6, 6] leaves some unused canvas space in corners - - Crossing labels at 15pt slightly smaller than ideal for canvas size - image_description: 'The plot displays a root locus diagram for the transfer function - G(s) = 1/s(s+1)(s+2) on a square canvas with equal axis scaling from -6 to 6. - Three branches are shown in distinct colors: Branch 1 (blue) extends along the - negative real axis from the origin toward negative infinity; Branch 2 (orange) - curves upward from the real axis into the upper half-plane; Branch 3 (green) curves - downward into the lower half-plane. Open-loop poles are marked with red cross - markers at s=0, s=-1, and s=-2 on the real axis. Red diamond markers indicate - imaginary axis crossings at jw = plus/minus sqrt(2) with labels showing K=6. A - dark gray square marks the breakaway point near s approximately -0.42. Light blue - thick lines highlight real axis segments between (-1, 0) and from -2 to negative - infinity. Subtle dashed gray lines show constant damping ratio guides radiating - from the origin and natural frequency semicircular arcs in the left half-plane. - Triangle-shaped direction arrows on the complex branches indicate the direction - of increasing gain. The title reads "root-locus-basic · altair · pyplots.ai" with - a subtitle describing the transfer function. A legend in the top-right identifies - the three branches.' + - Right half-plane is mostly empty, wasting some canvas space + - Damping ratio labels only shown for two of four guide lines + - Axis spines still present; removing top/right would improve visual refinement + image_description: 'The plot displays a root locus diagram on a square canvas with + equal axis scaling from -6 to 6 on both axes. Three branches are shown in distinct + colors: Branch 1 (blue) runs along the negative real axis from the origin leftward + toward negative infinity, Branch 2 (orange) curves upward into the upper half-plane, + and Branch 3 (green) curves downward into the lower half-plane. Open-loop poles + are marked with red cross markers at s=0, s=-1, and s=-2 on the real axis. Red + diamond markers indicate imaginary axis crossings at jω = ±j√2 (K=6) with bold + red labels. A gray square marks the breakaway point near s ≈ -0.42. Light dashed + gray lines show constant damping ratio guide lines radiating from the origin (ζ=0.4 + and ζ=0.8 labeled) and natural frequency arcs (semicircles for ωn=1,2,3,4). Real + axis segments of the root locus are shown as thick semi-transparent blue lines + between (-1, 0) and from -2 leftward. Triangle markers indicate direction of increasing + gain on the complex branches. Title reads "root-locus-basic · altair · pyplots.ai" + with subtitle showing the transfer function and description.' criteria_checklist: visual_quality: - score: 26 + score: 27 max: 30 items: - id: VQ-01 @@ -49,66 +48,60 @@ review: score: 7 max: 8 passed: true - comment: Font sizes explicitly set (title 28pt, axis 20pt, ticks 16pt, crossing - labels 15pt). All readable. + comment: Font sizes explicitly set throughout; damping labels slightly small + at 12pt - id: VQ-02 name: No Overlap - score: 5 + score: 6 max: 6 passed: true - comment: Minor visual crowding near origin but no text overlap. + comment: All text elements clearly separated - id: VQ-03 name: Element Visibility score: 5 max: 6 passed: true - comment: Branch lines, pole markers, and crossing markers well-sized. Guide - lines appropriately subtle. + comment: All elements visible; guide lines intentionally subtle - id: VQ-04 name: Color Accessibility score: 4 max: 4 passed: true - comment: Blue/orange/green palette is colorblind-safe. Red for critical markers - provides good contrast. + comment: Blue/orange/green colorblind-safe palette - id: VQ-05 name: Layout & Canvas score: 3 max: 4 passed: true - comment: Square 3600x3600 canvas appropriate. Axis range slightly generous - but needed for guide lines. + comment: Square canvas correct for root locus; right half mostly empty - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Real Axis (σ) and Imaginary Axis (jω) with proper engineering notation. + comment: Descriptive with engineering notation design_excellence: - score: 14 + score: 13 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 passed: true - comment: Custom palette with intentional color hierarchy. Professional appearance - above defaults. + comment: Custom palette, thoughtful typography, above defaults - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: View border removed, grid disabled, guide lines as subtle dashed - gray. + comment: View stroke removed, grid disabled, subtle guide lines - id: DE-03 name: Data Storytelling score: 4 max: 6 passed: true - comment: Visual hierarchy through color contrast, labeled crossings, direction - arrows guide the viewer. + comment: Clear engineering story with labeled crossings and breakaway spec_compliance: score: 15 max: 15 @@ -118,50 +111,47 @@ review: score: 5 max: 5 passed: true - comment: Correct root locus plot with pole trajectories in complex plane. + comment: Correct root locus plot - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All spec features present: poles, branches, arrows, crossings, real - axis segments, guide lines.' + comment: All spec features present - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Real/imaginary correctly mapped to X/Y axes. + comment: Correct real/imaginary mapping - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct. Legend labels match branch data. + comment: Correct title format and legend data_quality: - score: 15 + score: 14 max: 15 items: - id: DQ-01 name: Feature Coverage - score: 6 + score: 5 max: 6 passed: true - comment: 'Comprehensive: multiple branches, breakaway, crossings, real axis - segments, guide grid.' + comment: Shows breakaway, complex branches, crossing, real axis segments - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Classic control systems transfer function. Standard textbook example. + comment: Classic textbook transfer function - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: 'Mathematically accurate: breakaway at s≈-0.42, crossing at ω=√2 - for K=6.' + comment: Appropriate gain range and pole locations code_quality: score: 10 max: 10 @@ -171,31 +161,31 @@ review: score: 3 max: 3 passed: true - comment: Imports → data → chart layers → composition → save. No functions/classes. + comment: Flat structure, no functions/classes - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Fully deterministic polynomial root computation. + comment: Fully deterministic polynomial roots - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: 'All imports used: altair, numpy, pandas.' + comment: Only altair, numpy, pandas — all used - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean, well-organized. Necessary complexity for domain-specific plot. + comment: Clean, well-organized code - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves plot.png and plot.html. Current Altair API. + comment: Saves as plot.png and plot.html library_mastery: score: 7 max: 10 @@ -205,27 +195,26 @@ review: score: 4 max: 5 passed: true - comment: 'Good declarative Altair patterns: layered composition, encoding - types, tooltips, .interactive().' + comment: Good declarative grammar usage with layers, encodings, tooltips - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Interactive tooltips with gain values, HTML export, multi-layer composition - with varied marks. - verdict: REJECTED + comment: Layer composition, interactive exploration, tooltip encoding + verdict: APPROVED impl_tags: dependencies: [] techniques: - layer-composition - annotations - hover-tooltips + - custom-legend - html-export patterns: - data-generation - iteration-over-groups - - matrix-construction dataprep: [] styling: - grid-styling + - alpha-blending