跳到主内容
@wquguru
精选75MarkTechPost(RSS)技巧与观点

用 XY 库构建百万点可交互可视化:组合、流式与导出

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

原文
发到 X

In this tutorial, we explore the advanced visualization capabilities of the XY Python library by building interactive, scalable, and extensible charts. We begin with XY’s composition model, where we combine multiple marks, dual axes, annotations, tooltips, legends, themes, and interactive controls within a single chart declaration. We then work with Pandas DataFrames, faceted layouts, linked viewports, and million-point datasets that automatically switch to density-based rendering for efficient exploration. We also connect browser interactions back to Python through selections and callbacks, update charts dynamically through streaming, customize visual components with DOM slots and CSS, and extend the library with a reusable custom trendline mark. Also, we use the Matplotlib-compatible interface and export our visualizations as standalone HTML, SVG, and PNG files.

在本教程中,我们通过构建交互式、可扩展且可扩展的图表,探索 XY Python 库的高级可视化功能。我们从 XY 的组合模型开始,在单个图表声明中组合多个标记、双轴、注释、工具提示、图例、主题和交互控件。然后,我们使用 Pandas DataFrame、分面布局、链接视口和百万点数据集,这些数据集会自动切换到基于密度的渲染以实现高效探索。我们还通过选择和回调将浏览器交互连接回 Python,通过流式更新动态更新图表,使用 DOM 插槽和 CSS 自定义可视化组件,并使用可重用的自定义趋势线标记扩展库。此外,我们使用与 Matplotlib 兼容的接口,并将可视化导出为独立的 HTML、SVG 和 PNG 文件。

代码 · 51
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")
代码 · 51
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")

We install and initialize the XY library in Google Colab while enabling support for interactive widgets. We define a reusable rendering function that displays live charts and falls back to standalone HTML when widget support is unavailable. We then build a layered visualization with multiple marks, dual axes, annotations, tooltips, legends, themes, and interactive navigation controls.

我们在 Google Colab 中安装并初始化 XY 库,同时启用对交互式小部件的支持。我们定义了一个可重用的渲染函数,用于显示实时图表,并在小部件支持不可用时回退到独立的 HTML。然后,我们构建了一个分层可视化,包含多个标记、双轴、注释、工具提示、图例和交互式导航控件。

代码 · 40
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")
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']}")
代码 · 40
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")
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']}")

We create a structured Pandas DataFrame and use column names directly as visualization channels. We generate a color-encoded scatter plot, divide the dataset into linked regional facets, and preserve shared axis behavior across panels. We also visualize 1.5 million points through XY’s density rendering system and inspect its memory usage and data-transfer efficiency.

我们创建了一个结构化的 Pandas DataFrame,并直接使用列名作为可视化通道。我们生成了一个颜色编码的散点图,将数据集划分为链接的区域分面,并在面板之间保持共享的轴行为。我们还通过 XY 的密度渲染系统可视化 150 万个点,并检查其内存使用和数据传输效率。

代码 · 27
sel = big.select_range(-1.0, 1.0, -1.0, 1.0)
sx, sy = sel.xy(0)
print(f"\nselect_range hit {len(sel):,} rows; x array {sx.shape}")
print("first rows:", sel.rows(limit=2))
print("pick(trace=1, index=10):", layered.pick(1, 10))
def on_select(selection):
   xs, ys = selection.xy(0)
   print(f"[callback] {len(selection):,} rows selected, mean y = {ys.mean():.3f}")
def on_view_change(payload):
   print("[callback] viewport:", payload)
render(xy.scatter_chart(
   xy.scatter("x", "y", color="#ef4444", size=5, opacity=0.7),
   data=df, select=True, on_select=on_select, on_view_change=on_view_change,
   title="Shift-drag a box → payload lands in Python",
   width=760, height=380,
), "5 · Selections routed back to the kernel")
stream = xy.line_chart(
   xy.line([0.0], [0.0], color="#10b981", width=2, name="live"),
   xy.x_axis(label="t"), xy.y_axis(label="value", domain=(-3, 3)),
   title="Streaming via chart.append()", width=760, height=320,
)
render(stream, "6 · Streaming")
import time
for k in range(1, 60):
   t = k / 3.0
   stream.append(0, [t], [float(np.sin(t) + rng.normal(0, 0.08))])
   time.sleep(0.03)
代码 · 27
sel = big.select_range(-1.0, 1.0, -1.0, 1.0)
sx, sy = sel.xy(0)
print(f"\nselect_range hit {len(sel):,} rows; x array {sx.shape}")
print("first rows:", sel.rows(limit=2))
print("pick(trace=1, index=10):", layered.pick(1, 10))
def on_select(selection):
   xs, ys = selection.xy(0)
   print(f"[callback] {len(selection):,} rows selected, mean y = {ys.mean():.3f}")
def on_view_change(payload):
   print("[callback] viewport:", payload)
render(xy.scatter_chart(
   xy.scatter("x", "y", color="#ef4444", size=5, opacity=0.7),
   data=df, select=True, on_select=on_select, on_view_change=on_view_change,
   title="Shift-drag a box → payload lands in Python",
   width=760, height=380,
), "5 · Selections routed back to the kernel")
stream = xy.line_chart(
   xy.line([0.0], [0.0], color="#10b981", width=2, name="live"),
   xy.x_axis(label="t"), xy.y_axis(label="value", domain=(-3, 3)),
   title="Streaming via chart.append()", width=760, height=320,
)
render(stream, "6 · Streaming")
import time
for k in range(1, 60):
   t = k / 3.0
   stream.append(0, [t], [float(np.sin(t) + rng.normal(0, 0.08))])
   time.sleep(0.03)

We select exact data points from the large visualization and retrieve their original row values directly from Python. We define callback functions that receive browser-side selections and viewport changes while keeping the underlying data inside the kernel. We also create a streaming line chart and continuously append new observations to update the visualization in real time.

我们从大型可视化中选择精确的数据点,并直接从 Python 中检索其原始行值。我们定义了接收浏览器端选择和视口变化的回调函数,同时将底层数据保留在内核中。我们还创建了一个流式折线图,并不断追加新的观测值以实时更新可视化。

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

相似阅读

另一事件,读法相近