{"spec_id":"scatter-constellation-diagram","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nscatter-constellation-diagram: Digital Modulation Constellation Diagram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-06-18\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# This file is named pygal.py and would shadow the installed pygal package.\n# Remove this script's own directory from sys.path before importing pygal.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\ndel _here\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nANYPLOT_AMBER = \"#DDCC77\"\n\n# Data — 16-QAM constellation with received symbols under additive Gaussian noise\nnp.random.seed(42)\n\nideal_vals = [-3, -1, 1, 3]\nideal_i = np.array([i for i in ideal_vals for _ in ideal_vals])\nideal_q = np.array([q for _ in ideal_vals for q in ideal_vals])\n\nn_symbols = 1000\nsymbol_indices = np.random.randint(0, 16, n_symbols)\nsnr_db = 20\nsignal_power = np.mean(ideal_i**2 + ideal_q**2)\nnoise_std = np.sqrt(signal_power * 10 ** (-snr_db / 10))\nreceived_i = ideal_i[symbol_indices] + np.random.normal(0, noise_std, n_symbols)\nreceived_q = ideal_q[symbol_indices] + np.random.normal(0, noise_std, n_symbols)\n\n# Slight phase offset for realistic impairment\nphase_offset = 0.015\nr = np.sqrt(ideal_i[symbol_indices] ** 2 + ideal_q[symbol_indices] ** 2)\nreceived_i += -phase_offset * received_q * (r / r.max())\nreceived_q += phase_offset * received_i * (r / r.max())\n\n# EVM per symbol\nerror_vectors = np.sqrt((received_i - ideal_i[symbol_indices]) ** 2 + (received_q - ideal_q[symbol_indices]) ** 2)\navg_power = np.sqrt(signal_power)\nevm_percent = float(np.mean(error_vectors) / avg_power * 100)\n\nevm_per_symbol = error_vectors / avg_power * 100\np50, p85 = np.percentile(evm_per_symbol, 50), np.percentile(evm_per_symbol, 85)\nlow_mask = evm_per_symbol <= p50\nmid_mask = (evm_per_symbol > p50) & (evm_per_symbol <= p85)\nhigh_mask = evm_per_symbol > p85\n\n# Imprint palette with semantic EVM severity coloring:\n# Low EVM = good → #009E73 (brand green), Mid EVM = caution → #DDCC77 (amber),\n# High EVM = error → #AE3030 (matte red). Decision boundaries → INK_MUTED (subtle).\n# Ideal reference points → INK (theme-adaptive neutral).\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"2, 6\",\n    colors=(\n        INK_MUTED,  # boundary vertical 1\n        INK_MUTED,  # boundary vertical 2\n        INK_MUTED,  # boundary vertical 3\n        INK_MUTED,  # boundary horizontal 1\n        INK_MUTED,  # boundary horizontal 2\n        INK_MUTED,  # boundary horizontal 3\n        \"#009E73\",  # Low EVM — green (good)\n        ANYPLOT_AMBER,  # Mid EVM — amber (caution)\n        \"#AE3030\",  # High EVM — red (error)\n        INK,  # Ideal 16-QAM — neutral reference\n    ),\n    font_family=font,\n    title_font_family=font,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    legend_font_family=font,\n    value_font_size=36,\n    tooltip_font_size=30,\n    tooltip_font_family=font,\n    opacity=0.52,\n    opacity_hover=0.95,\n    stroke_opacity=1,\n    stroke_opacity_hover=1,\n)\n\naxis_labels = [{\"value\": v, \"label\": \"0\" if v == 0 else f\"{v:+.0f}\"} for v in [-4, -3, -2, -1, 0, 1, 2, 3, 4]]\n\n# Chart — square canvas for equal aspect ratio (2400×2400 canonical)\nchart = pygal.XY(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    title=\"scatter-constellation-diagram · python · pygal · anyplot.ai\",\n    x_title=\"In-Phase (I)\",\n    y_title=\"Quadrature (Q)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=26,\n    stroke=False,\n    dots_size=5,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_labels=axis_labels,\n    y_labels=axis_labels,\n    x_value_formatter=lambda x: f\"{x:+.1f}\",\n    value_formatter=lambda y: f\"{y:+.1f}\",\n    margin_bottom=140,\n    margin_left=90,\n    margin_right=55,\n    margin_top=65,\n    range=(-5, 5),\n    xrange=(-5, 5),\n    print_values=False,\n    print_zeroes=False,\n    truncate_legend=-1,\n    js=[],\n)\n\n# Decision boundaries — dashed lines at +/-2, 0 (6 series consuming first 6 color slots)\nboundary_vals = [-2.0, 0.0, 2.0]\nfor bv in boundary_vals:\n    chart.add(\n        None,\n        [{\"value\": (bv, -4.8)}, {\"value\": (bv, 4.8)}],\n        stroke=True,\n        show_dots=False,\n        stroke_style={\"width\": 2, \"dasharray\": \"12, 8\", \"linecap\": \"butt\"},\n    )\n    chart.add(\n        None,\n        [{\"value\": (-4.8, bv)}, {\"value\": (4.8, bv)}],\n        stroke=True,\n        show_dots=False,\n        stroke_style={\"width\": 2, \"dasharray\": \"12, 8\", \"linecap\": \"butt\"},\n    )\n\n# Received symbols split by EVM magnitude (semantic Imprint colors, size encodes severity)\nlow_points = [\n    {\n        \"value\": (float(received_i[i]), float(received_q[i])),\n        \"label\": f\"I={received_i[i]:+.2f}  Q={received_q[i]:+.2f}  EVM={evm_per_symbol[i]:.1f}%\",\n    }\n    for i in range(n_symbols)\n    if low_mask[i]\n]\nmid_points = [\n    {\n        \"value\": (float(received_i[i]), float(received_q[i])),\n        \"label\": f\"I={received_i[i]:+.2f}  Q={received_q[i]:+.2f}  EVM={evm_per_symbol[i]:.1f}%\",\n    }\n    for i in range(n_symbols)\n    if mid_mask[i]\n]\nhigh_points = [\n    {\n        \"value\": (float(received_i[i]), float(received_q[i])),\n        \"label\": f\"I={received_i[i]:+.2f}  Q={received_q[i]:+.2f}  EVM={evm_per_symbol[i]:.1f}%\",\n    }\n    for i in range(n_symbols)\n    if high_mask[i]\n]\n\nchart.add(f\"Low EVM (n={len(low_points)})\", low_points, stroke=False, dots_size=5)\nchart.add(f\"Mid EVM (n={len(mid_points)})\", mid_points, stroke=False, dots_size=7)\nchart.add(f\"High EVM (n={len(high_points)})\", high_points, stroke=False, dots_size=9)\n\n# Ideal constellation points — dots_size=15 → r=\"15\" circles, replaced with crosses below\nideal_points = [\n    {\"value\": (float(ideal_i[k]), float(ideal_q[k])), \"label\": f\"Ideal ({int(ideal_i[k]):+d}, {int(ideal_q[k]):+d})\"}\n    for k in range(16)\n]\nchart.add(\"Ideal 16-QAM\", ideal_points, stroke=False, dots_size=15)\n\nsvg_content = chart.render(is_unicode=True)\n\n\n# Replace ideal point circles (r=15) with cross markers via SVG post-processing\n# pygal's SVG-native architecture makes this the idiomatic way to use custom marker shapes\ndef replace_circle_with_cross(m):\n    full = m.group(0)\n    coords = re.search(r'cx=\"([^\"]+)\".*?cy=\"([^\"]+)\"', full)\n    if not coords:\n        return full\n    cx, cy = float(coords.group(1)), float(coords.group(2))\n    arm = 20\n    return (\n        f'<g><line x1=\"{cx - arm}\" y1=\"{cy}\" x2=\"{cx + arm}\" y2=\"{cy}\" '\n        f'stroke=\"{INK}\" stroke-width=\"6\" stroke-linecap=\"round\"/>'\n        f'<line x1=\"{cx}\" y1=\"{cy - arm}\" x2=\"{cx}\" y2=\"{cy + arm}\" '\n        f'stroke=\"{INK}\" stroke-width=\"6\" stroke-linecap=\"round\"/></g>'\n    )\n\n\nsvg_content = re.sub(r'<circle[^>]*r=\"15\"[^>]*/>', replace_circle_with_cross, svg_content)\n\n# Theme-adaptive EVM annotation box\nevm_box = (\n    f'<g transform=\"translate(260, 150)\">'\n    f'<rect x=\"0\" y=\"0\" width=\"230\" height=\"50\" rx=\"6\" ry=\"6\" '\n    f'fill=\"{ELEVATED_BG}\" fill-opacity=\"0.92\" stroke=\"{INK_SOFT}\" stroke-width=\"2\"/>'\n    f'<text x=\"115\" y=\"35\" font-size=\"30\" font-family=\"{font}\" '\n    f'font-weight=\"bold\" fill=\"{INK}\" text-anchor=\"middle\">'\n    f\"EVM = {evm_percent:.1f}%</text></g>\"\n)\nsvg_content = svg_content.replace(\"</svg>\", f\"{evm_box}</svg>\")\n\n# Save PNG and interactive HTML\ncairosvg.svg2png(bytestring=svg_content.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(f\"<!DOCTYPE html><html><body style='margin:0;background:{PAGE_BG}'>{svg_content}</body></html>\")\n"}