Skip to content

Commit 9af2f0a

Browse files
feat(altair): implement scatter-constellation-diagram (#4970)
## Implementation: `scatter-constellation-diagram` - altair Implements the **altair** version of `scatter-constellation-diagram`. **File:** `plots/scatter-constellation-diagram/implementations/altair.py` **Parent Issue:** #4562 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23220948598)* --------- 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 1dc42cb commit 9af2f0a

File tree

2 files changed

+370
-0
lines changed

2 files changed

+370
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
""" pyplots.ai
2+
scatter-constellation-diagram: Digital Modulation Constellation Diagram
3+
Library: altair 6.0.0 | Python 3.14.3
4+
Quality: 87/100 | Created: 2026-03-17
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data
13+
np.random.seed(42)
14+
15+
ideal_vals = [-3, -1, 1, 3]
16+
ideal_i, ideal_q = np.meshgrid(ideal_vals, ideal_vals)
17+
ideal_i = ideal_i.flatten()
18+
ideal_q = ideal_q.flatten()
19+
20+
n_symbols = 1000
21+
symbol_indices = np.random.randint(0, 16, size=n_symbols)
22+
23+
snr_db = 20
24+
snr_linear = 10 ** (snr_db / 10)
25+
signal_power = np.mean(ideal_i**2 + ideal_q**2)
26+
noise_std = np.sqrt(signal_power / snr_linear)
27+
28+
received_i = ideal_i[symbol_indices] + np.random.normal(0, noise_std, n_symbols)
29+
received_q = ideal_q[symbol_indices] + np.random.normal(0, noise_std, n_symbols)
30+
31+
error_vectors = np.sqrt((received_i - ideal_i[symbol_indices]) ** 2 + (received_q - ideal_q[symbol_indices]) ** 2)
32+
rms_signal = np.sqrt(signal_power)
33+
evm_pct = np.sqrt(np.mean(error_vectors**2)) / rms_signal * 100
34+
35+
# Per-symbol error magnitude for color encoding
36+
df_received = pd.DataFrame(
37+
{
38+
"I": received_i,
39+
"Q": received_q,
40+
"Error Magnitude": error_vectors,
41+
"Nearest I": ideal_i[symbol_indices],
42+
"Nearest Q": ideal_q[symbol_indices],
43+
}
44+
)
45+
df_ideal = pd.DataFrame({"I": ideal_i, "Q": ideal_q, "label": "Ideal"})
46+
47+
# Decision boundaries
48+
boundary_vals = [-4, -2, 0, 2, 4]
49+
boundary_h = pd.DataFrame([{"x": -5.2, "x2": 5.2, "y": v} for v in boundary_vals])
50+
boundary_v = pd.DataFrame([{"y": -5.2, "y2": 5.2, "x": v} for v in boundary_vals])
51+
52+
# EVM annotation
53+
df_evm = pd.DataFrame({"I": [4.2], "Q": [4.8], "label": [f"EVM = {evm_pct:.1f}%"]})
54+
55+
# Selection for interactive nearest-point highlighting
56+
nearest = alt.selection_point(on="pointerover", nearest=True, fields=["I", "Q"], empty=False)
57+
58+
# Plot layers
59+
scale_x = alt.Scale(domain=[-5.5, 5.5], nice=False)
60+
scale_y = alt.Scale(domain=[-5.5, 5.5], nice=False)
61+
62+
received_layer = (
63+
alt.Chart(df_received)
64+
.mark_circle(size=45)
65+
.encode(
66+
x=alt.X("I:Q", title="In-Phase (I)", scale=scale_x),
67+
y=alt.Y("Q:Q", title="Quadrature (Q)", scale=scale_y),
68+
color=alt.Color(
69+
"Error Magnitude:Q",
70+
scale=alt.Scale(scheme="viridis"),
71+
legend=alt.Legend(
72+
title="Error Mag.", titleFontSize=16, labelFontSize=14, orient="right", gradientLength=200
73+
),
74+
),
75+
opacity=alt.condition(nearest, alt.value(0.85), alt.value(0.3)),
76+
size=alt.condition(nearest, alt.value(120), alt.value(45)),
77+
tooltip=[
78+
alt.Tooltip("I:Q", format=".3f"),
79+
alt.Tooltip("Q:Q", format=".3f"),
80+
alt.Tooltip("Error Magnitude:Q", format=".3f", title="Error"),
81+
alt.Tooltip("Nearest I:Q", format=".0f", title="Ideal I"),
82+
alt.Tooltip("Nearest Q:Q", format=".0f", title="Ideal Q"),
83+
],
84+
)
85+
.add_params(nearest)
86+
)
87+
88+
ideal_layer = (
89+
alt.Chart(df_ideal)
90+
.mark_point(size=400, filled=False, strokeWidth=3.5)
91+
.encode(
92+
x="I:Q",
93+
y="Q:Q",
94+
color=alt.value("#E45756"),
95+
shape=alt.value("cross"),
96+
tooltip=[alt.Tooltip("I:Q", format=".0f", title="Ideal I"), alt.Tooltip("Q:Q", format=".0f", title="Ideal Q")],
97+
)
98+
)
99+
100+
h_rules = (
101+
alt.Chart(boundary_h)
102+
.mark_rule(strokeDash=[8, 5], strokeWidth=1, opacity=0.35)
103+
.encode(x=alt.X("x:Q", scale=scale_x), x2="x2:Q", y=alt.Y("y:Q", scale=scale_y), color=alt.value("#AAAAAA"))
104+
)
105+
106+
v_rules = (
107+
alt.Chart(boundary_v)
108+
.mark_rule(strokeDash=[8, 5], strokeWidth=1, opacity=0.35)
109+
.encode(y=alt.Y("y:Q", scale=scale_y), y2="y2:Q", x=alt.X("x:Q", scale=scale_x), color=alt.value("#AAAAAA"))
110+
)
111+
112+
evm_label = (
113+
alt.Chart(df_evm)
114+
.mark_text(fontSize=22, fontWeight="bold", align="right", font="monospace")
115+
.encode(x="I:Q", y="Q:Q", text="label:N", color=alt.value("#222222"))
116+
)
117+
118+
chart = (
119+
alt.layer(h_rules, v_rules, received_layer, ideal_layer, evm_label)
120+
.properties(
121+
width=1020,
122+
height=1100,
123+
title=alt.Title(
124+
"scatter-constellation-diagram \u00b7 altair \u00b7 pyplots.ai", fontSize=28, anchor="middle", offset=12
125+
),
126+
)
127+
.configure_axis(
128+
labelFontSize=18, titleFontSize=22, tickSize=8, domainColor="#666666", tickColor="#888888", grid=False
129+
)
130+
.configure_view(strokeWidth=0)
131+
)
132+
133+
# Save
134+
chart.save("plot.png", scale_factor=3.0)
135+
chart.save("plot.html")
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
library: altair
2+
specification_id: scatter-constellation-diagram
3+
created: '2026-03-17T23:22:50Z'
4+
updated: '2026-03-17T23:56:47Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23220948598
7+
issue: 4562
8+
python_version: 3.14.3
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot.html
13+
quality_score: 87
14+
review:
15+
strengths:
16+
- Perfect spec compliance with all required features correctly implemented (16-QAM,
17+
ideal crosses, received dots, decision boundaries, EVM, axis labels, title format)
18+
- Viridis color encoding of error magnitude adds a meaningful analytical dimension
19+
beyond spec requirements
20+
- Interactive features (selection highlighting, conditional encoding, tooltips)
21+
showcase Altair distinctive capabilities
22+
- Mathematically correct EVM calculation consistent with 20 dB SNR
23+
- Clean, well-structured code following KISS principles
24+
weaknesses: []
25+
image_description: The plot displays a 16-QAM constellation diagram on a near-square
26+
canvas (1020x1100). Sixteen ideal constellation points appear as red/salmon cross
27+
markers (#E45756) arranged in a 4x4 grid at I/Q coordinates +/-1 and +/-3. Approximately
28+
1000 received symbols are shown as semi-transparent circles color-encoded by error
29+
magnitude using the viridis colormap — dark purple clusters near ideal points
30+
(low error) transitioning to teal/green and yellow for higher-error outliers.
31+
Dashed gray decision boundary lines are drawn at I/Q values -4, -2, 0, 2, 4 on
32+
both axes. The title "scatter-constellation-diagram · altair · pyplots.ai" is
33+
displayed at top center in 28pt font. Axes are labeled "In-Phase (I)" (x) and
34+
"Quadrature (Q)" (y) with tick labels at 18pt. A bold monospace "EVM = 14.0%"
35+
annotation sits in the upper-right quadrant. A viridis color legend labeled "Error
36+
Mag." appears on the right side. The background is white with no default grid;
37+
view border stroke is removed. The overall layout is clean and approximately square
38+
with the constellation geometry well-preserved.
39+
criteria_checklist:
40+
visual_quality:
41+
score: 27
42+
max: 30
43+
items:
44+
- id: VQ-01
45+
name: Text Legibility
46+
score: 7
47+
max: 8
48+
passed: true
49+
comment: 'Font sizes explicitly set: title=28, axis titles=22, ticks=18, EVM=22,
50+
legend title=16, legend labels=14. All readable. Legend labels slightly
51+
small at 14pt.'
52+
- id: VQ-02
53+
name: No Overlap
54+
score: 6
55+
max: 6
56+
passed: true
57+
comment: No text or element overlap anywhere in the plot.
58+
- id: VQ-03
59+
name: Element Visibility
60+
score: 5
61+
max: 6
62+
passed: true
63+
comment: Size=45, opacity=0.3 for 1000 points is within guidelines. Ideal
64+
crosses at size=400 are prominent. Some peripheral points faint at base
65+
opacity.
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 4
69+
max: 4
70+
passed: true
71+
comment: Viridis colormap is perceptually uniform and colorblind-safe. Red
72+
crosses provide strong luminance contrast.
73+
- id: VQ-05
74+
name: Layout & Canvas
75+
score: 3
76+
max: 4
77+
passed: true
78+
comment: Canvas 1020x1100 at scale 3.0 = 3060x3300 px. Improved from 2700x2700
79+
but below 3600x3600 target. Not perfectly square.
80+
- id: VQ-06
81+
name: Axis Labels & Title
82+
score: 2
83+
max: 2
84+
passed: true
85+
comment: In-Phase (I) and Quadrature (Q) are descriptive with standard domain
86+
notation.
87+
design_excellence:
88+
score: 13
89+
max: 20
90+
items:
91+
- id: DE-01
92+
name: Aesthetic Sophistication
93+
score: 5
94+
max: 8
95+
passed: true
96+
comment: Viridis error magnitude encoding is thoughtful. Red crosses contrast
97+
well. Monospace EVM annotation. Above defaults but not publication-level.
98+
- id: DE-02
99+
name: Visual Refinement
100+
score: 4
101+
max: 6
102+
passed: true
103+
comment: Grid disabled, view stroke removed, custom domain/tick colors, dashed
104+
decision boundaries as structural context.
105+
- id: DE-03
106+
name: Data Storytelling
107+
score: 4
108+
max: 6
109+
passed: true
110+
comment: Error magnitude color encoding creates visual hierarchy. EVM annotation
111+
quantifies modulation quality. Viewer immediately understands signal integrity.
112+
spec_compliance:
113+
score: 15
114+
max: 15
115+
items:
116+
- id: SC-01
117+
name: Plot Type
118+
score: 5
119+
max: 5
120+
passed: true
121+
comment: Correct I/Q scatter constellation diagram for 16-QAM.
122+
- id: SC-02
123+
name: Required Features
124+
score: 4
125+
max: 4
126+
passed: true
127+
comment: 'All spec features present: ideal crosses, received dots, decision
128+
boundaries, equal aspect, EVM annotation, axis labels.'
129+
- id: SC-03
130+
name: Data Mapping
131+
score: 3
132+
max: 3
133+
passed: true
134+
comment: I mapped to x-axis, Q to y-axis, symmetric limits centered at origin.
135+
- id: SC-04
136+
name: Title & Legend
137+
score: 3
138+
max: 3
139+
passed: true
140+
comment: Title follows exact format. Legend shows Error Mag. which is appropriate.
141+
data_quality:
142+
score: 14
143+
max: 15
144+
items:
145+
- id: DQ-01
146+
name: Feature Coverage
147+
score: 5
148+
max: 6
149+
passed: true
150+
comment: Shows all 16 ideal points, noisy received symbols, error magnitude
151+
via color, decision boundaries, EVM. Color adds analytical depth.
152+
- id: DQ-02
153+
name: Realistic Context
154+
score: 5
155+
max: 5
156+
passed: true
157+
comment: 16-QAM is a real digital modulation scheme used in Wi-Fi, 5G NR,
158+
DVB. Neutral technical domain.
159+
- id: DQ-03
160+
name: Appropriate Scale
161+
score: 4
162+
max: 4
163+
passed: true
164+
comment: Standard grid values for 16-QAM, 20 dB SNR realistic, 1000 symbols
165+
appropriate, EVM=14.0% consistent.
166+
code_quality:
167+
score: 10
168+
max: 10
169+
items:
170+
- id: CQ-01
171+
name: KISS Structure
172+
score: 3
173+
max: 3
174+
passed: true
175+
comment: 'Clean linear flow: imports, data generation, plot layers, composition,
176+
save.'
177+
- id: CQ-02
178+
name: Reproducibility
179+
score: 2
180+
max: 2
181+
passed: true
182+
comment: np.random.seed(42) set at start.
183+
- id: CQ-03
184+
name: Clean Imports
185+
score: 2
186+
max: 2
187+
passed: true
188+
comment: All three imports (altair, numpy, pandas) are used.
189+
- id: CQ-04
190+
name: Code Elegance
191+
score: 2
192+
max: 2
193+
passed: true
194+
comment: Clean, well-structured. Proper EVM calculation with correct signal
195+
processing math.
196+
- id: CQ-05
197+
name: Output & API
198+
score: 1
199+
max: 1
200+
passed: true
201+
comment: Saves as plot.png and plot.html. Current Altair 6.0 API.
202+
library_mastery:
203+
score: 8
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 declarative grammar: alt.layer(), proper encoding types, Scale/Legend/Title
212+
configs, configure_axis/configure_view.'
213+
- id: LM-02
214+
name: Distinctive Features
215+
score: 4
216+
max: 5
217+
passed: true
218+
comment: Uses alt.selection_point for nearest-point highlighting, alt.condition
219+
for dynamic opacity/size, rich formatted tooltips, HTML export. Distinctive
220+
Altair features.
221+
verdict: APPROVED
222+
impl_tags:
223+
dependencies: []
224+
techniques:
225+
- layer-composition
226+
- annotations
227+
- hover-tooltips
228+
- html-export
229+
- custom-legend
230+
patterns:
231+
- data-generation
232+
dataprep: []
233+
styling:
234+
- custom-colormap
235+
- alpha-blending

0 commit comments

Comments
 (0)