Cuándo usar scatter
Scatter es la elección correcta cuando la pregunta es si dos measures continuos se mueven juntos entre una población — margin vs revenue entre marcas, tasa de conversión vs tráfico entre páginas, latency vs throughput entre endpoints. Cada fila de la query se vuelve un punto.
Usá line en su lugar cuando el eje x está ordenado (típicamente tiempo) y te importa trend. Usá heatmap cuando los datos son densos y necesitás ver distribución en vez de puntos individuales.
Mapping
mapping.x— requerido. Field numérico de coordenada x. Las filas donde este valor es no-finito se dropean silenciosamente.mapping.y— requerido. Field numérico de coordenada y. Mismo filtro de finite-only.mapping.label— opcional. Nombre de field usado como el label de tooltip por punto y como el payload de evento de cross-filter cuando la emisión está habilitada.mapping.series— opcional. Nombre de field categórico. Cuando está presente el chart cambia a modo multi-serie: cada valor distinto se vuelve su propio grupo de puntos coloreado con entrada en leyenda, todos compartiendo los mismos ejes x/y. Una fila sigue siendo un punto — el field sólo decide a qué grupo (y color) pertenece el punto.mapping.size— opcional. Field numérico codificado como diámetro del punto (bubble chart). Convierte el scatter en bubble chart: posición x × y más un tercer measure como tamaño. Funciona en modo de una serie y multi-serie; los tamaños usan un dominio global entre todas las series para que las cohortes sean comparables. Las filas con size no-finito o negativo se dropean, igual que coordenadas x/y inválidas. Mutuamente excluyente conchart.symbol_sizeychart.large_threshold— el schema rechaza la combinación.
mapping:
x: revenue
y: margin_pct
label: brand
series: cohort # opcional — un grupo coloreado por valor distinto
Shortcuts de chart
El bloque chart es tipado y cerrado.
chart.point_color— color base de los puntos (hex), usado en modo de una sola serie. El bloque emphasis (mirá abajo) overridea esto para el punto resaltado. En modo multi-serie usáchart.series_colorsen su lugar.chart.series_colors— sólo multi-serie. Un mapa devalor de serie → color, ej.{ Champions: "#6c47ff", Rest: "#94a3b8" }. Cualquier valor no listado cae al palette por defecto en orden. Usá colores hex/CSS, no nombres de clases brand de Tailwind.chart.symbol_size— tamaño base del punto en pixels para scatter sin size (sinmapping.size). El default depende de la densidad del chart. No se puede combinar conmapping.size.chart.size_range— sólo modo sized (mapping.sizeseteado). Diámetro mínimo y máximo del punto en pixels; el dominio del field size se escala a este rango. Default[8, 40]. Inerte cuandomapping.sizeestá ausente.chart.size_scale— sólo modo sized.sqrt(default) hace que el área del bubble sea proporcional al valor — la codificación perceptualmente correcta.linearmapea valor a diámetro directamente (sobre-enfatiza valores grandes). Inerte sinmapping.size.chart.large_threshold— sólo multi-serie, default2000. Cuando una serie tiene más puntos que esto, cambia a un modo de dibujo masivo más rápido; el trade-off es que el hover/emphasis por punto se apaga sólo para esa serie. Los grupos más chicos conservan el hover completo. Subilo si necesitás hover en un grupo grande y podés pagar el dibujo más lento; bajalo para mantener grupos muy grandes responsivos. No se puede combinar conmapping.size— el dibujo masivo ignora el sizing por punto.chart.cross_filter— boolean, defaulttrue. Seteá afalsepara deshabilitar la emisión de click enteramente; en ese casocross_filter_emitno es requerido.chart.cross_filter_emit—"label","x"o"series". Elige qué valor se emite al click;"series"emite el valor de grupo del punto clickeado y sólo tiene sentido en modo multi-serie. Requerido cada vez quechart.cross_filterno es explícitamentefalse; el schema rechaza un bloque chart que no tenga ninguno.chart.legend— seteálegend.show: truepara mostrar la leyenda de series (los nombres de cohorte) en modo multi-serie.chart.height— altura en pixels del container de la viz.
Legend & tooltip
chart.legend y chart.tooltip comparten el mismo shape que en bar. Para scatter el tooltip es más útil con trigger: item — hovereando revela el label y las dos coordenadas de un punto a la vez.
Ejes
chart.x_axis y chart.y_axis comparten el mismo shape (con un extra en x_axis):
name— título del eje. Setear los dos se recomienda para scatter así la audiencia puede leer la relación.name_location,name_gap.axis_label.show,axis_label.rotate,axis_label.interval,axis_label.color,axis_label.font_size,axis_label.font_weight,axis_label.formatter,axis_label.max_chars.x_axis.visible_window— entero ≥ 1. Restringe el rango x visible.
format
format.xoformat[<x_field_name>]— pattern para labels del eje x y valor x del tooltip.format.yoformat[<y_field_name>]— pattern para labels del eje y y valor y del tooltip.format.sizeoformat[<size_field_name>]— pattern para el valor size en el tooltip cuandomapping.sizeestá seteado.formaten root — fallback.
Comportamiento de cross-filter
- Clickear un punto cross-filtra el resto del dashboard por el valor de label / series / x del punto.
- El field clickeado tiene que estar declarado como parámetro en al menos un model usado por el dashboard, o el click se ignora silenciosamente.
- El bloque top-level
emphasispuede declarativamente resaltar el punto matcheante dentro de la misma viz cuando un cross-filter relacionado está activo. - Deshabilitá por viz con
chart.cross_filter: false.
emphasis:
field: brand
value_from_param: highlight_brand
marker_color: "#6c47ff"
marker_size: 18
El punto cuyo brand iguala al valor de runtime de highlight_brand se renderiza en marker_color con marker_size (scatter sin size) o con un boost relativo de tamaño (bubble / scatter sized); el resto se quedan con chart.point_color / chart.symbol_size.
Ejemplos trabajados
Margin vs revenue entre marcas:
id: brand_margin_vs_revenue
title: Margin vs Revenue by Brand
query: "models/ec_revenue.malloy::by_brand"
type: scatter
mapping:
x: revenue
y: margin_pct
label: brand
chart:
height: 360
point_color: "#0f766e"
symbol_size: 12
x_axis:
name: Revenue
y_axis:
name: Margin
tooltip:
trigger: item
formatter: "{b} — {c0} / {c1}"
format:
revenue: "$#,##0"
margin_pct: "#,##0.00%"
published: true
Con emphasis desde un pill de dashboard:
type: scatter
mapping:
x: revenue
y: margin_pct
label: brand
chart:
point_color: "#94a3b8"
symbol_size: 10
emphasis:
field: brand
value_from_param: highlight_brand
marker_color: "#6c47ff"
marker_size: 18
Multi-serie — comparando dos cohortes de clientes en un mismo plano frecuencia × ticket. Cada fila es un cliente; series los separa en grupos coloreados con escala compartida, así las dos cohortes son directamente comparables en una sola viz en vez de dos charts lado a lado:
id: cohorts_freq_vs_ticket
title: Frequency vs Ticket by cohort
query: "models/rfm.malloy::cohort_points"
type: scatter
mapping:
x: order_frequency
y: avg_ticket
label: customer_id
series: cohort # ej. "Champions" vs "Rest"
chart:
height: 360
series_colors:
Champions: "#6c47ff"
Rest: "#94a3b8"
large_threshold: 2000 # el grupo grande "Rest" dibuja rápido; el chico "Champions" conserva hover
legend:
show: true
cross_filter: true
cross_filter_emit: series
x_axis:
name: Order frequency
y_axis:
name: Avg ticket
format:
order_frequency: "#,##0"
avg_ticket: "$#,##0.00"
published: true
Bubble chart — frecuencia × ticket × lifetime revenue por cliente, anclado en el dataset público de ecommerce de BigQuery. Agregá mapping.size para codificar un tercer measure como diámetro del punto; el tooltip muestra x, y y size:
id: customer_value_bubbles
title: Frequency × Ticket × Lifetime value
query: "models/<workspace_slug>/customer_value.malloy::value_points"
type: scatter
mapping:
x: order_frequency
y: avg_ticket
size: lifetime_revenue
label: customer_id
series: segment # opcional — bubble multi-serie
chart:
size_range: [8, 40]
size_scale: sqrt # default — área proporcional al valor
series_colors:
Champions: "#6c47ff"
legend:
show: true
cross_filter: true
cross_filter_emit: series
x_axis:
name: Order frequency
y_axis:
name: Avg ticket
format:
order_frequency: "#,##0"
avg_ticket: "$#,##0.00"
size: "$#,##0"
published: true
El model Malloy debería consultar bigquery-public-data.thelook_ecommerce (o una vista derivada). Los scatter sized funcionan mejor hasta unos pocos miles de puntos — más allá, los bubbles se solapan y baja la legibilidad; para cohortes muy grandes usá scatter sin size con large_threshold.
Errores comunes
- Demasiados puntos solapados. Scatter pierde señal cuando hay miles de puntos en la misma área.
large_thresholdmantiene un grupo grande responsivo pero no lo desatura — para legibilidad, pre-agregá en la query Malloy (ej. agrupá por bucket y usá un heatmap), o filtrá al top-N por algún measure interesante. Los bubble charts (mapping.size) se solapan aún más rápido; mantené los conteos en pocos miles de puntos o cambiá a scatter sin size. - Mezclar bubble sizing con dibujo masivo.
mapping.sizeno se puede combinar conchart.symbol_sizenichart.large_threshold— el schema lo rechaza. Elegí un modo por viz: tamaños data-driven, o tamaño estático + dibujo masivo adaptativo para cohortes enormes. - Outliers negativos o extremos comprimen el resto. Filtrá outliers en la query, o usá una escala log formateando el field. Las filas con
mapping.sizenegativo se dropean silenciosamente. - Los ejes están sin nombrar. Siempre seteá
chart.x_axis.nameychart.y_axis.name— scatter es la viz donde la audiencia más necesita los labels para leer la relación. - Los clicks de cross-filter no tienen efecto. El field clickeado (label / series / x) tiene que estar declarado como parámetro en al menos un model usado por el dashboard.