Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions plots/scatter-constellation-diagram/implementations/altair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
""" pyplots.ai
scatter-constellation-diagram: Digital Modulation Constellation Diagram
Library: altair 6.0.0 | Python 3.14.3
Quality: 87/100 | Created: 2026-03-17
"""

import altair as alt
import numpy as np
import pandas as pd


# Data
np.random.seed(42)

ideal_vals = [-3, -1, 1, 3]
ideal_i, ideal_q = np.meshgrid(ideal_vals, ideal_vals)
ideal_i = ideal_i.flatten()
ideal_q = ideal_q.flatten()

n_symbols = 1000
symbol_indices = np.random.randint(0, 16, size=n_symbols)

snr_db = 20
snr_linear = 10 ** (snr_db / 10)
signal_power = np.mean(ideal_i**2 + ideal_q**2)
noise_std = np.sqrt(signal_power / snr_linear)

received_i = ideal_i[symbol_indices] + np.random.normal(0, noise_std, n_symbols)
received_q = ideal_q[symbol_indices] + np.random.normal(0, noise_std, n_symbols)

error_vectors = np.sqrt((received_i - ideal_i[symbol_indices]) ** 2 + (received_q - ideal_q[symbol_indices]) ** 2)
rms_signal = np.sqrt(signal_power)
evm_pct = np.sqrt(np.mean(error_vectors**2)) / rms_signal * 100

# Per-symbol error magnitude for color encoding
df_received = pd.DataFrame(
{
"I": received_i,
"Q": received_q,
"Error Magnitude": error_vectors,
"Nearest I": ideal_i[symbol_indices],
"Nearest Q": ideal_q[symbol_indices],
}
)
df_ideal = pd.DataFrame({"I": ideal_i, "Q": ideal_q, "label": "Ideal"})

# Decision boundaries
boundary_vals = [-4, -2, 0, 2, 4]
boundary_h = pd.DataFrame([{"x": -5.2, "x2": 5.2, "y": v} for v in boundary_vals])
boundary_v = pd.DataFrame([{"y": -5.2, "y2": 5.2, "x": v} for v in boundary_vals])

# EVM annotation
df_evm = pd.DataFrame({"I": [4.2], "Q": [4.8], "label": [f"EVM = {evm_pct:.1f}%"]})

# Selection for interactive nearest-point highlighting
nearest = alt.selection_point(on="pointerover", nearest=True, fields=["I", "Q"], empty=False)

# Plot layers
scale_x = alt.Scale(domain=[-5.5, 5.5], nice=False)
scale_y = alt.Scale(domain=[-5.5, 5.5], nice=False)

received_layer = (
alt.Chart(df_received)
.mark_circle(size=45)
.encode(
x=alt.X("I:Q", title="In-Phase (I)", scale=scale_x),
y=alt.Y("Q:Q", title="Quadrature (Q)", scale=scale_y),
color=alt.Color(
"Error Magnitude:Q",
scale=alt.Scale(scheme="viridis"),
legend=alt.Legend(
title="Error Mag.", titleFontSize=16, labelFontSize=14, orient="right", gradientLength=200
),
),
opacity=alt.condition(nearest, alt.value(0.85), alt.value(0.3)),
size=alt.condition(nearest, alt.value(120), alt.value(45)),
tooltip=[
alt.Tooltip("I:Q", format=".3f"),
alt.Tooltip("Q:Q", format=".3f"),
alt.Tooltip("Error Magnitude:Q", format=".3f", title="Error"),
alt.Tooltip("Nearest I:Q", format=".0f", title="Ideal I"),
alt.Tooltip("Nearest Q:Q", format=".0f", title="Ideal Q"),
],
)
.add_params(nearest)
)

ideal_layer = (
alt.Chart(df_ideal)
.mark_point(size=400, filled=False, strokeWidth=3.5)
.encode(
x="I:Q",
y="Q:Q",
color=alt.value("#E45756"),
shape=alt.value("cross"),
tooltip=[alt.Tooltip("I:Q", format=".0f", title="Ideal I"), alt.Tooltip("Q:Q", format=".0f", title="Ideal Q")],
)
)

h_rules = (
alt.Chart(boundary_h)
.mark_rule(strokeDash=[8, 5], strokeWidth=1, opacity=0.35)
.encode(x=alt.X("x:Q", scale=scale_x), x2="x2:Q", y=alt.Y("y:Q", scale=scale_y), color=alt.value("#AAAAAA"))
)

v_rules = (
alt.Chart(boundary_v)
.mark_rule(strokeDash=[8, 5], strokeWidth=1, opacity=0.35)
.encode(y=alt.Y("y:Q", scale=scale_y), y2="y2:Q", x=alt.X("x:Q", scale=scale_x), color=alt.value("#AAAAAA"))
)

evm_label = (
alt.Chart(df_evm)
.mark_text(fontSize=22, fontWeight="bold", align="right", font="monospace")
.encode(x="I:Q", y="Q:Q", text="label:N", color=alt.value("#222222"))
)

chart = (
alt.layer(h_rules, v_rules, received_layer, ideal_layer, evm_label)
.properties(
width=1020,
height=1100,
title=alt.Title(
"scatter-constellation-diagram \u00b7 altair \u00b7 pyplots.ai", fontSize=28, anchor="middle", offset=12
),
)
.configure_axis(
labelFontSize=18, titleFontSize=22, tickSize=8, domainColor="#666666", tickColor="#888888", grid=False
)
.configure_view(strokeWidth=0)
)

