Funnel Charts in Python
A funnel chart shows how a quantity survives an ordered process — visitors becoming signups becoming customers, candidates advancing through interviews, tickets moving toward resolution. Each stage is one centered segment, and the narrowing silhouette makes conversion and drop-off legible at a glance.
With xy, pass stage names and values to funnel_chart. Stage order is
always the declared order — a funnel is a categorical business process,
and XY never sorts it. Conversion and drop-off arithmetic ship with every
stage: labels, tooltips, and click events all carry the stage name, value,
prior value, overall share, previous-stage conversion, and drop-off.
Jump to the basic chart, geometry modes, horizontal funnels, necks, gaps, and floors, styling, or the legend.
Create a Funnel Chart
Each segment shows its value and overall conversion (show_conversion
appends the share of the first stage), and show_dropoff=True writes the
signed stage-over-stage change at each boundary. Hovering a segment reads the
full arithmetic: value, overall share, previous-stage conversion, and
drop-off. A label that cannot fit inside its segment moves beside it, and a
stage pitch too short for a text line hides the labels rather than
overlapping them — the tooltip always carries every number.
Choose Honest Geometry
geometry is explicit because the two classic funnel drawings encode
differently:
"area"(default) draws the tapering silhouette — each segment's far edge previews the next stage's width, so drop-off is visible as slope. The painted area of a segment is therefore not proportional to its value."bar"draws centered constant-width segments whose widths carry the values exactly — the faithful-width encoding.
Increasing stages are legal and drawn honestly: Re-engage is wider than
Invite, its conversion is above one, and its boundary label reads +9%.
Negative and missing values are refused by stage name. A zero stage draws
nothing and keeps its label and its keyboard stop, but with no drawn area
there is nothing for the pointer to land on — give it min_width to make it
hoverable as a floor sliver.
Run the Funnel Horizontally
orientation="horizontal" runs stage 0 from the left; vertical funnels put
stage 0 on top (the stage axis is reversed exactly like a Sankey's). The
cross axis is layout, not data — segments center on zero — so funnel_chart
hides it. Here min_width=0.03 keeps the zero-valued Eng fix stage visible
and hoverable as a floor sliver: drawn geometry is clamped, but every label,
tooltip, and event value stays exact.
Shape the Silhouette
gapseparates segments along the stage axis as a fraction of the stage pitch. It resolves per geometry when unset:0for"area"(a continuous silhouette),0.2for"bar"(bar-chart spacing).neckdecides the last area segment's far edge:"rect"(default) holds the stage's own width;"taper"runs it to a point — the classic spout.min_widthfloors drawn cross widths at a fraction of the widest stage so tiny stages stay visible. The taper spout deliberately ignores the floor.
Style a Funnel
Per-stage paint is a channel, not a style: pass colors= for one CSS color
per stage, color= for a single constant, or let the theme palette assign
colors in declared stage order. A xy.theme(palette={...}) mapping pins
colors by stage name, so a stage keeps its color across charts. Inside
labels pick a light or dark text color from each segment's own fill.
Trace-level style stays per-trace, the ribbon contract: opacity,
fill-opacity, stroke, stroke-width, and stroke-opacity (an omitted
stroke color outlines each segment with its own fill). fill is deliberately
rejected — per-stage paint rides the channel so every renderer draws it.
Chart chrome — title, axis ticks, tooltip, legend, and the funnel's own
value/drop-off labels (annotation_label) — styles through the standard
chrome slots with CSS classes, Tailwind
utilities, or styles={...}:
value_format and percent_format are str.format templates, and the
kernel applies them once for every surface — segment labels, hover tooltips,
and static exports all print the same string, because the client is handed
the formatted text rather than re-implementing the format spec.
Add a Legend
The legend is off by default — the stage axis already names every stage,
so a second list of the same names is usually noise. Pass an explicit
xy.legend(...) child to bring back one row per stage, drawn from the
categorical stage encoding:
Those rows are live. Clicking one hides that stage's segment and its
labels, leaving every other stage's geometry and arithmetic untouched — a
funnel's stage values are the data, not a running total to recompute — and
clicking again restores it. Hovering a row emphasizes its stage and dims the
rest. xy.legend(show=False) is the default; loc, title, and ncols
place and shape it like any other chart's legend, and the legend,
legend_item, legend_swatch, and legend_label
chrome slots style it.
Because stage colours come from a categorical channel keyed on the stage
names, a xy.theme(palette={...}) mapping keeps each legend swatch and its
segment in the same colour across every chart that names that stage.
Interact With a Funnel
Hover reads the full arithmetic for a stage, and because a segment covers an
area rather than a point, the tooltip follows the cursor within it. Clicking
emits xy:click carrying the stage name, value, prior value, overall share,
conversion, and drop-off — the same semantic row the tooltip shows. A ratio
with no meaningful value — a zero denominator, or one that would overflow to
infinity on an extreme dynamic range — arrives as null in events and prints
as an em dash (—) in the tooltip, so it reads as "no meaningful number"
rather than as missing data. Box
and lasso selection are deliberately absent rather than approximate.
With animation= configured, a funnel enters by growing out of its spine
(the way bars grow from their baseline), and data updates morph each
segment's geometry to its new shape. Stable key= identities plus
xy.animation(match="key") keep a stage's segment continuous across updates
even when stages are added or removed; without keys, stages match by
position. Keyboard navigation walks the visible stages in declared order —
arrow keys move stage to stage, Home/End jump to the ends, Enter
activates, Escape dismisses — and the screen-reader announcement reads
"Stage 2 of 5" followed by that stage's conversion arithmetic, so the funnel
is heard as the ordered process it is.
API Reference
xy.funnel_chart
A funnel chart: ordered stages, centered segments, hidden cross axis.
Props
| Prop | Type | Description |
|---|---|---|
*children | Any | Marks, axes, annotations, and chart chrome. |
title | Optional[str] | Title shown above the plot. |
width | int | str | Chart width in pixels or a CSS size such as ``"100%"``. |
height | int | str | Chart height in pixels or a CSS size such as ``"100%"``. |
padding | Union[float, Sequence[float], None] | Plot margins, as one value or a sequence of side values. Use zero for an edge-to-edge sparkline. |
data | TableLike | Chart-level data used by marks that omit their own ``data``. |
class_name | Optional[str] | CSS class applied to the chart container. |
class_names | Optional[dict[str, str]] | CSS classes keyed by stable chart DOM slot. |
style | Optional[dict[str, StyleValue]] | Inline style overrides for the chart container. |
styles | Optional[dict[str, dict[str, StyleValue]]] | Inline style mappings keyed by stable chart DOM slot. |
on_hover | Optional[Callable[[dict], None]] | Callback receiving hover event payloads. |
on_click | Optional[Callable[[dict], None]] | Callback receiving picked-mark click payloads. |
on_brush | Optional[Callable[[dict], None]] | Callback receiving brush event payloads. |
on_select | Optional[Callable[[Selection], None]] | Callback receiving data-space selections. |
on_view_change | Optional[Callable[[dict], None]] | Callback receiving viewport change payloads. |
hover | Optional[bool] | Whether pointer movement emits hover events. |
click | Optional[bool] | Whether picked marks emit click events. |
select | Optional[bool] | Whether shift-drag box selection is enabled. |
brush | Optional[bool] | Whether brush selection is enabled. |
crosshair | Optional[bool] | Whether plot-aligned hover guides are shown. |
navigation | Optional[bool] | Whether browser pan and zoom navigation is enabled. |
pan | Optional[bool] | Whether plain-drag panning is enabled. |
pan_axes | Optional[tuple[str, ...]] | Declared axis IDs translated by pan gestures. |
zoom | Optional[bool] | Whether viewport zoom is enabled. Defaults to on for Cartesian charts and off for polar ones (`wind_rose` excepted); |
default_drag_action | Optional[DefaultDragAction] | Initial action performed by a plain plot drag. |
zoom_axes | Optional[tuple[str, ...]] | Declared axis IDs changed by zoom gestures and controls. |
zoom_limits | Optional[ZoomLimits] | Minimum and maximum magnification globally or by axis. |
wheel_zoom | Optional[bool] | Whether wheel and trackpad zoom is available. |
box_zoom | Optional[bool] | Whether box zoom is available as a drag action. |
zoom_buttons | Optional[bool] | Whether modebar Zoom In/Out commands are available. |
double_click_reset | Optional[bool] | Whether double-click restores ``reset_axes``. |
reset_axes | Optional[tuple[str, ...]] | Declared axis IDs restored by reset. |
link_group | Optional[str] | Identifier used to synchronize charts in the browser. |
link_axes | Optional[tuple[str, ...]] | Axes synchronized within the link group. |
coords | str | Coordinate system, ``"cartesian"`` (default) or ``"polar"``. Under ``"polar"`` each mark's first channel is the angle and its second is the radius. Prefer ``xy.polar_chart(...)``, which sets this for you. |