Designing Scalable Interactive Visualizations with Reflex XY: Composition, Million-Point Rendering, Streaming, Custom Marks, and Export

Disclosure: Some links in this article are affiliate links. AI Maestro may earn a commission if you make a purchase, at no…

By Vane August 8, 2026 4 min read
Designing Scalable Interactive Visualizations with Reflex XY: Composition, Million-Point Rendering, Streaming, Custom Marks, and Export

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.

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.

Scroll to Top