Skip to content

Commit 90fc2cc

Browse files
feat(pygal): implement campbell-basic (#4249)
## Implementation: `campbell-basic` - pygal Implements the **pygal** version of `campbell-basic`. **File:** `plots/campbell-basic/implementations/pygal.py` **Parent Issue:** #4241 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/22043026953)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5f00b9f commit 90fc2cc

2 files changed

Lines changed: 394 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
""" pyplots.ai
2+
campbell-basic: Campbell Diagram
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 92/100 | Created: 2026-02-15
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data — natural frequencies of a rotor system vs rotational speed
13+
np.random.seed(42)
14+
speed_rpm = np.linspace(0, 6000, 80)
15+
speed_hz = speed_rpm / 60
16+
17+
# Natural frequency modes (Hz) with gyroscopic effects
18+
mode1 = 25 + 0.003 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
19+
mode2 = 48 + 0.005 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
20+
mode3 = 62 - 0.001 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
21+
mode4 = 78 - 0.002 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
22+
mode5 = 92 + 0.004 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
23+
24+
orders = [1, 2, 3]
25+
modes_data = [mode1, mode2, mode3, mode4, mode5]
26+
mode_names = ["1st Bending", "2nd Bending", "1st Torsional", "Axial", "2nd Torsional"]
27+
28+
# Find critical speed intersections
29+
critical_speeds = []
30+
critical_info = []
31+
for order in orders:
32+
eo_freq = order * speed_hz
33+
for mi, mode in enumerate(modes_data):
34+
diff = eo_freq - mode
35+
sign_changes = np.where(np.diff(np.sign(diff)))[0]
36+
for idx in sign_changes:
37+
frac = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1]))
38+
rpm_interp = speed_rpm[idx] + frac * (speed_rpm[idx + 1] - speed_rpm[idx])
39+
freq_interp = order * rpm_interp / 60
40+
if 0 < rpm_interp < 6000:
41+
critical_speeds.append((float(rpm_interp), float(freq_interp)))
42+
critical_info.append((order, mode_names[mi]))
43+
44+
# Style — stroke_width controls .reactive CSS base width for all line elements
45+
# Setting it high ensures EO dashed lines are fully visible in CairoSVG PNG rendering
46+
font = "DejaVu Sans, Helvetica, Arial, sans-serif"
47+
custom_style = Style(
48+
background="white",
49+
plot_background="white",
50+
foreground="#2a2a2a",
51+
foreground_strong="#2a2a2a",
52+
foreground_subtle="#d0d0d0",
53+
guide_stroke_color="#e4e4e4",
54+
guide_stroke_dasharray="4, 6",
55+
major_guide_stroke_dasharray="2, 4",
56+
colors=(
57+
"#306998", # 1st Bending — Python Blue
58+
"#1a9988", # 2nd Bending — teal
59+
"#7b5ea7", # 1st Torsional — purple
60+
"#d4812e", # Axial — orange
61+
"#5a8c3c", # 2nd Torsional — green
62+
"#b71c1c", # 1x EO — dark red
63+
"#0d47a1", # 2x EO — bold blue
64+
"#4a148c", # 3x EO — bold purple
65+
"#d50000", # Critical Speeds — vivid red
66+
),
67+
font_family=font,
68+
title_font_family=font,
69+
title_font_size=56,
70+
label_font_size=42,
71+
major_label_font_size=38,
72+
legend_font_size=32,
73+
legend_font_family=font,
74+
value_font_size=28,
75+
tooltip_font_size=28,
76+
tooltip_font_family=font,
77+
opacity=1.0,
78+
opacity_hover=1.0,
79+
stroke_opacity=1.0,
80+
stroke_opacity_hover=1.0,
81+
stroke_width=6,
82+
)
83+
84+
chart = pygal.XY(
85+
width=4800,
86+
height=2700,
87+
style=custom_style,
88+
title="campbell-basic · pygal · pyplots.ai",
89+
x_title="Rotational Speed (RPM)",
90+
y_title="Frequency (Hz)",
91+
show_legend=True,
92+
legend_at_bottom=True,
93+
legend_at_bottom_columns=3,
94+
legend_box_size=30,
95+
stroke=True,
96+
dots_size=0,
97+
show_x_guides=True,
98+
show_y_guides=True,
99+
x_value_formatter=lambda x: f"{x:,.0f}",
100+
value_formatter=lambda y: f"{y:.1f}",
101+
margin_bottom=80,
102+
margin_left=100,
103+
margin_right=60,
104+
margin_top=50,
105+
x_label_rotation=0,
106+
truncate_legend=-1,
107+
range=(0, 130),
108+
xrange=(0, 6000),
109+
print_values=False,
110+
print_zeroes=False,
111+
tooltip_fancy_mode=True,
112+
js=[],
113+
)
114+
115+
# Natural frequency mode curves — solid, thick lines with cubic interpolation
116+
# Add label point near right end of each curve for direct labeling
117+
for mode, label in zip(modes_data, mode_names, strict=True):
118+
points = []
119+
label_idx = int(len(speed_rpm) * 0.82)
120+
for j, (r, f) in enumerate(zip(speed_rpm, mode, strict=True)):
121+
if j == label_idx:
122+
points.append({"value": (float(r), float(f)), "label": label})
123+
else:
124+
points.append((float(r), float(f)))
125+
chart.add(label, points, stroke_style={"width": 10, "linecap": "round"}, show_dots=False, interpolate="cubic")
126+
127+
# Engine order lines — dashed, bold, many sample points for proper rendering
128+
# Using multiple points along each line ensures CairoSVG renders the full stroke
129+
eo_labels = ["1× EO", "2× EO", "3× EO"]
130+
eo_dash_patterns = ["28, 14", "20, 10, 8, 10", "14, 8"]
131+
for order, eo_label, dash in zip(orders, eo_labels, eo_dash_patterns, strict=True):
132+
eo_end_rpm = min(6000.0, 130.0 * 60.0 / order)
133+
eo_end_hz = order * eo_end_rpm / 60.0
134+
# Generate 40 evenly spaced points so pygal renders a proper visible path
135+
eo_rpms = np.linspace(0, eo_end_rpm, 40)
136+
eo_freqs = order * eo_rpms / 60.0
137+
# Place label near 70% of line length
138+
label_idx = int(len(eo_rpms) * 0.70)
139+
eo_points = []
140+
for j, (r, f) in enumerate(zip(eo_rpms, eo_freqs, strict=True)):
141+
if j == label_idx:
142+
eo_points.append({"value": (float(r), float(f)), "label": eo_label})
143+
else:
144+
eo_points.append((float(r), float(f)))
145+
chart.add(eo_label, eo_points, stroke_style={"width": 8, "dasharray": dash, "linecap": "round"}, show_dots=False)
146+
147+
# Critical speed markers — vivid red with tooltip showing intersection details
148+
critical_points = []
149+
for pt, info in zip(critical_speeds, critical_info, strict=True):
150+
order, mname = info
151+
critical_points.append({"value": pt, "label": f"{mname} × {order}× EO\n{pt[0]:.0f} RPM / {pt[1]:.1f} Hz"})
152+
chart.add("Critical Speeds", critical_points, stroke=False, dots_size=22)
153+
154+
# Render both SVG/HTML (leveraging pygal's native SVG interactivity) and PNG
155+
chart.render_to_file("plot.html")
156+
chart.render_to_png("plot.png")
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
library: pygal
2+
specification_id: campbell-basic
3+
created: '2026-02-15T21:09:18Z'
4+
updated: '2026-02-15T21:36:47Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 22043026953
7+
issue: 4241
8+
python_version: 3.14.3
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/campbell-basic/pygal/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- 'Comprehensive Campbell Diagram with all required elements: 5 mode curves, 3 engine
17+
order lines, and critical speed markers with detailed intersection info'
18+
- Excellent font sizing and styling explicitly configured for 4800x2700 resolution
19+
- all text is clearly legible
20+
- 'Effective visual hierarchy: thick solid lines for modes, dashed lines for engine
21+
orders, large red dots for critical speeds - viewer immediately understands the
22+
diagram'
23+
- Strong use of pygal-specific features including per-point label dicts, dual output
24+
(SVG+PNG), and cubic interpolation for smooth curves
25+
- Realistic engineering data with proper mode naming conventions and physically
26+
plausible frequency-speed relationships
27+
weaknesses:
28+
- The 2x EO dark blue dashed line (#0d47a1) is somewhat similar to the 1st Bending
29+
solid blue (#306998) - using a more contrasting color for EO lines would improve
30+
differentiation
31+
- Engine order lines could benefit from slightly thicker strokes or more prominent
32+
dash patterns to stand out more distinctly from the mode curves in the PNG rendering
33+
image_description: 'The plot displays a Campbell Diagram with the title "campbell-basic
34+
· pygal · pyplots.ai" at the top. The X-axis shows "Rotational Speed (RPM)" ranging
35+
from 0 to 6,000, and the Y-axis shows "Frequency (Hz)" from 0.0 to 130.0. Five
36+
natural frequency mode curves are rendered as thick solid lines: 1st Bending (blue,
37+
rising gently from ~25 Hz), 2nd Bending (teal, rising from ~49 Hz), 1st Torsional
38+
(purple, declining slightly from ~62 Hz), Axial (orange, declining from ~78 Hz),
39+
and 2nd Torsional (green, rising from ~92 Hz). Three engine order excitation lines
40+
are drawn as dashed diagonals from the origin: 1x EO (red dashed), 2x EO (dark
41+
blue dashed), and 3x EO (purple dashed). Large vivid red dots mark Critical Speed
42+
intersections where engine order lines cross natural frequency curves. A legend
43+
at the bottom in three columns identifies all nine series. The background is white
44+
with subtle gray grid lines. The overall layout is clean and well-proportioned.'
45+
criteria_checklist:
46+
visual_quality:
47+
score: 29
48+
max: 30
49+
items:
50+
- id: VQ-01
51+
name: Text Legibility
52+
score: 8
53+
max: 8
54+
passed: true
55+
comment: All font sizes explicitly set (title=56, label=42, major_label=38,
56+
legend=32). All text clearly readable.
57+
- id: VQ-02
58+
name: No Overlap
59+
score: 6
60+
max: 6
61+
passed: true
62+
comment: No overlapping text elements. Legend at bottom well-organized in
63+
3 columns.
64+
- id: VQ-03
65+
name: Element Visibility
66+
score: 6
67+
max: 6
68+
passed: true
69+
comment: Mode curves stroke width 10, EO lines width 8, critical dots size
70+
22. All well-visible.
71+
- id: VQ-04
72+
name: Color Accessibility
73+
score: 3
74+
max: 4
75+
passed: true
76+
comment: Colors generally distinguishable but 2x EO dark blue somewhat close
77+
to 1st Bending blue. Mitigated by different line styles.
78+
- id: VQ-05
79+
name: Layout Balance
80+
score: 4
81+
max: 4
82+
passed: true
83+
comment: Plot fills canvas well with balanced margins. Good utilization of
84+
4800x2700 canvas.
85+
- id: VQ-06
86+
name: Axis Labels & Title
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: 'Both axes have descriptive labels with units: Rotational Speed (RPM)
91+
and Frequency (Hz).'
92+
design_excellence:
93+
score: 14
94+
max: 20
95+
items:
96+
- id: DE-01
97+
name: Aesthetic Sophistication
98+
score: 6
99+
max: 8
100+
passed: true
101+
comment: Custom color palette with intentional hierarchy, explicit font family,
102+
subtle grid styling. Strong design above defaults.
103+
- id: DE-02
104+
name: Visual Refinement
105+
score: 4
106+
max: 6
107+
passed: true
108+
comment: Custom grid dash patterns, subtle guide colors, generous margins,
109+
clean white background.
110+
- id: DE-03
111+
name: Data Storytelling
112+
score: 4
113+
max: 6
114+
passed: true
115+
comment: 'Visual hierarchy guides the reader: thick solid for modes, dashed
116+
for EO, large red dots for critical speeds.'
117+
spec_compliance:
118+
score: 15
119+
max: 15
120+
items:
121+
- id: SC-01
122+
name: Plot Type
123+
score: 5
124+
max: 5
125+
passed: true
126+
comment: Correct Campbell Diagram with natural frequency curves, engine order
127+
lines, and critical speed markers.
128+
- id: SC-02
129+
name: Required Features
130+
score: 4
131+
max: 4
132+
passed: true
133+
comment: 'All spec features present: 5 modes, 3 EO lines, critical speed markers
134+
with labels.'
135+
- id: SC-03
136+
name: Data Mapping
137+
score: 3
138+
max: 3
139+
passed: true
140+
comment: X=RPM, Y=Frequency(Hz). Correct assignment with proper range.
141+
- id: SC-04
142+
name: Title & Legend
143+
score: 3
144+
max: 3
145+
passed: true
146+
comment: Title matches required format. Legend labels correctly identify all
147+
data series.
148+
data_quality:
149+
score: 15
150+
max: 15
151+
items:
152+
- id: DQ-01
153+
name: Feature Coverage
154+
score: 6
155+
max: 6
156+
passed: true
157+
comment: Shows 5 modes with increasing and decreasing frequency trends, 3
158+
engine orders, multiple critical speed intersections.
159+
- id: DQ-02
160+
name: Realistic Context
161+
score: 5
162+
max: 5
163+
passed: true
164+
comment: Rotordynamic analysis with proper engineering mode names. Authentic
165+
domain vocabulary.
166+
- id: DQ-03
167+
name: Appropriate Scale
168+
score: 4
169+
max: 4
170+
passed: true
171+
comment: 0-6000 RPM range, 25-92 Hz base frequencies. Realistic for rotating
172+
machinery.
173+
code_quality:
174+
score: 10
175+
max: 10
176+
items:
177+
- id: CQ-01
178+
name: KISS Structure
179+
score: 3
180+
max: 3
181+
passed: true
182+
comment: 'Clean flow: Imports, Seed, Data, Style, Chart, Series, Render. No
183+
functions or classes.'
184+
- id: CQ-02
185+
name: Reproducibility
186+
score: 2
187+
max: 2
188+
passed: true
189+
comment: np.random.seed(42) set at the start.
190+
- id: CQ-03
191+
name: Clean Imports
192+
score: 2
193+
max: 2
194+
passed: true
195+
comment: Only numpy, pygal, and Style imported - all used.
196+
- id: CQ-04
197+
name: Code Elegance
198+
score: 2
199+
max: 2
200+
passed: true
201+
comment: Clean, Pythonic code with appropriately complex intersection-finding
202+
logic.
203+
- id: CQ-05
204+
name: Output & API
205+
score: 1
206+
max: 1
207+
passed: true
208+
comment: Saves as plot.png and plot.html. No deprecated API usage.
209+
library_mastery:
210+
score: 9
211+
max: 10
212+
items:
213+
- id: LM-01
214+
name: Idiomatic Usage
215+
score: 5
216+
max: 5
217+
passed: true
218+
comment: Expert use of pygal.XY with Style, per-series stroke_style, value
219+
formatters, legend config, cubic interpolation.
220+
- id: LM-02
221+
name: Distinctive Features
222+
score: 4
223+
max: 5
224+
passed: true
225+
comment: Uses pygal-specific per-point label dicts, native SVG interactivity,
226+
cubic interpolation, stroke_style dasharray.
227+
verdict: APPROVED
228+
impl_tags:
229+
dependencies: []
230+
techniques:
231+
- html-export
232+
patterns:
233+
- data-generation
234+
- iteration-over-groups
235+
dataprep:
236+
- interpolation
237+
styling:
238+
- grid-styling

0 commit comments

Comments
 (0)