Skip to content

Commit 89f6f3b

Browse files
Merge remote-tracking branch 'origin/main'
2 parents 5abcb3b + d5b9733 commit 89f6f3b

4 files changed

Lines changed: 977 additions & 0 deletions

File tree

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
""" pyplots.ai
2+
campbell-basic: Campbell Diagram
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 93/100 | Created: 2026-02-15
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
from highcharts_core.chart import Chart
14+
from highcharts_core.options import HighchartsOptions
15+
from highcharts_core.options.series.area import LineSeries
16+
from highcharts_core.options.series.scatter import ScatterSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data — rotordynamic natural frequencies vs rotational speed
22+
np.random.seed(42)
23+
speed_rpm = np.linspace(0, 6000, 80)
24+
speed_hz = speed_rpm / 60
25+
26+
# Natural frequency modes (Hz) — pronounced gyroscopic effects for visual interest
27+
# 1st Bending: rises with speed (forward whirl gyroscopic stiffening)
28+
mode_1_bending = 18 + 0.004 * speed_rpm + 2e-7 * speed_rpm**2 + np.random.normal(0, 0.15, len(speed_rpm))
29+
# 2nd Bending: decreases with speed (backward whirl softening)
30+
mode_2_bending = 48 - 0.003 * speed_rpm + np.random.normal(0, 0.2, len(speed_rpm))
31+
# 1st Torsional: mild rise with centrifugal stiffening
32+
mode_1_torsional = 55 + 0.002 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))
33+
# Axial: slight decrease due to thermal growth at speed
34+
mode_axial = 82 - 0.0015 * speed_rpm + np.random.normal(0, 0.18, len(speed_rpm))
35+
# 2nd Torsional: nearly constant at higher frequency
36+
mode_2_torsional = 95 + 0.0005 * speed_rpm + np.random.normal(0, 0.2, len(speed_rpm))
37+
38+
modes = {
39+
"1st Bending": mode_1_bending,
40+
"2nd Bending": mode_2_bending,
41+
"1st Torsional": mode_1_torsional,
42+
"Axial": mode_axial,
43+
"2nd Torsional": mode_2_torsional,
44+
}
45+
46+
# Engine order lines: frequency = order * speed_rpm / 60
47+
orders = [1, 2, 3]
48+
49+
# Find critical speed intersections (engine order line crosses natural frequency curve)
50+
critical_speeds = []
51+
for order in orders:
52+
eo_freq = order * speed_hz
53+
for _, mode_freq in modes.items():
54+
diff = eo_freq - mode_freq
55+
sign_changes = np.where(np.diff(np.sign(diff)))[0]
56+
for idx in sign_changes:
57+
frac = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1]))
58+
rpm_val = speed_rpm[idx] + frac * (speed_rpm[idx + 1] - speed_rpm[idx])
59+
freq_val = order * rpm_val / 60
60+
critical_speeds.append((float(rpm_val), float(freq_val)))
61+
62+
# Vertical plotLines at critical speeds
63+
crit_rpm_values = sorted({round(rpm, 0) for rpm, _ in critical_speeds})
64+
crit_plot_lines = [
65+
{"value": rpm, "color": "rgba(231, 76, 60, 0.15)", "width": 2, "dashStyle": "ShortDot", "zIndex": 0}
66+
for rpm in crit_rpm_values[:6]
67+
]
68+
69+
# Chart
70+
chart = Chart(container="container")
71+
chart.options = HighchartsOptions()
72+
73+
chart.options.chart = {
74+
"width": 4800,
75+
"height": 2700,
76+
"backgroundColor": "#fafbfc",
77+
"style": {"fontFamily": "'Segoe UI', Helvetica, Arial, sans-serif"},
78+
"spacing": [60, 80, 80, 60],
79+
}
80+
81+
chart.options.title = {
82+
"text": "campbell-basic \u00b7 highcharts \u00b7 pyplots.ai",
83+
"style": {"fontSize": "64px", "fontWeight": "600", "color": "#1a1a2e", "letterSpacing": "1px"},
84+
"margin": 40,
85+
}
86+
87+
chart.options.subtitle = {
88+
"text": "Natural Frequencies vs Rotational Speed \u2014 Turbine Rotor",
89+
"style": {"fontSize": "38px", "color": "#6c757d", "fontWeight": "400"},
90+
}
91+
92+
# Highcharts-specific: plotBands for operating zones + plotLines at critical speeds
93+
chart.options.x_axis = {
94+
"title": {
95+
"text": "Rotational Speed (RPM)",
96+
"style": {"fontSize": "44px", "color": "#2d3436", "fontWeight": "500"},
97+
"margin": 25,
98+
},
99+
"labels": {"style": {"fontSize": "34px", "color": "#6c757d"}, "format": "{value}"},
100+
"min": 0,
101+
"max": 6000,
102+
"tickInterval": 1000,
103+
"gridLineWidth": 1,
104+
"gridLineColor": "rgba(0, 0, 0, 0.05)",
105+
"gridLineDashStyle": "Dot",
106+
"lineWidth": 0,
107+
"tickWidth": 0,
108+
"plotBands": [
109+
{
110+
"from": 0,
111+
"to": 800,
112+
"color": "rgba(46, 204, 113, 0.07)",
113+
"label": {
114+
"text": "Idle",
115+
"style": {"fontSize": "28px", "color": "#6c757d", "fontWeight": "400"},
116+
"verticalAlign": "bottom",
117+
"y": -15,
118+
},
119+
},
120+
{
121+
"from": 2800,
122+
"to": 4200,
123+
"color": "rgba(52, 152, 219, 0.06)",
124+
"label": {
125+
"text": "Normal Operating Range",
126+
"style": {"fontSize": "28px", "color": "#6c757d", "fontWeight": "400"},
127+
"verticalAlign": "bottom",
128+
"y": -15,
129+
},
130+
},
131+
],
132+
"plotLines": crit_plot_lines,
133+
}
134+
135+
chart.options.y_axis = {
136+
"title": {
137+
"text": "Frequency (Hz)",
138+
"style": {"fontSize": "44px", "color": "#2d3436", "fontWeight": "500"},
139+
"margin": 25,
140+
},
141+
"labels": {"style": {"fontSize": "34px", "color": "#6c757d"}},
142+
"min": 0,
143+
"max": 120,
144+
"tickInterval": 10,
145+
"gridLineWidth": 1,
146+
"gridLineColor": "rgba(0, 0, 0, 0.05)",
147+
"gridLineDashStyle": "Dot",
148+
"lineWidth": 0,
149+
}
150+
151+
chart.options.legend = {
152+
"enabled": True,
153+
"align": "right",
154+
"verticalAlign": "top",
155+
"layout": "vertical",
156+
"x": -30,
157+
"y": 60,
158+
"floating": True,
159+
"backgroundColor": "rgba(255, 255, 255, 0.95)",
160+
"borderWidth": 1,
161+
"borderColor": "#dee2e6",
162+
"borderRadius": 10,
163+
"shadow": {"color": "rgba(0,0,0,0.06)", "offsetX": 2, "offsetY": 2, "width": 8},
164+
"itemStyle": {"fontSize": "30px", "fontWeight": "400", "color": "#2d3436"},
165+
"padding": 20,
166+
"itemMarginTop": 6,
167+
"itemMarginBottom": 6,
168+
}
169+
170+
chart.options.credits = {"enabled": False}
171+
172+
chart.options.tooltip = {
173+
"headerFormat": "",
174+
"pointFormat": (
175+
'<span style="font-size:24px;color:{series.color}">\u25cf</span> '
176+
'<span style="font-size:26px">'
177+
"{series.name}<br/>"
178+
"Speed: <b>{point.x:.0f} RPM</b><br/>"
179+
"Frequency: <b>{point.y:.1f} Hz</b></span>"
180+
),
181+
"backgroundColor": "rgba(255, 255, 255, 0.96)",
182+
"borderRadius": 10,
183+
"borderWidth": 1,
184+
"shadow": {"color": "rgba(0,0,0,0.08)", "offsetX": 1, "offsetY": 1, "width": 4},
185+
"style": {"fontSize": "26px"},
186+
}
187+
188+
# Mode colors — refined palette starting with Python Blue
189+
mode_colors = ["#306998", "#27ae60", "#8e44ad", "#d35400", "#2980b9"]
190+
191+
# Natural frequency mode series
192+
for i, (mode_name, mode_freq) in enumerate(modes.items()):
193+
series = LineSeries()
194+
series.data = [[float(r), float(f)] for r, f in zip(speed_rpm, mode_freq, strict=False)]
195+
series.name = mode_name
196+
series.color = mode_colors[i]
197+
series.line_width = 5
198+
series.marker = {"enabled": False}
199+
series.z_index = 3
200+
chart.add_series(series)
201+
202+
# Engine order lines — darker color, on-chart labels via Highcharts dataLabels filter
203+
eo_color = "#5a6c7d"
204+
for order in orders:
205+
eo_freq = order * speed_hz
206+
mask = eo_freq <= 120
207+
eo_data = [[float(r), float(f)] for r, f in zip(speed_rpm[mask], eo_freq[mask], strict=False)]
208+
series = LineSeries()
209+
series.data = eo_data
210+
series.name = f"{order}x EO"
211+
series.color = eo_color
212+
series.line_width = 3
213+
series.dash_style = "LongDash"
214+
series.marker = {"enabled": False}
215+
series.enable_mouse_tracking = False
216+
series.z_index = 1
217+
# Highcharts-specific: dataLabels with filter to label only the last point on-chart
218+
series.data_labels = {
219+
"enabled": True,
220+
"format": f"{order}x",
221+
"style": {"fontSize": "30px", "fontWeight": "600", "color": "#5a6c7d", "textOutline": "3px white"},
222+
"align": "left",
223+
"x": 10,
224+
"y": -5,
225+
"filter": {"property": "x", "operator": ">", "value": float(speed_rpm[mask][-2])},
226+
}
227+
chart.add_series(series)
228+
229+
# Critical speed intersection markers
230+
if critical_speeds:
231+
crit_series = ScatterSeries()
232+
crit_series.data = [[rpm, freq] for rpm, freq in critical_speeds]
233+
crit_series.name = "Critical Speeds"
234+
crit_series.color = "#e74c3c"
235+
crit_series.marker = {
236+
"radius": 16,
237+
"symbol": "diamond",
238+
"lineWidth": 3,
239+
"lineColor": "#c0392b",
240+
"fillColor": "#e74c3c",
241+
"states": {"hover": {"radiusPlus": 5, "lineWidthPlus": 2}},
242+
}
243+
crit_series.z_index = 5
244+
chart.add_series(crit_series)
245+
246+
# Download Highcharts JS and annotations module
247+
highcharts_url = "https://code.highcharts.com/highcharts.js"
248+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
249+
highcharts_js = response.read().decode("utf-8")
250+
251+
annotations_url = "https://code.highcharts.com/modules/annotations.js"
252+
with urllib.request.urlopen(annotations_url, timeout=30) as response:
253+
annotations_js = response.read().decode("utf-8")
254+
255+
html_str = chart.to_js_literal()
256+
257+
# Save interactive HTML (CDN scripts)
258+
with open("plot.html", "w", encoding="utf-8") as f:
259+
f.write(f"""<!DOCTYPE html>
260+
<html>
261+
<head>
262+
<meta charset="utf-8">
263+
<script src="https://code.highcharts.com/highcharts.js"></script>
264+
<script src="https://code.highcharts.com/modules/annotations.js"></script>
265+
</head>
266+
<body style="margin:0; background:#fafbfc;">
267+
<div id="container" style="width: 100%; height: 100vh;"></div>
268+
<script>{html_str}</script>
269+
</body>
270+
</html>""")
271+
272+
# PNG via headless Chrome (inline scripts)
273+
png_html = f"""<!DOCTYPE html>
274+
<html>
275+
<head>
276+
<meta charset="utf-8">
277+
<script>{highcharts_js}</script>
278+
<script>{annotations_js}</script>
279+
</head>
280+
<body style="margin:0; background:#fafbfc;">
281+
<div id="container" style="width: 4800px; height: 2700px;"></div>
282+
<script>{html_str}</script>
283+
</body>
284+
</html>"""
285+
286+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
287+
f.write(png_html)
288+
temp_path = f.name
289+
290+
chrome_options = Options()
291+
for arg in ["--headless", "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--window-size=4800,2700"]:
292+
chrome_options.add_argument(arg)
293+
294+
driver = webdriver.Chrome(options=chrome_options)
295+
driver.get(f"file://{temp_path}")
296+
time.sleep(5)
297+
298+
container = driver.find_element("id", "container")
299+
container.screenshot("plot.png")
300+
driver.quit()
301+
302+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)