Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Layout

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 into columns equal columns.

They share two ideas:

  • gap sets the space between children on every primitive. Row/Column also take align — the cross-axis align-items (vertical for Row, horizontal for Column); Grid takes columns.

  • 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")