Cuándo usar grid
Grid es la elección correcta cuando la audiencia necesita leer records individuales, exportar data, o verificar el detalle detrás de un summary. El identifier de tipo es grid — no existe un tipo table; la validación lo rechaza.
Usa una viz tipo chart (bar, line) cuando la pregunta es sobre una comparación o un trend en vez de las filas raw. Usa report_matrix cuando los records necesitan agrupamiento con subtotales o están diseñados para PDF export.
Mapping
El mapping para grid es mínimo — las columnas vienen del resultado de la query.
mapping.columns— opcional. Array de nombres de columna. El subset a mostrar, en el orden dado. Usalo para dropear columnas ruidosas o forzar un orden de columna específico sin cambiar la query. Cuando se omite, cada columna en el resultado de la query se renderiza, en orden de query.
mapping:
columns:
- order_date
- category
- brand
- country
- status
- revenue
Bloque grid
Las opciones de grid viven bajo el bloque top-level grid. Grid no tiene un bloque chart.
Display de columna
grid.column_widths— objeto mapeando nombre de columna a un ancho fijo ("120px"), proporcional ("25%"), o valor numérico (pixels).grid.frozen_columns— número de columnas leftmost a congelar, o un array de nombres de columna. Útil cuando el grid scrollea horizontal y la audiencia necesita el identifier de fila siempre visible.grid.nowrap_columns— array de nombres de columna que nunca tienen que hacer wrap; el overflow muestra elipsis.grid.labels— objeto mapeando nombre de columna a display label (overridea el nombre raw de columna en el header).
Celdas compuestas
grid.composite_columns renderiza una celda como múltiples líneas tomadas de otros fields:
grid:
composite_columns:
customer:
lines:
- field: name
class: font-semibold
- field: city
prefix: "📍 "
show_empty: false
Celdas de comparación
grid.comparison_columns— array de nombres de columna a renderizar como indicador de trend up / down / dash al lado del valor. Útil para columnas de delta donde la audiencia necesita la dirección a primera vista.
Formats
grid.column_formats— objeto mapeando nombre de columna a una format key.grid.formats— objeto mapeando format key a un pattern. La indirección de dos pasos te deja reusar el mismo pattern en muchas columnas.
Cross-filter
grid.cross_filter— boolean, defaulttrue. Setea afalsepara suprimir click-to-filter en este grid.
Paginación
Las opciones de paginación viven bajo el bloque top-level pagination:
pagination.page_size— entero. Filas por página. Default25. Elige más grande cuando la audiencia hace data-export; más chico cuando el escaneo es el uso típico.pagination.column_page_size— entero. Cuando las columnas visibles exceden esto, se activa un column-pager horizontal. Default8. Aliases:columns_per_page,columns_page_size.
La paginación es server-side: el bloque pagination setea la ventana de página, y el model la aplica. Activarla requiere tres cosas — el bloque YAML de arriba, más dos en el model:
- Declarar
p_page_size::numberyp_page_offset::numberen la firma del source. - Cortar adentro del SQL con esos parámetros —
LIMIT @page_size OFFSET @page_offset, o el equivalente con ventana de row-number que se muestra abajo. - Devolver el total en una columna llamada
__looky_total_rows(típicamenteCOUNT(*) OVER()). Es lo que lee el paginador para saber cuántas páginas existen.
Cambiar de página re-corre la query subyacente con los nuevos parámetros de página. Las características de costo difieren por adapter — mira Diferencias entre adapters de source. El shape que funciona:
##! experimental.parameters
source: ec_orders_paged(
p_page_offset::number is null,
p_page_size::number is null
) is ecommerce.sql("""
WITH ranked AS (
SELECT
ROW_NUMBER() OVER (ORDER BY created_at DESC, order_id) AS ranking,
order_id, status, sale_price, created_at
FROM `bigquery-public-data.thelook_ecommerce.order_items`
),
tot AS (
SELECT COUNT(*) AS __looky_total_rows FROM ranked
)
SELECT r.order_id, r.status, r.sale_price, r.created_at, t.__looky_total_rows
FROM ranked AS r
CROSS JOIN tot AS t
WHERE r.ranking > COALESCE(SAFE_CAST(@page_offset AS INT64), 0)
AND r.ranking <= COALESCE(SAFE_CAST(@page_offset AS INT64), 0)
+ GREATEST(1, COALESCE(SAFE_CAST(@page_size AS INT64), 25))
ORDER BY r.ranking ASC
""") extend {
view: main is { select: * }
}
El grid manda la ventana de página a través de esos parámetros en cada cambio de página. Ojo que los parámetros se declaran como p_page_size / p_page_offset pero se referencian en SQL como @page_size / @page_offset — el prefijo p_ se cae del lado SQL. Los COALESCE son los que dan los valores de la primera carga, antes de que se haya elegido ninguna página.
Paginación junto con cross-filtering
Dale a un grid que necesita las dos — paginación de filas y cross-filtering — su propio archivo de model. La paginación trabaja sobre el resultado agregado con LIMIT / OFFSET en SQL, que es un grano distinto al del source compartido a nivel de línea que usa el resto del dashboard, y un source .sql("""…""") se sostiene solo en vez de extender un source de tabla importado.
Ese model combina ambos mecanismos — paginación SQL más un parámetro de filtro a nivel SQL — mientras las otras vizs siguen usando el model compartido:
##! experimental.parameters
source: orders_paged(
p_page_offset::number is null,
p_page_size::number is null,
p_country::string is null
) is ecommerce.sql("""
WITH agg AS (
SELECT category, country, SUM(sale_price) AS revenue
FROM `bigquery-public-data.thelook_ecommerce.order_items` oi
JOIN `bigquery-public-data.thelook_ecommerce.users` u ON oi.user_id = u.id
WHERE (@country IS NULL OR @country = '' OR @country = 'all' OR u.country = @country)
GROUP BY category, country
)
SELECT *, COUNT(*) OVER() AS __looky_total_rows
FROM agg
ORDER BY revenue DESC
LIMIT GREATEST(1, COALESCE(SAFE_CAST(@page_size AS INT64), 25))
OFFSET COALESCE(SAFE_CAST(@page_offset AS INT64), 0)
""") extend {
view: main is { select: * }
}
El parámetro de filtro es a nivel SQL acá para que el predicado corra antes de la agregación y del corte, y lleva la misma guarda que los parámetros de página para que la primera carga devuelva la primera página sin filtrar. Mira Cross-filtering para las reglas completas de alcance de parámetros.
format
El par grid.column_formats + grid.formats es la forma primaria de formatear columnas. El field format root actúa como fallback para cualquier columna no cubierta. Usa el patrón de indirección cuando el mismo estilo de número aplica a muchas columnas:
grid:
column_formats:
revenue: currency
avg_order_value: currency
refunds: currency
item_count: integer
formats:
currency: "$#,##0.00"
integer: "#,##0"
Comportamiento de cross-filter
- Clickear una celda cross-filtra el resto del dashboard por el nombre de columna y el valor clickeado. Una columna es clickeable exactamente cuando su nombre está declarado como parámetro en una firma de source del archivo de model propio de esta viz — no hay un toggle separado por columna.
- El bloque top-level
emphasispuede declarativamente resaltar una fila matcheando un valor de cross-filter relacionado. - Deshabilita por viz con
grid.cross_filter: false(el flag vive en el bloquegrid— grid no tiene bloquechart).
Mira Cross-filtering para el mecanismo completo.
Ejemplos trabajados
Detalle de orden con primera columna congelada, labels de header, formats de currency, y paginación de filas + columnas:
id: ec_orders_detail_grid
title: Order Detail
query: "models/ec_fulfillment.malloy::detail"
type: grid
mapping:
columns:
- order_date
- category
- brand
- country
- status
- item_count
- revenue
- avg_order_value
grid:
frozen_columns: 1
labels:
order_date: Date
item_count: Items
revenue: Revenue
avg_order_value: Avg order
column_widths:
order_date: "120px"
revenue: "140px"
column_formats:
revenue: currency
avg_order_value: currency
item_count: integer
formats:
currency: "$#,##0.00"
integer: "#,##0"
pagination:
page_size: 50
column_page_size: 8
published: true
Roster de customers con celdas compuestas:
id: customers_grid
title: Customers
query: "models/customers.malloy::roster"
type: grid
grid:
composite_columns:
customer:
lines:
- field: name
class: font-semibold
- field: email
prefix: "✉ "
- field: city
prefix: "📍 "
show_empty: false
column_widths:
customer: "260px"
lifetime_value: "140px"
column_formats:
lifetime_value: currency
formats:
currency: "$#,##0.00"
pagination:
page_size: 25
published: true
Notas de diseño
- Controla el espacio horizontal con un subset de
mapping.columnsygrid.column_widthsexplícitos — las keys de ancho matchean exacto los nombres de field del resultado. - El orden entre todas las filas vive en la query Malloy — con paginación server-side, un sort client-side ve una sola página.
- El texto del header sale de
grid.labels(o de un rename en la query). - El indicador de comparación lee el signo del valor — codifica los deltas "buenos" como positivos, los "malos" como negativos.