Customizing

September 14, 2026 ยท View on GitHub

FlexViz splits appearance into two layers.

  • Figure appearance is per chart: title, axis labels, legend, colors. It travels in FigureSpec.layout.
  • Dashboard layout is per page: panel positions, gap, toolbar buttons. It travels in LayoutSpec.

The two never overlap. A figure cannot resize its panel, and a dashboard cannot move a legend.

Figure appearance

Titles and axis labels

fig = dash.add_figure()
fig.add_line(x="timestamp", y="power")
fig.title("Power draw").xlabel("Time").ylabel("Watt")

FlexViz derives both axis labels from the trace columns when you set none.

Show or hide the legend

fig.legend(False)

With no call, the legend appears when the figure has more than one trace or a grouped trace.

Put the legend below the chart

Placement is renderer-specific, so it goes through update_layout(). Every key that is not title, xlabel, ylabel or legend passes straight to the renderer. With Plotly, that is any Plotly layout option.

fig.legend(True).update_layout(
    legend={"orientation": "h", "y": -0.25, "yanchor": "top",
            "x": 0.5, "xanchor": "center"},
    margin={"t": 40, "b": 80, "l": 60, "r": 20},
)

A legend below the chart sits outside the plotting area. Increase the bottom margin, or the legend overlaps the x-axis labels.

Passing a legend dict also makes the legend visible, so legend(True) is optional here.

Plotly owns both dicts. Its legend guide shows the positioning patterns, and the reference lists every key of layout.legend and layout.margin.

Other renderer options

fig.ylabel("Power").update_layout(
    yaxis={"type": "log"},
    colorway=["#345d8f", "#d19a32"],
    font={"family": "DM Sans", "size": 12},
)

Dict values merge one level deep. The example keeps the y-axis label and adds a log scale. A replacing merge would erase the label.

Every key comes from the Plotly layout reference.

!!! warning "Plotly only" ECharts is deprecated and reads title, xlabel, ylabel and legend visibility only. Every other update_layout() key is ignored there.

Colors

Pass color for a single trace and color_map for a grouped one. See Grouping for how group colors stay stable across interactions.

Dashboard layout

Rows and columns

The grid is 12 columns wide. By default figures are placed two per row, so a single figure fills half the width.

dash.show(cols=1)   # one full-width column
dash.show(rows=2)   # two rows, columns derived from the figure count

rows and cols are mutually exclusive.

Lock the layout

dash.show(draggable=False)

This renders a static CSS grid. Panels cannot move, and the layout button disappears.

Three separate fields decide how rearranging works. Each answers a different question.

FieldQuestion it answersFixed when the page renders
LayoutSpec.draggableWhich layout engine renders the page: GridStack, or a static CSS gridYes
LayoutSpec.grid_editableWhether panels can be moved and resized right nowNo, this is live state
ToolbarConfig.show_gridWhether the toolbar offers the built-in lock and unlock buttonYes

draggable is a rendering choice, grid_editable is live state, and show_grid is an affordance. They combine into five results.

draggablegrid_editableshow_gridResult
FalseignoredignoredStatic grid, no GridStack assets. The layout button is hidden either way.
TrueFalseTrueGridStack, locked. The button reads Layout: Locked and unlocks.
TrueTrueTrueGridStack, editable. The button reads Layout: Edit and locks.
TrueFalseFalseGridStack, locked, with no built-in layout button.
TrueTrueFalseGridStack, editable, with no built-in layout button to lock it.

Read the last two rows carefully. show_grid=False hides the built-in control. It does not remove the capability: a layout that starts editable stays editable, and your own JavaScript can call fvSetGridEditable. With show_import=True, an imported spec can also set grid_editable again. Set both show_grid=False and show_import=False to remove the built-in routes to that state. Use draggable=False when the layout must not move at all.

!!! note "GridStack is a CDN dependency" draggable=True loads GridStack's stylesheet and script from a CDN. draggable=False loads neither. Prefer it for a locked embed, and in any page that must not reach a CDN.

grid_editable keeps its value when draggable=False. It is inactive, but it is not cleared, so specs round-trip unchanged.

Exact panel positions

GridItem places one figure on the 12-column grid. One row unit is 80 px, so a panel of h rows is h * 80 pixels tall. The default h=5 is 400 px.

Both layout engines give the same height, because the space between panels sits inside the h * 80 box. On the static grid (draggable=False) that space is gap. GridStack keeps its own panel margin, so there gap only pads the outer edge of the grid.

from flexviz import GridItem, LayoutSpec

uids = [f.uid for f in dash.to_spec().figures]
dash.show(
    layout=LayoutSpec(
        gap="16px",
        draggable=False,
        grid_items=[
            GridItem(fig_uid=uids[0], x=0, y=0, w=12, h=4),   # full width, 320 px
            GridItem(fig_uid=uids[1], x=0, y=4, w=6, h=8),    # half width, 640 px
            GridItem(fig_uid=uids[2], x=6, y=4, w=6, h=8),
        ],
    )
)

grid_items cannot be combined with rows or cols. Passing both raises a ValueError, because the two describe the same thing.

Toolbar buttons

ToolbarConfig hides buttons in the top toolbar. Every button shows by default.

from flexviz import LayoutSpec, ToolbarConfig

dash.show(
    draggable=False,
    layout=LayoutSpec(toolbar=ToolbarConfig(show_export=False, show_import=False)),
)

The fields are show_reset, show_deselect, show_cfmode, show_hover, show_lock_all_axes, show_grid, show_share, show_export and show_import. An empty button group disappears with its divider.

Most of these hide a button whose state you can reach another way. show_grid is different: it hides the only built-in button for changing grid_editable. show_import is a separate route for restoring that state. See Lock the layout for what these flags do and do not change.

Toolbar versus panel controls

The top toolbar holds dashboard-wide state: reset, deselect, cross-filter mode, hover, axis lock, layout, share, export and import. ToolbarConfig controls these.

Each panel also has its own bottom bar with Zoom, Pan, CF, Reset and axis lock. These follow what the traces in that panel support, and they are not configurable today.

Precedence

layout owns every field it sets. rows, cols and draggable are convenience overrides that apply on top of it.

You passResult
nothingTwo columns, default h=5 panels, GridStack locked initially
cols=1One full-width column
layout.grid_itemsYour positions, untouched
layout.grid_items and colsValueError
draggable=FalseOverrides layout.draggable
layout.draggable=False onlyHonored, because draggable defaults to None