diff --git a/plots/scatter-constellation-diagram/implementations/altair.py b/plots/scatter-constellation-diagram/implementations/altair.py new file mode 100644 index 0000000000..6bf7201cee --- /dev/null +++ b/plots/scatter-constellation-diagram/implementations/altair.py @@ -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") diff --git a/plots/scatter-constellation-diagram/metadata/altair.yaml b/plots/scatter-constellation-diagram/metadata/altair.yaml new file mode 100644 index 0000000000..b2d6519f28 --- /dev/null +++ b/plots/scatter-constellation-diagram/metadata/altair.yaml @@ -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