Docs / Build Workflow

Cross-filtering

Qué es cross-filtering en Looky

Dentro de un dashboard, clickear un data point en una visualization estrecha cada otra visualization de la página. El par campo/valor clickeado se convierte en un "pill" removible que el usuario ve arriba del dashboard, y Looky aplica ese campo/valor a cada otra viz como parámetro en su próximo run.

Una visualization dice qué significa un click; un dashboard dice si ese click lo filtra. El cableo es automático, manejado por qué campos los models subyacentes declaran como parámetros, y el dashboard lo enciende o apaga por colocación con items[].cross_filter — o para todo el tablero con su propio cross_filter.

Dónde funciona el cross-filtering

  • En dashboards. Cualquier click en un chart en cualquier parte de un dashboard agrega (o saca) un pill.
  • No en la página standalone de visualization. Clickear un chart que está abierto como su propia página no produce un pill.

Los dos modos de layout se comportan igual: un dashboard fluid-grid y uno en modo document cablean los clicks en los mismos términos, decididos por los parámetros del modelo. Un export a PDF es una imagen fija, así que ahí los clicks simplemente no existen.

Qué viz types emiten clicks

  • bar — clickear una barra emite la categoría del eje x.
  • line — clickear un punto emite el valor x (single / dual-axis) o el nombre de la serie (multi-series).
  • pie — clickear un slice emite el label del slice.
  • scatter — clickear un punto emite lo que chart.cross_filter_emit nombre: "label", "x" o "series". Sin el knob de emit un click no emite nada.
  • funnel — clickear un stage emite el label del stage.
  • heatmap — emite lo que chart.cross_filter_emit nombre: "x" o "y". Sin él un click no emite nada.
  • sankey — clickear un nodo emite lo que chart.cross_filter_emit nombre: "source" o "target". Sin él un click no emite nada.
  • grid — clickear una celda en una columna configurada como clickeable emite el nombre de la columna y el valor de la celda.
  • report_matrix — clickear una celda del cuerpo emite el nombre de la columna y el valor de la celda, mismo shape que grid. Las filas de totales, las celdas de trend y los headers de sección no son clickeables.
  • kpi — no emite clicks (no tiene target categórico de click por estructura), pero consume pills: su query re-corre con los params activos igual que cada otra viz.
  • text — no emite clicks; la query de una nota dinámica re-corre con los params activos como cualquier otro consumer.

Pills

Los pills son la representación visual de los cross-filters activos arriba de un dashboard. El ciclo de vida:

  1. El usuario clickea un chart — el par campo/valor se convierte en un pill.
  2. Los pills son aditivos y combinan como AND — clickear en un segundo chart agrega otro pill, y los dos aplican a cada viz en el próximo run. Para semántica OR, codificala en el parámetro del model (ej. aceptar una lista, default "all").
  3. Clickear el botón de remove de un pill dropea ese filtro y re-corre el dashboard sin él.
  4. Los pills duran la sesión del dashboard — recargar la página los borra. Para estado de filtro compartible, usa un filtro declarado (Filters) — esos se reflejan en la URL.

Un click se vuelve pill cuando el campo clickeado es un parámetro declarado en la firma de source del propio archivo de model de esa viz. Para hacer un campo cross-filterable, declara un parámetro para él en el model que está detrás de la viz que el usuario clickea (mira Malloy support).

Emitir un pill y reaccionar a uno son pasos separados: el pill se le ofrece a cada viz del dashboard, y cada viz se estrecha si su propio model declara ese parámetro — se cubre a continuación.

Cómo llegan los valores de cross-filter a las queries subyacentes

Los valores de pill activos se mergean con cualquier valor de filtro a nivel dashboard y se pasan como parámetros a la query de cada viz en el próximo run. Desde el punto de vista del model, un pill de cross-filter es indistinguible de un valor que el usuario tipeó en un control de filtro — aplica el mismo parameter binding.

Los pills overridean los valores de filtro declarados cuando ambos setean el mismo parámetro. Así, un click en un chart que emite {country: "MX"} gana sobre el select "country" del dashboard que estaba seteado en "all". Sacar el pill restaura el valor declarado.

Declarar el parámetro

Declara el parámetro en la primera firma de source de cada archivo de model cuyas vizs tengan que reaccionar, y referencialo desde las views de ese mismo archivo. Una viz bindea los parámetros declarados en el archivo de model al que apunta su propio query, así que mantener la declaración local a ese archivo es lo que la cablea.

El match es por nombre de parámetro, y así es como un click se propaga: cada model que declara p_country se estrecha junto, entre tantos archivos como quieras. Cuando la semántica vive en un model compartido que importás, re-declara la firma en el archivo que importa — mira Semántica compartida, firma por model en Patrones, más abajo, para el shape.

Un parámetro se puede consumir en dos niveles. Nivel Malloy — p_country referenciado en un where: de Malloy, disponible para las views dentro del bloque extend { … } de su source. Nivel SQL — @country escrito dentro de un bloque .sql("""…"""), declarado en la firma como p_country, que es lo que le da su tipo.

Dale un default que signifique "sin filtro" — típicamente un sentinel como "all", un string vacío, o el valor válido más amplio — así la query corre sin filtrar hasta que un pill lo setea.

# in a Malloy model
##! experimental.parameters