# Save
chart.save("plot.png", scale_factor=3.0)
chart.save("plot.html")
235 changes: 235 additions & 0 deletions plots/scatter-constellation-diagram/metadata/altair.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
library: altair
specification_id: scatter-constellation-diagram
created: '2026-03-17T23:22:50Z'
updated: '2026-03-17T23:56:47Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 23220948598
issue: 4562
python_version: 3.14.3
library_version: 6.0.0
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot_thumb.png
preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-constellation-diagram/altair/plot.html
quality_score: 87
review:
strengths:
- Perfect spec compliance with all required features correctly implemented (16-QAM,
ideal crosses, received dots, decision boundaries, EVM, axis labels, title format)
- Viridis color encoding of error magnitude adds a meaningful analytical dimension
beyond spec requirements
- Interactive features (selection highlighting, conditional encoding, tooltips)
showcase Altair distinctive capabilities
- Mathematically correct EVM calculation consistent with 20 dB SNR
- Clean, well-structured code following KISS principles
weaknesses: []
image_description: The plot displays a 16-QAM constellation diagram on a near-square
canvas (1020x1100). Sixteen ideal constellation points appear as red/salmon cross
markers (#E45756) arranged in a 4x4 grid at I/Q coordinates +/-1 and +/-3. Approximately
1000 received symbols are shown as semi-transparent circles color-encoded by error
magnitude using the viridis colormap — dark purple clusters near ideal points
(low error) transitioning to teal/green and yellow for higher-error outliers.
Dashed gray decision boundary lines are drawn at I/Q values -4, -2, 0, 2, 4 on
both axes. The title "scatter-constellation-diagram · altair · pyplots.ai" is
displayed at top center in 28pt font. Axes are labeled "In-Phase (I)" (x) and
"Quadrature (Q)" (y) with tick labels at 18pt. A bold monospace "EVM = 14.0%"
annotation sits in the upper-right quadrant. A viridis color legend labeled "Error
Mag." appears on the right side. The background is white with no default grid;
view border stroke is removed. The overall layout is clean and approximately square
with the constellation geometry well-preserved.
criteria_checklist:
visual_quality:
score: 27
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: 'Font sizes explicitly set: title=28, axis titles=22, ticks=18, EVM=22,
legend title=16, legend labels=14. All readable. Legend labels slightly
small at 14pt.'
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: No text or element overlap anywhere in the plot.
- id: VQ-03
name: Element Visibility
score: 5
max: 6
passed: true
comment: Size=45, opacity=0.3 for 1000 points is within guidelines. Ideal
crosses at size=400 are prominent. Some peripheral points faint at base
opacity.
- id: VQ-04
name: Color Accessibility
score: 4
max: 4
passed: true
comment: Viridis colormap is perceptually uniform and colorblind-safe. Red
crosses provide strong luminance contrast.
- id: VQ-05
name: Layout & Canvas
score: 3
max: 4
passed: true
comment: Canvas 1020x1100 at scale 3.0 = 3060x3300 px. Improved from 2700x2700
but below 3600x3600 target. Not perfectly square.
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: In-Phase (I) and Quadrature (Q) are descriptive with standard domain
notation.
design_excellence:
score: 13
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 5
max: 8
passed: true
comment: Viridis error magnitude encoding is thoughtful. Red crosses contrast
well. Monospace EVM annotation. Above defaults but not publication-level.
- id: DE-02
name: Visual Refinement
score: 4
max: 6
passed: true
comment: Grid disabled, view stroke removed, custom domain/tick colors, dashed
decision boundaries as structural context.
- id: DE-03
name: Data Storytelling
score: 4
max: 6
passed: true
comment: Error magnitude color encoding creates visual hierarchy. EVM annotation
quantifies modulation quality. Viewer immediately understands signal integrity.
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct I/Q scatter constellation diagram for 16-QAM.
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: 'All spec features present: ideal crosses, received dots, decision
boundaries, equal aspect, EVM annotation, axis labels.'
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: I mapped to x-axis, Q to y-axis, symmetric limits centered at origin.
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title follows exact format. Legend shows Error Mag. which is appropriate.
data_quality:
score: 14
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 5
max: 6
passed: true
comment: Shows all 16 ideal points, noisy received symbols, error magnitude
via color, decision boundaries, EVM. Color adds analytical depth.
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: 16-QAM is a real digital modulation scheme used in Wi-Fi, 5G NR,
DVB. Neutral technical domain.
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Standard grid values for 16-QAM, 20 dB SNR realistic, 1000 symbols
appropriate, EVM=14.0% consistent.
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Clean linear flow: imports, data generation, plot layers, composition,
save.'
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: np.random.seed(42) set at start.
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: All three imports (altair, numpy, pandas) are used.
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Clean, well-structured. Proper EVM calculation with correct signal
processing math.
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot.png and plot.html. Current Altair 6.0 API.
library_mastery:
score: 8
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: 'Good declarative grammar: alt.layer(), proper encoding types, Scale/Legend/Title
configs, configure_axis/configure_view.'
- id: LM-02
name: Distinctive Features
score: 4
max: 5
passed: true
comment: Uses alt.selection_point for nearest-point highlighting, alt.condition
for dynamic opacity/size, rich formatted tooltips, HTML export. Distinctive
Altair features.
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- layer-composition
- annotations
- hover-tooltips
- html-export
- custom-legend
patterns:
- data-generation
dataprep: []
styling:
- custom-colormap
- alpha-blending
Loading