Создание масштабируемых интерактивных визуализаций с Reflex XY: композиция, визуализация миллионов точек, потоковая передача, пользовательские маркеры и экспорт
В этом руководстве мы исследуем расширенные возможности визуализации библиотеки Python XY, создавая интерактивные, масштабируемые и расширяемые графики. Мы начнём с композиционной модели XY, в которой объединим несколько элементов, двойные оси, аннотации, всплывающие подсказки, легенды, темы и интерактивные элементы управления в одном объявлении графика. Затем мы поработаем с DataFrame из Pandas, фасетными макетами, связанными областями просмотра и наборами данных из миллиона точек, которые автоматически переключаются на визуализацию на основе плотности для эффективного исследования. Мы также свяжем взаимодействия в браузере с Python через выборки и обратные вызовы, будем динамически обновлять графики посредством потоковой передачи данных, настраивать визуальные компоненты с помощью DOM-слотов и CSS, а также расширим библиотеку повторно используемым пользовательским элементом линии тренда. Кроме того, мы используем интерфейс, совместимый с Matplotlib, и экспортируем визуализации в отдельные файлы HTML, SVG и PNG.
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")
Мы устанавливаем и инициализируем библиотеку XY в Google Colab, включив поддержку интерактивных виджетов. Мы определяем повторно используемую функцию визуализации, которая отображает интерактивные графики и переключается на автономный HTML, если поддержка виджетов недоступна. Затем мы создаём многослойную визуализацию с несколькими элементами, двойными осями, аннотациями, всплывающими подсказками, легендами, темами и интерактивными элементами навигации.
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']}")
Мы создаём структурированный DataFrame Pandas и напрямую используем имена столбцов в качестве каналов визуализации. Мы генерируем точечный график с кодированием цвета, делим набор данных на связанные региональные фасеты и сохраняем общие настройки осей для всех панелей. Мы также визуализируем 1,5 миллиона точек с помощью системы XY для отображения плотности и проверяем использование памяти и эффективность передачи данных.
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)
Мы выбираем отдельные точки данных из большой визуализации и напрямую получаем их исходные значения строк из Python. Мы определяем функции обратного вызова, которые получают выборки и изменения области просмотра из браузера, сохраняя исходные данные внутри ядра. Мы также создаём потоковый линейный график и непрерывно добавляем новые наблюдения, чтобы обновлять визуализацию в реальном времени.
print("\navailable slots:", ", ".join(sorted(xy.CHART_DOM_SLOTS)))
CSS = """
.xy-card {background:#fafaf9;border:1px solid #e7e5e4;border-radius:16px;padding:10px}
.xy-title{font:600 16px/1.2 ui-sans-serif;letter-spacing:-.01em;color:#1c1917}
.xy-tip {border-radius:10px;background:#1c1917;color:#fafaf9}
"""
display(HTML(f"<style>{CSS}</style>"))
styled = xy.line_chart(
xy.line(days, revenue, color="#111827", width=2,
animation=xy.animation(duration=700,
easing=xy.spring(stiffness=180, damping=22))),
xy.x_axis(label="Day"), xy.y_axis(label="Revenue"),
title="Slot-addressed styling",
class_name="xy-card",
class_names={"title": "xy-title", "tooltip": "xy-tip"},
styles={"canvas": {"border-radius": "12px"}},
width=760, height=360,
)
render(styled, "7 · CSS slots, tokens, spring animation")
def _fit(cols):
x = np.asarray(cols["x"], float); y = np.asarray(cols["y"], float)
b, a = np.polyfit(x, y, 1)
order = np.argsort(x); xs = x[order]
fit = a + b * xs
resid = float(np.std(y - (a + b * x)))
return {"x": xs, "y": y[order], "fit": fit,
"lo": fit - 1.96 * resid, "hi": fit + 1.96 * resid}
def _build(ctx):
color = ctx.options.get("color", "#2563eb")
c, nm = ctx.columns, (ctx.name or "trend")
return [
xy.error_band(c["x"], c["lo"], c["hi"], color=color, opacity=0.18, name=f"{nm} CI"),
xy.line(c["x"], c["fit"], color=color, width=2.5, name=nm),
]
if "trendline" not in xy.registered_marks():
xy.register_mark(xy.MarkPlugin(name="trendline", build=_build,
columns=("x", "y"), calc=_fit,
doc="OLS fit with a 95% band."))
render(xy.chart(
xy.scatter("x", "y", color="#94a3b8", size=4, opacity=0.5, name="observations"),
xy.mark("trendline", x="x", y="y", color="#e11d48", name="OLS"),
xy.legend(loc="upper left"),
data=df, title="Third-party mark kind", width=760, height=400,
), "8 · Custom mark plugin")
Мы настраиваем компоненты графика с помощью стабильных DOM-слотов, CSS-классов, встроенных стилей и анимаций на основе пружинной модели. Мы определяем расчёт методом обычных наименьших квадратов, который строит линию тренда и 95%-ный доверительный интервал на основе предоставленных данных. Затем мы регистрируем этот расчёт как повторно используемый пользовательский элемент XY и объединяем его со встроенными компонентами точечного и линейного графиков, доверительного диапазона и легенды.
import xy.pyplot as plt
t = np.linspace(0, 10, 400)
fig, ax = plt.subplots(figsize=(8, 3.5))
ax.plot(t, np.sin(t), "r--", label="sin")
ax.plot(t, np.cos(t), label="cos")
ax.set_xlabel("t"); ax.set_ylabel("amplitude"); ax.set_title("xy.pyplot compatibility")
ax.legend(); ax.grid(True, alpha=0.3)
plt.show()
os.makedirs("out", exist_ok=True)
layered.to_html("out/chart.html")
layered.to_svg("out/chart.svg")
layered.to_png("out/chart.png", scale=2)
for f in ("chart.html", "chart.svg", "chart.png"):
print(f"out/{f}: {os.path.getsize('out/'+f)/1024:.0f} KB")
print("\n tutorial complete")
Мы используем слой совместимости xy.pyplot для создания графиков с помощью привычных команд в стиле Matplotlib. Мы строим кривые синуса и косинуса, настраиваем подписи, заголовки, легенды и параметры сетки, а затем отображаем получившийся рисунок. Наконец, мы экспортируем многослойный график в отдельные файлы HTML, SVG и PNG высокого разрешения и проверяем размер каждого созданного файла.
В заключение мы сформировали комплексное представление о том, как XY поддерживает современные процессы интерактивной визуализации непосредственно из Python. Мы создали многослойные и фасетные графики, эффективно проанализировали большие наборы данных, получили точные выбранные строки из ядра, передавали новые наблюдения в работающие визуализации и настраивали внешний вид графиков с помощью тем, анимаций, CSS-классов и стабильных DOM-слотов. Мы также показали, как расширить XY собственным плагином статистического элемента и повторно использовать привычные команды построения графиков в стиле Matplotlib через мост xy.pyplot. В завершение мы экспортировали графики в переносимые форматы, что позволяет делиться ими за пределами ноутбука, сохраняя интерактивность или качество графики, готовой к публикации.
Ознакомьтесь с ПОЛНЫМ КОДОМ здесь.Также подписывайтесь на нас в Twitter и не забудьте присоединиться к нашему сабреддиту по машинному обучению с более чем 150 тыс. участников и подписаться на нашу рассылку. Стоп! Вы есть в Telegram? теперь вы также можете присоединиться к нам в Telegram.
Хотите сотрудничать с нами для продвижения своего репозитория GitHub, страницы Hugging Face, релиза продукта, вебинара и т. д.? Свяжитесь с нами
Переведено автоматически с английского. Оригинал статьи — по ссылке ниже.