manywidgets ships three layout primitives that arrange other widgets while keeping them fully interactive and linked — live and in a static export with no kernel:
Row— children left-to-right (a flex row; wraps if it runs out of room).Column— children top-to-bottom (a flex column).Grid— children flowed row-major intocolumnsequal columns.
They share two ideas:
gapsets the space between children on every primitive.Row/Columnalso takealign— the cross-axisalign-items(vertical forRow, horizontal forColumn);Gridtakescolumns.They compose. Any layout can hold any widget, including another layout, so you build a whole screen by nesting
Row/Column/Grid.
Pass children positionally (Row(a, b)) or as a list (Row(children=[a, b])).
Linking works as usual (see the linking guide) — a
control and what it drives can sit side by side.
import numpy as np
from ipywidgets import jsdlink
from manywidgets import (
Chart, Slider, RangeSlider, Dropdown, Toggle,
Stat, NumberDisplay, Text, Row, Column, Grid,
)Row¶
Children sit left-to-right and wrap to the next line if they run out of room.
gap sets the spacing; align lines up children of different heights along the
cross (vertical) axis.
Row(
Stat(label="Sent", value=128),
Stat(label="Opened", value=94),
Stat(label="Clicked", value=31),
gap="16px",
)Column¶
The vertical sibling of Row — children stack top-to-bottom. Here a Slider
drives a NumberDisplay stacked beneath it.
s = Slider(label="Value", min=0, max=100, value=40)
nd = NumberDisplay(label="Selected", format="{}", duration=300)
jsdlink((s, "value"), (nd, "value"))
Column(s, nd, gap="12px")Grid¶
columns equal columns; children flow row-major (left-to-right, top-to-bottom).
A grid of Stat cards makes a compact dashboard.
Grid(
Stat(label="Revenue", value=1234, unit="USD", delta=12),
Stat(label="Users", value=987, delta=-3),
Stat(label="Latency", value=42, unit="ms"),
Stat(label="Uptime", value=99, unit="%", delta=1),
columns=2, gap="12px",
)Nesting¶
Layouts are widgets too, so they nest — a Row can hold Columns, a Column
can hold a Grid, and so on. That’s how the three primitives build up a whole
screen. Here a Row places two control groups side by side, each a Column
that pairs a slider with its live readout.
budget = Slider(label="Budget", min=0, max=10000, step=500, value=4000)
budget_out = NumberDisplay(label="Selected", format="${:,.0f}", duration=300)
jsdlink((budget, "value"), (budget_out, "value"))
headcount = Slider(label="Headcount", min=1, max=50, value=12)
headcount_out = NumberDisplay(label="Selected", format="{:,.0f}", duration=300)
jsdlink((headcount, "value"), (headcount_out, "value"))
# Row of two Columns — each Column groups a slider with its readout.
Row(
Column(budget, budget_out, gap="8px"),
Column(headcount, headcount_out, gap="8px"),
gap="32px",
)A linked dashboard¶
A bigger, real nesting: a header on top, a Column of controls beside a Chart,
and a Grid of summary cards below — the whole thing is one outer Column
(Column → Row → Column + Chart, then a Grid). The controls drive the chart
live (and statically) — switch its type, toggle the legend, or change its height.
x = np.linspace(0, 10, 40)
chart = Chart(title="Signal", x_label="x", y_label="y", height=300, width=480)
chart.add_series(x=x, y=np.sin(x), name="sin")
chart.add_series(x=x, y=np.cos(x), name="cos")
kind = Dropdown(label="Chart type", options=["line", "bar", "scatter"], value="line")
legend = Toggle(label="Legend", value=True)
height = Slider(label="Height", min=160, max=420, step=20, value=300)
jsdlink((kind, "value"), (chart, "chart_type"))
jsdlink((legend, "value"), (chart, "legend_enabled"))
jsdlink((height, "value"), (chart, "height"))
Column(
Text(value="## Signal dashboard", markdown=True),
Row(
Column(kind, legend, height, gap="10px"),
chart,
gap="24px", align="start",
),
Grid(
Stat(label="Series", value=2),
Stat(label="Points", value=40),
Stat(label="Range", value=10, unit="x"),
columns=3, gap="12px",
),
gap="16px",
)Controls beside a map¶
The same primitives wrap a lonboard map.
One thing to know: a plain flex Row sizes each child to its content, but a map
fills its container and has no intrinsic width — in a Row it would collapse to
nothing. A two-column Grid gives the controls and the map each a defined column,
so the map gets real width. (Install with pip install "manywidgets[lonboard]";
see the lonboard guide.)
import geopandas as gpd
from shapely.geometry import Point
from lonboard import Map, ScatterplotLayer
from lonboard.layer_extension import DataFilterExtension
from manywidgets.lonboard import LayerToggle, FilterBinder, LayerFilter
n = 6
vals = np.arange(n, dtype="float64")
cats = np.array([0, 1, 2, 0, 1, 2], dtype="uint8")
pts = [Point(-122.42 + 0.01 * i, 37.77 + 0.01 * i) for i in range(n)]
gdf = gpd.GeoDataFrame(geometry=pts, crs="EPSG:4326")
layer = ScatterplotLayer.from_geopandas(
gdf,
get_radius=300, radius_units="meters", get_fill_color=[200, 30, 30],
extensions=[DataFilterExtension(filter_size=1, category_size=1)],
get_filter_value=vals, filter_range=(0, 5),
get_filter_category=cats, filter_categories=[0, 1, 2],
)
m = Map(layer, view_state={"longitude": -122.4, "latitude": 37.8, "zoom": 11}, height=420)toggle = LayerToggle(layer, label="Points", value=True)
value_range = RangeSlider(label="Value range", min=0, max=5, low=0, high=5, step=1)
fb = FilterBinder(value_range, layer) # value_range.low/high -> layer.filter_range
cat = LayerFilter(layer, categories=[[0, "A"], [1, "B"], [2, "C"]], label="Category")
controls = Column(
Text(value="### Map controls", markdown=True),
toggle, value_range, fb, cat,
gap="10px",
)
Grid(controls, m, columns=2, gap="16px")