Skip to content

Commit ef6cee2

Browse files
feat(highcharts): implement smith-chart-basic (#3874)
## Implementation: `smith-chart-basic` - highcharts Implements the **highcharts** version of `smith-chart-basic`. **File:** `plots/smith-chart-basic/implementations/highcharts.py` **Parent Issue:** #3792 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047489104)* --------- 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 31276e3 commit ef6cee2

File tree

2 files changed

+519
-0
lines changed

2 files changed

+519
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
""" pyplots.ai
2+
smith-chart-basic: Smith Chart for RF/Impedance
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-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.scatter import ScatterSeries
16+
from selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
19+
20+
# Reference impedance
21+
Z0 = 50.0
22+
23+
# Generate example impedance data: antenna impedance sweep 1-6 GHz
24+
np.random.seed(42)
25+
n_points = 50
26+
frequencies = np.linspace(1e9, 6e9, n_points) # 1-6 GHz
27+
28+
# Simulate a resonant antenna with varying impedance
29+
# Near resonance around 3.5 GHz, impedance passes through Z0
30+
f_res = 3.5e9
31+
Q = 5.0
32+
f_norm = (frequencies - f_res) / (f_res / Q)
33+
z_real = Z0 * (1 + 0.3 * np.exp(-(f_norm**2)))
34+
z_imag = Z0 * 0.8 * np.tanh(f_norm) * (1 + 0.2 * np.sin(2 * np.pi * frequencies / 1e9))
35+
36+
# Normalize impedance and convert to reflection coefficient (gamma)
37+
z_normalized = (z_real + 1j * z_imag) / Z0
38+
gamma = (z_normalized - 1) / (z_normalized + 1)
39+
40+
# Convert gamma to Cartesian coordinates for plotting
41+
gamma_x = gamma.real
42+
gamma_y = gamma.imag
43+
44+
45+
def resistance_circle(r, n_points=200):
46+
"""Generate points for a constant resistance circle on Smith chart."""
47+
center_x = r / (r + 1)
48+
radius = 1 / (r + 1)
49+
theta = np.linspace(0, 2 * np.pi, n_points)
50+
x = center_x + radius * np.cos(theta)
51+
y = radius * np.sin(theta)
52+
# Clip to unit circle
53+
valid = x**2 + y**2 <= 1.001
54+
x[~valid] = np.nan
55+
y[~valid] = np.nan
56+
return x, y
57+
58+
59+
def reactance_arc(x_val, n_points=200):
60+
"""Generate points for a constant reactance arc on Smith chart."""
61+
if abs(x_val) < 0.001:
62+
return np.array([-1, 1]), np.array([0, 0])
63+
# Center at (1, 1/x), radius |1/x|
64+
center_y = 1.0 / x_val
65+
radius = abs(1.0 / x_val)
66+
# Full circle parametrization
67+
theta = np.linspace(0, 2 * np.pi, n_points)
68+
x = 1.0 + radius * np.cos(theta)
69+
y = center_y + radius * np.sin(theta)
70+
# Clip to unit circle
71+
valid = x**2 + y**2 <= 1.001
72+
x[~valid] = np.nan
73+
y[~valid] = np.nan
74+
return x, y
75+
76+
77+
# Create chart
78+
chart = Chart(container="container")
79+
chart.options = HighchartsOptions()
80+
81+
# Chart configuration for square 1:1 aspect ratio Smith chart
82+
chart.options.chart = {
83+
"width": 3600,
84+
"height": 3600,
85+
"backgroundColor": "#ffffff",
86+
"spacingTop": 100,
87+
"spacingBottom": 200,
88+
"spacingLeft": 100,
89+
"spacingRight": 100,
90+
}
91+
92+
# Title
93+
chart.options.title = {
94+
"text": "smith-chart-basic · highcharts · pyplots.ai",
95+
"style": {"fontSize": "48px", "fontWeight": "bold"},
96+
}
97+
98+
chart.options.subtitle = {"text": "Antenna Impedance Sweep 1-6 GHz (Z₀ = 50Ω)", "style": {"fontSize": "32px"}}
99+
100+
# Axes with 1:1 aspect ratio for circular chart
101+
chart.options.x_axis = {
102+
"title": {"text": "Real(Γ)", "style": {"fontSize": "28px"}},
103+
"labels": {"style": {"fontSize": "22px"}},
104+
"min": -1.2,
105+
"max": 1.2,
106+
"tickInterval": 0.5,
107+
"gridLineWidth": 0,
108+
"lineWidth": 0,
109+
}
110+
111+
chart.options.y_axis = {
112+
"title": {"text": "Imag(Γ)", "style": {"fontSize": "28px"}},
113+
"labels": {"style": {"fontSize": "22px"}},
114+
"min": -1.2,
115+
"max": 1.2,
116+
"tickInterval": 0.5,
117+
"gridLineWidth": 0,
118+
"lineWidth": 0,
119+
}
120+
121+
# Legend configuration
122+
chart.options.legend = {
123+
"enabled": True,
124+
"itemStyle": {"fontSize": "24px"},
125+
"verticalAlign": "bottom",
126+
"layout": "horizontal",
127+
"align": "center",
128+
"y": 60,
129+
}
130+
131+
# Unit circle (|Γ| = 1) - the boundary
132+
theta = np.linspace(0, 2 * np.pi, 300)
133+
unit_x = np.cos(theta)
134+
unit_y = np.sin(theta)
135+
unit_circle = ScatterSeries()
136+
unit_circle.data = [[float(x), float(y)] for x, y in zip(unit_x, unit_y, strict=True)]
137+
unit_circle.name = "Unit Circle"
138+
unit_circle.marker = {"enabled": False}
139+
unit_circle.line_width = 3
140+
unit_circle.color = "#333333"
141+
unit_circle.enable_mouse_tracking = False
142+
unit_circle.show_in_legend = False
143+
chart.add_series(unit_circle)
144+
145+
# Resistance circles (R = 0, 0.2, 0.5, 1, 2, 5)
146+
resistance_values = [0, 0.2, 0.5, 1, 2, 5]
147+
for r in resistance_values:
148+
rx, ry = resistance_circle(r, 300)
149+
# Remove NaN values and split into valid segments
150+
valid_mask = ~np.isnan(rx) & ~np.isnan(ry)
151+
rx_valid = rx[valid_mask]
152+
ry_valid = ry[valid_mask]
153+
if len(rx_valid) > 1:
154+
r_series = ScatterSeries()
155+
r_series.data = [[float(x), float(y)] for x, y in zip(rx_valid, ry_valid, strict=True)]
156+
r_series.name = f"R={r}"
157+
r_series.marker = {"enabled": False}
158+
r_series.line_width = 1.5
159+
r_series.color = "#306998"
160+
r_series.dash_style = "Dot"
161+
r_series.enable_mouse_tracking = False
162+
r_series.show_in_legend = False
163+
chart.add_series(r_series)
164+
165+
# Reactance arcs (X = ±0.2, ±0.5, ±1, ±2)
166+
reactance_values = [0.2, 0.5, 1, 2, -0.2, -0.5, -1, -2]
167+
for x_val in reactance_values:
168+
xa, ya = reactance_arc(x_val, 300)
169+
valid_mask = ~np.isnan(xa) & ~np.isnan(ya)
170+
xa_valid = xa[valid_mask]
171+
ya_valid = ya[valid_mask]
172+
if len(xa_valid) > 1:
173+
x_series = ScatterSeries()
174+
x_series.data = [[float(x), float(y)] for x, y in zip(xa_valid, ya_valid, strict=True)]
175+
x_series.name = f"X={x_val}"
176+
x_series.marker = {"enabled": False}
177+
x_series.line_width = 1.5
178+
x_series.color = "#B8860B"
179+
x_series.dash_style = "Dot"
180+
x_series.enable_mouse_tracking = False
181+
x_series.show_in_legend = False
182+
chart.add_series(x_series)
183+
184+
# Horizontal axis (X = 0)
185+
h_axis = ScatterSeries()
186+
h_axis.data = [[-1.0, 0.0], [1.0, 0.0]]
187+
h_axis.name = "Real Axis"
188+
h_axis.marker = {"enabled": False}
189+
h_axis.line_width = 2
190+
h_axis.color = "#333333"
191+
h_axis.enable_mouse_tracking = False
192+
h_axis.show_in_legend = False
193+
chart.add_series(h_axis)
194+
195+
# VSWR circles (constant |Γ|)
196+
vswr_values = [1.5, 2.0, 3.0]
197+
vswr_colors = ["#9467BD", "#17BECF", "#8C564B"]
198+
for i, vswr in enumerate(vswr_values):
199+
gamma_mag = (vswr - 1) / (vswr + 1)
200+
v_theta = np.linspace(0, 2 * np.pi, 150)
201+
vx = gamma_mag * np.cos(v_theta)
202+
vy = gamma_mag * np.sin(v_theta)
203+
vswr_series = ScatterSeries()
204+
vswr_series.data = [[float(x), float(y)] for x, y in zip(vx, vy, strict=True)]
205+
vswr_series.name = f"VSWR={vswr:.1f}"
206+
vswr_series.marker = {"enabled": False}
207+
vswr_series.line_width = 2.5
208+
vswr_series.color = vswr_colors[i]
209+
vswr_series.dash_style = "ShortDash"
210+
vswr_series.enable_mouse_tracking = False
211+
vswr_series.show_in_legend = True
212+
chart.add_series(vswr_series)
213+
214+
# Match center marker (Z = Z0)
215+
match_series = ScatterSeries()
216+
match_series.data = [[0.0, 0.0]]
217+
match_series.name = "Matched (Z=Z₀)"
218+
match_series.marker = {"enabled": True, "radius": 14, "symbol": "diamond", "fillColor": "#27AE60"}
219+
match_series.show_in_legend = True
220+
chart.add_series(match_series)
221+
222+
# Plot impedance locus curve
223+
impedance_series = ScatterSeries()
224+
impedance_series.data = [[float(x), float(y)] for x, y in zip(gamma_x, gamma_y, strict=True)]
225+
impedance_series.name = "Impedance Locus (1-6 GHz)"
226+
impedance_series.marker = {"enabled": True, "radius": 8, "symbol": "circle"}
227+
impedance_series.line_width = 4
228+
impedance_series.color = "#E74C3C"
229+
impedance_series.show_in_legend = True
230+
chart.add_series(impedance_series)
231+
232+
# Add frequency labels at key points
233+
freq_label_indices = [0, n_points // 4, n_points // 2, 3 * n_points // 4, n_points - 1]
234+
freq_annotations = []
235+
for idx in freq_label_indices:
236+
freq_ghz = frequencies[idx] / 1e9
237+
freq_annotations.append(
238+
{
239+
"point": {"x": float(gamma_x[idx]), "y": float(gamma_y[idx]), "xAxis": 0, "yAxis": 0},
240+
"text": f"{freq_ghz:.1f} GHz",
241+
"style": {"fontSize": "22px", "fontWeight": "bold"},
242+
"backgroundColor": "rgba(255, 255, 255, 0.9)",
243+
"borderWidth": 2,
244+
"borderColor": "#333333",
245+
"padding": 10,
246+
}
247+
)
248+
249+
chart.options.annotations = [{"labels": freq_annotations, "labelOptions": {"shape": "rect"}}]
250+
251+
# Export to PNG
252+
highcharts_url = "https://code.highcharts.com/highcharts.js"
253+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
254+
highcharts_js = response.read().decode("utf-8")
255+
256+
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
257+
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
258+
highcharts_more_js = response.read().decode("utf-8")
259+
260+
annotations_url = "https://code.highcharts.com/modules/annotations.js"
261+
with urllib.request.urlopen(annotations_url, timeout=30) as response:
262+
annotations_js = response.read().decode("utf-8")
263+
264+
html_str = chart.to_js_literal()
265+
html_content = f"""<!DOCTYPE html>
266+
<html>
267+
<head>
268+
<meta charset="utf-8">
269+
<script>{highcharts_js}</script>
270+
<script>{highcharts_more_js}</script>
271+
<script>{annotations_js}</script>
272+
</head>
273+
<body style="margin:0;">
274+
<div id="container" style="width: 3600px; height: 3600px;"></div>
275+
<script>{html_str}</script>
276+
</body>
277+
</html>"""
278+
279+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
280+
f.write(html_content)
281+
temp_path = f.name
282+
283+
chrome_options = Options()
284+
chrome_options.add_argument("--headless")
285+
chrome_options.add_argument("--no-sandbox")
286+
chrome_options.add_argument("--disable-dev-shm-usage")
287+
chrome_options.add_argument("--disable-gpu")
288+
chrome_options.add_argument("--window-size=3600,3600")
289+
290+
driver = webdriver.Chrome(options=chrome_options)
291+
driver.get(f"file://{temp_path}")
292+
time.sleep(5)
293+
driver.save_screenshot("plot.png")
294+
driver.quit()
295+
296+
Path(temp_path).unlink()
297+
298+
# Save HTML for interactive version
299+
with open("plot.html", "w", encoding="utf-8") as f:
300+
f.write(html_content)

0 commit comments

Comments
 (0)