The XY Python library now supports interactive charts that handle millions of data points without crashing the browser, using density-based rendering and streaming updates.
In this article
Core composition model
The tool allows developers to combine multiple marks, dual axes, annotations, and interactive controls within a single chart declaration. The code below installs the library in a Google Colab environment and defines a reusable rendering function. This function displays live widgets when available or falls back to standalone HTML for compatibility.
import subprocess, sys, os
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "xy"], check=True)
WIDGETS_OK = True
try:
from google.colab import output as _colab_output
_colab_output.enable_custom_widget_manager()
except Exception:
WIDGETS_OK = False
import numpy as np
import pandas as pd
import xy
from IPython.display import display, HTML
print("xy", xy.__version__, "| live widgets:", WIDGETS_OK)
def render(chart, note=""):
if note:
display(HTML(f"<h3 style='font:600 15px system-ui;margin:18px 0 6px'>{note}</h3>"))
try:
display(chart)
except Exception:
display(HTML(chart.to_html()))
return chart
rng = np.random.default_rng(7)
days = np.arange(180)
trend = 200 + 0.9 * days + 18 * np.sin(days / 9.0)
revenue = trend + rng.normal(0, 12, days.size)
sigma = 10 + 6 * np.abs(np.sin(days / 15.0))
conv = 0.06 + 0.02 * np.sin(days / 21.0) + rng.normal(0, 0.003, days.size)
peak = int(np.argmax(revenue))
layered = xy.chart(
xy.error_band(days, revenue - 1.96 * sigma, revenue + 1.96 * sigma,
name="95% band", color="#7c3aed", opacity=0.16),
xy.line(days, revenue, name="Revenue", color="#7c3aed", width=2.5,
curve="smooth"),
xy.scatter(days[::12], revenue[::12], name="Weekly check", color="#7c3aed",
size=7, stroke="#ffffff", stroke_width=1.5),
xy.line(days, conv, name="Conversion", color="#f59e0b", width=2,
dash="dashed", y_axis="y2"),
xy.x_axis(label="Day", grid=True),
xy.y_axis(label="Revenue (k)", grid=True, format=",.0f"),
xy.y_axis(id="y2", label="Conversion", side="right", grid=False, format=".1%"),
xy.x_band(120, 150, text="Campaign", color="#22c55e", opacity=0.10),
xy.hline(float(revenue.mean()), text="mean", color="#94a3b8"),
xy.callout(float(days[peak]), float(revenue[peak]), "peak", dx=-60, dy=-40),
xy.legend(loc="upper left", ncols=2, toggle=True),
xy.tooltip(title="Day", format={"y": ",.1f"}),
xy.modebar(True),
xy.theme(palette=["#7c3aed", "#f59e0b"], grid_color="#e6e6ef"),
title="Layered composition · dual axes · annotations",
width=900, height=440, crosshair=True,
)
render(layered, "1 · Composition model")
The example constructs a layered visualization containing multiple marks. It includes a 95% error band, a smooth revenue line, weekly check points, and a conversion metric on a secondary axis. Annotations highlight a campaign period and the peak revenue day. A toggleable legend and crosshair cursor complete the interactive setup.
DataFrames and faceted layouts
The library accepts Pandas DataFrames and resolves column names directly as visualization channels. The code below generates a scatter plot where the colour represents the absolute value of the y-axis. It then divides the dataset into regional facets, linking the axes across panels so selections in one view affect the others.
n = 4000
df = pd.DataFrame({
"x": rng.normal(0, 1, n),
"noise": rng.normal(0, 1, n),
"region": rng.choice(["North", "South", "East", "West"], n),
})
df["y"] = 2.1 * df["x"] + df["noise"] * 0.9
df["mag"] = np.abs(df["y"])
render(xy.scatter_chart(
xy.scatter("x", "y", color="mag", colormap="plasma",
size=5, opacity=0.7, color_domain=(0, 6)),
xy.colorbar(title="|y|"),
xy.x_axis(label="x"), xy.y_axis(label="y"),
data=df, title="Columns resolved by name", width=760, height=420,
), "2 · DataFrame-driven channels")
render(xy.facet_chart(
xy.scatter("x", "y", color="#0ea5e9", size=4, opacity=0.6),
by="region", data=df, cols=2,
share_x=True, share_y=True, link=True, link_select=True,
width=760, height=220, gap=12, title="Faceted by region",
), "3 · Facets with linked axes")
Handling million-point datasets
Performance becomes an issue with large datasets. XY switches to density-based rendering automatically when visualising 1.5 million points. The code generates a polar plot using beta and log distributions to create a dense cloud. It reports memory usage and the bytes sent for the initial paint.
N = 1_500_000
r = 6.0 * rng.beta(1.2, 3.0, N)
theta = 2.9 * np.log1p(r) + rng.integers(0, 4, N) * (np.pi / 2) + rng.normal(0, 0.05, N)
big = xy.scatter_chart(
xy.scatter(r * np.cos(theta), r * np.sin(theta),
color=np.exp(-r / 2.2), colormap="magma_r",
density=True,
size=2.5, opacity=0.85,
zoom_size_factor=2.6, zoom_opacity=0.95),
xy.colorbar(title="density"),
title=f"{N:,} points · drag to pan, scroll to zoom",
width=760, height=520, zoom=True, pan=True, wheel_zoom=True,
)
render(big, "4 · Million-point density surface")
mem = big.memory_report()
print(f"canonical f64 held in Python : {mem['canonical_bytes']/1e6:.1f} MB")
print(f"bytes sent for first paint : {mem['transport_bytes_first_paint']/1e6:.2f} MB "
f"({mem['transport_bytes_per_point']:.3f} B/point)")
print(f"compute backend : {mem['backend']}")
Selections and callbacks
Users can select exact data points from the large visualization and retrieve their original row values directly from Python. The code defines callback functions that receive browser-side selections and viewport changes while keeping the underlying data inside the kernel.




