Signal Integrity Eye Diagram — lets-plot

An eye diagram visualizes signal integrity by overlaying many periods of a digital signal onto a single time window spanning 1-2 unit intervals (UI). The overlapping traces form a characteristic eye-shaped opening whose height, width, and clarity reveal signal quality metrics such as jitter, noise, and inter-symbol interference (ISI). A wide-open eye indicates clean signal transmission, while a closed or blurred eye signals degradation.

Signal Integrity Eye Diagram rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
eye-diagram-basic: Signal Integrity Eye Diagram
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 92/100 | Updated: 2026-06-18
"""

import os

import numpy as np
import pandas as pd
from lets_plot import (
    LetsPlot,
    aes,
    element_blank,
    element_rect,
    element_text,
    geom_hline,
    geom_raster,
    geom_segment,
    geom_text,
    ggplot,
    ggsize,
    guide_colorbar,
    labs,
    layer_tooltips,
    scale_fill_gradientn,
    scale_x_continuous,
    scale_y_continuous,
    theme,
)
from lets_plot.export import ggsave


LetsPlot.setup_html()

# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Oscilloscope screen always dark (domain-appropriate regardless of theme)
SCOPE_BG = "#000004"

# Imprint palette — imprint_seq colormap anchored to scope background:
# scope black → brand green → Imprint blue (imprint_seq with dark low)
DENSITY_COLORS = ["#000004", "#009E73", "#4467A3"]

# Imprint cyan for eye measurement annotations
ANN_COLOR = "#2ABCCD"

# Data — Simulated NRZ eye diagram
np.random.seed(42)
n_traces = 400
samples_per_ui = 150
# Use 7 bits total and extract from interior to eliminate boundary artifacts
n_bits_total = 7
n_samples = samples_per_ui * n_bits_total
time_full = np.linspace(0, n_bits_total, n_samples, endpoint=False)

amplitude = 1.0
noise_sigma = 0.05 * amplitude
jitter_sigma = 0.03
steepness = 8.0 / 0.7

all_time = []
all_voltage = []

for _ in range(n_traces):
    bits = np.random.randint(0, 2, n_bits_total + 1)
    voltage = np.ones(n_samples) * bits[0] * amplitude

    for bit_idx in range(1, n_bits_total + 1):
        transition_time = bit_idx + np.random.normal(0, jitter_sigma)
        if bits[bit_idx] != bits[bit_idx - 1]:
            direction = (bits[bit_idx] - bits[bit_idx - 1]) * amplitude
            voltage += direction / (1.0 + np.exp(-steepness * (time_full - transition_time)))

    voltage += np.random.normal(0, noise_sigma, n_samples)

    # Extract 2 UI window from bits 3–5 — well away from signal boundaries
    mask = (time_full >= 3.0) & (time_full < 5.0)
    t_window = time_full[mask] - 3.0
    v_window = voltage[mask]

    all_time.extend(t_window)
    all_voltage.extend(v_window)

all_time = np.array(all_time)
all_voltage = np.array(all_voltage)

# 2D density heatmap
n_time_bins = 300
n_voltage_bins = 180
time_edges = np.linspace(0, 2.0, n_time_bins + 1)
voltage_edges = np.linspace(-0.3, 1.3, n_voltage_bins + 1)

density, _, _ = np.histogram2d(all_time, all_voltage, bins=[time_edges, voltage_edges])
density = density / density.max()

# Long-form DataFrame
time_centers = (time_edges[:-1] + time_edges[1:]) / 2
voltage_centers = (voltage_edges[:-1] + voltage_edges[1:]) / 2
time_grid, voltage_grid = np.meshgrid(time_centers, voltage_centers, indexing="ij")

df = pd.DataFrame({"time_ui": time_grid.ravel(), "voltage": voltage_grid.ravel(), "density": density.ravel()})
df = df[df["density"] > 0].reset_index(drop=True)

# Eye measurements at center column
center_col = n_time_bins // 2
center_density = density[center_col, :]
threshold = 0.05
low_density_mask = center_density < threshold
voltage_center_vals = voltage_centers[low_density_mask]
eye_region = voltage_center_vals[(voltage_center_vals > 0.15) & (voltage_center_vals < 0.85)]
eye_bottom = eye_region.min() if len(eye_region) > 0 else 0.25
eye_top = eye_region.max() if len(eye_region) > 0 else 0.75
eye_height = eye_top - eye_bottom
eye_mid_v = (eye_top + eye_bottom) / 2

mid_row = np.argmin(np.abs(voltage_centers - eye_mid_v))
row_density = density[:, mid_row]
low_density_time = time_centers[row_density < threshold]
eye_time_region = low_density_time[(low_density_time > 0.6) & (low_density_time < 1.4)]
eye_left = eye_time_region.min() if len(eye_time_region) > 0 else 0.75
eye_right = eye_time_region.max() if len(eye_time_region) > 0 else 1.25
eye_width = eye_right - eye_left

# Annotation DataFrames
height_x = 1.34
height_seg = pd.DataFrame({"x": [height_x], "y": [eye_bottom], "xend": [height_x], "yend": [eye_top]})
width_seg = pd.DataFrame({"x": [eye_left], "y": [eye_mid_v], "xend": [eye_right], "yend": [eye_mid_v]})
height_label = pd.DataFrame({"x": [height_x + 0.08], "y": [eye_mid_v], "label": [f"Eye Height: {eye_height:.2f} V"]})
width_label = pd.DataFrame(
    {"x": [(eye_left + eye_right) / 2], "y": [eye_mid_v - 0.09], "label": [f"Eye Width: {eye_width:.2f} UI"]}
)

# Title with font scaling
title = "eye-diagram-basic · python · letsplot · anyplot.ai"
title_size = round(16 * min(1.0, 67 / len(title)))

# Plot
plot = (
    ggplot(df, aes(x="time_ui", y="voltage", fill="density"))
    + geom_raster(
        tooltips=layer_tooltips()
        .format("@time_ui", ".2f")
        .format("@voltage", ".2f")
        .format("@density", ".3f")
        .line("Time: @time_ui UI")
        .line("Voltage: @voltage V")
        .line("Density: @density")
    )
    + geom_hline(yintercept=0, color=INK_SOFT, size=0.6, linetype="dashed", alpha=0.5)
    + geom_hline(yintercept=1, color=INK_SOFT, size=0.6, linetype="dashed", alpha=0.5)
    + geom_segment(
        aes(x="x", y="y", xend="xend", yend="yend"), data=height_seg, color=ANN_COLOR, size=1.2, inherit_aes=False
    )
    + geom_segment(
        aes(x="x", y="y", xend="xend", yend="yend"), data=width_seg, color=ANN_COLOR, size=1.2, inherit_aes=False
    )
    + geom_text(
        aes(x="x", y="y", label="label"), data=height_label, color=ANN_COLOR, size=5, hjust=0, inherit_aes=False
    )
    + geom_text(
        aes(x="x", y="y", label="label"), data=width_label, color=ANN_COLOR, size=5, hjust=0.5, inherit_aes=False
    )
    + scale_fill_gradientn(
        colors=DENSITY_COLORS, name="Trace\nDensity", guide=guide_colorbar(barwidth=10, barheight=200, nbin=256)
    )
    + scale_x_continuous(name="Time (UI)", breaks=[0.0, 0.5, 1.0, 1.5, 2.0], expand=[0, 0])
    + scale_y_continuous(name="Voltage (V)", breaks=[0.0, 0.5, 1.0], labels=["0.0", "0.5", "1.0"], expand=[0, 0])
    + labs(title=title)
    + theme(
        plot_title=element_text(size=title_size, face="bold", color=INK),
        axis_title=element_text(size=12, color=INK_SOFT),
        axis_text=element_text(size=10, color=INK_SOFT),
        axis_ticks=element_blank(),
        axis_line=element_blank(),
        legend_text=element_text(size=10, color=INK_SOFT),
        legend_title=element_text(size=12, face="bold", color=INK),
        panel_grid=element_blank(),
        panel_background=element_rect(fill=SCOPE_BG, color=SCOPE_BG),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        plot_margin=[40, 30, 20, 20],
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
    )
    + ggsize(800, 450)
)

# Save
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Signal Integrity Eye Diagram on anyplot.ai.

Other implementations