Docs / Build Workflow

Visualization — funnel

When to use funnel

Funnel is the right choice for conversion flows, fulfillment pipelines, or any process where order between steps and drop-off matters — site visitor → product view → cart → checkout → purchase, or order created → packed → shipped → delivered. Each row of the query is one stage.

Use bar instead when the categories are unordered or when you do not care about cumulative drop-off. Use grid when the audience needs the exact step counts and conversion rates side by side.

Mapping

  • mapping.stage — required. String field whose values become the per-stage labels.
  • mapping.value — required. Numeric field for the per-stage size.
mapping:
  stage: stage
  value: order_count

The order of stages in the chart follows the order of rows in the query result. Sort in the underlying Malloy query to control sequence — the standard shape is one SELECT … UNION ALL per stage with an explicit ordering column:

# models/ec_fulfillment_funnel.malloy — one row per stage, in order.
##! experimental.parameters

source: ec_fulfillment_funnel() is ecommerce.sql("""
  SELECT 'Created' AS stage, COUNT(*) AS order_count, 1 AS stage_order
  FROM `bigquery-public-data.thelook_ecommerce.orders`
  UNION ALL
  SELECT 'Shipped', COUNT(*), 2
  FROM `bigquery-public-data.thelook_ecommerce.orders`
  WHERE status IN ('Shipped', 'Complete')
  UNION ALL
  SELECT 'Complete', COUNT(*), 3
  FROM `bigquery-public-data.thelook_ecommerce.orders`
  WHERE status = 'Complete'
""") extend {
  view: fulfillment_funnel is {
    select: stage, order_count, stage_order
    order_by: stage_order asc
  }
}

chart shortcuts

The chart block is typed and closed.

  • chart.percent_mode"first" (default), "total", or "none". Picks the base for the percent shown next to each stage value:
    • first — relative to the first stage (conversion cascade: 100% → 80% → 60% …).
    • total — relative to the sum of all stages (share of the funnel).
    • none — no percentages, raw values only.
  • chart.show_value_labels — boolean. Turns stage labels on. Style them with chart.label below.
  • chart.cross_filter — boolean, default true. Set to false to suppress click emission for this viz (it still consumes pills set elsewhere).
  • chart.height — pixel height of the viz container.

Pass-through layout fields:

  • chart.left / chart.top / chart.bottom — distance from the corresponding edge of the chart container.
  • chart.width — funnel horizontal width (string percent or pixel number).
  • chart.gap — pixels between stages.
  • chart.min_size — minimum stage width (e.g. "10%"). The narrowest stage will not get smaller than this even if its value is tiny.
  • chart.max_size — maximum stage width.
  • chart.sort — stage sort order: "ascending", "descending", or "none". Omit the key (or set none) to let the query's row order win; the other two make the renderer reorder by value.

Stage labels

chart.label styles each stage's label when chart.show_value_labels is on:

  • position"inside" (label sits on top of the stage), "left", "right", "top", "bottom".
  • rotate, color, font_size, font_weight.
  • formatter — string template. Use {b} for the stage name, {c} for the value, {d} for the percent (when percent_mode is "first" or "total").
  • distance, align, vertical_align, clip.

Legend & tooltip

chart.legend and chart.tooltip share the same shape as on bar. For a single-funnel viz the legend is usually redundant; set chart.legend.show: false.

format

  • format.y or format[<value_field_name>] — pattern for stage value labels and tooltip values.
  • format at the root — fallback.

Cross-filter behavior

Funnel participates fully in cross-filtering. Inside a dashboard:

  • Emits — clicking a stage adds a pill { field: <the stage field>, value: <clicked stage name> }, provided chart.cross_filter !== false AND the field is declared as a parameter on a source signature in this viz's own model file.
  • Consumes — pills set elsewhere, and dashboard-level filters, become parameters on the next run; the funnel's underlying query re-runs and the stages recompute accordingly.
  • Opt-out — set chart.cross_filter: false to suppress click emission while still consuming pills.

Worked examples

Standard conversion funnel with first-stage percent:

id: ec_fulfillment_funnel
title: Fulfillment Funnel
query: "models/ec_fulfillment.malloy::funnel"
type: funnel
mapping:
  stage: stage
  value: order_count
chart:
  height: 360
  width: "80%"
  show_value_labels: true
  label:
    position: inside
    formatter: "{b}: {c} ({d}%)"
  percent_mode: first
  legend:
    show: false
format:
  order_count: "#,##0"
published: true

Total-share funnel (each stage shown as % of the sum):

chart:
  percent_mode: total
  show_value_labels: true

Compact funnel embedded in a dashboard row alongside KPIs:

chart:
  height: 240
  width: "100%"
  gap: 6
  min_size: "20%"
  max_size: "100%"
  show_value_labels: true
  label:
    position: right
    formatter: "{b}: {c}"
  percent_mode: none

Design notes

  • The funnel renders stages in query-result order — sort in the Malloy query to control the sequence.
  • chart.min_size keeps small final stages visible.
  • Pick the percent mode deliberately: "first" for conversion cascades (each stage ÷ first), "total" for share of total, "none" for raw counts.
  • On tight funnels, move stage labels outside (chart.label.position: "right") or reduce font_size.
  • For A/B comparison, place two funnels side by side in a dashboard — one funnel shows one sequence.