For AI agents: the complete XY documentation index is at llms.txt. Markdown versions are available by appending .md or sending Accept: text/markdown.
Chart Gallery

/

Funnel Chart in Python

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

Start Building Now!

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.
Start Building Now!

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

Start Building Now!

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

  • gap separates segments along the stage axis as a fraction of the stage pitch. It resolves per geometry when unset: 0 for "area" (a continuous silhouette), 0.2 for "bar" (bar-chart spacing).
  • neck decides 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_width floors drawn cross widths at a fraction of the widest stage so tiny stages stay visible. The taper spout deliberately ignores the floor.
Start Building Now!

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.

Start Building Now!

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:

Start Building Now!

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

PropTypeDescription
*childrenAny

Marks, axes, annotations, and chart chrome.

titleOptional[str]

Title shown above the plot.

widthint | str

Chart width in pixels or a CSS size such as ``"100%"``.

heightint | str

Chart height in pixels or a CSS size such as ``"100%"``.

paddingUnion[float, Sequence[float], None]

Plot margins, as one value or a sequence of side values. Use zero for an edge-to-edge sparkline.

dataTableLike

Chart-level data used by marks that omit their own ``data``.

class_nameOptional[str]

CSS class applied to the chart container.

class_namesOptional[dict[str, str]]

CSS classes keyed by stable chart DOM slot.

styleOptional[dict[str, StyleValue]]

Inline style overrides for the chart container.

stylesOptional[dict[str, dict[str, StyleValue]]]

Inline style mappings keyed by stable chart DOM slot.

on_hoverOptional[Callable[[dict], None]]

Callback receiving hover event payloads.

on_clickOptional[Callable[[dict], None]]

Callback receiving picked-mark click payloads.

on_brushOptional[Callable[[dict], None]]

Callback receiving brush event payloads.

on_selectOptional[Callable[[Selection], None]]

Callback receiving data-space selections.

on_view_changeOptional[Callable[[dict], None]]

Callback receiving viewport change payloads.

hoverOptional[bool]

Whether pointer movement emits hover events.

clickOptional[bool]

Whether picked marks emit click events.

selectOptional[bool]

Whether shift-drag box selection is enabled.

brushOptional[bool]

Whether brush selection is enabled.

crosshairOptional[bool]

Whether plot-aligned hover guides are shown.

navigationOptional[bool]

Whether browser pan and zoom navigation is enabled.

panOptional[bool]

Whether plain-drag panning is enabled.

pan_axesOptional[tuple[str, ...]]

Declared axis IDs translated by pan gestures.

zoomOptional[bool]

Whether viewport zoom is enabled. Defaults to on for Cartesian charts and off for polar ones (`wind_rose` excepted);

default_drag_actionOptional[DefaultDragAction]

Initial action performed by a plain plot drag.

zoom_axesOptional[tuple[str, ...]]

Declared axis IDs changed by zoom gestures and controls.

zoom_limitsOptional[ZoomLimits]

Minimum and maximum magnification globally or by axis.

wheel_zoomOptional[bool]

Whether wheel and trackpad zoom is available.

box_zoomOptional[bool]

Whether box zoom is available as a drag action.

zoom_buttonsOptional[bool]

Whether modebar Zoom In/Out commands are available.

double_click_resetOptional[bool]

Whether double-click restores ``reset_axes``.

reset_axesOptional[tuple[str, ...]]

Declared axis IDs restored by reset.

link_groupOptional[str]

Identifier used to synchronize charts in the browser.

link_axesOptional[tuple[str, ...]]

Axes synchronized within the link group.

coordsstr

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.

Built with Reflex