source: ec_orders(
  p_country::string is "all",
  p_brand::string   is "all"
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  join_one: products is ecommerce.table('bigquery-public-data.thelook_ecommerce.products')
    on product_id = products.id
  join_one: users is ecommerce.table('bigquery-public-data.thelook_ecommerce.users')
    on user_id = users.id
  dimension: country is users.country
  dimension: brand   is products.brand

  view: by_category is {
    where:
      (p_country = "all" or country = p_country)
      and (p_brand = "all" or brand = p_brand)
    group_by: products.category
    aggregate:
      revenue is sum(sale_price)
  }
}

Con los parámetros de arriba, clickear una barra de country agrega un pill {country: "MX"}, el dashboard re-corre con p_country = "MX", y cada chart usando esta view se estrecha correspondientemente.

Escribir un predicado a nivel SQL

Dentro de un bloque .sql("""…"""), escribe el predicado de forma que "sin valor" signifique "sin filtro":

WHERE (@country IS NULL OR @country = '' OR @country = 'all' OR country = @country)

La guarda cubre los tres estados en que puede estar el parámetro — ausente, limpiado por un control de filtro, y el sentinel "all" — y hace que el resultado sin filtrar sea el default hasta que un pill setea un valor. Para parámetros numéricos y de fecha, envuelve el cast y dale el default: COALESCE(SAFE_CAST(@page_size AS INT64), 25).

Apagarlo en el dashboard

Si un click filtra el dashboard se decide donde se coloca la visualization, así una misma visualization puede ser interactiva en un dashboard y estática en otro. items[].cross_filter: false lo apaga para esa colocación; el cross_filter: false del propio dashboard lo apaga para todos los items, y el cross_filter: true de un item lo enciende para ese. El consumo no cambia: una visualization igual se acota cuando una pill nombra un parámetro que su model declara. kpi y text nunca emiten por estructura.

# todo el dashboard se lee; nada en él lo filtra
cross_filter: false
items:
  - visualization: ec_revenue_by_status_bar
    cross_filter: true        # salvo esta colocación

# o solo una colocación
items:
  - visualization: ec_revenue_headline_bar
    cross_filter: false

Un chart "headline" que siempre debería mostrar el total sin filtrar es la colocación típica a apagar — un chart top-line de revenue que no debería cambiar cuando el usuario clickea en otro lado.

Un item que dice cross_filter: true es una intención, y validate la sostiene: cuando esa visualization no puede emitir un parámetro que su model declare, el item se rechaza, nombrando el campo. Declara el parámetro en la firma de source del model, o quita el true.

Diferencias entre adapters

El routing del cross-filter en sí es el mismo en los tres adapters. Los valores de pill se vuelven parámetros de query; desde ahí aplican las mismas caveats de adapter que para valores de filtro — mira Diferencias entre adapters de source. Los pills numéricos y de string no se afectan; los pills de date/timestamp siguen el patrón de Postgres / MySQL.

Patrones

Un drill path por dashboard

Decide qué campos los usuarios deberían poder drillear (típicamente las dimensions en tus cláusulas group-by) y declara parámetros para esos en cada model usado por el dashboard. Saltea parámetros para campos que no deberían manejar cross-filter — los clicks en ellos caen silenciosamente.

Headline + drill grid

Usa un KPI arriba mostrando el total (el KPI no reacciona a pills, por diseño). Debajo, un bar chart que emite clicks. Debajo del bar, un grid que consume el cross-filter y muestra las filas subyacentes. Cada click en la barra estrecha el grid al subset matcheante.

Cross-filter + filtros declarados juntos

Mezcla un filtro de fecha arriba del dashboard (ej. date_range_preset) con cross-filtering implícito en el layout de charts. Ambos terminan como parámetros en las mismas queries.

Semántica compartida, firma por model

El cross-filtering no te obliga a meter los models de un dashboard en un solo archivo. Mantén la tabla, los joins, las dimensions y las measures en un model compartido, impórtalo, y re-declara la firma de parámetros en el source contra el que cada viz realmente corre. La duplicación es una línea de firma por model de entrada, no la semántica:

# shared/ecommerce.malloy — se importa, ninguna viz le apunta
##! experimental.parameters

source: base() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  join_one: users is ecommerce.table('bigquery-public-data.thelook_ecommerce.users')
    on user_id = users.id
  dimension: country is users.country
  measure:   revenue is sum(sale_price)
}
# models/by_category.malloy — acá sí apunta el `query` de una viz
##! experimental.parameters
import "shared/ecommerce.malloy"

source: by_category_src(p_country::string is "all") is base extend {
  view: main is {
    where: (p_country = "all" or country = p_country)
    group_by: category
    aggregate: revenue
  }
}

El pill bindea porque by_category_src es el primer source parametrizado de este archivo y declara p_country acá. Manteniendo la firma en el archivo que importa, el model compartido queda libre de plomería de filtros.

Un grid que pagina y cross-filtra a la vez

Esta es la única combinación que sí necesita su propio archivo de model. La paginación de filas tiene que pasar en SQL (LIMIT / OFFSET sobre el resultado agregado, más el conteo total), o sea que el model es un source .sql("""…""") — que no puede extender un source de tabla importado, y trabaja a un grano distinto del source compartido a nivel de línea. Dale a ese grid un model dedicado que combine paginación SQL con un parámetro de filtro a nivel SQL NULL-safe, y deja que el resto del dashboard siga usando el model compartido. Mira grid para el shape completo.