Keep the libraries you already know: plotly, altair, lonboard, k3d, itables, and dozens more. One decorator puts them in a reactive web app, and what users do with them comes back as Python values.
Each group opens a set of guides. Every guide shows the widget rendering, the user's gestures flowing back to Python, and the widget updating in place.
11plotly · altair · bokeh · matplotlib …
09lonboard · maplibre · ipyleaflet · kepler.gl …
04k3d · pythreejs · pyvista · ipyvolume
06nglview · niivue · vizarr · mols2grid …
04itables · reactable · perspective · ipydatagrid
04embedding-atlas · mosaic · quak · pygwalker
02ipycytoscape · ipysigma
03drawdata · ipycanvas · ipyfilechooser
Pick a widget. The highlighted call is how your app reads what the user did; flip between before and after the gesture.
Every example runs as-is: shiny run app.py. See all 43 guides →
Learn them once with one widget and they carry over to the other 42. Each one is a page in the guide.

Return a figure, map, or scene from a decorated function. It fills its card like any Shiny output.
@render_plotly def plot(): return px.scatter(df, ...)

Every gesture is an accessor. Call it from any reactive context, the way you'd call input.x().
@render.data_frame def picked(): return plot.selected_rows(df)

Change the live widget from Python. Here a summary of the selection is written onto the chart, and the lasso and zoom stay put.
@reactive.effect def _summarize(): rows = plot.selected_rows(df) plot.widget.layout.annotations = [ dict(text=f"{len(rows)} countries…", ...) ]
Real data, several views driving each other, a hundred or two lines of Python. Every one is open source and runs with shiny run app.py.

Draw on the GPU, aggregate on the server, or query only what's visible. Three approaches, measured.
Large data & performance →
Let a chat filter your data, read a selection, or steer a map. The LLM sees the same accessors you do.
AI exploration →import anywidget, traitlets class Counter(anywidget.AnyWidget): _esm = "…" count = traitlets.Int(0).tag(sync=True) @render_widget def counter(): return Counter() counter.read("count")
Any ipywidget or anywidget renders with @render_widget, including one you write yourself.