Cuándo usar sankey
Sankey es la opción correcta para preguntas de flujo y atribución — cómo se mueve el volumen de un conjunto de categorías a otro y dónde se ramifica o converge: traffic source → categoría de producto → estado de orden, presupuesto → equipo → resultado, movimiento etapa a etapa. Cada fila del query es un link: un nodo de origen, un nodo de destino y la magnitud que fluye entre ellos.
Usa funnel cuando el proceso es una cascada ordenada única sin ramificación. Usa bar para una comparación categórica simple, o heatmap para una grilla de intensidad bidimensional.
Mapping
Sankey toma una lista de aristas (edge list) — una fila por link.
mapping.source— requerido. Campo con el nombre del nodo de origen del link.mapping.target— requerido. Campo con el nombre del nodo de destino del link.mapping.value— requerido. Campo numérico con la magnitud del link (el ancho de la cinta).
mapping:
source: source
target: target
value: order_count
Los nodos se infieren de la unión de los valores de source y target — no los declaras. Los pares (source, target) duplicados se suman, los self-loops (source == target) se descartan, y los valores no positivos se ignoran.
Cómo darle forma al dato: una fila = un link
Un sankey normalmente abarca varias etapas, pero la entrada es una edge list plana. Armala en el modelo: produce un query por cada par de etapas adyacentes y componelos con UNION ALL — el pivot va en el modelo, donde es revisable, no en el chart. Para un source construido desde SQL crudo:
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
Cada etapa adicional agrega un bloque SELECT … UNION ALL más. El resultado tiene exactamente las tres columnas que espera el mapping: source, target, order_count.
El SQL vive dentro del modelo Malloy que referencia el query del viz — el archivo completo lo envuelve en un source de SQL crudo:
# models/traffic_attribution.malloy
##! experimental.parameters
source: traffic_attribution() is ecommerce.sql("""
-- the edge-list SQL above, verbatim
""") extend {
view: edges is { select: * }
}
Atajos de chart
El bloque chart es tipado y cerrado.
chart.orientation—"horizontal"(default) o"vertical". Dirección en que viaja el flujo.chart.node_align—"justify"(default),"left"o"right". Cómo se alinean los nodos en el diagrama.chart.node_width— ancho en píxeles de cada barra de nodo.chart.node_gap— espacio en píxeles entre nodos de la misma columna.chart.layout_iterations— número de iteraciones de posicionamiento (más = layout más asentado).chart.draggable— booleano; permite al lector arrastrar nodos para reordenar.chart.show_value_labels— booleano, default off. Prende los labels de nodo. Un sankey etiqueta todos los nodos, así que dejalo off salvo que el diagrama sea chico. Estilo conchart.label.chart.height— alto en píxeles del contenedor del viz.chart.cross_filter— booleano, defaulttrue. Siempre que no sea explícitamentefalse, el schema exigechart.cross_filter_emitjunto a cualquier bloquechart.chart.cross_filter_emit—"source"o"target". Qué rol de nodo emite el click como filtro.
Bloques de estilo pass-through: chart.label (estilo del label de nodo), chart.line_style (color, opacity, curveness del link) y chart.tooltip.
Dos cosas que sankey deliberadamente no tiene: no hay legend (una única serie de flujo no tiene nada que alternar) ni bloques de eje (un sankey no tiene ejes).
Los links necesitan dirección: nada de flujo circular
Un sankey tiene que ser un grafo dirigido acíclico — el flujo avanza por las etapas y nunca vuelve atrás. Si el dato contiene un ciclo (por ejemplo A → B, B → C, C → A), el diagrama no se puede posicionar y el viz muestra un mensaje claro: "Sankey data contains a circular flow." Es una señal sobre el dato, no un resultado vacío — rastrea el loop en el query (normalmente una etapa cuyos valores reaparecen como nombres de nodo de una etapa anterior) y rompelo.
Comportamiento de cross-filter
Sankey participa del cross-filter en el click sobre nodos; los clicks sobre los links entre nodos se ignoran. Dentro de un dashboard:
- Emite — al hacer click en un nodo agrega un pill para el campo nombrado por
chart.cross_filter_emit(sourceotarget) con el nombre del nodo clickeado, siempre que ese campo esté declarado como parámetro en una firma de source del archivo de model propio de esta viz. - Consume — los pills y filtros de dashboard seteados en otro lado se vuelven parámetros en la siguiente corrida; el query del sankey se re-ejecuta y los flujos se recalculan.
- Opt-out — pon
chart.cross_filter: falsepara suprimir la emisión de clicks pero seguir consumiendo pills.
Ejemplo
Atribución de tráfico en dos etapas, anclada en el dataset público thelook_ecommerce:
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
Un diagrama de solo lectura (sin cross-filter), en orientación vertical:
chart:
height: 320
orientation: vertical
cross_filter: false
Notas de diseño
- Los flujos forman un grafo dirigido acíclico — arma la query de forma que ningún link apunte de vuelta a un stage anterior.
- Un link renderiza cuando su
valuees positivo, ambos nombres de nodo son no-vacíos, y source ≠ target. - Los nombres de nodo se mergean entre stages: el mismo label apareciendo como categoría y como status se vuelve un solo nodo. Dale a los stages nombres distintos en la query salvo que el merge sea intencional.
- En diagramas densos, deja
chart.show_value_labelsapagado, o aumentachart.height/chart.node_gap.