diff --git a/plots/campbell-basic/implementations/pygal.py b/plots/campbell-basic/implementations/pygal.py new file mode 100644 index 0000000000..284790259e --- /dev/null +++ b/plots/campbell-basic/implementations/pygal.py @@ -0,0 +1,156 @@ +""" pyplots.ai +campbell-basic: Campbell Diagram +Library: pygal 3.1.0 | Python 3.14.3 +Quality: 92/100 | Created: 2026-02-15 +""" + +import numpy as np +import pygal +from pygal.style import Style + + +# Data — natural frequencies of a rotor system vs rotational speed +np.random.seed(42) +speed_rpm = np.linspace(0, 6000, 80) +speed_hz = speed_rpm / 60 + +# Natural frequency modes (Hz) with gyroscopic effects +mode1 = 25 + 0.003 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm)) +mode2 = 48 + 0.005 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm)) +mode3 = 62 - 0.001 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm)) +mode4 = 78 - 0.002 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm)) +mode5 = 92 + 0.004 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm)) + +orders = [1, 2, 3] +modes_data = [mode1, mode2, mode3, mode4, mode5] +mode_names = ["1st Bending", "2nd Bending", "1st Torsional", "Axial", "2nd Torsional"] + +# Find critical speed intersections +critical_speeds = [] +critical_info = [] +for order in orders: + eo_freq = order * speed_hz + for mi, mode in enumerate(modes_data): + diff = eo_freq - mode + sign_changes = np.where(np.diff(np.sign(diff)))[0] + for idx in sign_changes: + frac = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1])) + rpm_interp = speed_rpm[idx] + frac * (speed_rpm[idx + 1] - speed_rpm[idx]) + freq_interp = order * rpm_interp / 60 + if 0 < rpm_interp < 6000: + critical_speeds.append((float(rpm_interp), float(freq_interp))) + critical_info.append((order, mode_names[mi])) + +# Style — stroke_width controls .reactive CSS base width for all line elements +# Setting it high ensures EO dashed lines are fully visible in CairoSVG PNG rendering +font = "DejaVu Sans, Helvetica, Arial, sans-serif" +custom_style = Style( + background="white", + plot_background="white", + foreground="#2a2a2a", + foreground_strong="#2a2a2a", + foreground_subtle="#d0d0d0", + guide_stroke_color="#e4e4e4", + guide_stroke_dasharray="4, 6", + major_guide_stroke_dasharray="2, 4", + colors=( + "#306998", # 1st Bending — Python Blue + "#1a9988", # 2nd Bending — teal + "#7b5ea7", # 1st Torsional — purple + "#d4812e", # Axial — orange + "#5a8c3c", # 2nd Torsional — green + "#b71c1c", # 1x EO — dark red + "#0d47a1", # 2x EO — bold blue + "#4a148c", # 3x EO — bold purple + "#d50000", # Critical Speeds — vivid red + ), + font_family=font, + title_font_family=font, + title_font_size=56, + label_font_size=42, + major_label_font_size=38, + legend_font_size=32, + legend_font_family=font, + value_font_size=28, + tooltip_font_size=28, + tooltip_font_family=font, + opacity=1.0, + opacity_hover=1.0, + stroke_opacity=1.0, + stroke_opacity_hover=1.0, + stroke_width=6, +) + +chart = pygal.XY( + width=4800, + height=2700, + style=custom_style, + title="campbell-basic · pygal · pyplots.ai", + x_title="Rotational Speed (RPM)", + y_title="Frequency (Hz)", + show_legend=True, + legend_at_bottom=True, + legend_at_bottom_columns=3, + legend_box_size=30, + stroke=True, + dots_size=0, + show_x_guides=True, + show_y_guides=True, + x_value_formatter=lambda x: f"{x:,.0f}", + value_formatter=lambda y: f"{y:.1f}", + margin_bottom=80, + margin_left=100, + margin_right=60, + margin_top=50, + x_label_rotation=0, + truncate_legend=-1, + range=(0, 130), + xrange=(0, 6000), + print_values=False, + print_zeroes=False, + tooltip_fancy_mode=True, + js=[], +) + +# Natural frequency mode curves — solid, thick lines with cubic interpolation +# Add label point near right end of each curve for direct labeling +for mode, label in zip(modes_data, mode_names, strict=True): + points = [] + label_idx = int(len(speed_rpm) * 0.82) + for j, (r, f) in enumerate(zip(speed_rpm, mode, strict=True)): + if j == label_idx: + points.append({"value": (float(r), float(f)), "label": label}) + else: + points.append((float(r), float(f))) + chart.add(label, points, stroke_style={"width": 10, "linecap": "round"}, show_dots=False, interpolate="cubic") + +# Engine order lines — dashed, bold, many sample points for proper rendering +# Using multiple points along each line ensures CairoSVG renders the full stroke +eo_labels = ["1× EO", "2× EO", "3× EO"] +eo_dash_patterns = ["28, 14", "20, 10, 8, 10", "14, 8"] +for order, eo_label, dash in zip(orders, eo_labels, eo_dash_patterns, strict=True): + eo_end_rpm = min(6000.0, 130.0 * 60.0 / order) + eo_end_hz = order * eo_end_rpm / 60.0 + # Generate 40 evenly spaced points so pygal renders a proper visible path + eo_rpms = np.linspace(0, eo_end_rpm, 40) + eo_freqs = order * eo_rpms / 60.0 + # Place label near 70% of line length + label_idx = int(len(eo_rpms) * 0.70) + eo_points = [] + for j, (r, f) in enumerate(zip(eo_rpms, eo_freqs, strict=True)): + if j == label_idx: + eo_points.append({"value": (float(r), float(f)), "label": eo_label}) + else: + eo_points.append((float(r), float(f))) + chart.add(eo_label, eo_points, stroke_style={"width": 8, "dasharray": dash, "linecap": "round"}, show_dots=False) + +# Critical speed markers — vivid red with tooltip showing intersection details +critical_points = [] +for pt, info in zip(critical_speeds, critical_info, strict=True): + order, mname = info + critical_points.append({"value": pt, "label": f"{mname} × {order}× EO\n{pt[0]:.0f} RPM / {pt[1]:.1f} Hz"}) +chart.add("Critical Speeds", critical_points, stroke=False, dots_size=22) + +# Render both SVG/HTML (leveraging pygal's native SVG interactivity) and PNG +chart.render_to_file("plot.html") +chart.render_to_png("plot.png") diff --git a/plots/campbell-basic/metadata/pygal.yaml b/plots/campbell-basic/metadata/pygal.yaml new file mode 100644 index 0000000000..4fde78663b --- /dev/null +++ b/plots/campbell-basic/metadata/pygal.yaml @@ -0,0 +1,238 @@ +library: pygal +specification_id: campbell-basic +created: '2026-02-15T21:09:18Z' +updated: '2026-02-15T21:36:47Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 22043026953 +issue: 4241 +python_version: 3.14.3 +library_version: 3.1.0 +preview_url: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot.html +quality_score: 92 +review: + strengths: + - 'Comprehensive Campbell Diagram with all required elements: 5 mode curves, 3 engine + order lines, and critical speed markers with detailed intersection info' + - Excellent font sizing and styling explicitly configured for 4800x2700 resolution + - all text is clearly legible + - 'Effective visual hierarchy: thick solid lines for modes, dashed lines for engine + orders, large red dots for critical speeds - viewer immediately understands the + diagram' + - Strong use of pygal-specific features including per-point label dicts, dual output + (SVG+PNG), and cubic interpolation for smooth curves + - Realistic engineering data with proper mode naming conventions and physically + plausible frequency-speed relationships + weaknesses: + - The 2x EO dark blue dashed line (#0d47a1) is somewhat similar to the 1st Bending + solid blue (#306998) - using a more contrasting color for EO lines would improve + differentiation + - Engine order lines could benefit from slightly thicker strokes or more prominent + dash patterns to stand out more distinctly from the mode curves in the PNG rendering + image_description: 'The plot displays a Campbell Diagram with the title "campbell-basic + · pygal · pyplots.ai" at the top. The X-axis shows "Rotational Speed (RPM)" ranging + from 0 to 6,000, and the Y-axis shows "Frequency (Hz)" from 0.0 to 130.0. Five + natural frequency mode curves are rendered as thick solid lines: 1st Bending (blue, + rising gently from ~25 Hz), 2nd Bending (teal, rising from ~49 Hz), 1st Torsional + (purple, declining slightly from ~62 Hz), Axial (orange, declining from ~78 Hz), + and 2nd Torsional (green, rising from ~92 Hz). Three engine order excitation lines + are drawn as dashed diagonals from the origin: 1x EO (red dashed), 2x EO (dark + blue dashed), and 3x EO (purple dashed). Large vivid red dots mark Critical Speed + intersections where engine order lines cross natural frequency curves. A legend + at the bottom in three columns identifies all nine series. The background is white + with subtle gray grid lines. The overall layout is clean and well-proportioned.' + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: All font sizes explicitly set (title=56, label=42, major_label=38, + legend=32). All text clearly readable. + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No overlapping text elements. Legend at bottom well-organized in + 3 columns. + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Mode curves stroke width 10, EO lines width 8, critical dots size + 22. All well-visible. + - id: VQ-04 + name: Color Accessibility + score: 3 + max: 4 + passed: true + comment: Colors generally distinguishable but 2x EO dark blue somewhat close + to 1st Bending blue. Mitigated by different line styles. + - id: VQ-05 + name: Layout Balance + score: 4 + max: 4 + passed: true + comment: Plot fills canvas well with balanced margins. Good utilization of + 4800x2700 canvas. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'Both axes have descriptive labels with units: Rotational Speed (RPM) + and Frequency (Hz).' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Custom color palette with intentional hierarchy, explicit font family, + subtle grid styling. Strong design above defaults. + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Custom grid dash patterns, subtle guide colors, generous margins, + clean white background. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: 'Visual hierarchy guides the reader: thick solid for modes, dashed + for EO, large red dots for critical speeds.' + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct Campbell Diagram with natural frequency curves, engine order + lines, and critical speed markers. + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 'All spec features present: 5 modes, 3 EO lines, critical speed markers + with labels.' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=RPM, Y=Frequency(Hz). Correct assignment with proper range. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title matches required format. Legend labels correctly identify all + data series. + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Shows 5 modes with increasing and decreasing frequency trends, 3 + engine orders, multiple critical speed intersections. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Rotordynamic analysis with proper engineering mode names. Authentic + domain vocabulary. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 0-6000 RPM range, 25-92 Hz base frequencies. Realistic for rotating + machinery. + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Clean flow: Imports, Seed, Data, Style, Chart, Series, Render. No + functions or classes.' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: np.random.seed(42) set at the start. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only numpy, pygal, and Style imported - all used. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean, Pythonic code with appropriately complex intersection-finding + logic. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot.png and plot.html. No deprecated API usage. + library_mastery: + score: 9 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Expert use of pygal.XY with Style, per-series stroke_style, value + formatters, legend config, cubic interpolation. + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Uses pygal-specific per-point label dicts, native SVG interactivity, + cubic interpolation, stroke_style dasharray. + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - html-export + patterns: + - data-generation + - iteration-over-groups + dataprep: + - interpolation + styling: + - grid-styling