Docs / Build Workflow

Visualization — grid (alias: table)

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, default true. Setea a false para 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. Default 25. 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. Default 8. 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:

  1. Declarar p_page_size::number y p_page_offset::number en la firma del source.
  2. 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.
  3. Devolver el total en una columna llamada __looky_total_rows (típicamente COUNT(*) 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 emphasis puede declarativamente resaltar una fila matcheando un valor de cross-filter relacionado.
  • Deshabilita por viz con grid.cross_filter: false (el flag vive en el bloque grid — grid no tiene bloque chart).

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.columns y grid.column_widths explí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.