For AI agents: the complete documentation index is at llms.txt. Remove the trailing slash from a page URL and append .md to read its Markdown version. For the docs home, use index.md.

Build linked XY charts with shared state

You can link XY charts in Reflex by storing a selection in application state and deriving the data displayed by other components from it. In this tutorial, selecting points in an XY scatter chart filters an XY bar chart and a table. Reset restores all records.

This is cross-filtering through Python state. XY also supports browser-side axis linking through link_group and link_axes; synchronized zoom is a different behavior from filtering records. The example below sends a completed selection to a Python handler rather than attaching Python work to every hover.

Install and configure XY

Start with a blank project using the installation guide. This example uses Python 3.11 or newer and the XY 0.0.7 Reflex integration:

uv add "xy[reflex]==0.0.7"

Register the XY plugin in rxconfig.py. Keep your app's existing name and other plugins:

import reflex as rx
import reflex_xy as rxy

config = rx.Config(
    app_name="linked_dashboard",
    plugins=[rxy.XYPlugin()],
)

The plugin connects XY's data channel to your Reflex app. Use state-backed chart data for event handlers: a static chart does not provide the same backend event connection. See the XY Reflex integration for the supported chart tiers.

Run the example

Copy the following code into your app module. The four-record dataset is fixed, so selections can be mapped to stable application IDs without a database or credentials.

Start Building Now!

Interactive preview loads as you scroll.

Add these lines at the end of the module, then run uv run reflex run:

app = rx.App()
app.add_page(linked_charts)

How the views stay linked

  1. source_data supplies the full, fixed trace through @rxy.data.
  2. XY's on_select_end event supplies a selection envelope with canonical row information, a count, and clear/truncation flags.
  3. The handler maps selected source row indexes to application IDs and stores them in selected_ids.
  4. The table reads visible_rows; revenue_data supplies those same rows to the XY bar chart.
  5. Reset clears the filter and changes the source chart's key so its selection highlight clears too. Remounting also resets that chart's zoom.

The demo uses cache=False for these tiny datasets so an empty pre-session data handle is recomputed after hydration. For expensive queries, cache the query results separately rather than rerunning them for every event.

XY's numeric chart columns travel through its data channel. The table's four displayed records remain ordinary Reflex state. Choosing XY for the chart does not remove the need to paginate a large table.

Empty selections and bounded events

A cleared selection means “remove the filter,” so selected_ids becomes None. A completed box containing no points means an empty result, so it becomes []. These states must not be conflated.

Selection event rows are bounded. The handler uses resolve_selection when truncated is true rather than silently filtering to only the rows included in the event. This tiny example does not reach that limit, but the branch matters when adapting it to larger data. If the chart registry can no longer resolve the selection, or any row has an unrecognized trace or invalid source position, the example retains the previous filter and asks the user to select again.

The Reflex adapter event is distinct from a notebook's xy.Selection callback. See XY interactions and selections for both contracts.

Preserve record identity

XY returns canonical source row positions, including when a rendering representation differs from the original rows. Those positions are still not database primary keys. This example maps trace zero's index through the fixed RECORDS sequence to obtain an application ID.

If you reorder or replace the source data, maintain the exact row-ID mapping for that displayed generation. Account for selection events that arrive during a refresh, and reject stale generations rather than mapping an old index against new records. For multiple traces, keep the mapping for each trace. The linked chart can change independently here because the source trace remains fixed.

For charts with compatible axes, give them the same link_group and select link_axes, such as ("x",), to synchronize their view in the browser. Use that for a shared time window. Use the state/event pattern above when a selection must change records in another chart or table.

The source chart here uses hours, while the bar chart uses order IDs. Their x axes are not compatible, so they deliberately do not share an axis-link group.

Check the behavior

Select the first two points: the table should show order-a and order-b, and the bar chart should show revenues 120 and 180. Select a different region and verify the old rows disappear. Select an empty region and verify the empty-result message. Reset and verify that all four rows return and the highlight clears. Open a separate browser session and check that one session's selection does not change the other.

Larger datasets

Keep the original row-ID mapping on the backend, authorize data access there, and query or aggregate only what the views need. Do not serialize a huge resolved selection into frontend state merely because the chart can render a large dataset. Choose a query or selection representation appropriate to the data source and paginate the table.

Continue with dashboards and internal tools and performance and execution.