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.

Cross-filtering es un comportamiento de runtime, no un feature de YAML. No hay bloque cross_filter en el YAML del dashboard — el cableo es automático, manejado por qué campos los models subyacentes declaran como parámetros.

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.
  • No en modo document. Un dashboard con layout_mode: document renderiza para lectura y export a PDF — el cableo de click se suprime por completo.

Si quieres comportamiento cross-filter, el usuario tiene que estar mirando el chart a través de un dashboard fluid-grid.

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". El knob de emit es requerido siempre que la viz tenga un bloque chart y cross_filter no sea explícitamente false.
  • funnel — clickear un stage emite el label del stage.
  • heatmap — emite lo que chart.cross_filter_emit nombre: "x" o "y". Misma regla de knob requerido que scatter.
  • sankey — clickear un nodo emite lo que chart.cross_filter_emit nombre: "source" o "target". Misma regla de knob requerido que scatter.
  • 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; el render en modo document suprime todo el cableo de click.
  • 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 Malloyp_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).

Opt-out por viz

Suprime la emisión de clicks para una viz específica seteando el flag de cross-filter a false en su YAML. El flag vive en el bloque de config primario de la viz — chart para vizs basadas en ECharts, el bloque con nombre del tipo para vizs basadas en DOM (porque no son "charts" en el sentido ECharts).

  • bar, line, pie, scatter, heatmap, funnel, sankeychart.cross_filter: false
  • gridgrid.cross_filter: false
  • report_matrixmatrix.cross_filter: false
  • kpi, text → sin flag; ninguno emite por estructura.
# ECharts-based viz (bar, line, pie, scatter, heatmap, funnel, sankey)
chart:
  cross_filter: false

# grid
grid:
  cross_filter: false

# report_matrix
matrix:
  cross_filter: false

Usa opt-out para charts "headline" que siempre deberían mostrar el total sin filtrar — un chart top-line de revenue que no debería cambiar cuando el usuario clickea en otro lado, por ejemplo.

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.