{"spec_id":"eye-diagram-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\neye-diagram-basic: Signal Integrity Eye Diagram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-06-18\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nimport numpy as np\n\n\n# Import pygal avoiding name collision with this filename\n_cwd = sys.path[0]\nsys.path[:] = [p for p in sys.path if p != _cwd]\npygal = importlib.import_module(\"pygal\")\nStyle = importlib.import_module(\"pygal.style\").Style\nsys.path.insert(0, _cwd)\n\n# Theme tokens (Imprint palette)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nANYPLOT_AMBER = \"#DDCC77\"  # annotation / measurement marker\n\n# Imprint sequential colormap: brand green → blue for density heatmap\n# imprint_seq: #009E73 (low density) → #4467A3 (high density)\nN_BANDS = 8\n_seq_t = np.linspace(0, 1, N_BANDS)\n_seq_start = np.array([0x00, 0x9E, 0x73])  # #009E73\n_seq_end = np.array([0x44, 0x67, 0xA3])  # #4467A3\ndensity_colors = tuple(\n    \"#\" + \"\".join(f\"{int(round(c)):02X}\" for c in (_seq_start + (_seq_end - _seq_start) * t)) for t in _seq_t\n)\nband_names = [\"Rare\", \"Low\", \"Low-Med\", \"Medium\", \"Med-High\", \"High\", \"V.High\", \"Peak\"]\n\n# Data — simulated NRZ eye diagram\nnp.random.seed(42)\n\nn_traces = 400\nsamples_per_ui = 200\ntotal_samples = samples_per_ui * 2  # 2 UI window\ntime_ui = np.linspace(0, 2, total_samples)\ntransition_width = 0.07  # UI, controls bandwidth-limited rise/fall\n\nall_voltages = []\nfor _ in range(n_traces):\n    bits = np.random.randint(0, 2, 6)\n    extended_time = np.linspace(-1, 3, samples_per_ui * 4)\n    voltage = np.zeros_like(extended_time)\n\n    for bit_idx in range(1, len(bits)):\n        boundary = bit_idx - 2\n        voltage += (bits[bit_idx] - bits[bit_idx - 1]) / (1.0 + np.exp(-(extended_time - boundary) / transition_width))\n    voltage += bits[0]\n    voltage = np.clip(voltage, -0.2, 1.2)\n\n    mask = (extended_time >= 0) & (extended_time <= 2)\n    trace_voltage = voltage[mask][:total_samples]\n\n    trace_voltage = trace_voltage + np.random.normal(0, 0.05, len(trace_voltage))\n    jittered_time = time_ui + np.random.normal(0, 0.03)\n    all_voltages.append((jittered_time, trace_voltage))\n\n# Build 2D density histogram\nn_xbins, n_ybins = 240, 140\nx_edges = np.linspace(-0.05, 2.05, n_xbins + 1)\ny_edges = np.linspace(-0.25, 1.25, n_ybins + 1)\ndensity = np.zeros((n_ybins, n_xbins))\n\nfor jittered_time, trace_voltage in all_voltages:\n    xi = ((jittered_time - x_edges[0]) / (x_edges[-1] - x_edges[0]) * n_xbins).astype(int)\n    yi = ((trace_voltage - y_edges[0]) / (y_edges[-1] - y_edges[0]) * n_ybins).astype(int)\n    valid = (xi >= 0) & (xi < n_xbins) & (yi >= 0) & (yi < n_ybins)\n    for x, y in zip(xi[valid], yi[valid], strict=True):\n        density[y, x] += 1\n\n# 4-pass 3×3 box filter for smoother density gradients\nfor _ in range(4):\n    padded = np.pad(density, 1, mode=\"edge\")\n    density = (\n        padded[:-2, :-2]\n        + padded[:-2, 1:-1]\n        + padded[:-2, 2:]\n        + padded[1:-1, :-2]\n        + padded[1:-1, 1:-1]\n        + padded[1:-1, 2:]\n        + padded[2:, :-2]\n        + padded[2:, 1:-1]\n        + padded[2:, 2:]\n    ) / 9.0\n\nmax_density = density.max()\n\n# Eye opening metrics for annotations\ncenter_col = int(0.5 * n_xbins / 2.1)\ncol_density = density[:, center_col]\ny_centers = (y_edges[:-1] + y_edges[1:]) / 2\n\nthreshold = max_density * 0.15\nlow_indices = np.where(col_density < threshold)[0]\neye_region = low_indices[(y_centers[low_indices] > 0.15) & (y_centers[low_indices] < 0.85)]\nif len(eye_region) > 2:\n    eye_bottom = y_centers[eye_region[0]]\n    eye_top = y_centers[eye_region[-1]]\n    eye_height = round(eye_top - eye_bottom, 3)\nelse:\n    eye_bottom, eye_top, eye_height = 0.2, 0.8, 0.6\n\nmid_row = int((0.5 - y_edges[0]) / (y_edges[-1] - y_edges[0]) * n_ybins)\nmid_row = np.clip(mid_row, 0, n_ybins - 1)\nrow_density = density[mid_row, :]\nx_centers = (x_edges[:-1] + x_edges[1:]) / 2\nlow_x_indices = np.where(row_density < threshold)[0]\neye_x_region = low_x_indices[(x_centers[low_x_indices] > 0.2) & (x_centers[low_x_indices] < 0.8)]\nif len(eye_x_region) > 2:\n    eye_left = x_centers[eye_x_region[0]]\n    eye_right = x_centers[eye_x_region[-1]]\n    eye_width = round(eye_right - eye_left, 3)\nelse:\n    eye_left, eye_right, eye_width = 0.3, 0.7, 0.4\n\n# Assign density cells to Imprint sequential color bands\nband_data = [[] for _ in range(N_BANDS)]\nfor yi in range(n_ybins):\n    for xi in range(n_xbins):\n        val = density[yi, xi]\n        if val < 0.2:\n            continue\n        t = min(0.999, val / (max_density * 0.55))\n        band_idx = min(int(t * N_BANDS), N_BANDS - 1)\n        x_center = round((x_edges[xi] + x_edges[xi + 1]) / 2, 3)\n        y_center = round((y_edges[yi] + y_edges[yi + 1]) / 2, 3)\n        pct = round(val / max_density * 100, 1)\n        band_data[band_idx].append(\n            {\"value\": (x_center, y_center), \"label\": f\"{x_center:.2f} UI, {y_center:.3f} V — {pct}%\"}\n        )\n\n# Eye measurement annotation series — amber for high visibility against density field\neye_height_line = [\n    {\"value\": (0.5, round(eye_bottom, 3)), \"label\": f\"Eye Height: {eye_height:.3f} V\"},\n    {\"value\": (0.5, round(eye_top, 3)), \"label\": f\"Eye Height: {eye_height:.3f} V\"},\n]\neye_width_line = [\n    {\"value\": (round(eye_left, 3), 0.5), \"label\": f\"Eye Width: {eye_width:.3f} UI\"},\n    {\"value\": (round(eye_right, 3), 0.5), \"label\": f\"Eye Width: {eye_width:.3f} UI\"},\n]\n# Rectangle bounding the eye aperture — closes the box so the open eye is the clear focal point\neye_aperture_box = [\n    {\"value\": (round(eye_left, 3), round(eye_bottom, 3)), \"label\": \"Eye Aperture\"},\n    {\"value\": (round(eye_left, 3), round(eye_top, 3)), \"label\": \"Eye Aperture\"},\n    {\"value\": (round(eye_right, 3), round(eye_top, 3)), \"label\": \"Eye Aperture\"},\n    {\"value\": (round(eye_right, 3), round(eye_bottom, 3)), \"label\": \"Eye Aperture\"},\n    {\"value\": (round(eye_left, 3), round(eye_bottom, 3)), \"label\": \"Eye Aperture\"},\n]\n\n# pygal Style — theme-adaptive with Imprint sequential palette\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    colors=density_colors + (ANYPLOT_AMBER, ANYPLOT_AMBER, ANYPLOT_AMBER),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    tooltip_font_size=32,\n    stroke_width=0,\n    font_family=\"'Courier New', monospace\",\n    title_font_family=\"'Courier New', monospace\",\n    label_font_family=\"'Courier New', monospace\",\n    legend_font_family=\"'Courier New', monospace\",\n    value_font_family=\"'Courier New', monospace\",\n)\n\n# XY scatter chart — 3200×1800 landscape canvas\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"eye-diagram-basic · python · pygal · anyplot.ai\",\n    x_title=\"Time (UI)\",\n    y_title=\"Voltage (V)\",\n    stroke=False,\n    dots_size=12,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=6,\n    legend_box_size=22,\n    show_x_guides=False,\n    show_y_guides=False,\n    x_labels=[0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],\n    range=(-0.25, 1.25),\n    x_label_rotation=0,\n    truncate_label=-1,\n    print_values=False,\n    show_dots=True,\n    margin=30,\n    margin_bottom=100,\n    spacing=15,\n    js=[],\n)\n\nfor i in range(N_BANDS):\n    chart.add(band_names[i], band_data[i] if band_data[i] else [], allow_interruptions=True)\n\nchart.add(\n    f\"Eye H {eye_height:.2f}V\",\n    eye_height_line,\n    stroke=True,\n    show_dots=True,\n    dots_size=10,\n    stroke_style={\"width\": 4, \"dasharray\": \"10, 5\"},\n    allow_interruptions=False,\n)\nchart.add(\n    f\"Eye W {eye_width:.2f}UI\",\n    eye_width_line,\n    stroke=True,\n    show_dots=True,\n    dots_size=10,\n    stroke_style={\"width\": 4, \"dasharray\": \"10, 5\"},\n    allow_interruptions=False,\n)\nchart.add(\n    \"Eye Aperture\",\n    eye_aperture_box,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"6, 4\"},\n    allow_interruptions=False,\n)\n\n# Save PNG and interactive HTML\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nsvg_content = chart.render(is_unicode=True)\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>eye-diagram-basic · python · pygal · anyplot.ai</title>\n    <style>\n        body {{ margin: 0; display: flex; justify-content: center; align-items: center;\n               min-height: 100vh; background: {PAGE_BG}; }}\n        .chart {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    <figure class=\"chart\">\n        {svg_content}\n    </figure>\n</body>\n</html>\n\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as fout:\n    fout.write(html_content)\n"}