Skip to content

Commit ce6efd0

Browse files
feat(pygal): implement nyquist-basic (#5137)
## Implementation: `nyquist-basic` - pygal Implements the **pygal** version of `nyquist-basic`. **File:** `plots/nyquist-basic/implementations/pygal.py` **Parent Issue:** #4412 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23365453282)* --------- 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 59fbc72 commit ce6efd0

2 files changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
""" pyplots.ai
2+
nyquist-basic: Nyquist Plot for Control Systems
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 76/100 | Created: 2026-03-20
5+
"""
6+
7+
import math
8+
9+
import numpy as np
10+
import pygal
11+
from pygal.style import Style
12+
13+
14+
# Data — Transfer function G(s) = 2 / [s(s+1)(s+2)]
15+
omega = np.logspace(-2, 2, 800)
16+
s = 1j * omega
17+
G = 2.0 / (s * (s + 1) * (s + 2))
18+
19+
real_part = G.real
20+
imag_part = G.imag
21+
22+
# Mirror for negative frequencies (Nyquist contour reflection)
23+
real_mirror = real_part[::-1]
24+
imag_mirror = -imag_part[::-1]
25+
26+
# Custom style
27+
custom_style = Style(
28+
background="white",
29+
plot_background="white",
30+
foreground="#333333",
31+
foreground_strong="#333333",
32+
foreground_subtle="#cccccc",
33+
colors=(
34+
"#306998", # Nyquist curve (positive freq)
35+
"#7BA1C7", # Nyquist curve (negative freq)
36+
"#E74C3C", # Critical point
37+
"#AAAAAA", # Unit circle
38+
"#306998", # Frequency annotations (same as positive curve)
39+
),
40+
title_font_size=72,
41+
label_font_size=48,
42+
major_label_font_size=42,
43+
legend_font_size=42,
44+
tooltip_font_size=36,
45+
stroke_width=4,
46+
opacity=0.9,
47+
opacity_hover=0.95,
48+
)
49+
50+
# Chart
51+
chart = pygal.XY(
52+
width=4800,
53+
height=4800,
54+
style=custom_style,
55+
title="nyquist-basic · pygal · pyplots.ai",
56+
x_title="Real",
57+
y_title="Imaginary",
58+
show_legend=True,
59+
legend_at_bottom=True,
60+
legend_box_size=36,
61+
dots_size=3,
62+
stroke=True,
63+
show_x_guides=True,
64+
show_y_guides=True,
65+
explicit_size=True,
66+
range=(-2.5, 2.5),
67+
xrange=(-2.5, 2.5),
68+
value_formatter=lambda x: f"{x:.3f}",
69+
)
70+
71+
# Positive frequency curve with tooltips showing frequency values
72+
step = 4
73+
nyquist_positive = [
74+
{"value": (float(real_part[i]), float(imag_part[i])), "label": f"ω = {omega[i]:.3f} rad/s"}
75+
for i in range(0, len(omega), step)
76+
]
77+
chart.add("G(jω), ω ≥ 0", nyquist_positive, show_dots=False, stroke_style={"width": 5})
78+
79+
# Negative frequency curve (mirror) with tooltips
80+
nyquist_negative = [
81+
{"value": (float(real_mirror[i]), float(imag_mirror[i])), "label": f"ω = -{omega[len(omega) - 1 - i]:.3f} rad/s"}
82+
for i in range(0, len(omega), step)
83+
]
84+
chart.add("G(jω), ω < 0", nyquist_negative, show_dots=False, stroke_style={"width": 4, "dasharray": "12,6"})
85+
86+
# Critical point (-1, 0)
87+
chart.add(
88+
"Critical Point (-1, 0)", [{"value": (-1.0, 0.0), "label": "Critical Point: (-1, 0)"}], stroke=False, dots_size=24
89+
)
90+
91+
# Unit circle
92+
circle_points = [
93+
{"value": (math.cos(math.radians(a)), math.sin(math.radians(a))), "label": f"{a}°"} for a in range(0, 361, 3)
94+
]
95+
chart.add("Unit Circle", circle_points, stroke=True, show_dots=False, stroke_style={"width": 3, "dasharray": "8,6"})
96+
97+
# Frequency annotations at key points along the curve (▶ markers with labels)
98+
freq_targets = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
99+
freq_annotations = []
100+
for ft in freq_targets:
101+
idx = int(np.argmin(np.abs(omega - ft)))
102+
freq_annotations.append({"value": (float(real_part[idx]), float(imag_part[idx])), "label": f"ω = {ft} rad/s"})
103+
chart.add("Frequency ω (rad/s)", freq_annotations, stroke=False, dots_size=12)
104+
105+
# Save
106+
chart.render_to_png("plot.png")
107+
chart.render_to_file("plot.html")
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
library: pygal
2+
specification_id: nyquist-basic
3+
created: '2026-03-20T22:44:57Z'
4+
updated: '2026-03-20T23:23:04Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23365453282
7+
issue: 4412
8+
python_version: 3.14.3
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/nyquist-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/nyquist-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/nyquist-basic/pygal/plot.html
13+
quality_score: 76
14+
review:
15+
strengths:
16+
- Solid transfer function choice G(s) = 2/[s(s+1)(s+2)] demonstrates a classic control
17+
systems example
18+
- Good use of pygal tooltip/label system for frequency information in interactive
19+
HTML output
20+
- Clean linear code structure with deterministic data generation
21+
- Both positive and negative frequency curves with distinct styling (solid vs dashed)
22+
- Critical point prominently highlighted with large red marker at (-1, 0)
23+
weaknesses:
24+
- Frequency annotations only exist as tooltip labels, invisible in static PNG —
25+
spec requires visible text annotations along curve
26+
- No arrows showing direction of increasing frequency on the curve as required by
27+
spec
28+
- Wrong canvas dimensions 4800x4800 instead of allowed 3600x3600 square format
29+
- Left portion of canvas underutilized with data concentrated in center-right area
30+
image_description: The plot displays a Nyquist diagram on the complex plane. The
31+
positive frequency curve (G(jω), ω ≥ 0) is rendered as a solid blue line sweeping
32+
from the origin downward to approximately (-1, -1.3) and curving back. The negative
33+
frequency mirror (G(jω), ω < 0) is shown as a dashed brown/orange line reflecting
34+
the positive curve across the real axis. A prominent red dot marks the critical
35+
point at (-1, 0). A dashed gray unit circle is centered at the origin. Several
36+
orange dots mark frequency values along the curve. The title reads "nyquist-basic
37+
· pygal · pyplots.ai" at top. Axes are labeled "Real Axis" and "Imaginary Axis".
38+
A legend at the bottom lists all series. The plot uses a white background with
39+
subtle gray grid lines and equal axis ranges of approximately -2.5 to 2.5 on both
40+
axes.
41+
criteria_checklist:
42+
visual_quality:
43+
score: 23
44+
max: 30
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 7
49+
max: 8
50+
passed: true
51+
comment: Font sizes explicitly set (title=72, label=48, major_label=42, legend=42).
52+
All text readable.
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 5
56+
max: 6
57+
passed: true
58+
comment: Main plot area clear. Minor crowding between frequency marker dots
59+
near origin.
60+
- id: VQ-03
61+
name: Element Visibility
62+
score: 5
63+
max: 6
64+
passed: true
65+
comment: Curves visible with appropriate stroke widths. Critical point large.
66+
Unit circle faint but visible.
67+
- id: VQ-04
68+
name: Color Accessibility
69+
score: 3
70+
max: 4
71+
passed: true
72+
comment: Blue, brown, red, gray palette is distinguishable. Blue vs brown
73+
could challenge some colorblind users.
74+
- id: VQ-05
75+
name: Layout & Canvas
76+
score: 1
77+
max: 4
78+
passed: false
79+
comment: Canvas 4800x4800 is not an allowed format (should be 3600x3600).
80+
Data concentrated center-right, left half mostly empty.
81+
- id: VQ-06
82+
name: Axis Labels & Title
83+
score: 2
84+
max: 2
85+
passed: true
86+
comment: Real and Imaginary are correct standard labels for Nyquist plot on
87+
complex plane.
88+
design_excellence:
89+
score: 11
90+
max: 20
91+
items:
92+
- id: DE-01
93+
name: Aesthetic Sophistication
94+
score: 5
95+
max: 8
96+
passed: true
97+
comment: Custom color palette, intentional hierarchy with stroke widths and
98+
dash patterns. Above defaults.
99+
- id: DE-02
100+
name: Visual Refinement
101+
score: 3
102+
max: 6
103+
passed: true
104+
comment: Customized grid via foreground_subtle, dash patterns for unit circle
105+
and mirror, explicit stroke widths.
106+
- id: DE-03
107+
name: Data Storytelling
108+
score: 3
109+
max: 6
110+
passed: true
111+
comment: Critical point prominently highlighted. Mirror reflection adds context.
112+
Frequency markers guide understanding.
113+
spec_compliance:
114+
score: 11
115+
max: 15
116+
items:
117+
- id: SC-01
118+
name: Plot Type
119+
score: 5
120+
max: 5
121+
passed: true
122+
comment: Correct Nyquist plot on complex plane.
123+
- id: SC-02
124+
name: Required Features
125+
score: 2
126+
max: 4
127+
passed: false
128+
comment: 'Missing: frequency text annotations visible in PNG (only tooltips).
129+
Missing: directional arrows on curve.'
130+
- id: SC-03
131+
name: Data Mapping
132+
score: 3
133+
max: 3
134+
passed: true
135+
comment: Real on X, imaginary on Y, log-spaced frequencies.
136+
- id: SC-04
137+
name: Title & Legend
138+
score: 1
139+
max: 3
140+
passed: false
141+
comment: Title format correct. Legend labels vague for static output — frequency
142+
values only in tooltips.
143+
data_quality:
144+
score: 14
145+
max: 15
146+
items:
147+
- id: DQ-01
148+
name: Feature Coverage
149+
score: 5
150+
max: 6
151+
passed: true
152+
comment: Shows positive/negative curves, critical point, unit circle. Classic
153+
transfer function.
154+
- id: DQ-02
155+
name: Realistic Context
156+
score: 5
157+
max: 5
158+
passed: true
159+
comment: Classical control systems transfer function — standard engineering
160+
domain.
161+
- id: DQ-03
162+
name: Appropriate Scale
163+
score: 4
164+
max: 4
165+
passed: true
166+
comment: Frequency range 0.01-100 rad/s, 800 log-spaced points, axis range
167+
±2.5.
168+
code_quality:
169+
score: 10
170+
max: 10
171+
items:
172+
- id: CQ-01
173+
name: KISS Structure
174+
score: 3
175+
max: 3
176+
passed: true
177+
comment: 'Linear flow: imports, data, style, chart, series, save.'
178+
- id: CQ-02
179+
name: Reproducibility
180+
score: 2
181+
max: 2
182+
passed: true
183+
comment: Fully deterministic transfer function, no randomness.
184+
- id: CQ-03
185+
name: Clean Imports
186+
score: 2
187+
max: 2
188+
passed: true
189+
comment: 'All imports used: math, numpy, pygal, Style.'
190+
- id: CQ-04
191+
name: Code Elegance
192+
score: 2
193+
max: 2
194+
passed: true
195+
comment: Clean, Pythonic. List comprehensions for data points.
196+
- id: CQ-05
197+
name: Output & API
198+
score: 1
199+
max: 1
200+
passed: true
201+
comment: Saves as plot.png via render_to_png. Current API.
202+
library_mastery:
203+
score: 7
204+
max: 10
205+
items:
206+
- id: LM-01
207+
name: Idiomatic Usage
208+
score: 4
209+
max: 5
210+
passed: true
211+
comment: Good use of pygal.XY with dict data points, custom Style, stroke_style.
212+
- id: LM-02
213+
name: Distinctive Features
214+
score: 3
215+
max: 5
216+
passed: true
217+
comment: Leverages pygal tooltips, HTML export, SVG-native stroke_style with
218+
dasharray.
219+
verdict: APPROVED
220+
impl_tags:
221+
dependencies: []
222+
techniques:
223+
- annotations
224+
- html-export
225+
patterns:
226+
- data-generation
227+
dataprep: []
228+
styling:
229+
- grid-styling

0 commit comments

Comments
 (0)