When to use sankey

Sankey is the right choice for flow and attribution questions — how volume moves from one set of categories to another and where it branches or merges: traffic source → product category → order status, budget → team → outcome, stage-to-stage movement. Each row of the query is one link: an origin node, a destination node, and the magnitude flowing between them.

Use funnel instead when the process is a single ordered cascade with no branching. Use bar for a plain categorical comparison, or heatmap for a two-dimensional intensity grid.

Mapping

Sankey takes an edge list — one row per link.

mapping:
  source: source
  target: target
  value: order_count

Nodes are inferred from the union of the source and target values — you do not declare them. Duplicate (source, target) pairs are summed, self-loops (source == target) are dropped, and non-positive values are ignored.

Shaping the data: one row = one link

A sankey usually spans several stages, but the input is a flat edge list. Build it in the model by producing one query per adjacent stage pair and composing them with UNION ALL — the pivot belongs in the model, where it is reviewable, not in the chart. For a source built from raw SQL:

WITH base AS (
  SELECT
    u.traffic_source AS traffic_source,
    p.category       AS category,
    oi.status        AS order_status
  FROM `bigquery-public-data.thelook_ecommerce.order_items` AS oi
  JOIN `bigquery-public-data.thelook_ecommerce.users`    AS u ON oi.user_id = u.id
  JOIN `bigquery-public-data.thelook_ecommerce.products` AS p ON oi.product_id = p.id
)
SELECT traffic_source AS source, category AS target, COUNT(*) AS order_count
FROM base GROUP BY 1, 2
UNION ALL
SELECT category AS source, order_status AS target, COUNT(*) AS order_count
FROM base GROUP BY 1, 2

Each additional stage adds one more SELECT … UNION ALL block. The result has exactly the three columns the mapping expects: source, target, order_count.

chart shortcuts

The chart block is typed and closed.

Pass-through style blocks: chart.label (node label styling), chart.line_style (link color, opacity, curveness), and chart.tooltip.

Two things sankey deliberately does not have: no legend (a single flow series has nothing to toggle) and no axis blocks (a sankey has no axes).

Links need a direction: no circular flow

A sankey must be a directed acyclic graph — flow moves forward through the stages and never loops back. If the data contains a cycle (for example A → B, B → C, C → A), the diagram cannot be laid out and the viz shows a clear message: "Sankey data contains a circular flow." That is a signal about the data, not an empty result — trace the loop in the query (usually a stage whose values reappear as an earlier stage's node names) and break it.

Cross-filter behavior

Sankey participates in cross-filtering on node clicks; clicks on the links between nodes are ignored. Inside a dashboard:

Worked example

Traffic attribution across two stages, anchored on the public thelook_ecommerce dataset:

id: traffic_attribution_sankey
title: Traffic Attribution Flow
query: "models/traffic_attribution.malloy::edges"
type: sankey
mapping:
  source: source
  target: target
  value: order_count
chart:
  height: 420
  orientation: horizontal
  node_align: justify
  show_value_labels: true
  cross_filter: true
  cross_filter_emit: target
format:
  order_count: "#,##0"
published: true

A read-only diagram (no cross-filter), laid out vertically:

chart:
  height: 320
  orientation: vertical
  cross_filter: false

Common pitfalls