# Looky Studio — Documentation (full Markdown dump, language: es) > Authoritative reference for building Looky workspaces, models, visualizations, dashboards, filters, and exports. Each page below is also available as Markdown by appending `.md` to the URL. > Concatenation of every documentation page in Markdown form. Pages are separated by `---PAGE---` boundaries; each page begins with its own YAML frontmatter. ---PAGE--- --- title: Empezando slug: docs/getting-started language: es description: "Arrancá de cero en Looky: aceptar tu invitación, hacer sign in, conectar el CLI, crear el primer workspace e invitar a otros developers." last_modified: "2026-06-11T14:17:41.633000+00:00" docs_section: getting-started docs_summary: El camino de día cero para un nuevo owner o builder que entra a Looky por primera vez. --- ## Dos roles, dos puntos de partida Tu invitación determina lo que tenés que hacer el primer día. Elegí el camino que matchea con tu rol. BA Owner / Account Operator ### Explorar y gobernar Aceptar invitación → sign in → setear contexto de billing → listar workspaces en tu billing account → abrir dashboards en la UI. No hace falta setup de BigQuery ni autoría de models. Cualquier workspace que tus developers hayan publicado en el billing account está listo para explorar en la UI. Developer / Analytics Engineer ### Construir y publicar Aceptar invitación → sign in → setear contexto de billing → crear el service account de BigQuery → crear workspace → autorear sources, models, visualizations, dashboards → push. Esta guía usa BigQuery como camino inicial, así que se configura un service account de BigQuery antes de escribir models. Postgres y MySQL están igual de soportados — mirá [Sources](/docs/build-workflow/sources) si tus datos viven ahí. ## Prerequisitos antes de correr comandos - Tenés una invitación a la cuenta target en `https://my.looky.studio`. - El CLI de Looky está instalado y disponible como `looky` en tu shell. - Tenés una carpeta local que va a actuar como el linked root (ejemplo: ``). - Sabés el id del billing account en el que tenés que operar (ejemplo: ``). ## Secuencia de día cero 1. Aceptar invitación y verificar tu rol y alcance ([Invitación de Owner]({{ elemental_url_for_slug(slug='docs/getting-started/owner-invitation') }})). 2. Hacer sign in en la UI, después linkear el CLI a la misma instancia y root ([Sign In y conectar el CLI]({{ elemental_url_for_slug(slug='docs/getting-started/access-instance') }})). 3. **Solo developers:** Emití un JSON de service account de GCP — lo vas a dejar en la carpeta `secrets/` del workspace cuando exista, en el paso siguiente ([Acceso al Dataset de BigQuery]({{ elemental_url_for_slug(slug='docs/getting-started/bigquery-setup') }})). 4. Seteá el contexto de billing, creá el workspace, configurá `runtime/sources.runtime.yml`, y desplegá las settings con `looky push --settings` ([Creá tu primer workspace]({{ elemental_url_for_slug(slug='docs/getting-started/create-first-workspace') }})). 5. Invitá más builders solo después de que el primer workspace tenga sus sources desplegadas y al menos un push de content funcione ([Invitar developers y colaboradores]({{ elemental_url_for_slug(slug='docs/getting-started/invite-developers') }})). ## Flow base de comandos macOS / Linux Windows (PowerShell) ``` looky login https://my.looky.studio looky whoami cd looky billing list looky billing use cd / looky create --name "My Workspace" cd looky status # Editá runtime/sources.runtime.yml con tus valores reales de source # y dejá el JSON de credenciales en secrets/. Después desplegá: looky push --settings # Armá contenido bajo content/ (models, viz, dashboards). Después: looky validate looky push ``` ``` looky login https://my.looky.studio looky whoami Set-Location looky billing list looky billing use Set-Location \ looky create --name "My Workspace" Set-Location looky status # Editá runtime\sources.runtime.yml con tus valores reales de source # y dejá el JSON de credenciales en secrets\. Después desplegá: looky push --settings # Armá contenido bajo content\ (models, viz, dashboards). Después: looky validate looky push ``` ## Success criteria antes de pasar a Build Workflow - `looky whoami` devuelve tu usuario autenticado para el linked root. - `looky billing use ` tiene éxito desde ``. - `looky status` resuelve un workspace id con shape `/`. - `looky push --settings` deploya la config de runtime sources sin errores. Si alguno de estos falla, parate y arreglalo antes de autorear dashboards. La mayoría de las fallas downstream son fallas de setup disfrazadas. Correr `looky validate` tiene sentido solo después de que las sources estén deployadas y haya contenido para compilar. ---PAGE--- --- title: Invitación de Owner slug: docs/getting-started/owner-invitation language: es description: Cómo un nuevo owner de billing entra a Looky, acepta la invitación y entiende qué accesos desbloquea. last_modified: "2026-06-11T14:17:49.480000+00:00" docs_section: getting-started docs_summary: Empezá acá si alguien te invitó a ser owner de un billing account y armar el primer setup real de trabajo. --- ## Por qué importa la calidad de la invitación La invitación no es una formalidad. Define el límite de cuenta y de permisos en el que van a operar tus sesiones de CLI y UI. Si el scope de la invitación está mal, cada paso posterior (billing, pull, push) se vuelve confuso o queda bloqueado. ## Checklist de aceptación 1. Abrí el link de invitación y verificá el email target antes de aceptar. 2. Confirmá que el tipo de invitación (owner, billing developer, workspace access) matchea con lo que tenés que hacer. 3. Aceptá y completá el sign-in en `https://my.looky.studio`. 4. Abrí las áreas que tu rol debería exponer y confirmá que podés acceder sin errores de permisos. ## Guía de decisión por rol ### Invitación a nivel owner Usá solo para responsabilidades de ownership de la cuenta como gobernanza y management amplio de billing. ### Billing developer Usá cuando la persona necesita crear o manejar múltiples workspaces bajo un billing account. ### Workspace access Usá cuando la persona solo tiene que trabajar dentro de uno o pocos workspaces específicos. ## Fallas comunes y fixes ### Invitación expirada o revocada Pedí un link nuevo. Los links viejos no se pueden recuperar. ### Mismatch de email Si el email de la invitación y el email de login son distintos, pedile al que invita que regenere con el email correcto. ### Rol demasiado amplio o demasiado estrecho Arreglá el tipo de invitación antes de hacer setup del CLI. No hagas workaround del mismatch de rol con cambios locales ad-hoc. Salí de esta página solo cuando el scope del rol esté correcto. Eso te previene perder tiempo troubleshooteando comandos que fallan por razones de autorización. ---PAGE--- --- title: Sign In y conectar el CLI slug: docs/getting-started/access-instance language: es description: Hacé sign in en la instancia corriendo con OTP, validá acceso y linkeá tu local root con el CLI de Looky. last_modified: "2026-06-11T14:17:43.198000+00:00" docs_section: getting-started docs_summary: Usá OTP en la UI, después autenticá el CLI contra la misma instancia y local root. --- ## Primero sign in en la nube Empezá en `https://my.looky.studio` y completá el login OTP antes de tocar el CLI. Eso confirma tu rol y el acceso a la cuenta de manera independiente del estado de tu máquina local. 1. Abrí `https://my.looky.studio`. 2. Autenticate con email OTP. 3. Confirmá que el shell carga sin errores de acceso. ## Linkear el CLI con la misma instancia y local root El CLI guarda autenticación y contexto de billing por linked root. Elegí una carpeta root estable y seguila usando. ``` looky login https://my.looky.studio looky whoami ``` Si el CLI te pide email durante el login, dale la misma identidad que usaste en la UI. ## Activar contexto de billing desde el root macOS / Linux Windows (PowerShell) ``` cd looky billing list looky billing use ``` ``` Set-Location looky billing list looky billing use ``` Corré los comandos de billing desde el linked root mismo. El contexto de billing es root-scoped y tenés que setearlo antes de operaciones de workspace. ## Verificar el link del CLI antes de seguir 1. `looky whoami` devuelve tu email y la URL de la instancia. 2. `looky billing list` muestra al menos un billing account. 3. `looky billing use ` tiene éxito desde ``. Los chequeos workspace-scoped (`looky status`, `looky validate`) van en el paso siguiente, una vez que el workspace existe — mirá [Creá tu primer workspace]({{ elemental_url_for_slug(slug='docs/getting-started/create-first-workspace') }}). Si un colega ya te compartió un workspace, `looky workspaces` desde el linked root te lista los que podés acceder; podés hacer `cd` dentro de cualquiera y correr ahí los comandos workspace. ## Paridad entre nube y runtime local La nube (`my.looky.studio`) es tu target compartido. El runtime local (`http://localhost:8000`) es opcional para iteración rápida y debug. Usá los dos para verificación cuando haga falta. Si nube y local difieren, empezá con `looky status` y `looky diff` en el root del workspace. La mayoría de los mismatches son problemas de contexto o sync, no bugs de chart. ---PAGE--- --- title: Acceso al Dataset de BigQuery slug: docs/getting-started/bigquery-setup language: es description: Creá un service account de GCP con permisos mínimos, descargá el JSON key y cableá las credentials en el runtime config de tu workspace. last_modified: "2026-06-11T14:17:44.771000+00:00" docs_section: getting-started docs_summary: Creá un service account con permisos mínimos y cableá credentials en el runtime config. --- ## Qué necesitás antes de escribir un solo model Looky corre queries Malloy contra BigQuery a tu nombre. Para que eso funcione, tu workspace necesita un service account de GCP con suficientes permisos para leer datos y correr jobs. Esta página te lleva por crear uno, descargar el archivo de credentials y ponerlo donde el runtime lo espera. Vas a necesitar dos cosas de GCP: - Un **billing project**: el proyecto de GCP que paga el costo de las queries. Este es tu propio proyecto. - Una **dataset location**: el proyecto y dataset donde los datos realmente viven. Puede ser un proyecto distinto — incluyendo datasets públicos como `bigquery-public-data`. Estas dos cosas se confunden seguido. Tu service account vive en el billing project, pero se le puede dar read access a datasets en cualquier otro proyecto. ## Paso 1: Crear un service account en GCP 1. Abrí la [consola de Service Accounts de GCP](https://console.cloud.google.com/iam-admin/serviceaccounts) y seleccioná tu billing project. 2. Clickeá **Create service account**. 3. Dale un nombre claro, por ejemplo: `looky-workspace-reader`. 4. Clickeá **Create and continue**. ## Paso 2: Dar los roles mínimos requeridos En el paso "Grant this service account access to project", agregá estos dos roles: - **BigQuery Data Viewer** — permite leer data de tablas y schemas. - **BigQuery Job User** — permite correr query jobs (requerido incluso para queries read-only). Eso es el mínimo. No agregues Owner, Editor ni ningún rol más amplio. Clickeá **Done**. Si los datos que necesitás consultar viven en otro proyecto de GCP (por ejemplo un data warehouse compartido), también tenés que agregar **BigQuery Data Viewer** en ese proyecto para este mismo service account. Eso se hace desde la página de IAM del proyecto de datos, no del billing project. ## Paso 3: Descargar el JSON key 1. En la lista de service accounts, clickeá el account que recién creaste. 2. Abrí la pestaña **Keys**. 3. Clickeá **Add key → Create new key**. 4. Seleccioná **JSON** y clickeá **Create**. El archivo se descarga al toque. Renombrá el archivo a algo legible, por ejemplo: `my-workspace-bq.json`. ## Paso 4: Poner el key en tu workspace Copiá el JSON key en la carpeta `secrets/` de tu workspace: ``` / / / secrets/ my-workspace-bq.json ← acá va ``` Asegurate de que `.gitignore` excluya `secrets/` antes de commitear nada: ``` cat .gitignore ``` Tenés que ver `secrets/` o `secrets/*` listado. Si no está, agregalo antes de pushear a cualquier remote — Looky no lo enforcea; es tu responsabilidad. Nunca commitees el JSON key a git. Cualquiera con el archivo puede correr queries facturadas a tu proyecto de GCP. La exclusión de `secrets/` existe exactamente por esta razón. ## Paso 5: Referenciar el key en sources.runtime.yml Abrí `runtime/sources.runtime.yml` y poné `credentials_file` con el nombre del JSON que dejaste en `secrets/`. Es un filename plano — sin paths, sin slashes; la plataforma lo resuelve contra la carpeta `secrets/` del workspace. ``` sources: ecommerce: name: The Look Ecommerce type: bigquery project_id: my-gcp-billing-project credentials_file: my-workspace-bq.json datasets: - bigquery-public-data.thelook_ecommerce ``` - `project_id`: el proyecto de GCP que paga el costo de las queries — tu billing project. - `credentials_file`: filename del JSON de service-account dentro de `secrets/`. Pattern `^[A-Za-z0-9_-][A-Za-z0-9._-]*$` — sin separadores de path, sin punto inicial. - `datasets`: una o más referencias a datasets que el runtime tiene permitido consultar. Pueden estar en un proyecto de GCP distinto a `project_id`. ## Paso 6: Validar la conexión Desde el root del workspace, corré: ``` looky sources list looky validate ``` Si `sources list` devuelve tu alias sin errores y `validate` no muestra issues bloqueantes, la declaración del source es estructuralmente válida y el runtime puede llegar a BigQuery con las credentials que pasaste. La validación default **no** verifica que el service account tenga read access a cada dataset que referenciás — esos errores aparecen cuando una query real toca el dataset. Usá `looky validate --strict` para upgradear la validación a checks live-source por visualization (el `estimateQueryCost` de BigQuery es gratis y atrapa issues de permisos antes del push). Si la validación falla, las causas más comunes son: - El nombre de archivo en `credentials_file` no coincide con el archivo en `secrets/`, o trae separadores de path (debe ser filename plano). - Al service account le falta **BigQuery Job User** — las queries quedan bloqueadas incluso si los datos son legibles. - La referencia al dataset usa el proyecto o nombre de dataset equivocado — verificá los nombres exactos en la consola de BigQuery. ---PAGE--- --- title: Creá tu primer workspace slug: docs/getting-started/create-first-workspace language: es description: Usá el CLI de Looky para seleccionar contexto de billing, crear el primer workspace y scaffoldear el esqueleto local correctamente. last_modified: "2026-06-11T14:17:46.339000+00:00" docs_section: getting-started docs_summary: Setear contexto de billing, crear el workspace y arrancar desde un esqueleto local limpio. --- ## El escenario que cubre esta página Hiciste sign in en tu instancia de looky y tenés cero workspaces — nadie te compartió ninguno y tu operator no seedeó un starter. Esta página te lleva por crear tu primer workspace desde cero y publicar un dashboard chiquito para que veas el loop end-to-end. Si un colega ya te compartió un workspace, no necesitás esta página — necesitás [Sources](/docs/build-workflow/sources) y [Publish](/docs/build-workflow/publish) para empezar a contribuir. `looky workspaces` desde tu linked root te dice a qué workspaces tenés acceso de verdad. ## Antes de empezar Esta página asume que ya pasaste por [Acceder a tu instancia](/docs/getting-started/access-instance) — es decir, tenés: 1. Una URL de instancia (la que te dio tu operator, o `https://my.looky.studio` si estás en el deploy gestionado) y un `looky login ` funcionando. 2. Un billing account activo seteado en ese linked root (`looky billing list` lo muestra, `looky billing use ` lo selecciona). 3. Un directorio local root en disco y un subdirectorio de billing adentro: `//`. Cada workspace que crees vive bajo ese path. Si te falta alguno, arreglalo primero — los comandos de abajo asumen que el CLI sabe a qué instancia hablarle y a qué billing account asignar el nuevo workspace. Sanity check rápido: corré `looky whoami` desde `` y confirmá que imprime la instancia y email correctos. Después `cd /` — desde ahí vas a correr todos los comandos de esta página. ## Crear el workspace 1. Desde el directorio de billing, registrá el workspace en el servidor y scaffoldeá el esqueleto local en un solo paso: macOS / Linux Windows (PowerShell) ``` cd / looky create --name "My Workspace" --description "First build workspace" ``` ``` Set-Location \ looky create --name "My Workspace" --description "First build workspace" ``` El CLI llama a la API de tu instancia para registrar el workspace (así aparece en la UI y en `looky workspaces`) y después escribe la estructura local de carpetas bajo `.//`. Vos sos el owner del workspace con acceso de write. 2. Entrá a la carpeta nueva y confirmá que el CLI la resuelve: macOS / Linux Windows (PowerShell) ``` cd looky status ``` ``` Set-Location looky status ``` `looky status` resuelve el workspace id como `/` y confirma que el CLI está hablando con la instancia correcta. No corras `looky validate` todavía — no hay sources deployadas ni contenido para compilar, así que el resultado no aporta. Validá después de la sección "Publish y verificar" más abajo. ## Configurar tu data source Editá `runtime/sources.runtime.yml` para apuntar al warehouse que querés consultar. El shape exacto está documentado en [Sources](/docs/build-workflow/sources); ejemplos mínimos abajo. **BigQuery** — usando el dataset público `thelook_ecommerce` como playground: ``` sources: ecommerce: type: bigquery project_id: my-gcp-billing-project credentials_file: my-workspace-bq.json datasets: - bigquery-public-data.thelook_ecommerce ``` Dropeá el JSON del service account en `secrets/my-workspace-bq.json`. Mirá [Acceso al Dataset de BigQuery](/docs/getting-started/bigquery-setup) para saber cómo emitir ese archivo. **Postgres** — DSN (URI libpq sin `user:password@`) más un JSON de credenciales en `secrets/`: ``` sources: warehouse: type: postgres dsn: "postgresql://db.example.com:5432/analytics?sslmode=require" credentials_file: warehouse.json ``` Dropeá el JSON con user/password en `secrets/warehouse.json`: ``` {"user": "looky_reader", "password": "..."} ``` **MySQL** — DSN `mysql://host:port/database` (sin `user:password@`, sin query string) más un JSON de credenciales en `secrets/`: ``` sources: shop: type: mysql dsn: "mysql://db.example.com:3306/shop" credentials_file: shop.json ``` Dropeá el JSON con user/password en `secrets/shop.json` (mismo shape que Postgres). Una vez que el archivo está en lugar, pusheá el runtime config al servidor antes de cualquier content push: ``` looky push --settings ``` Este es el único push que sube `runtime/**` y `secrets/**`. Cada `looky push` subsiguiente envía solo content y asume que las settings ya están deployadas. ## Archivos mínimos para el primer dashboard ### `content/models/sales.malloy` ``` ##! experimental.parameters source: sales() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend { dimension: product is product_name measure: sales_amount is sum(sale_price) } query: total_sales is sales() -> { aggregate: sales_amount } query: sales_by_product is sales() -> { group_by: product aggregate: sales_amount order_by: sales_amount desc limit: 10 } ``` ### `content/visualizations/sales_total_kpi.yml` ``` id: sales_total_kpi title: Total Sales query: "models/sales.malloy::total_sales" type: kpi mapping: value: sales_amount published: true ``` ### `content/visualizations/sales_by_product_bar.yml` ``` id: sales_by_product_bar title: Sales by Product query: "models/sales.malloy::sales_by_product" type: bar mapping: x: product y: sales_amount published: true ``` ### `content/dashboards/sales_overview.yml` ``` id: sales_overview title: Sales Overview layout_mode: fluid_grid items: - visualization: sales_total_kpi - visualization: sales_by_product_bar published: true ``` ## Publish y verificar ``` looky validate looky diff looky push looky list visualizations looky list dashboards ``` Abrí la URL de tu instancia en un browser (la misma que usaste en `looky login`), navegá al workspace, y confirmá que el dashboard `sales_overview` es visible y renderiza los dos items. Un `looky push` verde significa que el servidor aceptó los archivos — no que el dashboard esté correcto. Siempre abrí la UI y mirá antes de dar por hecho el workspace. ## Estructura de directorios esperada ``` / / / workspace.yml content/ models/ visualizations/ dashboards/ exports/ runtime/ sources.runtime.yml secrets/ ``` Todos los comandos de workspace del CLI asumen que corrés desde el root de ``. ---PAGE--- --- title: Invitar developers y colaboradores slug: docs/getting-started/invite-developers language: es description: Sumá gente a tu billing account o a workspaces específicos. Invitá a un developer para darle acceso de construcción a nivel billing account, o a un user (viewer) para acceso de lectura a un workspace. last_modified: "2026-06-11T14:17:47.913000+00:00" docs_section: getting-started docs_summary: Invitá developers a tu billing account, o users a workspaces específicos. --- ## A quién podés invitar ### Developer Se suma a tu billing account. Puede crear y manejar workspaces, modelos, visualizations y dashboards adentro. Solo el owner del billing account puede mandar esta invitación. ### User (viewer) Se suma a uno o más workspaces específicos con acceso de lectura. Puede ver dashboards y visualizations, no puede editar. El owner o cualquier developer del billing account puede mandar esta invitación, para workspaces de ese billing account. Un billing account nuevo no se otorga por invitación — Looky Studio lo provee cuando un cliente se da de alta. ## Qué tiene que existir antes de mandar una invitación - **Invitar a un developer:** solo tu billing account. El developer puede arrancar desde un billing account vacío y crear sus workspaces desde cero después de aceptar. - **Invitar a un user (viewer):** al menos un workspace en tu billing account con al menos una visualization o dashboard publicada. Si no, la persona invitada entra a una pantalla vacía. ## Flow operativo de onboarding para cada developer nuevo 1. Creá y mandá la invitación con scope explícito en el mensaje. 2. Después de que acepte, pedile que corra: macOS / Linux Windows (PowerShell) ``` looky login https://my.looky.studio looky whoami cd looky billing list looky billing use ``` ``` looky login https://my.looky.studio looky whoami Set-Location looky billing list looky billing use ``` 3. Que el developer entre en un workspace asignado y corra: macOS / Linux Windows (PowerShell) ``` cd // looky status looky validate ``` ``` Set-Location \\ looky status looky validate ``` 4. Confirmá que puede leer dashboards en la UI antes de pedir writes. Usá el rol más estrecho que igual permita el trabajo. Esto evita cambios cross-workspace accidentales y mantiene la propiedad clara. ---PAGE--- --- title: Build Workflow slug: docs/build-workflow language: es description: La secuencia builder end-to-end desde la estructura del workspace hasta la publicación. last_modified: "2026-06-11T14:16:57.585000+00:00" docs_section: build-workflow docs_summary: Seguí el flow builder end-to-end antes de bajar a tipos de archivo individuales. --- ## El único orden de workflow que escala Looky es determinístico cuando construís en este orden. Si invertís los pasos, te quedan fallas ambiguas y delivery lenta. 1. La estructura y el contexto del workspace están correctos. 2. Los aliases de runtime sources están definidos y son válidos. 3. Los [models Malloy](/docs/build-workflow/models) exponen contratos de query estables e idiomáticos (capa semántica, named views — no SQL ad-hoc esparcido por todo el viz YAML). 4. Las visualizations mapean campos de query a renderers. 5. Los dashboards componen visualizations validadas. 6. Validate, diff, push, y verificar en la UI. ## La cadena canónica dentro de un workspace Cada workspace de Looky son las mismas cuatro capas, en este orden de dependencia — leé los archivos de tu workspace en este orden cada vez que necesites debuggear o extender: - `runtime/sources.runtime.yml` declara aliases de source (ej.: un alias `ecommerce` apuntando a un dataset de BigQuery, o a un connection string de Postgres / MySQL). - `content/models/*.malloy` define dimensions, measures y queries con nombre reutilizables sobre esos aliases de source. - `content/visualizations/*.yml` conecta una query de model con un renderer de chart/table. - `content/dashboards/*.yml` compone visualizations en las superficies finales que ve la audiencia. Cada capa referencia solo la de arriba. Si una capa falla la validación, arreglala antes de tocar nada debajo — las capas debajo no pueden recuperarse solas. ## Loop de builder que tenés que correr todos los días ``` cd // looky status looky validate looky diff looky push looky list visualizations looky list dashboards ``` Corré este loop por cada cambio significativo. Atrapa issues estructurales antes de que los usuarios vean dashboards rotos. ## Definition of done para un cambio de contenido - `looky validate` no tiene errores bloqueantes. - `looky diff` solo muestra archivos intencionados. - `looky push` tiene éxito en el workspace target. - El dashboard es visible y renderiza correcto en `https://my.looky.studio`. ---PAGE--- --- title: Workspaces slug: docs/build-workflow/workspaces language: es description: El contrato de la carpeta de workspace y la estructura mínima que un builder tiene que preservar. last_modified: "2026-06-11T14:17:40.060000+00:00" docs_section: build-workflow docs_summary: Mantené archivos de content, runtime config y handoff de deployment separados desde el principio. --- ## La carpeta de workspace es el límite de delivery El CLI de Looky resuelve contexto a partir de tu directorio actual. Directorio equivocado significa identidad de workspace equivocada, contexto de billing equivocado, y resultados de push poco confiables. ## Estructura de directorios requerida ``` / / / workspace.yml content/ models/ visualizations/ dashboards/ exports/ runtime/ sources.runtime.yml secrets/ ``` ## Cómo verificar el contexto del workspace rápido 1. Andá al root del workspace: ``` cd // ``` 2. Corré: ``` looky status looky validate ``` 3. Confirmá que el workspace id sea `/`. 4. Abrí `workspace.yml` y confirmá que el `id` matchea el slug de la carpeta. ## Borrar un workspace Corré `looky delete ` desde `/`. El CLI te pide tipear el slug como confirmación; pasá `--yes` para saltear el prompt en automatización. Quién puede borrar: - **Los owners del billing account** pueden borrar cualquier workspace bajo su billing account. - **super_admin** solo puede borrar workspaces que él mismo creó. - **Los developers del billing account** no pueden borrar workspaces — ni siquiera los que ellos crearon. Borrar es decisión del billing owner. Qué se borra en el servidor, atómicamente: - El directorio del workspace bajo `workspaces_root///` (workspace.yml, content, runtime, secrets, caches de runtime). - Cualquier staging de validate en vuelo o zombie bajo `workspaces_root/.validation-staging///`. - Las filas en `auth_workspace_owners`, `auth_workspace_memberships`, `auth_invitation_workspace_grants`, `catalog_items`, `ui_user_folders` y `ui_user_folder_items`. Qué se queda: - Tu copia **local** bajo `///`. El CLI la deja como está y te imprime un hint con `rm -rf` para que vos decidas cuándo eliminarla. - Las invitaciones pendientes que daban acceso al workspace borrado siguen siendo válidas para los *otros* workspaces que otorguen — solo se borra la fila del grant para este workspace. No hay soft-delete ni undo. Recrear el workspace requiere `looky create` seguido de un nuevo `looky push`. ## Qué se rompe cuando la estructura está mal - `looky validate` puede fallar al resolver el workspace local. - `looky push` puede apuntar al workspace id equivocado. - `looky list dashboards` puede mostrar resultados de un workspace inesperado. Tres niveles de directorio, enforced por el CLI. Los comandos que crean o borran un workspace corren un nivel *arriba* de la carpeta del workspace, porque la carpeta no existe todavía (`create`, `pull`) o está por desaparecer (`delete`). - **Desde `` exacto** — `looky billing list/use`, `looky workspaces`. Correrlos en un subdirectorio se rechaza. - **Desde `/`** — `looky create`, `looky pull`, `looky delete`. - **Desde cualquier subdirectorio bajo `//`** — todo el resto (`status`, `validate`, `diff`, `push`, `list`, `sources`). El CLI sube por el path para inferir el root del workspace, así que `content/models/` también funciona. ---PAGE--- --- title: Sources slug: docs/build-workflow/sources language: es description: Definí aliases de runtime sources antes de que los models dependan de ellos. last_modified: "2026-06-11T14:17:20.137000+00:00" docs_section: build-workflow docs_summary: Aliaseá tus runtime data sources de forma limpia para que los models sean estables entre environments. --- ## Sources son el paso uno de la confiabilidad del contenido Los models dependen de aliases de source. Si el alias, el filename de credentials o la referencia al dataset están mal, cada model, query y visualization construida arriba falla. Configurá y validá `runtime/sources.runtime.yml` antes de escribir un solo model. Si todavía no creaste tu service account de GCP, hacé eso primero. Mirá [Acceso al Dataset de BigQuery](/docs/getting-started/bigquery-setup). ## Working example: dataset público de BigQuery Working example usando el dataset público `bigquery-public-data.thelook_ecommerce`. Copialo como tu punto de partida; reemplazá `project_id` con tu propio billing project de GCP y `credentials_file` con el nombre del archivo JSON de tu service account (el archivo que dropeás en `secrets/`). El dataset es público, así que cualquiera con un service account de BigQuery funcionando lo puede consultar. ``` sources: ecommerce: name: The Look Ecommerce type: bigquery project_id: my-gcp-billing-project credentials_file: my-workspace-bq.json datasets: - bigquery-public-data.thelook_ecommerce ``` Campo por campo: - `ecommerce`: el alias que van a usar tus models de Malloy. Elegí algo corto y estable — cambiarlo después rompe cada model que lo referencia. - `project_id`: el proyecto de GCP que **paga el costo de las queries**. Este es tu billing project, no necesariamente donde viven los datos. - `credentials_file`: filename plano del JSON de service-account dropeado en la carpeta `secrets/` del workspace. Pattern `^[A-Za-z0-9_-][A-Za-z0-9._-]*$` — sin separadores de path, sin punto inicial. - `datasets`: las ubicaciones de dataset que el runtime puede consultar. Acá los datos están en `bigquery-public-data`, un proyecto distinto al billing — eso es normal y esperado. ## Working example: tu propio dataset privado Cuando los datos viven en tu propio proyecto de GCP, `project_id` y el proyecto del dataset son el mismo: ``` sources: sales: name: Sales Data type: bigquery project_id: my-company-gcp-project credentials_file: my-workspace-bq.json datasets: - my-company-gcp-project.sales_warehouse ``` Podés definir múltiples sources en el mismo archivo: ``` sources: sales: name: Sales Data type: bigquery project_id: my-company-gcp-project credentials_file: my-workspace-bq.json datasets: - my-company-gcp-project.sales_warehouse marketing: name: Marketing Data type: bigquery project_id: my-company-gcp-project credentials_file: my-workspace-bq.json datasets: - my-company-gcp-project.marketing_events ``` Cada alias se vuelve referenciable de forma independiente en los models. ## Working example: Postgres source Los sources de Postgres separan coordenadas de red y credenciales: el `dsn` es una URI estándar de libpq (host, port, database, flags TLS o de pool) **sin** el `user:password@`; el user y password viven en un JSON dentro de `secrets/` referenciado por `credentials_file`. La plataforma lee los dos, inyecta las credenciales en el DSN en memoria y lo pasa al driver de Postgres. `runtime/sources.runtime.yml` nunca contiene una password. ``` sources: warehouse: name: Internal Warehouse type: postgres dsn: "postgresql://pg.internal.example.com:5432/warehouse?sslmode=require" credentials_file: warehouse.json schemas: - reporting - public ``` Dropeá el JSON correspondiente en `secrets/warehouse.json`: ``` { "user": "looky_reader", "password": "..." } ``` Campo por campo: - `type`: tiene que ser `postgres`. - `name`: label humano opcional que se muestra en la UI cuando el source aparece en herramientas de operator. Puramente cosmético. - `dsn`: URI libpq sin userinfo. Tiene que empezar con `postgres://` o `postgresql://`; el regex del schema rechaza cualquier URI que contenga `@` (es decir, `user:password@`). Cualquier parámetro de query-string libpq — `sslmode`, `application_name`, `connect_timeout`, `target_session_attrs`, `channel_binding`, `gssencmode`, … — pasa intacto. - `credentials_file`: filename plano de un JSON adentro de la carpeta `secrets/` del workspace. Pattern `^[A-Za-z0-9_-][A-Za-z0-9._-]*$` — sin separadores de path, sin punto inicial. El JSON tiene que declarar `{"user": "…", "password": "…"}`. - `schemas`: array opcional que limita la introspección. Default a todos los schemas no-sistema (`pg_catalog` e `information_schema` quedan siempre excluidos). ## Working example: MySQL source Los sources de MySQL siguen la misma separación que Postgres: el `dsn` es una URI `mysql://host:port/database` **sin** el `user:password@`, y el user y password viven en un JSON dentro de `secrets/` referenciado por `credentials_file`. A diferencia de Postgres, un DSN de MySQL **no lleva parámetros de query** — Looky parsea el host, port y database de la URI y rechaza cualquier `?…`. `runtime/sources.runtime.yml` nunca contiene una password. ``` sources: shop: name: Online Shop type: mysql dsn: "mysql://db.internal.example.com:3306/shop" credentials_file: shop.json schemas: - shop ``` Dropeá el JSON correspondiente en `secrets/shop.json`: ``` { "user": "looky_reader", "password": "..." } ``` Campo por campo: - `type`: tiene que ser `mysql`. - `name`: label humano opcional que se muestra en la UI cuando el source aparece en herramientas de operator. Puramente cosmético. - `dsn`: URI `mysql://host:port/database` sin userinfo. El regex del schema rechaza cualquier URI que contenga `@` (es decir, `user:password@`) **o** un query string con `?`. El port es opcional y default a `3306`. - `credentials_file`: filename plano de un JSON adentro de la carpeta `secrets/` del workspace. Pattern `^[A-Za-z0-9_-][A-Za-z0-9._-]*$` — sin separadores de path, sin punto inicial. El JSON tiene que declarar `{"user": "…", "password": "…"}`. - `schemas`: array opcional de databases a exponer para introspección. Default a la database nombrada en el `dsn`. Dos cosas específicas de MySQL que conviene saber: las conexiones **no van encriptadas** (todavía no se puede configurar TLS hacia MySQL), así que usalo sobre una red de confianza; y MySQL no tiene un tipo boolean real, así que las columnas que parecen boolean vuelven como números — agregá un `cast(… as boolean)` explícito en tu model cuando necesites un filtro true/false. ## Matriz de campos por adapter Referencia rápida de qué requiere cada adapter: - **BigQuery** — requeridos: `type: bigquery`, `project_id`, `credentials_file`; opcionales: `location`, `datasets` (requeridos en tiempo de introspección, no en tiempo de validation). - **Postgres** — requeridos: `type: postgres`, `dsn` (URI libpq sin userinfo), `credentials_file` (filename de un JSON en `secrets/` con `{"user","password"}`); opcionales: `schemas`. - **MySQL** — requeridos: `type: mysql`, `dsn` (`mysql://host:port/database`, sin userinfo, sin query string), `credentials_file` (filename de un JSON en `secrets/` con `{"user","password"}`); opcionales: `schemas` (default a la database del DSN). Para la matriz de divergencias completa por adapter (auth, binding de parámetros, manejo de NULL, dryRun, introspección, caching), mirá [la comparación de adapters de source](/docs/reference/source-adapters). ## Workflow de validación de sources 1. Editá `runtime/sources.runtime.yml` desde el root de tu workspace. 2. Corré: ``` looky sources list looky sources diff looky validate ``` 3. Arreglá errores de alias, credentials o dataset antes de tocar models. `sources list` muestra los aliases de runtime actualmente registrados. `sources diff` muestra qué cambiaría en el push. `validate` chequea consistencia estructural sobre todo el workspace. ## Errores comunes en sources - **Alias mismatch:** el model referencia `sales` pero el runtime define `ecommerce`. Cada query usando ese alias falla. - **Filename de credentials equivocado:** `credentials_file` tiene que ser un filename plano dentro de `secrets/` — sin slashes, sin path prefix. - **Dataset fuera de la lista:** si un model consulta una tabla cuyo dataset no está listado en `datasets:`, la query se rechaza en runtime. - **Billing project equivocado:** si `project_id` no tiene **BigQuery Job User** otorgado al service account, las queries fallan incluso si los datos son legibles. Nunca hagas workaround de problemas de source dentro del model. Arreglá el alias y el runtime config una vez, y mantené los models limpios. ---PAGE--- --- title: Models slug: docs/build-workflow/models language: es description: "Capas semánticas de Malloy — shape estricto parametrizado y estructura idiomática: dimensions/measures/views reutilizables, archivos modulares por dominio y contratos de query estables para dashboards y tooling." last_modified: "2026-06-11T14:17:16.973000+00:00" docs_section: build-workflow docs_summary: Lógica gobernada compartida — shape obligatorio de Malloy, layering idiomático, named queries reutilizables. --- ## Los models son tu API semántica Poné la lógica de negocio en los models de Malloy, no en el YAML de visualization. Si una métrica sirve en más de un chart, definila una vez en la capa de model y reusala en todos lados. Cambiar la definición de "revenue" debería ser editar una línea en un archivo, no rastrear configs de visualization. ## Escribí Malloy idiomático — no solo Malloy válido Looky requiere un shape de archivo estricto (pragma arriba, sources parametrizados, parámetros en la signature — cubierto en Model mínimo abajo). Más allá del compliance, los workspaces mantenibles usan Malloy para lo que está diseñado: una capa semántica clara sobre tus datos. ### Preferí la capa semántica antes que defaultear a SQL crudo Declará **dimensions**, **measures** y **joins** sobre `alias.table(...)` o sobre una base importada con `extend`. Poné la analítica chart-ready en **named views** o queries top-level. Recurrí a bloques grandes de `alias.sql("""…""")` cuando el SQL legacy es la única forma práctica del dato, o cuando necesitás sustitución `@param` en SQL crudo (frecuente en Postgres o MySQL — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters)). Cuando todo vive dentro de strings opacos de SQL, perdés reusabilidad entre views y hacés los reviews más difíciles. ### Modularizá: scope de archivos por dominio Mantené analítica no relacionada en archivos `.malloy` separados. Compartí una base estable vía `import` y extendé una vez por tópico — mirá el patrón **Reusing a base source** abajo. Evitá un solo archivo que crezca sin límite mezclando varios dominios; es difícil reusar, auditar o partir después. ### Los nombres estables son el contrato — para teammates y tooling Las visualizations se atan a `path/model.malloy::query_or_view_name`; tratá esos nombres como APIs. Preferí labels de campo output que reflejen significado de negocio. Alineá los nombres de parámetros `p_*` entre models que estén en el mismo dashboard donde representan la misma dimension, así filtros y cross-filtros se comportan predecibles. La semántica estructurada y con nombre es más fácil para gente revisando diffs y para assistants o automatización localizando la definición autoritativa. ## Model mínimo que podés shippear hoy ``` ##! experimental.parameters source: sales() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend { dimension: product is product_name dimension: order_date is created_at::date measure: sales_amount is sum(sale_price) } query: total_sales is sales() -> { aggregate: sales_amount } query: sales_by_product is sales() -> { group_by: product aggregate: sales_amount order_by: sales_amount desc limit: 10 } ``` Dos cosas que cada model necesita: - `##! experimental.parameters` al tope del archivo. El engine de Looky compila cada model bajo ese flag. - Paréntesis vacíos `()` después del nombre del source (`source: sales()`) y después de cada referencia (`sales() -> {...}`). Incluso cuando el source no toma parámetros, los paréntesis son requeridos — declaran la lista de parámetros. El alias del source (`ecommerce`) tiene que matchear un alias definido en `runtime/sources.runtime.yml`. Los nombres de query (`total_sales`, `sales_by_product`) se vuelven los handles de referencia que se usan en el YAML de visualization. ## Views: definir named views dentro del source Para models más grandes, definí named views dentro del source usando `view:` en vez de declaraciones `query:` top-level. Las views viven dentro del source y pueden referenciar sus dimensions y measures directo. ``` ##! experimental.parameters source: ec_revenue() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend { dimension: category is products.category dimension: order_month is created_at::month measure: revenue is sum(sale_price) measure: order_count is count(order_id) view: over_time is { group_by: order_month aggregate: revenue, order_count order_by: order_month asc } view: by_category is { group_by: category aggregate: revenue, order_count order_by: revenue desc limit: 12 } } ``` Una visualization referencia una view exactamente igual que a una query top-level: ``` query: "models/ec_revenue.malloy::over_time" ``` Usá views cuando todas las queries pertenecen al mismo source semántico. Usá queries top-level cuando necesitás referenciar múltiples sources o correr joins cross-source — y acordate de invocar al source con `()` en la forma top-level (`query: x is ec_revenue() -> {...}`). ## Parámetros: cablear filtros de dashboard a queries de model Los filtros de dashboard controlan queries vía parámetros. Declará el parámetro en la signature del source (entre los paréntesis) y el filtro del dashboard pasa su valor en tiempo de render. ``` ##! experimental.parameters source: ec_orders( p_cutoff_date::date is @2024-01-01 ) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend { measure: revenue is sum(sale_price) ? created_at <= p_cutoff_date view: kpi is { aggregate: revenue } } ``` Cada parámetro lleva un nombre, un tipo y un default — `p_cutoff_date::date is @2024-01-01`. Tipos comunes: `date`, `string`, `number`, `boolean`. El default es lo que el engine usa cuando el dashboard no provee un valor (y lo que `looky validate` usa para el dry-run del model). El filtro del dashboard se ata al parámetro por nombre: ``` # en el YAML del dashboard filters: - id: global_period type: cutoff_date granularity: year param: p_cutoff_date # tiene que matchear el nombre del parámetro en el model default: "{{today}}" ``` Cuando un usuario cambia el filtro, el valor del parámetro se pasa a cada query del dashboard que lo referencia. ### Contrato estricto de `@param` para sources de SQL crudo Si tu source se construye desde SQL crudo (`connection.sql("""…""")`) y el SQL usa placeholders `@param`, cada `@param` tiene que tener una declaración matcheante en la signature del source. No hay fallback implícito — los placeholders no declarados hacen fallar el validate con un error claro apuntando a la declaración faltante. ``` ##! experimental.parameters source: nv_asesor_kpi_sql( p_date_from::date is null, p_date_to::date is null ) is natural_vitality_ventas.sql(""" WITH date_range AS ( SELECT COALESCE(CAST(@date_from AS DATE), DATE '2026-02-01') AS sd, COALESCE(CAST(@date_to AS DATE), DATE '2026-02-28') AS ed ) -- ... """) extend { view: kpi_asesor_resumen is { select: * } } ``` Los placeholders `@date_from` y `@date_to` en el SQL matchean las declaraciones `p_date_from` y `p_date_to` en la signature del source (el prefix `p_` es convencional). El filtro del dashboard pasa el valor en tiempo de render; el validate usa el default declarado. ## Reusar un source base entre models El patrón es: definir un source base paramétrico una vez y extenderlo en cada model de dominio. Esto evita duplicar definiciones de join, declaraciones de dimension y declaraciones de parámetro en cada archivo. ``` # ec_orders_base.malloy — base compartida ##! experimental.parameters source: ec_orders_base() is ecommerce.table('...order_items') extend { join_one: products is ecommerce.table('...products') on product_id = products.id measure: revenue is sum(sale_price) measure: order_count is count(order_id) } # ec_revenue.malloy — model de dominio extiende la base ##! experimental.parameters import "ec_orders_base.malloy" source: ec_revenue() is ec_orders_base() extend { view: by_category is { group_by: products.category aggregate: revenue limit: 12 } } ``` Mantené el model base estable. Agregá views nuevas en archivos por dominio, no en la base. Tanto el source base como el que extiende necesitan sus propios `()` — y el source que extiende tiene que invocar a la base con `()` también (`is ec_orders_base() extend {...}`). ## Cómo se resuelve el alias del source de un model Un model de Malloy usa un alias de conexión cuando llama a `alias.table(...)` o `alias.sql(...)`. Looky matchea cada alias contra las declaraciones de `runtime/sources.runtime.yml` del workspace. - Si el model usa un alias, Looky lo elige automáticamente. - Si el model usa varios aliases (directo o a través de imports), el run tiene que decir cuál usar; si no, se rechaza. Mirá [Sources](/docs/build-workflow/sources) para la sintaxis de declaración de alias por adapter. ## Notas de adapter para autores de model Looky bundlea una versión específica y pineada de Malloy con los adapters de BigQuery, Postgres y MySQL. No hay extensiones específicas de Looky a Malloy más allá de lo que esa versión de Malloy documenta para esos adapters. La diferencia más común entre adapters en models es alrededor de **parámetros de date y timestamp**. En Postgres y MySQL, preferí el patrón de placeholder `@param` en SQL crudo (apareado con una declaración matcheante en la signature del source, como se muestra arriba) así Looky sustituye el valor dentro del string de SQL en vez de bindearlo nativo. Mirá [Diferencias entre adapters de source](/docs/reference/source-adapters) para el patrón completo. ## Checklist de calidad de model - `##! experimental.parameters` arriba de cada archivo `.malloy`. - Cada declaración `source:` usa paréntesis, incluso cuando están vacíos (`source: foo() is …`). - Cada placeholder `@param` en SQL crudo tiene una declaración matcheante en la signature del source. - El alias del source existe en `runtime/sources.runtime.yml` (mirá [Sources](/docs/build-workflow/sources)). - Los nombres de view y query son estables — renombrarlos rompe las referencias de visualization. - Los nombres de campo reflejan significado de negocio, no necesidades de formato de chart. - Los parámetros se declaran con defaults razonables así las queries funcionan sin un filtro. - Cada view o query puede ser reusada por múltiples visualizations. - Preferí `table` + `extend` + views declarativo antes de apoyarte en sources `sql(...)` muy grandes, salvo donde shape legacy o patrones `@param` de Postgres lo requieran. - Scope de archivos por un dominio o una base compartida deliberada — evitá meter queries no relacionadas en un único archivo monolítico `.malloy`. ``` looky validate looky diff ``` No empieces trabajo de visualization hasta que la validación de models esté limpia. ---PAGE--- --- title: Soporte de Malloy slug: docs/build-workflow/malloy-support language: es description: "Referencia autoritativa del subset de Malloy que Looky corre: versión pineada, adapters BigQuery, Postgres y MySQL, parámetros con nombre, clases de error y la ausencia de extensiones específicas de Looky." last_modified: "2026-06-11T14:17:15.503000+00:00" docs_section: build-workflow docs_summary: Versión pineada de Malloy, adapters soportados, reglas de parameter binding. --- ## La versión de Malloy que corre Looky Looky shippea una versión específica y pineada de Malloy con los adapters de BigQuery, Postgres y MySQL en la misma versión. Cualquier sintaxis de Malloy más allá de lo que esa versión soporta no se entiende; cualquier feature agregado en releases posteriores de Malloy no está disponible hasta que Looky actualice. No hay **extensiones específicas de Looky a Malloy**. El dialecto que escribís es lo que Malloy mismo documenta — ni más, ni menos. ## Adapters soportados - **BigQuery** — conecta con un JSON key de service account. - **Postgres** — conecta con un connection string libpq (host / port / database, más flags TLS o de pool) y un secret de user/password. - **MySQL** — conecta con un connection string `mysql://host:port/database` y un secret de user/password. Dos detalles: las conexiones no van encriptadas (todavía no hay opción TLS), y MySQL no tiene un tipo boolean real, así que las columnas boolean vuelven como números — casteá explícitamente cuando necesites un filtro true/false. No hay otros adapters bundleados. Mirá [Sources](/docs/build-workflow/sources) para el schema YAML del source por adapter. ## Cómo se cargan los models El content de cada workspace está rooteado en `workspaces///content/`. Looky resuelve los archivos `.malloy` relativos a ese root. Las declaraciones `import "..."` dentro de un model se resuelven contra archivos `.malloy` hermanos en el mismo directorio. Usá imports para compartir una declaración de source base entre múltiples models de dominio — declará el source y sus joins una vez, extendelo por tópico. ``` # models/orders_base.malloy — base compartida ##! experimental.parameters source: orders_base() is bigquery.table('...order_items') extend { join_one: products on product_id = products.id measure: revenue is sum(sale_price) } # models/revenue.malloy — model de dominio extiende la base ##! experimental.parameters import "orders_base.malloy" source: revenue() is orders_base() extend { view: by_category is { group_by: products.category aggregate: revenue } } ``` Cada model necesita el pragma `##! experimental.parameters` arriba y paréntesis en cada declaración de source (y en cada referencia a un source) — incluso cuando el source no toma parámetros. Los paréntesis vacíos `()` declaran la lista de parámetros. ## Aliases de source dentro de los models Un model Malloy usa un alias de conexión cuando llama a `alias.table(...)` o `alias.sql(...)`. Looky matchea cada alias contra las declaraciones de `runtime/sources.runtime.yml` del workspace. - Si el model usa un alias, Looky lo elige automático. - Si el model usa varios aliases (directo o vía imports), el run tiene que decir cuál usar; si no, se rechaza. Mirá [Sources](/docs/build-workflow/sources) para la sintaxis de declaración de alias por adapter. ## Parámetros con nombre Los valores de filtro y cross-filter llegan a una query a través de parámetros con nombre declarados en la signature del source, entre los paréntesis. Cada parámetro lleva un nombre, un tipo y un default — `p_country::string is "all"`. Hay una regla de naming: los parámetros cuyo nombre Malloy empieza con `p_` tienen el prefix strippeado para el nombre externo. Un parámetro del model declarado como `p_start_date` se setea mandando `start_date` desde el filtro del dashboard. ``` ##! experimental.parameters source: orders( p_country::string is "all", p_start_date::date is @2024-01-01 ) is bigquery.table('...') extend { view: revenue is { where: (p_country = "all" or country = p_country) and created_at::date >= p_start_date aggregate: revenue is sum(sale_price) } } # el filtro / pill manda params: country: "MX" start_date: "2024-12-01" ``` Los parámetros pueden referenciarse de dos maneras dentro del cuerpo del source: - **Expresiones Malloy** — escribí `p_country`, `p_start_date` directo en `where:`, `aggregate:`, etc. (el ejemplo de arriba). - **Placeholders `@param` en SQL crudo** — cuando el cuerpo del source se construye desde SQL crudo (`connection.sql("""…""")`), usá placeholders `@param` dentro del bloque SQL. Cada `@param` tiene que tener una declaración matcheante en la signature del source; si no, validate falla con un error claro. ## Parámetros de date y timestamp en Postgres y MySQL Postgres y MySQL comparten una limitación conocida cuando Malloy bindea un parámetro de date o timestamp nativo en algunos shapes de query. El camino confiable en ambos es el **patrón `@param` en SQL crudo**: armá el source desde `connection.sql("""…""")`, referenciá placeholders `@param` dentro del SQL, y declará cada placeholder en la signature del source. ``` ##! experimental.parameters source: orders( p_date_from::date is null, p_date_to::date is null ) is postgres.sql(""" select * from orders where (@date_from::date is null or created_at::date >= @date_from::date) and (@date_to::date is null or created_at::date <= @date_to::date) """) extend { view: revenue is { aggregate: revenue is sum(sale_price) } } ``` Looky sustituye los placeholders `@date_from` / `@date_to` con valores literales (o `NULL` tipado cuando el dashboard no proveyó un valor). En BigQuery y MySQL el mismo patrón funciona — la sustitución es dialect-aware. Mirá [la comparación de adapters de source](/docs/reference/source-adapters) para la lista completa de divergencias. ## Cache y parámetros Cada entry cacheada está scoped a una query en un model con una combinación de parámetro específica. Un usuario filtrando por "MX" obtiene un resultado cacheado para esa combinación específica; un usuario filtrando por "AR" dispara una entry de cache separada la primera vez, después hace hit al cache en cargas posteriores. Editar el archivo `.malloy` invalida cada entry cacheada para queries adentro en el próximo request. Mirá [Cache](/docs/build-workflow/cache) para el shape del cache sidecar. ## Patrones trabajados #### Parámetro string con un sentinel "all" ``` ##! experimental.parameters source: orders( p_status::string is "all" ) is bigquery.table('...') extend { view: detail is { where: p_status = "all" or status = p_status select: * } } ``` #### Par de parámetros de date range (estilo expresión Malloy) ``` ##! experimental.parameters source: orders( p_date_from::date is @2024-01-01, p_date_to::date is @2024-12-31 ) is bigquery.table('...') extend { view: revenue is { where: created_at::date >= p_date_from and created_at::date <= p_date_to aggregate: revenue is sum(sale_price) } } ``` #### Par de parámetros de date range (estilo `@param` en SQL crudo) ``` ##! experimental.parameters source: orders( p_date_from::date is null, p_date_to::date is null ) is postgres.sql(""" select * from orders where (@date_from::date is null or created_at::date >= @date_from::date) and (@date_to::date is null or created_at::date <= @date_to::date) """) extend { view: revenue is { aggregate: revenue is sum(sale_price) } } ``` Los placeholders `@date_from` / `@date_to` se sustituyen en tiempo de run con los valores del filtro (o con `NULL` tipado cuando el dashboard no proveyó un valor). #### Parámetro string de mes ``` ##! experimental.parameters source: orders( p_month::string is "2024-12" ) is bigquery.table('...') extend { view: monthly is { where: format_datetime('%Y-%m', created_at) = p_month aggregate: revenue is sum(sale_price) } } ``` ## Errores comunes - **Falta `##! experimental.parameters` o faltan `()` en el source.** Validate falla con `unsupported_model_shape`. Agregá el pragma arriba del archivo y paréntesis en cada declaración `source:` (y cada referencia a ella). - **SQL crudo `@param` sin una declaración matcheante en la signature del source.** Validate falla con `unbound_param` nombrando el parámetro faltante. Agregalo a los paréntesis, ej. `p_date_from::date is null`. - **Mismatch de nombre de parámetro entre filtro y model.** El filtro manda el nombre externo; el model recibe el nombre Malloy con prefix (`p_`). Auditá los dos lados. - **La query falla cuando el parámetro no está seteado.** Declará un default en el model así la query igual corre. Para semántica "all", defaulteá a un valor sentinel que el where-clause trate como no-op. - **El model usa un feature de Malloy que el engine no conoce.** Looky pinea una versión de Malloy. Features más nuevos fallan en compile time. Quedate con features documentados en esa versión. - **Dos models en el mismo dashboard declaran nombres de parámetro distintos para la misma dimension.** Los clicks de cross-filter en esa dimension solo estrechan el model que usa el nombre matcheante. Estandarizá los nombres de parámetro entre los models en el mismo dashboard. - **El cache devuelve datos stale.** El cache está keyed por combinación de parámetro; si la data cambió pero la combinación de parámetro es la misma, la entry cacheada gana hasta que expire su TTL (o cambie el archivo del model). ---PAGE--- --- title: Cache de Queries slug: docs/build-workflow/cache language: es description: Agregá un archivo cache sidecar al lado de cualquier model para evitar re-correr las mismas queries de BigQuery en cada carga de dashboard. last_modified: "2026-06-11T14:16:59.166000+00:00" docs_section: build-workflow docs_summary: Agregá un sidecar .cache.yml al lado de un model para controlar freshness de query y reducir costos de BigQuery. --- ## Por qué importa el cache Cada carga de dashboard dispara queries Malloy contra BigQuery. Sin cache, las mismas agregaciones corren en cada page view — lo que significa que costo y latencia escalan con el tráfico, no con qué tan seguido cambian tus datos en realidad. Para la mayoría de los workloads analíticos, los datos cambian una vez al día o menos. Agregar un cache con un TTL razonable hace que los dashboards sean rápidos para los usuarios y predecibles en costo para vos. ## Cómo funciona: el archivo sidecar El cache se configura por model usando un archivo sidecar al lado del archivo `.malloy`. El sidecar tiene el mismo nombre que el model, con sufijo `.cache.yml`: ``` content/ models/ ec_revenue.malloy ec_revenue.cache.yml ← cache config para ec_revenue.malloy ec_performance.malloy ec_performance.cache.yml ``` Si no existe sidecar, las queries corren live en cada request. ## Working example ``` # content/models/ec_revenue.cache.yml model: models/ec_revenue.malloy defaults: cache: mode: auto ttl_seconds: 1800 ``` Campo por campo: - `model`: path al model al que aplica esta config de cache, relativo al root del workspace. - `defaults.cache.mode`: seteá a `auto`. Es el único modo soportado en la versión actual. Cachea resultados de query keyed por nombre de query y combinación de parámetro. - `defaults.cache.ttl_seconds`: cuánto tiempo los resultados cacheados son válidos. Después de este tiempo, el siguiente request dispara una query fresh y repobla el cache. `1800` = 30 minutos. ## Elegir un TTL Matcheá TTL a qué tan seguido cambian los datos subyacentes: - Pipelines batch diarios: `ttl_seconds: 86400` (24 horas) - Refreshes por hora: `ttl_seconds: 3600` (1 hora) - Near real-time: salteá el cache o usá `ttl_seconds: 300` (5 minutos) - Reportes y dashboards document: `ttl_seconds: 1800` (30 minutos) es un default seguro Setear un TTL muy corto en queries pesadas no las hace más frescas — solo las hace caras. Matcheá TTL al SLA real de freshness de los datos, no a qué tan seguido los usuarios abren el dashboard. ## Cache y filtros de dashboard Las cache keys incluyen los parámetros de query pasados por los filtros del dashboard. Un usuario filtrando por "2024" obtiene un resultado cacheado para esa combinación específica de parámetro. Un usuario filtrando por "2023" dispara una entry de cache separada la primera vez, después hace hit al cache en cargas posteriores. Esto significa que dashboards con muchas combinaciones distintas de filtro tienen un costo de warm-up de cache más grande. Para dashboards document con una fecha default fija, el cache típicamente está warm dentro de una carga. ## Qué invalida una entry cacheada Cada entry cacheada está scoped a una query en un model con una combinación de parámetro específica. Editar el archivo `.malloy` invalida cada entry cacheada para queries adentro en el siguiente request — no hay paso de eviction manual. ## Diferencias entre adapters El comportamiento del cache en sí es el mismo en los tres adapters. Las implicancias de costo difieren: - **BigQuery** — cada run sin cache es un scan facturado; favorecer TTLs más largos en queries estables domina la ecuación de costo. - **Postgres y MySQL** — el costo es mayormente latencia per-roundtrip; el cache ayuda menos cuando las tablas subyacentes son chicas y están bien indexadas. Mirá [la comparación de adapters de source](/docs/reference/source-adapters). ---PAGE--- --- title: Visualizations slug: docs/build-workflow/visualizations language: es description: Atá views reutilizables a queries de model sin empujar la lógica de negocio de vuelta a la capa de visualization. last_modified: "2026-06-11T14:17:21.712000+00:00" docs_section: build-workflow docs_summary: Mantené cada visualization enfocada en presentación y selección de query, no en reinventar métricas. --- ## Para qué sirve un YAML de visualization Un archivo de visualization ata una query de Malloy a un chart o una tabla. Cada transformación pertenece a la query del model; el YAML de visualization es solo presentación — chart type, qué campos van dónde, formatting, énfasis opcional. Para saber cómo formar esa capa de Malloy (dimensions, measures, views, archivos modulares), mirá [Models](/docs/build-workflow/models). Para la referencia por tipo (cada propiedad `chart.*` que acepta cada viz type), mirá [Tipos de visualization](/docs/build-workflow/viz-types) y la página de cada tipo. ## Campos top-level requeridos - `id` — identifier único en el workspace. Estable en el tiempo: los dashboards lo referencian, así que renombrarlo rompe dashboards. - `title` — título legible que se muestra en el dashboard. - `type` — discriminador del viz type. Tiene que ser uno de los tipos soportados listados en el [índice de viz-types](/docs/build-workflow/viz-types); cualquier otro se rechaza en tiempo de validación. - `query` — referencia a query de Malloy, exactamente en formato `path/to/file.malloy::query_name`. El path es desde el root del workspace; las dos mitades tienen que existir o la validación falla. ## Otros campos top-level - `mapping` — asignaciones de rol de campo. El shape varía por viz type — mirá la página por tipo. - `chart` — bloque de config tipado para los tipos basados en ECharts (bar, line, pie, scatter, heatmap, funnel). Superficie cerrada: solo las keys listadas en la referencia por tipo son aceptadas. - `kpi`, `grid`, `matrix` — bloques específicos del tipo para los tipos basados en DOM (kpi, grid/table, report_matrix). Usá estos en vez de `chart`. - `pagination` — para grid y report_matrix. - `filters` — lista de controles user-facing. Mirá [Filters](/docs/build-workflow/filters). - `format` — patrones de format de número/fecha por campo; field name → pattern string. - `emphasis` — regla declarativa de highlight (por tipo soportado — mirá las páginas por tipo). - `execution` — hints de ejecución de query (timeout, concurrency, override de cache). Shape permisivo hoy. - `tags` — labels free-form para búsqueda. - `published` — boolean. Solo visualizations con `true` aparecen en dashboards. ## El formato de query reference Cada visualization apunta a una query de Malloy con este formato exacto: ``` query: "models/ec_revenue.malloy::by_category" ``` La parte antes de `::` es el path al archivo `.malloy` desde el root del workspace. La parte después de `::` es el nombre de la view o query definida dentro de ese archivo. Las dos tienen que matchear exacto. La validación local (`looky validate`) chequea que el archivo existe y que la referencia usa el separador `::`. La validación server-side adicionalmente compila el model y hace dry-run de la query; si el nombre de la view no existe, o el model no compila, te queda un error claro antes del push. ## Ejemplos trabajados KPI mínimo: ``` id: ec_revenue_kpi title: Revenue query: "models/ec_revenue.malloy::kpi" type: kpi mapping: value: revenue delta: revenue_delta_pct format: revenue: "$#,##0a" revenue_delta_pct: "#,##0.00%" published: true ``` Bar con una serie — notá `mapping.series[]` (el legacy `mapping.y` ya no se acepta en bar): ``` id: ec_revenue_by_category_bar title: Revenue by Category query: "models/ec_revenue.malloy::by_category" type: bar mapping: x: category series: - field: revenue label: Revenue chart: show_value_labels: true value_label: position: top format: revenue: "$#,##0.00" published: true ``` Line con doble eje: ``` id: ec_revenue_over_time_line title: Revenue Over Time query: "models/ec_revenue.malloy::over_time" type: line mapping: x: order_month y: revenue y2: order_count series_label: Revenue series_label_2: Orders format: revenue: "$#,##0" order_count: "#,##0" published: true ``` Para cada otro shape (grouped bars, percent-stacked, donut pie, scatter, heatmap, funnel, grid con paginación, report_matrix con totales), mirá las páginas por tipo bajo [Tipos de visualization](/docs/build-workflow/viz-types). ## Cross-filtering de un vistazo Dentro de un dashboard, clickear un chart puede agregar un "pill" que estrecha cada otra viz de la página. Comportamiento de emit por tipo: - **Emiten clicks**: bar, line, pie, scatter, funnel, grid, report_matrix, heatmap (este último requiere `chart.cross_filter_emit` seteado a `"x"` o `"y"`). - **No emiten pero consumen pills**: kpi (no tiene target categórico de click por estructura). - **Opt-out por viz**: `chart.cross_filter: false` para vizs basadas en ECharts (bar, line, pie, scatter, heatmap, funnel); `grid.cross_filter: false` para grid; `matrix.cross_filter: false` para report_matrix. KPI no emite por estructura. Mecanismo completo en [Cross-filtering](/docs/build-workflow/cross-filtering). ## Qué chequea la validación de verdad (y qué no) La [Validación](/docs/build-workflow/publish) es de dos pasadas — local y después server-side. Por visualization, los chequeos son: - **Local**: el YAML parsea; `id`, `title`, `type`, `query` están presentes y no vacíos; `query` usa el separador `::`; el archivo de model existe; el `id` es único en todo el workspace; el bloque `chart` es schema-válido (cada propiedad se reconoce, los valores están en el rango permitido — error `VZ020`). - **Server-side**: el model Malloy subyacente compila, los sources son alcanzables, y la query hace dry-run contra el engine de runtime (gratis en BigQuery, EXPLAIN-only en Postgres en `--strict`). La validación **no** verifica que cada campo en `mapping` exista en el resultado de la query, ni que cada campo en `format` aparezca en `mapping`, ni que el chart "se vea bien". Esos issues aparecen solo en tiempo de render. Abrí la página de detalle de la visualization después del push para atraparlos. ``` looky validate looky list visualizations ``` ---PAGE--- --- title: Tipos de visualization slug: docs/build-workflow/viz-types language: es description: Referencia de todos los tipos de visualization soportados con ejemplos YAML funcionando para cada uno. last_modified: "2026-06-11T14:17:23.275000+00:00" docs_section: build-workflow docs_summary: Todos los tipos de visualization soportados con ejemplos funcionando y referencia de campos. --- ## Un tipo para un propósito Cada tipo de visualization responde una clase específica de pregunta. Elegir el tipo correcto no es sobre estética — es sobre hacer la respuesta legible a primera vista. Cada tipo tiene su propia página de referencia profunda abajo; cada página enumera exactamente qué campos `mapping.*` y `chart.*` lee el renderer, el comportamiento de cross-filter, ejemplos trabajados y errores comunes. Todas las visualizations comparten los mismos campos top-level (`id`, `title`, `query`, `type`, `filters`, `format`, `published`). La configuración por tipo vive en `mapping` más exactamente uno de: un bloque `chart` (para tipos basados en ECharts) o un bloque type-específico (`kpi`, `grid`, `matrix`) para los tipos basados en DOM. ## Tipos soportados Nueve renderers están expuestos detrás del campo `type:`. Dos tienen aliases (kpi/number → mismo renderer; grid/table → mismo renderer). - [`kpi`](/docs/build-workflow/viz-types/kpi) — métrica única con delta y comparación opcionales. Alias: `number`. - [`bar`](/docs/build-workflow/viz-types/bar) — comparación categórica; soporta stacking, dual-axis, orientación horizontal. - [`line`](/docs/build-workflow/viz-types/line) — series de tiempo; soporta dual axis y multi-series. - [`pie`](/docs/build-workflow/viz-types/pie) — composición part-of-whole; soporta variantes donut y rose. - [`scatter`](/docs/build-workflow/viz-types/scatter) — correlación de dos measures; un punto por fila. - [`heatmap`](/docs/build-workflow/viz-types/heatmap) — grid de intensidad bidimensional con scale de color explícita. - [`funnel`](/docs/build-workflow/viz-types/funnel) — stages de conversión o pipeline con drop-off. - [`grid`](/docs/build-workflow/viz-types/grid) — tabla de datos row-level; soporta paginación, columnas congeladas, celdas compuestas. Alias: `table`. - [`report_matrix`](/docs/build-workflow/viz-types/report-matrix) — reporte jerárquico con filas agrupadas, totales y PDF export. Cualquier otra cosa pasada como `type:` no está soportada y se rechaza en tiempo de validación. ## El bloque chart es tipado y cerrado Para tipos basados en ECharts (bar, line, pie, scatter, heatmap, funnel) el bloque `chart` es una superficie chica y tipada. Tres categorías de propiedades viven adentro: - **Shortcuts de Looky** — keys únicas que colapsan múltiples decisiones coordinadas en una sola decisión (ej. `chart.stack: percent`, `chart.variant: donut`, `chart.show_value_labels`). - **Campos pass-through** — escalares o arrays espejados a una opción subyacente específica (ej. `chart.center`, `chart.symbol_size`, `chart.gap`). - **Bloques pass-through** — objetos anidados con un set curado snake_case de propiedades (ej. `chart.legend`, `chart.tooltip`, `chart.x_axis`, `chart.y_axis`, `chart.value_label`, `chart.label`, `chart.visual_map`). Cualquier propiedad no listada en la página de referencia por tipo se rechaza en tiempo de validación. No hay escape hatch para opciones raw de la chart library. ## Los tipos basados en DOM usan su propio bloque kpi, grid y report_matrix no están construidos sobre una chart library, así que no tienen un bloque `chart` en absoluto. En su lugar usan un bloque top-level type-específico — `kpi`, `grid`, `matrix` — más bloques auxiliares (`pagination` para grid y report_matrix, `comparison` para kpi). ## Dónde buscar cada tópico - **Qué campos acepta cada viz type** — la página de referencia por tipo arriba. - **Cómo se cablean los filtros** — [Filters](/docs/build-workflow/filters). - **Comportamiento de cross-filter** — [Cross-filtering](/docs/build-workflow/cross-filtering). - **Qué sintaxis de Malloy entiende el engine** — [Soporte de Malloy](/docs/build-workflow/malloy-support). - **Cómo los dashboards componen visualizations** — [Dashboards](/docs/build-workflow/dashboards). - **Divergencias de adapter** — [Diferencias entre adapters de source](/docs/reference/source-adapters). ## Referencia de format strings (compartida por cada tipo) El formato de número es field-keyed en todos los tipos de viz. Los patterns por field ganan sobre patterns de slot; los patterns de slot ganan sobre el pattern root. La gramática de patterns: - `$#0,0a` — currency abreviada: $1.2M, $340K - `$#0,00` — currency completa con dos decimales: $1,234.56 - `#0` — entero: 1234 - `#.##0,0` — número con un decimal: 1,234.5 - `#0,00%` — porcentaje con dos decimales: 12.34% - `#.##0,00%` — porcentaje con más precisión: 12.345% - `EUR#0` — prefix de código de currency: EUR1234 - `#0a` — compacto: 1.2K, 340K, 1.2M - `#0b` — bytes: 1.2KB, 340MB, 1.2GB Si no hay format pattern seteado para un field, la plataforma cae a un format decimal default con dos dígitos de fracción. ---PAGE--- --- title: Visualization — kpi slug: docs/build-workflow/viz-types/kpi language: es description: "Referencia autoritativa del tipo de visualization kpi (alias: number): cada campo mapping soportado, opciones del bloque kpi, manejo de format y notas de adapter." last_modified: "2026-06-11T14:17:32.196000+00:00" docs_section: build-workflow docs_summary: "Métrica única con delta y comparación opcionales; alias: number." --- ## Cuándo usar kpi KPI es la elección correcta cuando la respuesta es un solo número — total de revenue, count de orders, tasa de conversión, error budget consumido. Agregá un delta o un valor secundario para mostrar dirección o comparación al lado del headline. KPI es el "headline" típico de un dashboard: no emite clicks (no hay dimension para clickear), pero reacciona a filtros de dashboard y pills de cross-filter como cualquier otra viz — su query subyacente re-corre con los params activos y el número headline se recomputa. Si necesitás un número por categoría en vez de un solo número, usá [bar](/docs/build-workflow/viz-types/bar). Si necesitás un trend en el tiempo, usá [line](/docs/build-workflow/viz-types/line). Si necesitás muchos números en un layout tabular, usá [grid](/docs/build-workflow/viz-types/grid). Los identifiers de tipo `kpi` y `number` mapean al mismo renderer; cualquiera de los dos se acepta. ## Mapping El KPI lee la primera fila del resultado de la query. Los campos de mapping nombran qué columnas de esa fila se vuelven qué elemento visual: - `mapping.value` — requerido. El field cuyo valor es el número headline. - `mapping.subtitle` — field string para una línea de contexto label / período debajo del valor. - `mapping.delta` — field numérico mostrado como indicador de cambio. El signo maneja la flecha up/down; la config de polaridad controla el color. - `mapping.secondary_value` — field numérico para un valor de comparación (ej. "período anterior") cuando no hay `delta`. - `mapping.secondary_label` — caption para `secondary_value`, escrito como **string literal** (ej. `"Invoices"`, `"Previous quarter"`). También acepta un *nombre de campo*: si el valor coincide con una columna de la fila de resultado, se usa el valor de esa columna como caption. Default a `"Previous"` cuando se omite. - `mapping.secondary_plain` — field de texto plano mostrado al lado del headline cuando no hay delta ni valor secundario. ``` mapping: value: revenue subtitle: period_label delta: revenue_delta_pct ``` ## Bloque kpi Las opciones específicas de KPI viven bajo el bloque top-level `kpi`. KPI no tiene un bloque `chart`. - `kpi.comparison_label` — caption mostrado debajo del delta (ej. `"vs. last week"`). Cuando se omite, el renderer compone un default tipo "Up vs previous period" desde el signo del delta. - `kpi.delta_good_when` — `"increase"` (default) o `"decrease"`. Setea la polaridad que colorea el delta en verde vs rojo. Usá `"decrease"` para métricas donde menos es mejor — refunds, error rate, time-to-resolution. ## format El format de número es field-keyed. Patterns por field ganan sobre el pattern root. - `format.value` — pattern para el número headline. - `format.delta` — pattern para el indicador delta. - `format.secondary_value` — pattern para el valor de comparación secundario. - `format` en root — fallback cuando falta una entry field-específica. ``` # headline como currency abreviada, delta como porcentaje format: revenue: "$#,##0a" revenue_delta_pct: "#,##0.00%" ``` La gramática de patterns completa vive en el [overview de viz-types](/docs/build-workflow/viz-types). ## Comportamiento de cross-filter KPI participa en cross-filtering **solo como consumer**: - **No emite.** Clickear un KPI no agrega un pill — no hay dimension para clickear. - **Sí reacciona.** Los pills seteados en otra parte del dashboard, y cualquier filtro a nivel dashboard, se vuelven parámetros en el próximo run, y la query subyacente del KPI re-corre con ellos. El número headline se recomputa en consecuencia. Si querés un "headline que siempre muestre el total dashboard-wide" sin importar los pills, escribí la query Malloy subyacente para que los defaults de su parámetro ignoren los valores de pill (ej. aceptar el parámetro pero no usarlo en el where-clause), o usá dos items KPI separados: uno atado a un model que ignora el parámetro de cross-filter, otro atado a un model que lo respeta. ## Ejemplos trabajados Headline con delta y polaridad de porcentaje favoreciendo decrease (menor error rate es mejor): ``` id: ec_error_rate_kpi title: Error Rate query: "models/ec_quality.malloy::error_rate_kpi" type: kpi mapping: value: error_rate delta: error_rate_delta_pct subtitle: period_label format: error_rate: "#,##0.00%" error_rate_delta_pct: "#,##0.00%" kpi: comparison_label: "vs previous period" delta_good_when: decrease published: true ``` Headline con un valor de comparación secundario (sin delta): ``` id: ec_revenue_vs_prev_kpi title: Revenue query: "models/ec_revenue.malloy::current_vs_previous" type: kpi mapping: value: revenue_current secondary_value: revenue_previous secondary_label: "Previous quarter" format: revenue_current: "$#,##0a" revenue_previous: "$#,##0a" published: true ``` Headline con una anotación de texto plano cuando la métrica no tiene una comparación numérica: ``` id: ec_top_brand_kpi title: Top Brand query: "models/ec_revenue.malloy::top_brand" type: kpi mapping: value: brand_name secondary_plain: market_share_label published: true ``` ## Errores comunes - **El KPI muestra el número equivocado.** Solo se lee la primera fila del resultado de query. Asegurate que la query subyacente agregue a una fila, o ordenala para que el valor headline sea la fila que querés. - **La polaridad del delta está invertida.** Seteá `kpi.delta_good_when: decrease` para métricas donde menos es mejor (refunds, errores, latency). El default `"increase"` asume que más es mejor. - **El KPI está reaccionando a un pill cuando querías un headline global.** KPI consume pills como cada otra viz. Para hacer un KPI "siempre-total", escribí el model subyacente así el parámetro se acepta pero no se usa en el where-clause de esa view. - **El valor headline no se formatea.** Seteá `format.value` (o la entry field-específica bajo `format`) — sin un pattern, el valor se renderiza con el default de la plataforma. - **Mezclar `delta` y `secondary_value` en el mismo KPI.** Elegí uno o el otro; mezclar hace el chart ocupado y el renderer prefiere `delta`. ---PAGE--- --- title: Visualization — bar slug: docs/build-workflow/viz-types/bar language: es description: "Referencia autoritativa del tipo de visualization bar: cada campo mapping soportado, cada opción chart, manejo de format, comportamiento de cross-filter y notas de adapters de source." last_modified: "2026-06-11T14:17:24.932000+00:00" docs_section: build-workflow docs_summary: Comparación categórica; soporta stacking, dual-axis, orientación horizontal. --- ## Cuándo usar bar Bar es la elección correcta cuando la pregunta es *"¿cómo se comparan estas categorías?"*. El eje de categoría lista cosas discretas — productos, regiones, channels, statuses — y el eje de valor mide una o más cantidades numéricas para cada una. El mismo renderer cubre cuatro shapes comunes: - **Single-series** — una barra por categoría, un measure. El default. - **Grouped** — múltiples barras por categoría, una por measure. Andá por ella cuando los measures están en la misma escala (revenue y refunds, por ejemplo). - **Stacked** — barras apiladas una arriba de otra; la altura total es la suma de todos los measures. Usá `chart.stack: normal`. - **Percent-stacked** — cada categoría reescalada a 100%, con las barras mostrando la parte de cada componente. Usá `chart.stack: percent`. - **Dual-axis** — dos measures en escalas distintas compartiendo un chart, uno bindeado a un eje y izquierdo y el otro a uno derecho. Seteá `axis: right` en la segunda series. - **Combo (bar + line)** — barras para un measure más una línea sobre las mismas categorías para otro (por ejemplo un overlay de porcentaje acumulado). Seteá `type: line` en la series de línea; normalmente con `axis: right` y `chart.y2_axis`. Usá [line](/docs/build-workflow/viz-types/line) en su lugar cuando el eje x está ordenado por tiempo y la pregunta es sobre *trend*. Usá [pie](/docs/build-workflow/viz-types/pie) cuando hay muy pocas categorías (≤ 6) y la pregunta es puramente sobre composición. Usá [grid](/docs/build-workflow/viz-types/grid) cuando la audiencia necesita los números reales al lado de los nombres de categoría en vez de una comparación visual. ## Mapping Dos campos de mapping manejan el chart: - `mapping.x` — requerido. El campo categórico. Cada valor distinto se vuelve un tick en el eje de categoría. Usá un field string para labels limpios; los fields numéricos o de fecha se formatean con el default de la plataforma a menos que overrides vía `format`. - `mapping.series` — requerido. Array. Una entry por measure a plotear. Cada entry tiene que declarar un `field`; lo demás es opcional. ``` mapping: x: category series: - field: revenue ``` Usá una entry para un chart simple, dos o más para grouped o stacked, y el `axis: right` opcional en una series para ponerla en el eje y secundario. Opciones por series: - `field` — requerido. El field numérico del resultado de la query que maneja la altura de barra para esta series. - `label` — string. Se muestra en la legend y el tooltip. Default al nombre del field. - `label_from_field` — string. Trae el label de legend desde un field en los datos en vez de declararlo en YAML. - `color` — color hex para esta series. Default al próximo slot en la paleta de la plataforma. - `axis` — `"left"` (default) o `"right"`. Right pone la series en el eje y secundario. - `type` — `"bar"` (default) o `"line"`. `line` dibuja el measure como línea sobre las mismas categorías (combo chart). Las series line nunca se apilan. No combinable con `chart.orientation: horizontal` — la validación rechaza esa combinación. ``` # dos measures en la misma escala (grouped) series: - field: revenue label: Revenue - field: refunds label: Refunds # dos measures en escalas distintas (dual-axis) series: - field: revenue label: Revenue - field: order_count label: Orders axis: right # un measure con color explícito series: - field: revenue label: Revenue color: "#6c47ff" ``` ## Shortcuts de chart Keys top-level del bloque `chart`. El bloque `chart` es tipado y cerrado — cualquier cosa no listada en esta página se rechaza en tiempo de validación. - `chart.orientation` — `"vertical"` (default) o `"horizontal"`. Elegí horizontal cuando los labels de categoría son largos, o cuando la audiencia lee top-to-bottom (rankings, leaderboards). - `chart.stack` — `"normal"` (sum stacking) o `"percent"` (reescalar cada categoría a 100% y switchear los labels del eje de valor a porcentajes). Omitirlo para grouped bars. - `chart.show_value_labels` — boolean. Prende el value label para cada series. El contenido del label es el valor de la barra, formateado por `format`; estilizalo con `chart.value_label` abajo. - `chart.cross_filter` — boolean, default `true`. Seteá a `false` para charts que siempre tienen que mostrar la vista sin filtrar (ej. un headline de "total revenue"). - `chart.height` — altura en pixels del container de la viz. Seteala explícita cuando el chart necesita más espacio vertical — por ejemplo, una bar horizontal con muchas categorías. ## Value labels `chart.value_label` es un objeto aplicado a cada series cuando `chart.show_value_labels` está prendido: - `position` — placement del label relativo a la barra. Valores comunes: `"top"`, `"inside"`, `"insideTop"`, `"insideBottom"`, `"outside"`. - `rotate` — grados, entre -90 y 90. Útil cuando el label es más ancho que la barra. - `color`, `font_size`, `font_weight`. - `formatter` — template string (sin callbacks). Útil para agregar un prefix o suffix de unidad (ej. `"{c}M"`). - `distance`, `align` (`"left"` | `"center"` | `"right"`), `vertical_align` (`"top"` | `"middle"` | `"bottom"`), `clip`. ``` # label compacto dentro de la barra con texto blanco chart: show_value_labels: true value_label: position: inside color: "#fff" font_size: 11 # label arriba de cada barra vertical con formatter de currency chart: show_value_labels: true value_label: position: top formatter: "${c}" distance: 4 # labels rotados para barras estrechas chart: show_value_labels: true value_label: rotate: -90 position: insideBottom ``` ## Legend & tooltip `chart.legend`: - `show` — boolean. Seteá a `false` cuando el chart tiene una sola series y la legend es redundante. - `position` — shortcut: `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`. - `orient` — `"horizontal"` | `"vertical"`. Usalo para overridear el orient derivado de `position`. - `top` / `bottom` / `left` / `right` — número de pixels o string de porcentaje para posicionamiento directo. - `text_style` — estilo de texto: `color`, `font_style`, `font_weight`, `font_family`, `font_size`, `line_height`. - `item_width`, `item_height`, `item_gap` — tamaños en pixels para los swatches coloreados y gaps. `chart.tooltip`: - `show` — boolean. - `trigger` — `"item"` (una barra a la vez, default para single-series), `"axis"` (group-wise; preferido cuando hay múltiples series para que hover las compare), o `"none"`. - `confine` — boolean. Mantiene el tooltip dentro de los bounds del chart. - `formatter` — template string. Usá placeholders como `{a}` (series), `{b}` (categoría), `{c}` (valor). - `background_color`, `border_color`, `border_width`. - `padding` — número único o array de 2 a 4 números (top/right/bottom/left). - `text_style` — mismo shape que en la legend. - `axis_pointer.type` — `"line"` | `"shadow"` | `"none"` | `"cross"`. Elección común para bar: `"shadow"`. ## Ejes `chart.x_axis`, `chart.y_axis` y `chart.y2_axis` comparten el mismo shape para bloques de eje de valor (`y2_axis` configura el eje derecho cuando una series usa `axis: right`; con un extra en x_axis): - `name` — título del eje. - `name_location` — `"start"` | `"middle"` | `"center"` | `"end"`. - `name_gap` — pixels entre la línea del eje y el nombre. - `min`, `max` — límites numéricos del eje. Usá `y2_axis.max: 1` para fijar una línea de porcentaje acumulado a un dominio 0–100% cuando los valores están en 0–1. - `axis_label.show` — boolean. - `axis_label.rotate` — grados, -90 a 90. Usalo para evitar que nombres largos de categoría se solapen. - `axis_label.interval` — entero ≥ 0 (saltear cada N labels) o `"auto"`. - `axis_label.color`, `axis_label.font_size`, `axis_label.font_weight`. - `axis_label.formatter` — template string. - `axis_label.max_chars` — entero ≥ 1. Trunca los labels con una elipsis. - `x_axis.visible_window` — entero ≥ 1. Restringe la cantidad visible de categorías y habilita un range slider horizontal; útil cuando hay muchas categorías. ``` # nombres largos de categoría con rotación y elipsis chart: x_axis: axis_label: rotate: -30 max_chars: 14 # eje y nombrado con un formatter de porcentaje chart: y_axis: name: Margin name_gap: 28 axis_label: formatter: "{value}%" # data-zoom slider para muchas categorías chart: x_axis: visible_window: 12 ``` ## format El format de número es field-keyed en el top level del YAML de la viz. Los patterns por field ganan sobre el pattern root. Patterns comunes para un bar chart: ``` format: revenue: "$#,##0.00" # currency completa revenue: "$#,##0a" # abreviada: $1.2M, $340K order_count: "#,##0" # entero con grouping margin_pct: "#,##0.00%" # porcentaje ``` El format aplica a value labels, tooltips y axis tick labels de valor. La gramática completa de patterns vive en el [overview de viz-types](/docs/build-workflow/viz-types). ## Comportamiento de cross-filter Dentro de un dashboard, clickear una barra agrega un "pill" que estrecha cada otra viz de la página a la categoría clickeada. Mecanismo completo en [Cross-filtering](/docs/build-workflow/cross-filtering). Específicos de bar: - La categoría de la barra clickeada se vuelve el valor de cross-filter. - Si los models del dashboard no declaran un parámetro con el nombre del field clickeado, el click se ignora silenciosamente. Para hacer una columna cross-filterable, declará un parámetro para ella en el model que potencia los charts afectados. - Deshabilitá por viz con `chart.cross_filter: false`. Útil para charts "headline" que siempre tienen que mostrar totales. - Para resaltar una barra específica desde un valor externo, usá el bloque top-level `emphasis`: ``` emphasis: field: category value_from_param: highlight_category bar_color: "#6c47ff" ``` La barra cuyo `category` iguala al valor de runtime de `highlight_category` se colorea con `bar_color`. ## Ejemplos trabajados Comparación simple: ``` id: ec_revenue_by_category_bar title: Revenue by Category query: "models/ec_revenue.malloy::by_category" type: bar mapping: x: category series: - field: revenue label: Revenue chart: height: 320 show_value_labels: true value_label: position: top x_axis: axis_label: rotate: -30 max_chars: 14 y_axis: name: Revenue format: revenue: "$#,##0.00" published: true ``` Grouped (dos measures, misma escala): ``` id: ec_revenue_vs_refunds_bar title: Revenue vs Refunds query: "models/ec_revenue.malloy::revenue_vs_refunds" type: bar mapping: x: category series: - field: revenue label: Revenue color: "#6c47ff" - field: refunds label: Refunds color: "#fb7185" chart: legend: show: true position: top tooltip: trigger: axis axis_pointer: type: shadow format: revenue: "$#,##0.00" refunds: "$#,##0.00" published: true ``` Composición percent-stacked: ``` id: ec_channel_mix_bar title: Channel Mix per Category query: "models/ec_revenue.malloy::channel_mix" type: bar mapping: x: category series: - field: rev_direct label: Direct - field: rev_organic label: Organic - field: rev_paid label: Paid chart: stack: percent show_value_labels: true value_label: position: inside color: "#fff" legend: show: true position: top format: rev_direct: "#,##0.0%" rev_organic: "#,##0.0%" rev_paid: "#,##0.0%" published: true ``` Pareto (barras ordenadas + línea de porcentaje acumulado en el eje derecho). Construí una query Malloy sobre `bigquery-public-data.thelook_ecommerce` que devuelva el conteo de cada categoría ordenado descendente más una columna de porcentaje acumulado (valores 0–1). Mapeá el conteo como barras y el porcentaje acumulado como series `type: line` en `axis: right`: ``` id: ec_return_reason_pareto title: Return reasons — Pareto query: "models/ec_returns.malloy::by_reason_pareto" type: bar mapping: x: return_reason series: - field: return_count label: Returns - field: cumulative_pct label: Cumulative % type: line axis: right color: "#6c47ff" chart: y2_axis: { max: 1 } legend: { show: true } format: return_count: "#,##0" cumulative_pct: "#0%" published: true ``` Hacé push desde tu workspace con `looky push -w ` después de que el modelo y el YAML del viz existan bajo `content/visualizations/`. Dual-axis (revenue vs orders): ``` id: ec_revenue_orders_bar title: Revenue and Orders query: "models/ec_revenue.malloy::by_month" type: bar mapping: x: order_month series: - field: revenue label: Revenue - field: order_count label: Orders axis: right chart: legend: show: true position: top y_axis: name: Revenue format: revenue: "$#,##0" order_count: "#,##0" published: true ``` Ranking horizontal con muchas categorías: ``` id: ec_top_brands_bar title: Top Brands by Revenue query: "models/ec_revenue.malloy::by_brand_desc" type: bar mapping: x: brand series: - field: revenue label: Revenue chart: orientation: horizontal height: 540 show_value_labels: true value_label: position: right x_axis: axis_label: max_chars: 18 y_axis: visible_window: 25 format: revenue: "$#,##0a" published: true ``` Nota: con orientación horizontal, los nombres largos de categoría viven en el eje y, así que el `axis_label` del eje y recibe `max_chars`, y `visible_window` se mueve a `y_axis`. ## Errores comunes - **El percent stack no suma 100%.** Asegurate que cada series en `mapping.series[]` represente una parte del mismo todo. Si uno de los measures está en una escala distinta, el chart muestra lo que pediste pero no es significativo como composición. - **Los labels de dual-axis chocan.** Dos escalas de valor necesitan espacio. O reducí la densidad de datos (menos categorías), seteá `name_gap` explícito en los dos ejes, o partilo en dos charts. - **Los labels largos de categoría hacen wrap o se solapan.** Agregá `chart.x_axis.axis_label.rotate: -30` y `max_chars: 14`, o cambiá a `chart.orientation: horizontal` (las barras horizontales no pueden incluir series `type: line` — usá combo charts verticales para overlays tipo Pareto). - **Los clicks de cross-filter no tienen efecto.** El field clickeado tiene que estar declarado como parámetro en al menos un model usado por el dashboard. Sin eso, los clicks se ignoran silenciosamente. - **Los value labels se clipean en barras chicas.** Seteá `chart.value_label.clip: false`, o movelo a position `"top"` / `"outside"`, o reducí `font_size`. - **La legend ocupa demasiado espacio.** Usá `position: bottom` o seteá `show: false` cuando solo hay una series. - **Demasiadas categorías explotan el layout.** Usá `chart.x_axis.visible_window` para habilitar un slider, o ordená y limitá en la query Malloy así el chart muestra el top N. - **El color es por series, no por barra individual.** Para resaltar una barra específica, usá el bloque top-level `emphasis` (mirá arriba) en vez de tratar de colorear una categoría directo. ---PAGE--- --- title: Visualization — line slug: docs/build-workflow/viz-types/line language: es description: "Referencia autoritativa del tipo de visualization line: cada campo mapping soportado, cada opción chart, modos dual-axis y multi-series, manejo de format, comportamiento de cross-filter." last_modified: "2026-06-11T14:17:33.864000+00:00" docs_section: build-workflow docs_summary: Series de tiempo y trends; soporta dual axis y multi-series. --- ## Cuándo usar line Line es la elección correcta cuando el eje x está ordenado y la pregunta es sobre *trend* — típicamente una series de tiempo (revenue diario, orders mensuales, errores por hora) o cualquier otra categoría naturalmente ordenada. El mismo renderer cubre tres shapes: - **Single line** — un measure ploteado a través del eje x. El default. - **Dual-axis** — dos measures en escalas distintas compartiendo un chart. Agregá `mapping.y2`; el segundo measure renderiza contra un eje y derecho. - **Multi-series** — una line por valor de categoría, todas compartiendo el mismo eje y. Agregá `mapping.series` con el field categórico. `y2` y `series` son mutuamente excluyentes en intención: dual-axis es para dos measures en el mismo x; multi-series es para un measure partido por una categoría. No los combines en una viz. Usá [bar](/docs/build-workflow/viz-types/bar) en su lugar cuando el eje x no está ordenado y la pregunta es sobre *comparación*. Usá [scatter](/docs/build-workflow/viz-types/scatter) cuando la relación es entre dos measures continuos en vez de un measure a través de un eje ordenado. ## Mapping - `mapping.x` — requerido. El field del eje ordenado (fecha, mes, entero, categoría ordinal). - `mapping.y` — requerido. Field numérico para los valores del eje y (izquierdo). - `mapping.y2` — opcional. Field numérico. Cuando está presente, el chart cambia a modo **dual-axis**. - `mapping.series` — opcional. Field categórico. Cuando está presente, el chart cambia a modo **multi-series**. - `mapping.series_label` — label de legend / tooltip para la series primaria y. Default `"value"`. - `mapping.series_label_2` — label de legend / tooltip para la series y2 en modo dual-axis. Default `"y2"`. ``` # single line mapping: x: order_month y: revenue series_label: Revenue # dual-axis mapping: x: order_month y: revenue y2: order_count series_label: Revenue series_label_2: Orders # multi-series (una line por channel) mapping: x: order_month y: revenue series: channel ``` ## Shortcuts de chart El bloque `chart` es tipado y cerrado — cualquier cosa no listada en esta página se rechaza en tiempo de validación. - `chart.show_value_labels` — boolean. Prende labels de data-point para cada series. Estilizalos con `chart.value_label` abajo. - `chart.cross_filter` — boolean, default `true`. Seteá `false` para suprimir click-to-filter en esta viz. - `chart.height` — altura en pixels del container de la viz. Line expone intencionalmente una superficie chica de opciones — la mayoría de las decisiones de styling se toman del theme de la plataforma. La customización a nivel series va por `mapping`, no por `chart`. ## Value labels `chart.value_label` es un objeto aplicado a cada series cuando `chart.show_value_labels` está prendido: - `position` — placement del label relativo al punto. Valores comunes: `"top"`, `"bottom"`, `"right"`, `"insideTop"`, `"insideBottom"`. - `rotate` — grados, entre -90 y 90. - `color`, `font_size`, `font_weight`. - `formatter` — template string (sin callbacks). Usá `{c}` para el valor. - `distance`, `align`, `vertical_align`, `clip`. Los value labels en una line son ruidosos para series densas; considerá mostrarlos solo en el último punto post-procesando los datos, o confiá en el tooltip hover en su lugar. ## Legend & tooltip `chart.legend`: - `show` — boolean. - `position` — shortcut: `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`. - `orient` — `"horizontal"` | `"vertical"`. - `top` / `bottom` / `left` / `right` — número de pixels o string de porcentaje. - `text_style` — `color`, `font_style`, `font_weight`, `font_family`, `font_size`, `line_height`. - `item_width`, `item_height`, `item_gap`. `chart.tooltip`: - `show` — boolean. - `trigger` — `"item"`, `"axis"` (recomendado para line — muestra todas las series en la x hovereada), o `"none"`. - `confine`, `formatter`, `background_color`, `border_color`, `border_width`, `padding`, `text_style`. - `axis_pointer.type` — `"line"` (recomendado para line), `"shadow"`, `"none"`, `"cross"`. ## Ejes `chart.x_axis` y `chart.y_axis` comparten el mismo shape (con un extra en x_axis): - `name` — título del eje. - `name_location` — `"start"` | `"middle"` | `"center"` | `"end"`. - `name_gap` — pixels entre línea de eje y nombre. - `axis_label.show` — boolean. - `axis_label.rotate` — grados, -90 a 90. - `axis_label.interval` — entero o `"auto"`. - `axis_label.color`, `axis_label.font_size`, `axis_label.font_weight`. - `axis_label.formatter` — template string. - `axis_label.max_chars` — entero ≥ 1, elipsiza labels largos. - `x_axis.visible_window` — entero ≥ 1. Restringe el rango visible y habilita un range slider horizontal; útil para series de tiempo largas. ## format - `format.y` o `format[]` — pattern para labels del eje y izquierdo y valores de tooltip. - `format.y2` o `format[]` — pattern para el eje y derecho (modo dual-axis). - `format` en root — fallback. ``` format: revenue: "$#,##0" order_count: "#,##0" ``` ## Comportamiento de cross-filter Dentro de un dashboard, clickear una line cross-filtra el resto del dashboard. Mecanismo completo en [Cross-filtering](/docs/build-workflow/cross-filtering). Específicos de line: - En modo single / dual-axis, clickear un punto cross-filtra por el valor x clickeado. - En modo multi-series, clickear una line cross-filtra por el nombre de la series. - El field clickeado tiene que estar declarado como parámetro en al menos un model usado por el dashboard, si no el click se ignora silenciosamente. - Deshabilitá por viz con `chart.cross_filter: false`. ## Ejemplos trabajados Single line, con un axis label rotado para períodos mensuales: ``` id: ec_revenue_over_time_line title: Revenue Over Time query: "models/ec_revenue.malloy::over_time" type: line mapping: x: order_month y: revenue series_label: Revenue chart: height: 320 x_axis: axis_label: rotate: -30 y_axis: name: Revenue tooltip: trigger: axis axis_pointer: type: line format: revenue: "$#,##0" published: true ``` Dual-axis (revenue a la izquierda, order count a la derecha): ``` id: ec_revenue_orders_line title: Revenue and Orders query: "models/ec_revenue.malloy::over_time" type: line mapping: x: order_month y: revenue y2: order_count series_label: Revenue series_label_2: Orders chart: height: 320 legend: show: true position: top format: revenue: "$#,##0" order_count: "#,##0" published: true ``` Multi-series (una line por channel): ``` id: ec_revenue_by_channel_line title: Revenue by Channel query: "models/ec_revenue.malloy::by_channel_over_time" type: line mapping: x: order_month y: revenue series: channel chart: legend: show: true position: top tooltip: trigger: axis format: revenue: "$#,##0" published: true ``` Series de tiempo larga con un data-zoom slider horizontal: ``` chart: x_axis: visible_window: 30 ``` ## Errores comunes - **Mezclar dual-axis y multi-series.** Setear los dos `y2` y `series` en la misma viz produce comportamiento indefinido — elegí uno. - **Los labels de dual-axis chocan.** Dos escalas de valor necesitan espacio. Seteá `name_gap` explícito en los dos ejes, reducí densidad de datos, o partilo en dos charts. - **La legend multi-series es muy ancha.** Si el field categórico tiene muchos valores distintos, la legend puede dominar el chart. Movela a `position: bottom`, o filtrá al top-N en la query Malloy subyacente. - **El format de fecha en el eje x está mal.** Formateá el valor x con `format[]` o pre-formateá en la query Malloy (ej. derivá un string `order_month_label`). - **Shifts de timezone en los valores x.** Los valores x de series de tiempo se interpretan en la timezone de sesión del usuario. Pre-bucketeá fechas a días o meses en la query Malloy si necesitás agrupamientos consistentes entre usuarios. - **Los clicks de cross-filter no tienen efecto.** El field clickeado tiene que estar declarado como parámetro en al menos un model usado por el dashboard. Sin eso, los clicks se ignoran silenciosamente. ---PAGE--- --- title: Visualization — pie slug: docs/build-workflow/viz-types/pie language: es description: "Referencia autoritativa del tipo de visualization pie: cada campo mapping soportado, cada opción chart, variante donut, manejo de format, comportamiento de cross-filter y el escape hatch de ECharts." last_modified: "2026-06-11T14:17:35.335000+00:00" docs_section: build-workflow docs_summary: Composición part-of-whole; soporta variante donut. --- ## Cuándo usar pie Pie es la elección correcta para composición part-of-whole cuando el número de slices es chico (típicamente ≤ 6) y el peso proporcional importa. Usá la variante donut cuando querés liberar el centro del chart para un label interior o un KPI relacionado; usá la variante rose para una encoding alternativa donde el ángulo del slice es fijo pero el radio refleja el valor. Usá [bar](/docs/build-workflow/viz-types/bar) con `chart.stack: percent` en su lugar cuando las categorías son muchas, o cuando querés comparar composición entre múltiples grupos. Usá [grid](/docs/build-workflow/viz-types/grid) cuando la audiencia necesita los porcentajes reales en forma tabular. ## Mapping - `mapping.x` — requerido. Field string usado como nombre de slice y label de legend. - `mapping.y` — requerido. Field numérico que maneja el ángulo del slice. ``` mapping: x: status y: order_count ``` Los colores de slice vienen de la paleta de la plataforma en orden de query. Para controlar el orden, ordená en la query Malloy subyacente. ## Shortcuts de chart El bloque `chart` es tipado y cerrado. - `chart.variant` — `"pie"` (default), `"donut"`, o `"rose"`. Donut agrega un radio interno (centro libre); rose cambia a un layout rose-area donde el ángulo del slice es uniforme y el radio codifica el valor. - `chart.inner_radius` — número (pixels) o string (ej. `"55%"`). Usar con `variant: donut`; cuanto mayor el radio interno, más fino el anillo del donut. - `chart.outer_radius` — número o string. - `chart.slice_border_radius` — pixels. Esquinas redondeadas en slices para un look más suave. - `chart.show_value_labels` — boolean. Prende los labels de slice. Estilizalos con `chart.label` abajo. - `chart.cross_filter` — boolean, default `true`. - `chart.height` — altura en pixels del container de la viz. Campos pass-through (1:1 con el chart subyacente): - `chart.center` — centro del pie. Array `[x, y]` de strings de porcentaje (ej. `["50%", "50%"]`) o números de pixels. - `chart.start_angle` — grados. Default `90`. - `chart.min_angle` — grados. Ángulo mínimo para que slices chiquitos sigan visibles. - `chart.rose_type` — cuando se usa un layout rose: `"radius"` o `"area"`. ## Labels de slice `chart.label` es un objeto que estiliza labels de slice cuando `chart.show_value_labels` está prendido: - `position` — `"inside"`, `"outside"`, `"top"`, `"bottom"`, `"left"`, `"right"`, más las variantes `"insideLeft"` / `"insideRight"`. - `rotate` — grados, entre -90 y 90. - `color`, `font_size`, `font_weight`. - `formatter` — template string. Usá `{b}` para el nombre del slice, `{c}` para el valor, `{d}` para el porcentaje. - `distance`, `align`, `vertical_align`, `clip`. ``` chart: show_value_labels: true label: position: outside formatter: "{b}: {d}%" ``` ## Legend & tooltip `chart.legend` controla la legend; `chart.tooltip` controla el tooltip hover. Los dos comparten el mismo shape que en [bar](/docs/build-workflow/viz-types/bar) — mirá esa página para la referencia completa del sub-bloque. Tip específico de pie: para una variante donut con la legend a la derecha, seteá `center: ["42%", "50%"]` para empujar el donut hacia la izquierda y que la legend tenga más espacio. ## format - `format.y` o `format[]` — pattern para valores de tooltip de slice y labels dentro de slice. - `format` en root — fallback. ## Comportamiento de cross-filter - Clickear un slice cross-filtra el resto del dashboard por el label del slice. - El field clickeado (el field del label del slice, ej. `status`) tiene que estar declarado como parámetro en al menos un model usado por el dashboard, o el click se ignora silenciosamente. - Deshabilitá por viz con `chart.cross_filter: false`. Mirá [Cross-filtering](/docs/build-workflow/cross-filtering) para el mecanismo completo. ## Ejemplos trabajados Donut con labels afuera y legend abajo: ``` id: ec_orders_by_status_pie title: Orders by Status query: "models/ec_revenue.malloy::by_status" type: pie mapping: label: status value: order_count chart: variant: donut inner_radius: "55%" outer_radius: "78%" show_value_labels: true label: position: outside formatter: "{b}: {d}%" legend: show: true position: bottom format: order_count: "#,##0" published: true ``` Pie sólida con labels adentro: ``` type: pie mapping: label: category value: revenue chart: show_value_labels: true label: position: inside color: "#fff" font_weight: bold legend: show: false format: revenue: "$#,##0" ``` Layout rose (ángulo de slice uniforme, radio codifica valor): ``` chart: variant: rose rose_type: area start_angle: 0 show_value_labels: true ``` ## Errores comunes - **Demasiados slices.** Un pie con más de ~6 slices se vuelve difícil de leer. Ordená + limitá en la query Malloy, o agregá slices chiquitos en un bucket "Other". - **Slices con valores cero o cercanos a cero desaparecen.** Usá `chart.min_angle` para forzar un ángulo visible mínimo, o filtrá ceros en la query. - **Los labels se solapan en un chart apretado.** Movelos adentro (`chart.label.position: "inside"`) o rotalos. - **El contenido interior del donut choca con los labels.** Cuando ponés un KPI en el centro del donut vía layout de dashboard, seteá `chart.label.position: "outside"` así los labels de slice no chocan. - **Los colores de slice no son los que esperabas.** Los colores se asignan en orden del resultado de query desde la paleta de la plataforma. Ordená la query así el slice más grande o más importante recibe el color dominante. - **Los clicks de cross-filter no tienen efecto.** El field del label del slice tiene que estar declarado como parámetro en al menos un model usado por el dashboard. ---PAGE--- --- title: Visualization — scatter slug: docs/build-workflow/viz-types/scatter language: es description: "Referencia autoritativa del tipo de visualization scatter: cada campo mapping soportado, cada opción chart, manejo de format, comportamiento de cross-filter, emphasis y el escape hatch de ECharts." last_modified: "2026-06-11T14:17:38.477000+00:00" docs_section: build-workflow docs_summary: Correlación de dos o tres measures; un punto por fila. --- ## 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](/docs/build-workflow/viz-types/line) en su lugar cuando el eje x está ordenado (típicamente tiempo) y te importa *trend*. Usá [heatmap](/docs/build-workflow/viz-types/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 con `chart.symbol_size` y `chart.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_colors` en su lugar. - `chart.series_colors` — sólo multi-serie. Un mapa de `valor 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** (sin `mapping.size`). El default depende de la densidad del chart. No se puede combinar con `mapping.size`. - `chart.size_range` — sólo modo sized (`mapping.size` seteado). 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 cuando `mapping.size` está 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. `linear` mapea valor a diámetro directamente (sobre-enfatiza valores grandes). Inerte sin `mapping.size`. - `chart.large_threshold` — sólo multi-serie, default `2000`. 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 con `mapping.size`** — el dibujo masivo ignora el sizing por punto. - `chart.cross_filter` — boolean, default `true`. Seteá a `false` para deshabilitar la emisión de click enteramente; en ese caso `cross_filter_emit` no 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 que `chart.cross_filter` no es explícitamente `false`; el schema rechaza un bloque chart que no tenga ninguno. - `chart.legend` — seteá `legend.show: true` para 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](/docs/build-workflow/viz-types/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.x` o `format[]` — pattern para labels del eje x y valor x del tooltip. - `format.y` o `format[]` — pattern para labels del eje y y valor y del tooltip. - `format.size` o `format[]` — pattern para el valor size en el tooltip cuando `mapping.size` está seteado. - `format` en 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 `emphasis` puede 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//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_threshold` mantiene 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.size` no se puede combinar con `chart.symbol_size` ni `chart.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.size` negativo se dropean silenciosamente. - **Los ejes están sin nombrar.** Siempre seteá `chart.x_axis.name` y `chart.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. ---PAGE--- --- title: Visualization — heatmap slug: docs/build-workflow/viz-types/heatmap language: es description: "Referencia autoritativa del tipo de visualization heatmap: cada campo mapping soportado, cada opción chart, color scale, manejo de format, comportamiento de cross-filter, emphasis y el escape hatch de ECharts." last_modified: "2026-06-11T14:17:30.459000+00:00" docs_section: build-workflow docs_summary: Grid de intensidad bidimensional con una color scale explícita. --- ## Cuándo usar heatmap Heatmap es la elección correcta cuando querés renderizar un measure a través de dos dimensions categóricas o temporales a la vez. Usos comunes: day-of-week vs hour-of-day para patterns de actividad; channel vs region para densidad de revenue; categoría de producto vs mes para estacionalidad. Usá [grid](/docs/build-workflow/viz-types/grid) en su lugar cuando la audiencia necesita leer los números reales y una de las dimensions es de alta cardinalidad. Usá [scatter](/docs/build-workflow/viz-types/scatter) cuando las dimensions son continuas en vez de categóricas. ## Mapping - `mapping.x` — requerido. Field del eje-columna (un tick por valor distinto). - `mapping.y` — requerido. Field del eje-fila. - `mapping.value` — requerido. Field numérico que maneja la intensidad de color de cada celda. ``` mapping: x: hour_of_day y: day_of_week value: order_count ``` ## Shortcuts de chart El bloque `chart` es tipado y cerrado. - `chart.show_cell_labels` — boolean. Renderiza el valor de la celda adentro de cada celda. Estilizá los labels con `chart.label` abajo. - `chart.cross_filter_emit` — `"x"` o `"y"`. Heatmap tiene dos ejes categóricos, así que tenés que elegir cuál valor se emite al click. **Requerido** cada vez que `chart.cross_filter` no es explícitamente `false`; el schema rechaza un bloque chart que no tenga ninguno. - `chart.cross_filter` — boolean, default `true`. Seteá a `false` para deshabilitar la emisión de click entera; en ese caso `cross_filter_emit` no es requerido. - `chart.height` — altura en pixels del container de la viz. ## Color scale `chart.visual_map` es un objeto que controla el gradiente de color y la legend opcional de color-scale: - `show` — boolean. Muestra la legend de color-scale al lado del chart. - `orient` — `"horizontal"` | `"vertical"`. - `min`, `max` — piso / techo explícitos. Overridean el rango derivado de datos — útil cuando varios heatmaps en el mismo dashboard necesitan compartir una escala. - `left` / `right` / `top` / `bottom` — número de pixels o string de porcentaje para placear la legend de color-scale. - `in_range.color` — array de strings hex: los stops del gradiente de low a high. Dos stops producen un gradiente simple low-to-high. - `text_style` — estilo de texto para los labels de la color-scale. ``` chart: visual_map: show: true orient: vertical in_range: color: ["#e0f7f4", "#0d9488"] ``` ## Labels de celda `chart.label` estiliza el label numérico por-celda cuando `chart.show_cell_labels` está prendido: - `position`, `rotate`, `color`, `font_size`, `font_weight`. - `formatter` — template string (sin callbacks). Usá `{c}` para el valor. - `distance`, `align`, `vertical_align`, `clip`. Los labels de celda son útiles cuando el heatmap es chico y la audiencia necesita el número exacto; en heatmaps densos los labels se vuelven ruido — dejalos apagados y confiá en el tooltip. ## Legend & tooltip `chart.legend` y `chart.tooltip` comparten el mismo shape que en [bar](/docs/build-workflow/viz-types/bar). Para heatmap el tooltip con `trigger: item` revela una celda a la vez con ambos valores de eje y el valor de celda. ## Ejes `chart.x_axis` y `chart.y_axis` comparten el mismo shape: - `name`, `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`. ## format - `format.value` o `format[]` — pattern para labels de celda y valores de tooltip. - `format` en root — fallback. ## Comportamiento de cross-filter - El click emite un valor **solo** cuando `chart.cross_filter_emit` está seteado a `"x"` o `"y"`. - El eje elegido se vuelve el field de cross-filter; el label clickeado se vuelve el valor. - El field clickeado tiene que estar declarado como parámetro en al menos un model usado por el dashboard. - Deshabilitá por viz con `chart.cross_filter: false` (o simplemente dejá `cross_filter_emit` unset). Mirá [Cross-filtering](/docs/build-workflow/cross-filtering) para el mecanismo completo. ## Ejemplos trabajados Actividad por hora y día de semana: ``` id: orders_by_day_hour title: Orders by Day and Hour query: "models/ec_orders.malloy::by_day_hour" type: heatmap mapping: x: hour_of_day y: day_of_week value: order_count chart: height: 320 show_cell_labels: true cross_filter_emit: x visual_map: show: true orient: vertical in_range: color: ["#e0f7f4", "#0d9488"] x_axis: name: Hour y_axis: name: Day format: order_count: "#,##0" published: true ``` Color scale compartida entre múltiples heatmaps (seteá min / max explícitos): ``` chart: visual_map: show: true min: 0 max: 5000 in_range: color: ["#e0f7f4", "#0d9488"] ``` ## Errores comunes - **El chart se ve vacío aunque la query tiene filas.** Asegurate que tanto `mapping.x` como `mapping.y` contengan los fields correctos y que el field de valor de celda no sea null. - **La color scale está dominada por un outlier.** Seteá `visual_map.max` para clipear la escala; los valores arriba del cap renderizan con el color top. - **Los labels son ilegibles en celdas oscuras.** Seteá un `chart.label.color` que contraste, o apagá los labels de celda y confiá en el tooltip. - **El click no hace nada.** Heatmap requiere `chart.cross_filter_emit` seteado a `"x"` o `"y"`. Sin eso, no se dispara ningún evento sin importar `cross_filter`. - **El click debería emitir tanto x como y.** Heatmap puede emitir solo un eje. Si necesitás los dos, usá un grid. - **Color scale divergente con un midpoint.** El `visual_map.in_range.color` expuesto es un gradiente simple low-to-high; para escalas divergentes, codificá la divergencia en el valor subyacente (ej. delta con signo). ---PAGE--- --- title: Visualization — funnel slug: docs/build-workflow/viz-types/funnel language: es description: "Referencia autoritativa del tipo de visualization funnel: cada campo mapping soportado, cada opción chart, modos de porcentaje, manejo de format, comportamiento de cross-filter y el escape hatch de ECharts." last_modified: "2026-06-11T14:17:26.789000+00:00" docs_section: build-workflow docs_summary: Stages de conversión o pipeline con visualización de drop-off. --- ## Cuándo usar funnel Funnel es la elección correcta para flows de conversión, pipelines de fulfillment, o cualquier proceso donde importa el orden entre pasos y el drop-off — visitor del sitio → product view → cart → checkout → purchase, u orden creado → packed → shipped → delivered. Cada fila de la query es un stage. Usá [bar](/docs/build-workflow/viz-types/bar) en su lugar cuando las categorías no están ordenadas o no te importa el drop-off cumulativo. Usá [grid](/docs/build-workflow/viz-types/grid) cuando la audiencia necesita los counts exactos de cada step y las tasas de conversión lado a lado. ## Mapping - `mapping.stage` — requerido. Field string cuyos valores se vuelven los labels por stage. - `mapping.value` — requerido. Field numérico para el tamaño por stage. ``` mapping: stage: stage value: order_count ``` El orden de los stages en el chart sigue el orden de las filas en el resultado de la query. Ordená en la query Malloy subyacente para controlar la secuencia (típicamente por `order_index` o por valor descendente). ## Shortcuts de chart El bloque `chart` es tipado y cerrado. - `chart.percent_mode` — `"first"` (default), `"total"`, o `"none"`. Elige la base para el porcentaje mostrado al lado de cada valor de stage: - **first** — relativo al primer stage (cascade de conversión: 100% → 80% → 60% …). - **total** — relativo a la suma de todos los stages (share del funnel). - **none** — sin porcentajes, solo valores raw. - `chart.show_value_labels` — boolean. Prende los labels de stage. Estilizalos con `chart.label` abajo. - `chart.cross_filter` — boolean, default `true`. Seteá a `false` para suprimir la emisión de click en esta viz (igual consume pills seteados en otro lado). - `chart.height` — altura en pixels del container de la viz. Fields pass-through de layout: - `chart.left` / `chart.top` / `chart.bottom` — distancia desde el borde correspondiente del container del chart. - `chart.width` — ancho horizontal del funnel (string porcentaje o número de pixels). - `chart.gap` — pixels entre stages. - `chart.min_size` — ancho mínimo de stage (ej. `"10%"`). El stage más estrecho no se va a achicar más allá de esto incluso si su valor es chiquito. - `chart.max_size` — ancho máximo de stage. - `chart.sort` — orden de stage. El default deja ganar el orden de fila; seteá a un valor conocido si querés que el renderer reordene. ## Labels de stage `chart.label` estiliza el label de cada stage cuando `chart.show_value_labels` está prendido: - `position` — `"inside"` (label se sienta sobre el stage), `"left"`, `"right"`, `"top"`, `"bottom"`. - `rotate`, `color`, `font_size`, `font_weight`. - `formatter` — template string. Usá `{b}` para el nombre del stage, `{c}` para el valor, `{d}` para el porcentaje (cuando `percent_mode` es `"first"` o `"total"`). - `distance`, `align`, `vertical_align`, `clip`. ## Legend & tooltip `chart.legend` y `chart.tooltip` comparten el mismo shape que en [bar](/docs/build-workflow/viz-types/bar). Para una viz de funnel único la legend es usualmente redundante; seteá `chart.legend.show: false`. ## format - `format.y` o `format[]` — pattern para labels de valor de stage y valores de tooltip. - `format` en root — fallback. ## Comportamiento de cross-filter Funnel participa completamente en cross-filtering. Dentro de un dashboard: - **Emite** — clickear un stage agrega un pill `{ field: , value: }`, siempre que `chart.cross_filter !== false` Y el field esté declarado como parámetro en al menos un model del dashboard. - **Consume** — pills seteados en otros lados, y filtros a nivel dashboard, se vuelven parámetros en el próximo run; la query subyacente del funnel re-corre y los stages se recomputan en consecuencia. - **Opt-out** — seteá `chart.cross_filter: false` para suprimir emisión de click mientras igual consume pills. ## Ejemplos trabajados Funnel de conversión estándar con porcentaje de primer stage: ``` id: ec_fulfillment_funnel title: Fulfillment Funnel query: "models/ec_fulfillment.malloy::funnel" type: funnel mapping: stage: stage value: order_count chart: height: 360 width: "80%" show_value_labels: true label: position: inside formatter: "{b}: {c} ({d}%)" percent_mode: first legend: show: false format: order_count: "#,##0" published: true ``` Funnel total-share (cada stage mostrado como % de la suma): ``` chart: percent_mode: total show_value_labels: true ``` Funnel compacto embedded en una fila de dashboard al lado de KPIs: ``` chart: height: 240 width: "100%" gap: 6 min_size: "20%" max_size: "100%" show_value_labels: true label: position: right formatter: "{b}: {c}" percent_mode: none ``` ## Errores comunes - **Los stages están en el orden equivocado.** El funnel respeta el orden del resultado de query. Ordená en la query Malloy para controlar la secuencia. - **Los últimos stages chiquitos desaparecen.** Seteá `chart.min_size` para forzar un ancho visible mínimo. - **El percent mode produce números inesperados.** Elegí deliberadamente: `"first"` para cascades de conversión (cada stage ÷ primer stage), `"total"` para share del total (cada stage ÷ suma), `"none"` para counts raw. - **Los labels de stage se solapan con las barras.** Movelos afuera (`chart.label.position: "right"`) o reducí `font_size`. - **Clickear un stage no hace nada.** Funnel emite pills de cross-filter solo dentro de un dashboard, y solo si (a) `chart.cross_filter` no es `false`, y (b) el field de stage está declarado como parámetro en al menos un model del dashboard. Agregá la declaración de parámetro en el model Malloy subyacente para habilitar el pill. - **Comparación multi-funnel.** No soportada en una viz; poné dos funnels lado a lado en un dashboard para comparación A/B. ---PAGE--- --- title: "Visualization — grid (alias: table)" slug: docs/build-workflow/viz-types/grid language: es description: "Referencia autoritativa del tipo de visualization grid / table: cada campo mapping soportado, opciones del bloque grid, columnas congeladas, celdas compuestas, paginación, manejo de format, comportamiento de cross-filter y notas de adapter." last_modified: "2026-06-11T14:17:28.444000+00:00" docs_section: build-workflow docs_summary: Tabla de datos row-level; soporta paginación, columnas congeladas, celdas compuestas. --- ## 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. Los identifiers de tipo `grid` y `table` mapean al mismo renderer; cualquiera de los dos se acepta. Usá una viz tipo chart ([bar](/docs/build-workflow/viz-types/bar), [line](/docs/build-workflow/viz-types/line)) cuando la pregunta es sobre una comparación o un trend en vez de las filas raw. Usá [report_matrix](/docs/build-workflow/viz-types/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` (el alias `table` también se acepta). 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`. Seteá 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`. Elegí 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: 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 — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters). ## 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. Usá 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 (cuando la columna está configurada como clickeable) cross-filtra el resto del dashboard por el nombre de columna y el valor clickeado. - 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 `emphasis` puede declarativamente resaltar una fila matcheando un valor de cross-filter relacionado. - Deshabilitá por viz con `chart.cross_filter: false`. Mirá [Cross-filtering](/docs/build-workflow/cross-filtering) para el mecanismo completo. ## Ejemplos trabajados Detalle de orden con primera columna congelada, formats de currency y una columna de comparación: ``` 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 column_widths: order_date: "120px" revenue: "140px" column_formats: revenue: currency avg_order_value: currency item_count: integer formats: currency: "$#,##0.00" integer: "#,##0" comparison_columns: - revenue pagination: page_size: 50 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 ``` ## Errores comunes - **El grid ocupa demasiado espacio horizontal.** Dropeá columnas de `mapping.columns` o seteá anchos más estrechos explícitos en `grid.column_widths`. - **Los anchos de columna no quedan.** Asegurate que los nombres de columna en `grid.column_widths` matcheen los nombres de field en el resultado de query exacto. - **El sorteo es solo por-página.** La paginación server-side significa que los sorts client-side solo ven una página. Para ordenar entre todas las filas, ordená en la query Malloy. - **El label del header está mal.** Seteá `grid.labels` para overridear el nombre raw de columna (o renombrá en la query Malloy). - **El indicador de comparación apunta en la dirección equivocada.** El renderer deriva dirección del signo del valor. Codificá deltas "buenos" con signo positivo y "malos" con negativo. - **Los clicks de cross-filter no tienen efecto.** La columna clickeada tiene que estar declarada como parámetro en al menos un model usado por el dashboard. - **Grid esperado para agrupar filas con subtotales.** Grid es plano; usá [report_matrix](/docs/build-workflow/viz-types/report-matrix) para agrupamiento. ---PAGE--- --- title: Visualization — report_matrix slug: docs/build-workflow/viz-types/report-matrix language: es description: "Referencia autoritativa del tipo de visualization report_matrix: configuración del bloque matrix, secciones, totales, formats, comportamiento y opciones de PDF export." last_modified: "2026-06-11T14:17:36.910000+00:00" docs_section: build-workflow docs_summary: Reporte jerárquico con filas agrupadas, totales y PDF export. --- ## Cuándo usar report_matrix report_matrix es la elección correcta cuando la audiencia necesita un reporte estructurado con filas agrupadas, secciones anidadas, subtotales y totales de grupo. Está diseñado para dashboards modo document y PDF export, mientras igual soporta click-to-filter dentro de dashboards interactivos. Usá [grid](/docs/build-workflow/viz-types/grid) en su lugar cuando las filas no necesitan agrupamiento. Usá una viz tipo chart cuando la pregunta es sobre una métrica única entre categorías en vez de un reporte tabular estructurado. ## Mapping report_matrix toma su estructura de columnas del resultado de la query. No hay un bloque `mapping` requerido; la configuración de matrix vive bajo el bloque `matrix` en su lugar. ## Columnas & presentación Estas keys controlan qué columnas muestra el matrix y cómo renderiza cada columna: - `matrix.key_columns` — array de nombres de columna renderizados como **key cells** (bold, coloreo basado en polaridad). Usalo para la columna de métrica que los readers tienen que enfocar (ej. cumplimiento, conversion rate). - `matrix.trend_columns` — array de nombres de columna renderizados con un trend icon (up / down / dash) al lado del valor. - `matrix.labels` — objeto mapeando nombre de columna a display label (overridea el nombre raw de columna en el header). - `matrix.column_formats` — objeto mapeando nombre de columna a una format key. - `matrix.formats` — objeto mapeando format key a un pattern (indirección de dos pasos, como `grid.formats`). - `matrix.column_widths` — objeto mapeando nombre de columna a un ancho (`"150px"`, `"20%"`, o numérico). - `matrix.uniform_column_widths` — boolean o `"auto"`. Cuando está seteado, todas las columnas comparten el mismo ancho auto-fit. - `matrix.uniform_column_widths_min_px` — entero. Default `80`. - `matrix.uniform_column_widths_max_px` — entero. Default `520`. ## Tabla de summary El matrix puede renderizar un summary comparativo arriba de las filas de detalle: - `matrix.show_summary` — boolean. Default `true`. - `matrix.summary_title` — título string arriba del summary. - `matrix.summary_columns` — array de nombres de columna incluidos en el summary. ## Agrupamiento & secciones Dos maneras de organizar las filas: agrupamiento automático por una columna, o secciones manuales. #### Agrupamiento automático - `matrix.group_by` — nombre de columna. Agrupa las filas resultado por este field; cada grupo se vuelve una sección colapsable. - `matrix.group_key_field` — nombre de columna a renderizar como la primera columna dentro de cada sección de grupo. - `matrix.group_columns` — array de nombres de columna renderizados dentro de cada grupo. #### Secciones manuales ``` matrix: sections: - id: revenue title: Revenue breakdown columns: [period, units, revenue] - id: cost title: Cost breakdown columns: [period, cogs, opex] ``` ## Totales Los totales viven en dos hermanos bajo `matrix`: `group_totals` (una fila por sección de grupo) y `overall_totals` (una fila entre todos los grupos). Ambos comparten el mismo shape: - `enabled` — boolean. - `label` — string. Default `"TOTAL"`. - `columns` — array de nombres de columna a sumar. Si está vacío, las columnas numéricas se suman automáticamente. - `computed` — array de objetos de field computado renderizados al lado de los sums. Cada item: - `field` — nombre del field output. - `type` — `"ratio"` o `"percent_delta"`. - `numerator` — nombre del field para el dividendo (ratio) o período actual (delta). - `denominator` — nombre del field para el divisor (ratio) o baseline (delta). ``` matrix: group_totals: enabled: true label: TOTAL ZONA columns: [unidades, ventas] computed: - field: cumplimiento type: percent_delta numerator: ventas denominator: presupuesto overall_totals: enabled: true label: TOTAL GENERAL columns: [unidades, ventas] ``` ## Behavior `matrix.behavior` controla cómo se expanden las secciones y cómo renderiza el matrix para PDF export: - `web_mode` — `"all_open"`, `"single_open"`, `"expand_all"`, o vacío. Cómo se comportan las secciones en el browser cuando el usuario clickea un header. - `default_open` — `"all"` o un id de sección específico. Qué secciones empiezan expandidas en first load. - `pdf_expand_all` — boolean. Default `false`. **Crítico para PDF export**: sin esto, los grupos colapsados desaparecen del documento impreso. ## format Usá `matrix.column_formats` + `matrix.formats` como el camino primario de formatting; el field `format` root se chequea solo como fallback. ## Comportamiento de cross-filter - Clickear una celda del cuerpo cuya columna está declarada como parámetro en al menos un model del dashboard emite un pill `{ field: , value: }`. - Las celdas en filas de **group totals** y **overall totals** **no** son clickeables — agregan entre múltiples valores key, así que un click sería ambiguo. - Las celdas en **trend columns** (renderizadas con badges de dirección up/down) no son clickeables — muestran dirección sobre un valor numérico, no un pick categórico. - Los headers de sección (auto-agrupados vía `matrix.group_by` o hechos a mano vía `matrix.sections`) no son clickeables en esta versión. El click de `
` está reservado para expand/collapse. - **Modo document** (`tableContext: document`) suprime el cableo de cell-click enteramente — el render document/PDF no tiene target interactivo. - Deshabilitá por viz con `chart.cross_filter: false`. Mirá [Cross-filtering](/docs/build-workflow/cross-filtering) para el mecanismo completo. ## Ejemplo trabajado ``` id: nv_visitador_resumen title: Resumen por visitador query: "models/nv_comercial.malloy::resumen_por_visitador" type: report_matrix matrix: group_by: zona group_key_field: visitador group_columns: [periodo, unidades, ventas, cumplimiento] key_columns: [cumplimiento] trend_columns: [cumplimiento] group_totals: enabled: true label: TOTAL ZONA columns: [unidades, ventas] computed: - field: cumplimiento type: percent_delta numerator: ventas denominator: presupuesto overall_totals: enabled: true label: TOTAL GENERAL columns: [unidades, ventas] column_formats: ventas: currency presupuesto: currency cumplimiento: percent formats: currency: "$#,##0" percent: "#,##0.0%" behavior: web_mode: single_open default_open: all pdf_expand_all: true published: true ``` ## Errores comunes - **El PDF export muestra menos filas que el browser.** Seteá `matrix.behavior.pdf_expand_all: true`. Sin eso, los grupos colapsados se quedan colapsados en el documento impreso. - **Los totales auto-sumados incluyen las columnas equivocadas.** Listá las columnas que querés sumadas explícitamente en `group_totals.columns` / `overall_totals.columns`; dejarlas vacías auto-suma todas las columnas numéricas, lo que puede incluir cosas tipo números de ID. - **El field de total computado muestra un número raro.** Tanto `numerator` como `denominator` tienen que referenciar fields presentes en las filas que se suman. Chequeá la ortografía contra el resultado de query. - **Las secciones no empiezan expandidas.** Seteá `matrix.behavior.default_open: "all"`, o el id de sección específico que querés abrir en first load. - **El click en una celda no hace nada.** Chequeá que (a) el nombre de columna esté declarado como parámetro en al menos un model del dashboard, (b) el valor no sea null, (c) la celda no esté en una fila de totals o trend (esas son no-clickeables por diseño), y (d) `matrix.cross_filter` no esté seteado a `false`. - **Los formats de celda son inconsistentes entre columnas.** Usá la indirección `matrix.column_formats` + `matrix.formats` así el mismo pattern de currency / percent aplica en todos lados donde debería. ---PAGE--- --- title: Filters slug: docs/build-workflow/filters language: es description: "Referencia de filtros: cómo los controles de filtro user-facing producen params de Malloy, el flow del resolver, y las subpáginas por tipo." last_modified: "2026-06-11T14:17:05.474000+00:00" docs_section: build-workflow docs_summary: Cómo los filtros se cablean a queries de Malloy; referencia por tipo. --- ## Qué es un filtro en Looky Un filtro es un control user-facing en una visualization o un dashboard. El usuario elige un valor; Looky lo manda a la query Malloy subyacente como un parámetro con nombre. Los filtros viven en el YAML de la visualization (o del dashboard) bajo `filters: []`, una entry por control. Filtros y cross-filtering son mecanismos distintos. Los filtros son controles declarativos con los que el usuario interactúa explícitamente. [Cross-filtering](/docs/build-workflow/cross-filtering) es automático — clickear un chart dentro de un dashboard agrega un "pill" que se convierte en un parámetro sin ningún YAML. ## Elegir un tipo de filtro Hay cinco tipos de filtro soportados. Elegí el que su UI matchee el input que el usuario está eligiendo. - [`select`](/docs/build-workflow/filters/select) — picker dropdown. La lista de opciones se declara en YAML o se resuelve dinámicamente corriendo una query Malloy en page load. - [`cutoff_date`](/docs/build-workflow/filters/cutoff-date) — una sola fecha que se expande automáticamente a un rango `date_from` / `date_to` que va desde el primer día de ese mes hasta el día elegido. Usalo para reportes "month-to-date as of". - [`date_range`](/docs/build-workflow/filters/date-range) — picker abierto de from / to date. Usalo cuando el usuario necesita elegir los dos extremos libremente. - [`date_range_preset`](/docs/build-workflow/filters/date-range-preset) — date range elegido desde presets con nombre (*last_30_days*, *this_month*, *last_month*, *this_year*, etc.). El default para la mayoría de los dashboards operacionales. - [`month`](/docs/build-workflow/filters/month) — picker de año + mes con bounds configurables. Usalo cuando el análisis está keyed a un solo mes (cierre mensual, billing mensual). Otros tipos de filtro no están soportados. ## Anatomía de una declaración de filtro Cada entry de filtro es un objeto con al menos un `type`. Las otras keys dependen del tipo — mirá la página de referencia de cada tipo para la lista completa. ``` id: orders_grid type: grid query: "models/ec_fulfillment.malloy::detail" filters: - type: select id: status label: Status param: status options_query: "models/ec_fulfillment.malloy::status_options" default: all - type: date_range_preset id: period label: Period default: preset: last_30_days ``` El orden en el array es el orden en el que los controles renderizan en la UI. ## Cómo llega el valor del filtro a la query de Malloy El valor elegido en el control de filtro se manda a la query como un parámetro con nombre. El model declara el parámetro y lo usa adentro de la query. - Para **select**, **cutoff_date** y **month**, el nombre del parámetro es lo que pongas en el campo `param` del filtro (o, cuando se omite, el `id` del filtro). - Para **date_range** y **date_range_preset**, los parámetros son `date_from` y `date_to` por default. Overrideá los nombres a través del bloque `bindings: { date_from, date_to }` del filtro cuando el model espera nombres de parámetro distintos. Convención de naming: los parámetros cuyo nombre Malloy empieza con `p_` tienen el prefix strippeado para el nombre externo. Un parámetro de model declarado como `p_start_date` se setea mandando `start_date`. Mirá [Soporte de Malloy](/docs/build-workflow/malloy-support). ## Dónde viven los filtros - **En una sola visualization.** Usá el `filters: []` de la visualization cuando el control es local a ese chart — por ejemplo, un filtro de status por-grid. - **En un dashboard.** Usá el `filters: []` del dashboard cuando el control debería estrechar cada viz de la página — por ejemplo, un filtro global "Period" arriba de un dashboard ejecutivo. Un filtro a nivel dashboard aplica a cada viz que el dashboard contiene, pero solo si el model subyacente de cada viz declara el parámetro matcheante. ## Filtros vs cross-filtering Los filtros son declarativos — vos decidís en YAML qué controles ve el usuario. El cross-filtering es interactivo — el usuario clickea un chart y el dashboard agrega el filtro implícito como un pill. Los dos terminan en el mismo lugar: un parámetro de Malloy en el próximo run. La única diferencia es quién inicia el filtro. Para la mayoría de los dashboards, mezclá los dos: un set chico de filtros declarados arriba (period, region, channel) más el cross-filter implícito que responde a clicks. Mirá [Cross-filtering](/docs/build-workflow/cross-filtering). ## Diferencias entre adapters La UI de filtro se comporta igual sobre cualquier source. Las diferencias aparecen cuando el valor del filtro se bindea al parámetro subyacente — más importante para filtros de date / timestamp contra models con backing de Postgres o MySQL, que usan el patrón de placeholder `@param` en SQL crudo (cada `@param` declarado en la signature del source, valor sustituido en el string SQL en tiempo de run). Mirá [Diferencias entre adapters de source](/docs/reference/source-adapters) y [Models](/docs/build-workflow/models). ## Errores comunes - **El filtro aparece pero no estrecha los datos.** El model tiene que declarar un parámetro con el nombre matcheante (después de strippear el prefix `p_`) dentro de los paréntesis del source. Chequeá la signature del source en el archivo `.malloy`. - **Dos filtros se bindean al mismo parámetro.** Solo uno gana. Elegí una sola fuente de verdad por parámetro — o un filtro a nivel dashboard o un filtro por-viz, no los dos. - **Validate falla con `unbound_param`.** El model usa un placeholder `@param` en SQL crudo que no está declarado en la signature del source. El error nombra el parámetro faltante — agregalo a los paréntesis del source. - **Un filtro de fecha en un model Postgres o MySQL falla.** Usá el patrón de placeholder `@param` en SQL crudo en el SQL del model con una declaración matcheante en la signature del source — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters). - **El valor default no carga.** Para `select`, el default tiene que matchear el `id` de una de las opciones. Para filtros de fecha, usá los tokens soportados (`{{today}}`, `{{start_of_month}}`, etc.) o un string ISO literal. - **El filtro espera un valor que el usuario no provee.** O seteás un default razonable en el filtro, o declarás un default en la signature del source así la query igual corre cuando el parámetro falta. ---PAGE--- --- title: Filtro — select slug: docs/build-workflow/filters/select language: es description: "Referencia autoritativa del filtro select: campos requeridos y opcionales, opciones estáticas vs options_query, manejo de default y cómo el valor llega a la query Malloy." last_modified: "2026-06-11T14:17:13.768000+00:00" docs_section: build-workflow docs_summary: Filtro dropdown con opciones estáticas o un options_query de Malloy. --- ## Cuándo usar select Usá `select` cuando el usuario tiene que elegir un valor de un set conocido: un status, una región, una marca, una categoría. El dropdown refleja o bien una lista de opciones estáticas declarada en YAML o una lista dinámica resuelta corriendo una query Malloy en page load. Recurrí a uno de los filtros de fecha en su lugar cuando el input es una fecha o un date range. Recurrí a [cross-filtering](/docs/build-workflow/cross-filtering) cuando se espera que el usuario drillee clickeando un chart en vez de elegir de un dropdown. ## Campos requeridos - `type: select` O `options` u `options_query` tiene que estar presente (y solo uno de los dos). ## Campos opcionales - `id` — identifier interno; también el nombre de parámetro default cuando `param` no está seteado. - `label` — display label arriba del dropdown. - `param` — el nombre del parámetro Malloy al que bindear el valor seleccionado. Default a `id`. - `default` — valor seleccionado inicial. Tiene que matchear el `id` de una de las opciones. - `options` — array de objetos `{id, label}`. Usalo para una lista de opciones estática. - `options_query` — string en formato `path/to/source.mal::QueryName`. Looky corre esta query en page load y convierte las filas en opciones. ## Opciones estáticas Declará la lista inline. Mejor para enumeraciones chicas y estables — statuses, toggles on/off, periodicidades. ``` filters: - type: select id: status label: Status param: status default: all options: - { id: all, label: All } - { id: active, label: Active } - { id: cancelled, label: Cancelled } - { id: refunded, label: Refunded } ``` Cada opción tiene que tener un `id` (el valor mandado a la query) y un `label` (el texto mostrado en el dropdown). El `id` es lo que tu model Malloy recibe — diseñá el parámetro del model para aceptar el mismo shape. ## Opciones dinámicas (options_query) Usalo cuando la lista de opciones viene de datos — marcas, clientes, países, cualquier cosa que cambia con el tiempo. ``` filters: - type: select id: brand label: Brand param: brand options_query: "models/ec_revenue.malloy::brand_options" default: all ``` La query `brand_options` tiene que devolver filas con al menos columnas `id` y `label`. El lado Malloy típicamente se ve así: ``` view: brand_options is { group_by: id is brand label is brand order_by: brand asc } ``` Cada fila de la query se vuelve una opción. Looky normaliza cada fila a un `{id, label}`: - **id** — primer non-null de `id`, `indicator_code`, `group_code`. - **label** — primer non-null de `label`, `indicator_label`, `group_label`. - **sort key** — si una fila tiene `sort_order` (o `indicator_order` / `group_order`), las opciones se ordenan numéricamente por ese campo; si no, se ordenan alfabéticamente por label. Las filas a las que les faltan tanto un id como un label se dropean. ## Valor default Si `default` está seteado, es el valor del parámetro cuando carga la página. El botón de reset restaura este valor. Si no se setea default, el parámetro queda unset hasta que el usuario elija un valor, y la query subyacente tiene que aceptar la ausencia del parámetro (típicamente declarando su propio default). Para listas de opciones dinámicas, declarar una opción sentinel "all" y usarla como default es un patrón común — tu model Malloy trata `"all"` como "sin filtro". ## Cómo llega el valor a la query Malloy El `id` de la opción elegida se manda como el parámetro con nombre — por default con el nombre del `id` del filtro, o `param` si lo seteás. El model Malloy declara un parámetro con el nombre matcheante (después de strippear el prefix `p_` — mirá [Soporte de Malloy](/docs/build-workflow/malloy-support)) y lo usa en la query. ``` # en el model Malloy ##! experimental.parameters source: orders( p_status::string is "all" ) is bigquery.table('...') extend { view: detail is { where: p_status = "all" or status = p_status select: * } } ``` ## Diferencias entre adapters Los valores de `select` son típicamente strings o identifiers cortos; los tres adapters los bindean idéntico. El único edge case es cuando el parámetro del lado del model se declara como date o timestamp — ese patrón sigue las reglas de [Diferencias entre adapters de source](/docs/reference/source-adapters). ## Ejemplos trabajados Enumeración estática con un sentinel "all": ``` filters: - type: select id: status label: Status param: status default: all options: - { id: all, label: All } - { id: active, label: Active } - { id: cancelled, label: Cancelled } ``` Dinámica desde una query, con binding de parámetro custom: ``` filters: - type: select id: brand_filter label: Brand param: p_brand options_query: "models/ec_revenue.malloy::brand_options" default: all ``` Múltiples selects en el mismo dashboard, cada uno filtrando una dimension distinta: ``` filters: - type: select id: country label: Country options_query: "models/ec_revenue.malloy::country_options" default: all - type: select id: channel label: Channel options: - { id: all, label: All channels } - { id: organic, label: Organic } - { id: paid, label: Paid } - { id: direct, label: Direct } default: all ``` ## Errores comunes - **El valor default no carga.** El default tiene que matchear el `id` de una de las opciones. Para listas dinámicas, asegurate que el sentinel "all" esté incluido en el resultado de la query (o en `options` al lado). - **El filtro tarda mucho en cargar.** El `options_query` corre en page load. Si la query subyacente es lenta, el dropdown va a bloquear. Pre-agregá la lista de opciones en la query Malloy, o cacheala vía un sidecar. - **El model Malloy no acepta el parámetro.** Declará el parámetro en el model con un default razonable, así la query igual corre cuando el parámetro falta o está seteado a un sentinel. - **Los labels y los ids de opción se desincronizan.** Mantené el `label` legible para humanos y el `id` estable; el `id` es el contrato con la query, no el label. - **Mismatch de capitalización entre el id de opción y los datos.** El where-clause del model es exact-match por default. O normalizá los dos lados en el model (`lower(status) = lower(p_status)`) o alineá los ids de opción con los datos exacto. ---PAGE--- --- title: Filtro — cutoff_date slug: docs/build-workflow/filters/cutoff-date language: es description: "Referencia autoritativa del filtro cutoff_date: defaults con tokens, expansión de rango y mapeo de param Malloy." last_modified: "2026-06-11T14:17:07.024000+00:00" docs_section: build-workflow docs_summary: Una sola fecha de cutoff que se expande a un rango date_from / date_to. --- ## Cuándo usar cutoff_date Usá `cutoff_date` cuando el análisis es "todo desde el comienzo del mes hasta un día elegido" — revenue month-to-date, reportes de cierre mensual, snapshots "as-of". El usuario elige una sola fecha; Looky manda automáticamente tanto `date_from` (el primer día de ese mes) como `date_to` (la fecha elegida) como parámetros. Usá [date_range](/docs/build-workflow/filters/date-range) cuando el usuario necesita elegir los dos extremos libremente. Usá [date_range_preset](/docs/build-workflow/filters/date-range-preset) cuando la elección es entre presets con nombre como "last 30 days" o "this quarter". ## Campos requeridos - `type: cutoff_date` ## Campos opcionales - `label` — display label arriba del picker. - `default` — token de fecha (mirá abajo) o string ISO de fecha. Default a `"{{today}}"`. - `granularity` — controla cómo se deriva `date_from` desde la fecha elegida. Uno de `day`, `month` (default), `quarter`, `year`. Con `day`, `date_from` iguala a la fecha elegida (ventana de un día). Con `month`, el primer día del mes de la fecha elegida. Con `quarter`, el primer día del calendar quarter de la fecha elegida (Ene, Abr, Jul u Oct). Con `year`, 1 de enero del año de la fecha elegida. Honrado tanto a nivel dashboard como visualization. - `include_current_day` — boolean. Default `false`. Mirá [Máximo seleccionable](#maximo-seleccionable). ## Máximo seleccionable — hoy o ayer Por default, el filtro `cutoff_date` **capa la fecha máxima en `hoy − 1 día`**. El picker no permite seleccionar hoy, el token `{{today}}` resuelve a hoy pero se baja a ayer, y los `date_to` enviados a la query siempre terminan en ayer. Esa es la convención histórica de reportes "as-of cierre del día anterior": los datos del día en curso suelen estar incompletos hasta el cierre del día, así que el reporte se ancla al último día cerrado. Para casos operativos en vivo — donde sí querés ver datos del día en curso — agregá `include_current_day: true`: ``` filters: - type: cutoff_date label: Fecha de corte default: "{{today}}" include_current_day: true ``` Con el flag prendido: - El picker permite seleccionar hoy como fecha máxima. - `{{today}}` resuelve a hoy y no se clampa. - Los `date_to` enviados a la query son inclusivos del día en curso. El flag es opt-in (default `false`) para preservar back-compat con dashboards de reporte diario (PDFs, cierres de período) cuya semántica espera "hasta ayer cerrado". **Cuándo prenderlo:** dashboards operativos en vivo, KPIs de "lo que llevo hasta este instante", facturación en curso, monitoreo de avance. **Cuándo dejarlo apagado (default):** reportes que se exportan a PDF y se envían como cierre de día, snapshots históricos, reportes que requieren consolidación post-cierre. El flag se declara independientemente en cada filter — en la visualization (si se abre suelta) y en el dashboard (si tiene un filter global que propaga vía `param:`). Si la viz vive dentro de un dashboard con filter global, el flag del dashboard es suficiente; el valor resuelto llega a la viz vía param. ## Tokens de fecha para defaults Estos tokens resuelven contra la timezone actual del usuario en page load: - `{{today}}` - `{{yesterday}}` - `{{start_of_week}}` / `{{end_of_week}}` - `{{start_of_month}}` / `{{end_of_month}}` Cualquier cosa que no esté en esta lista se trata como un string ISO de fecha literal (ej. `"2024-12-31"`). ## Cómo llega el valor a la query Malloy La fecha elegida setea dos parámetros en cada query a la que aplica el filtro: - `date_from` — derivado de la fecha elegida según `granularity` (default al primer día de su mes). - `date_to` — la fecha elegida misma. Tu model de Malloy declara esos dos parámetros y los usa en cláusulas `where:`: ``` ##! experimental.parameters source: orders( p_date_from::date is @2024-01-01, p_date_to::date is @2024-12-31 ) is bigquery.table('...') extend { view: revenue_mtd is { where: created_at::date >= p_date_from and created_at::date <= p_date_to aggregate: revenue is sum(sale_price) } } ``` ## Diferencias entre adapters Tanto `date_from` como `date_to` son parámetros de date / timestamp. En BigQuery bindean nativo. En Postgres y MySQL el model tiene que usar el patrón de placeholder `@param` en el SQL subyacente — mirá [la comparación de adapters de source](/docs/reference/source-adapters) y [Soporte de Malloy](/docs/build-workflow/malloy-support). ## Ejemplos trabajados Cutoff default es hoy: ``` filters: - type: cutoff_date label: Cutoff default: "{{today}}" ``` Cutoff default es fin del mes pasado (para reportes de cierre mensual): ``` filters: - type: cutoff_date label: Closing date default: "{{end_of_month}}" ``` Cutoff default es una fecha literal (para un snapshot histórico fijo): ``` filters: - type: cutoff_date label: As of default: "2024-12-31" ``` ## Errores comunes - **La query necesita otro shape de rango.** `cutoff_date` es opinionado: `date_from` hace snap al comienzo de la ventana de `granularity` de la fecha elegida (day, month, quarter, year). Si necesitás que el usuario elija el lower bound libremente, usá [date_range](/docs/build-workflow/filters/date-range). - **El model Malloy espera nombres de parámetro distintos.** `cutoff_date` siempre emite `date_from` / `date_to`. Adaptá los nombres de parámetro del model en consecuencia, o usá [date_range_preset](/docs/build-workflow/filters/date-range-preset) que soporta bindings custom. - **La query falla en Postgres o MySQL con un error de date-binding.** Agregá el patrón de placeholder `@param` en el SQL del model — mirá [la comparación de adapters de source](/docs/reference/source-adapters). - **El token default no se reconoce.** Solo los tokens listados arriba son válidos. Cualquier otra cosa se trata como un string de fecha literal y silenciosamente no hace nada útil si está malformado. - **Múltiples filtros cutoff_date en una viz.** Solo un cutoff aplica. Usá otro tipo de filtro (o dos pares de parámetros separados) si necesitás múltiples ventanas temporales. - **"El reporte muestra hasta ayer aunque puse `{{today}}`".** Es el comportamiento por default — el cutoff se clampa a `hoy − 1`. Si querés incluir el día en curso, agregá `include_current_day: true` al filter. Mirá [Máximo seleccionable](#maximo-seleccionable). - **El flag `include_current_day` está en la viz pero el dashboard sigue mostrando ayer.** Cuando la viz vive dentro de un dashboard con filter global (`param: cutoff_date`), es el flag del filter del *dashboard* el que manda — el dashboard resuelve el valor y lo propaga a la viz vía param. Prendé el flag en el filter del dashboard, no solo en la viz. ---PAGE--- --- title: Filtro — date_range slug: docs/build-workflow/filters/date-range language: es description: "Referencia autoritativa del filtro date_range: defaults de from/to, resolución de tokens y mapeo de param Malloy." last_modified: "2026-06-11T14:17:10.361000+00:00" docs_section: build-workflow docs_summary: Picker abierto de from/to. --- ## Cuándo usar date_range Usá `date_range` para un picker abierto de from / to. El usuario elige los dos extremos libremente; los valores se mandan verbatim como parámetros. Usá [cutoff_date](/docs/build-workflow/filters/cutoff-date) cuando el lower bound siempre es el comienzo del mes de la fecha elegida. Usá [date_range_preset](/docs/build-workflow/filters/date-range-preset) cuando el usuario elige entre presets con nombre (last 30 days, this month, etc.) — preferido para la mayoría de dashboards operacionales. ## Campos requeridos - `type: date_range` ## Campos opcionales - `label` — display label arriba del picker. - `default` — objeto con keys `from` y `to`. Cada uno acepta un token de fecha (mirá abajo) o un string ISO de fecha. Uno o los dos pueden omitirse. ## Tokens de fecha para defaults El mismo set de tokens funciona en las sub-keys `from` y `to` (resuelven contra la timezone actual del usuario en page load): - `{{today}}` - `{{yesterday}}` - `{{start_of_week}}` / `{{end_of_week}}` - `{{start_of_month}}` / `{{end_of_month}}` Cualquier cosa que no esté en la lista se trata como un string ISO de fecha literal. ## Cómo llega el valor a la query Malloy El picker emite un par `{from, to}` al submit. Looky setea dos parámetros en cada query a la que aplica el filtro: - `date_from` — la fecha "from" elegida. - `date_to` — la fecha "to" elegida. El model Malloy declara esos parámetros y los usa en cláusulas `where:`, exactamente como con [cutoff_date](/docs/build-workflow/filters/cutoff-date). ## Diferencias entre adapters Misma caveat de date / timestamp que [cutoff_date](/docs/build-workflow/filters/cutoff-date) — mirá [la comparación de adapters de source](/docs/reference/source-adapters) para el patrón de Postgres / MySQL. ## Ejemplos trabajados Default a month-to-date: ``` filters: - type: date_range label: Period default: from: "{{start_of_month}}" to: "{{today}}" ``` Default a un período histórico fijo (para un reporte congelado): ``` filters: - type: date_range label: Period default: from: "2024-01-01" to: "2024-12-31" ``` Default abierto al día actual, sin lower bound (el default del parámetro del model entra en juego): ``` filters: - type: date_range label: Period default: to: "{{today}}" ``` ## Errores comunes - **La query falla en Postgres o MySQL con un error de date-binding.** Agregá el patrón de placeholder `@param` en el SQL del model — mirá [la comparación de adapters de source](/docs/reference/source-adapters). - **"From" es posterior a "to".** El picker previene al usuario de elegir un rango invertido, pero un default con `from` posterior a `to` renderiza una selección vacía. Usá un default razonable y dejá que el usuario ajuste. - **La precisión time-of-day importa pero el picker es solo date.** El picker solo manda fechas. Codificá el time-of-day en la query Malloy (ej. comparar contra `created_at::date`) o usá otro patrón de parámetro. - **Múltiples filtros date_range en una viz.** Todos bindean a `date_from` / `date_to` — solo uno gana. O mantenés un filtro date_range, o usás [date_range_preset](/docs/build-workflow/filters/date-range-preset) con bindings custom `date_from_param` / `date_to_param` para usar nombres de parámetro distintos. - **Los tokens default no se reconocen.** Solo los tokens listados son válidos; cualquier otra cosa se toma como un string de fecha literal. ---PAGE--- --- title: Filtro — date_range_preset slug: docs/build-workflow/filters/date-range-preset language: es description: "Referencia autoritativa del filtro date_range_preset: presets built-in, bindings de param custom y mapeo de param Malloy." last_modified: "2026-06-11T14:17:08.603000+00:00" docs_section: build-workflow docs_summary: Date range con presets nombrados como last_30_days, this_month. --- ## Cuándo usar date_range_preset Usá `date_range_preset` cuando el usuario elige un date range desde un set chico de presets nombrados — last 30 days, this month, this quarter, year-to-date, etc. Este es el default correcto para la mayoría de los dashboards operacionales: le da al usuario un click para cambiar período sin tipear fechas. Usá [date_range](/docs/build-workflow/filters/date-range) cuando el usuario tiene que elegir extremos arbitrarios. Usá [cutoff_date](/docs/build-workflow/filters/cutoff-date) cuando el input es una sola fecha en vez de un rango. ## Campos requeridos - `id` — único dentro del bloque filters del dashboard. - `type: date_range_preset` ## Campos opcionales - `label` — display label arriba del picker. - `default` — objeto seleccionando el preset a aplicar en first load: `{ preset: }`. Tiene que referenciar un id de `presets` (o uno de los built-ins si `presets` se omite). - `presets` — array de objetos `{ id }` en el orden en el que deberían aparecer en el picker. El id tiene que ser uno de los preset ids built-in de abajo; el schema rechaza ids desconocidos en tiempo de `looky validate`. - `bindings` — objeto overrideando los nombres de parámetro: `{ date_from: , date_to: }`. Usalo cuando el model espera nombres de parámetro distintos a los defaults `date_from` / `date_to`. ## Preset ids built-in Todos los preset ids se computan en el runtime del picker. Los autores no declaran tokens `from`/`to` — el picker resuelve el id a un `{date_from, date_to}` concreto en cada run, usando la timezone del workspace. - `today` — un solo día: la fecha de hoy. - `yesterday` — un solo día: hoy menos uno. - `last_7_days`, `last_30_days`, `last_90_days` — ventanas rolling de N días terminando hoy (inclusive). - `this_week`, `last_week` — semana ISO (lunes a domingo). - `this_month`, `last_month` — calendar month. - `this_quarter`, `last_quarter` — calendar quarter (Ene-Mar, Abr-Jun, Jul-Sep, Oct-Dic). - `this_year`, `last_year` — calendar year. Escribir cualquier otro id se rechaza con `looky validate` con un schema error apuntando al `presets[i].id` ofensor. Si un preset que necesitás no está en la lista, eso es un request para un nuevo built-in — no algo que los autores parcheen en YAML. ## Cómo llega el valor a la query Malloy 1. El picker resuelve el preset id seleccionado a un par `{date_from, date_to}` usando la timezone del workspace. 2. Esos valores fluyen como los parámetros `date_from` y `date_to` a cada viz del dashboard. 3. Si `bindings` está seteado, los mismos valores también se bindean a los nombres de parámetro listados ahí. El model Malloy declara los parámetros y los usa en cláusulas `where:`, exactamente como con los otros filtros de fecha. ## Diferencias entre adapters Misma caveat de date / timestamp que [cutoff_date](/docs/build-workflow/filters/cutoff-date) — mirá [la comparación de adapters de source](/docs/reference/source-adapters) para el patrón de Postgres / MySQL. ## Ejemplos trabajados Default a this year, con cinco presets en el picker: ``` filters: - id: period type: date_range_preset label: Period default: preset: this_year presets: - id: last_30_days - id: this_month - id: this_quarter - id: this_year - id: last_year ``` Bind a un par de parámetro custom (el model usa `start_date` / `end_date`): ``` filters: - id: period type: date_range_preset label: Period default: preset: this_month bindings: date_from: start_date date_to: end_date ``` Dos rangos independientes en el mismo dashboard, cada uno en sus propios parámetros: ``` filters: - id: current_period type: date_range_preset label: Current default: preset: this_month bindings: date_from: current_from date_to: current_to - id: comparison_period type: date_range_preset label: Compare to default: preset: last_month bindings: date_from: compare_from date_to: compare_to ``` ## Errores comunes - **`looky validate` rechaza un preset id.** El id tiene que ser uno de los built-ins. Typos como `last_7_dys` o ids inventados como `fiscal_q1` se rechazan en tiempo de push para que el dashboard nunca aterrice en prod con un preset silenciosamente roto. - **El default no aplica en first load.** `default.preset` tiene que matchear exacto un id de `presets` (o, si `presets` se omite, uno de los built-ins). - **La query no toma el date range.** El model Malloy tiene que declarar los parámetros a los que el picker bindea — por default `date_from` / `date_to`, o los nombres en `bindings` si los setás. - **Dos filtros de fecha pelean por `date_from` / `date_to`.** Si tenés múltiples filtros date_range_preset en el mismo dashboard, seteá `bindings` en cada uno para bindear a nombres de parámetro distintos. ---PAGE--- --- title: Filtro — month slug: docs/build-workflow/filters/month language: es description: "Referencia autoritativa del filtro month: defaults con tokens, bounds de ventana de año y mapeo de param Malloy." last_modified: "2026-06-11T14:17:12.106000+00:00" docs_section: build-workflow docs_summary: Picker de año + mes con bounds configurables. --- ## Cuándo usar month Usá `month` cuando el análisis está keyed a un solo mes — cierre mensual, billing mensual, reporting comparable-month-over-comparable-month. El usuario elige año + mes; el valor se manda como un string `YYYY-MM` al parámetro configurado. Usá un filtro de fecha ([cutoff_date](/docs/build-workflow/filters/cutoff-date), [date_range](/docs/build-workflow/filters/date-range), [date_range_preset](/docs/build-workflow/filters/date-range-preset)) en su lugar cuando el análisis abarca un date range arbitrario en vez de un calendar month. ## Campos requeridos - `type: month` ## Campos opcionales - `id` — identifier interno. - `label` — display label arriba del picker. - `param` — nombre de parámetro al que bindear el valor. Default a `id`; si los dos faltan, default al literal `"month"`. - `default` — token de mes (mirá abajo) o string ISO en formato `YYYY-MM`. Default al mes actual. - `year_min` / `year_max` — bounds de año explícitos para el picker. - `year_window`, `year_window_past`, `year_window_future` — bounds relativos. Default a una ventana de 5 años centrada en el año actual. - `month_picker` — objeto anidado que te deja overridear la config de year-bound sin polucionar el top level. ## Tokens default Tokens reconocidos para el campo `default`: - `{{current_month}}` — el calendar month actual en la timezone del usuario. Aliases: `{{this_month}}`, `{{month}}`. - `{{previous_month}}` — el calendar month antes del actual (rolla hacia atrás cruzando boundaries de año). Alias: `{{last_month}}`. Cualquier otra cosa se trata como un string literal `YYYY-MM` (ej. `"2024-12"`). Tokens no reconocidos caen a parsing literal y silenciosamente defaultean al mes actual si están malformados. ## Cómo llega el valor a la query Malloy El picker emite un string `YYYY-MM` al submit. Looky setea el parámetro con nombre a ese string. No hay expansión automática a `date_from` / `date_to` — si el model necesita un date range, derivalo adentro de la query desde el string del mes. ``` ##! experimental.parameters source: revenue( p_month::string is "2024-01" ) is bigquery.table('...') extend { view: monthly_revenue is { where: format_datetime('%Y-%m', created_at) = p_month aggregate: revenue is sum(sale_price) } } ``` ## Diferencias entre adapters El valor de month es un string, no un parámetro de date / timestamp, así que la caveat de Postgres / MySQL no aplica directo. Si el model castea el string del mes a una fecha dentro de la declaración de parámetro, aplica la misma caveat para ese parámetro derivado — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters). ## Ejemplos trabajados Default al mes actual, ventana de 5 años centrada en este año: ``` filters: - type: month id: month label: Month param: month default: "{{current_month}}" ``` Default a un mes histórico literal, con ventana de año custom (3 años pasados, 1 año futuro): ``` filters: - type: month id: month label: Month default: "2024-12" year_window_past: 3 year_window_future: 1 ``` Mes histórico congelado (default fijo a literal): ``` filters: - type: month label: Reporting month default: "2024-12" year_min: 2020 year_max: 2024 ``` Dos filtros month comparando períodos: ``` filters: - type: month id: current_month label: Current param: current_month default: "{{current_month}}" - type: month id: comparison_month label: Compare to param: comparison_month default: "2024-11" ``` ## Errores comunes - **El picker no muestra el año que querés.** Seteá `year_min` / `year_max` explícitos, o expandí `year_window_past` / `year_window_future`. - **El model espera una fecha, no un string.** El picker de month emite `YYYY-MM`. O hacés que el parámetro del model sea un string y lo parseás en la query, o usás un filtro de fecha ([date_range_preset](/docs/build-workflow/filters/date-range-preset) con los presets *this_month*/*last_month*) en su lugar. - **Dos filtros month bindean al mismo parámetro.** Cada uno tiene que tener un `param` distinto (o `id` cuando `param` se omite), si no solo uno gana. - **Se necesita selección multi-mes.** No soportado por este filtro — usá `date_range` o `date_range_preset`. - **Se necesita selección de quarter o semana.** No implementado — derivá esos adentro del model desde un filtro de fecha, o pre-computá la periodicidad en la query subyacente. ---PAGE--- --- title: Cross-filtering slug: docs/build-workflow/cross-filtering language: es description: "Referencia autoritativa de cross-filtering: qué viz types emiten, el mecanismo de pills del dashboard, el opt-out por viz y qué NO está soportado (sin declaración cross_filter en el YAML del dashboard)." last_modified: "2026-06-11T14:17:00.734000+00:00" docs_section: build-workflow docs_summary: Cómo los eventos de click se convierten en pills de dashboard y parámetros de Malloy. --- ## 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. Si querés comportamiento cross-filter, el usuario tiene que estar mirando el chart a través de un dashboard. ## 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 su label / serie / valor x. - **funnel** — clickear un stage emite el label del stage. - **heatmap** — emite solo cuando la viz tiene `chart.cross_filter_emit` seteado a `"x"` o `"y"`. - **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. ## 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 — clickear en un segundo chart agrega otro pill, y los dos aplican a cada viz en el próximo run. 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. Un click solo se mantiene como pill si el campo clickeado también es un parámetro declarado por al menos un model en el dashboard. Los clicks en campos que ningún model acepta se ignoran silenciosamente — no hay error, simplemente no hay pill. Para hacer una columna cross-filterable, declará un parámetro para ella en el model que potencia los charts afectados (mirá [Malloy support](/docs/build-workflow/malloy-support)). ## 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. Consecuencia práctica: cada model usado por un dashboard debería declarar un parámetro de Malloy para cualquier campo que querés que los usuarios puedan cross-filtrar. Usá un default razonable (típicamente un sentinel como `"all"`, un string vacío, o el valor válido más amplio) así la query igual corre cuando el parámetro no está seteado. ``` # en un model de Malloy ##! experimental.parameters source: orders( p_country::string is "all", p_brand::string is "all" ) is bigquery.table('...') extend { view: by_category is { where: (p_country = "all" or country = p_country) and (p_brand = "all" or brand = p_brand) group_by: 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. ## Opt-out por viz Suprimí 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** → `chart.cross_filter: false` - **grid** → `grid.cross_filter: false` - **report_matrix** → `matrix.cross_filter: false` - **kpi** → sin flag; KPI no emite por estructura (no tiene target categórico de click). Usá 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 — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters). 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 Decidí qué campos los usuarios deberían poder drillear (típicamente las dimensions en tus cláusulas group-by) y declará parámetros para esos en cada model usado por el dashboard. Salteá parámetros para campos que no deberían manejar cross-filter — los clicks en ellos caen silenciosamente. #### Headline + drill grid Usá 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 Mezclá un filtro de fecha arriba del dashboard (ej. [date_range_preset](/docs/build-workflow/filters/date-range-preset)) con cross-filtering implícito en el layout de charts. Ambos terminan como parámetros en las mismas queries. ## Errores comunes - **El click no hace nada.** El campo clickeado tiene que estar declarado como parámetro en al menos un model en el dashboard. Si ningún model lo declara, el click se ignora silenciosamente. - **Los pills desaparecen después de reload.** Los pills no persisten entre recargas de página. Si necesitás estado de filtro shareable, usá un filtro declarado ([Filters](/docs/build-workflow/filters)) — esos se reflejan en la URL. - **Una viz reacciona al pill, otra no.** Las dos vizs tienen que usar models que declaren el mismo parámetro. Auditá el model subyacente de cada viz. - **El KPI "headline" cambia cuando no debería.** El KPI reacciona a pills como cada otra viz. Si querés un KPI que los ignore, escribí el model subyacente para que el parámetro no influya el where-clause; o usá una viz no-KPI con `chart.cross_filter: false` como headline. - **Clickear un kpi no agrega un pill.** KPI es una viz consumer-only (no tiene target categórico de click por estructura). Su query re-corre con params activos pero no puede emitir. report_matrix y grid emiten en clicks de body-cell. - **Los clicks de funnel SON pills.** Funnel emite como bar / line / pie / scatter. Si querés un funnel que no emita, seteá `chart.cross_filter: false` en él. - **Dos pills deberían combinarse como OR pero combinan como AND.** El comportamiento actual es AND entre todos los pills. Para soportar semántica OR, codificalo en el parámetro del model subyacente (ej. aceptar una lista, default `"all"`). ---PAGE--- --- title: Dashboards slug: docs/build-workflow/dashboards language: es description: Componé múltiples visualizations en vistas de exploración guiada o de reporting. last_modified: "2026-06-11T14:17:02.315000+00:00" docs_section: build-workflow docs_summary: Armá vistas para exploración, análisis guiado y reporting sin romper el contrato de lógica subyacente. --- ## Un dashboard, una pregunta La mayoría de las herramientas BI te empujan a un dashboard gigante que trata de responder todo a la vez. El resultado es una pantalla llena de charts que nadie lee. Looky se construye alrededor del principio opuesto: un dashboard enfocado por pregunta de negocio. Un dashboard que responde "¿Dónde está perdiendo revenue este quarter?" es útil. Un dashboard que muestra todas las métricas para todas las dimensions para todos los períodos es una pantalla de loading con charts. Construí chico, agudo y publicable. Dos layout modes soportan distintos casos de uso. `layout_mode` es requerido; los únicos valores permitidos son: - **`fluid_grid`**: canvas interactivo y responsive. Los charts se sientan lado a lado y se cross-filtran entre sí cuando los usuarios clickean. Mejor para exploración y monitoreo operacional. - **`document`**: orden de lectura fijo top-to-bottom. Diseñado para ser leído, compartido y exportado a PDF. Mejor para reportes, briefings y deliveries scheduleados. Cualquier otro valor es rechazado por el validator. ## Dashboard fluid grid con control de layout Usá `width` en los items para controlar cuántas columnas del grid ocupa cada visualization. El grid default es de 3 columnas. Width 1 = una columna, width 2 = dos columnas, width 3 = full width. Spans avanzados opcionales: width 4 = cuarto de fila, width 6 = mitad de fila. ``` {% raw %}id: ec_sales_breakdown title: Where Is Revenue Coming From? layout_mode: fluid_grid published: true filters: - id: global_period type: cutoff_date granularity: year label: Period default: "{{today}}" items: - visualization: ec_revenue_kpi - visualization: ec_orders_kpi - visualization: ec_revenue_by_category_bar - visualization: ec_revenue_by_country_bar - visualization: ec_top_brands_bar - visualization: ec_traffic_source_bar width: 2 - visualization: ec_revenue_by_department_bar width: 1 - visualization: ec_orders_detail_grid{% endraw %} ``` ## Dashboard document para reportes y exports El modo document renderiza en un layout fijo de una columna. Es la elección correcta cuando el dashboard se va a compartir vía PDF export o cuando el orden de lectura tiene significado. ``` {% raw %}id: ec_business_overview title: What Is Happening In The Business? layout_mode: document published: true filters: - id: global_period type: cutoff_date granularity: year label: Period default: "{{today}}" items: - visualization: ec_revenue_kpi - visualization: ec_orders_kpi - visualization: ec_aov_kpi - visualization: ec_revenue_over_time_line - visualization: ec_category_brands_matrix - visualization: ec_orders_by_status_bar{% endraw %} ``` Los dashboards document se pueden exportar a PDF en un schedule. Mirá [Scheduled Exports](/docs/build-workflow/exports). ## Qué NO es un campo del YAML de dashboard Las relaciones de cross-filter **no** se declaran a nivel dashboard. No hay bloque `cross_filter` en el dashboard. El cross-filter es un toggle por viz y un comportamiento runtime automático dashboard-wide — mirá [Cross-filtering](/docs/build-workflow/cross-filtering). Similar, el shape de las entries en `items[]` es chico: cada item es o bien `{visualization: }` o el mismo más un `width:` opcional. No hay `height`, `position`, `row` ni `col`; el fluid grid acomoda los items en orden de declaración y el layout document es de una columna. ## Paginación dentro de un dashboard Los items grid y report_matrix dentro de un dashboard paginan server-side; cambiar de página re-corre la query subyacente con los nuevos parámetros de página. El comportamiento es idéntico para sources de BigQuery, Postgres y MySQL. Las características de costo difieren — mirá [Diferencias entre adapters de source](/docs/reference/source-adapters). ## Tipos de filter Los filtros declarados en un dashboard aplican a cada viz que el dashboard contiene. La referencia completa por tipo vive en [Filters](/docs/build-workflow/filters); los dos shapes más comunes para dashboards se muestran abajo. ### cutoff_date — selección de período de tiempo El filtro de dashboard más común. El usuario elige una sola fecha y Looky manda dos parámetros a cada query: `date_from` y `date_to`. El lower bound depende de `granularity` — month (default) setea `date_from` al primer día del mes de la fecha elegida; year setea `date_from` al 1 de enero de ese año. ``` {% raw %}filters: - id: global_period type: cutoff_date granularity: month # month (default) o year label: Period default: "{{today}}"{% endraw %} ``` Valores de `granularity` soportados para filtros cutoff_date a nivel dashboard: - `month` (default): `date_from` = primer día del mes de la fecha elegida, `date_to` = la fecha elegida. Útil para dashboards month-to-date. - `year`: `date_from` = 1 de enero del año de la fecha elegida, `date_to` = la fecha elegida. Útil para dashboards year-to-date. La key `granularity` es solo a nivel dashboard; los filtros `cutoff_date` por visualization siempre se comportan como `month`. Default tokens: `{{ '{{today}}' }}`, `{{ '{{yesterday}}' }}`, `{{ '{{start_of_week}}' }}`, `{{ '{{end_of_week}}' }}`, `{{ '{{start_of_month}}' }}`, `{{ '{{end_of_month}}' }}`. Cualquier otra cosa se trata como un string literal de fecha ISO. ### select — filtro de dimension desde una query de Malloy Pobla un dropdown desde una query live. Usá cuando los usuarios necesitan filtrar por un valor de dimension (región, categoría, marca) en vez de por tiempo. ``` filters: - id: category type: select label: Category param: category options_query: "models/ec_revenue.malloy::category_options" default: all ``` `options_query` apunta a una query de Malloy que devuelve filas con al menos las columnas `id` y `label` (o uno de los aliases reconocidos — mirá [select](/docs/build-workflow/filters/select)). El parámetro del model declarado como `p_category` recibe el valor (el prefix `p_` se strippea para el nombre externo; el filtro manda `category`). ## Cross-filtering en fluid grid En dashboards `fluid_grid`, clickear una barra, punto o celda en cualquier chart interactivo filtra todos los otros charts del dashboard a esa selección. Esto funciona automático — no requiere configuración. Para deshabilitar el cross-filtering en una visualization específica (por ejemplo un KPI summary que no debería cambiar cuando se filtran otros charts), seteá `cross_filter: false` en el bloque `chart` de la visualization: ``` # en el YAML de la visualization, no del dashboard chart: cross_filter: false ``` El cross-filtering no está disponible en modo document. Los dashboards document están diseñados para lectura, no exploración. El mecanismo completo — pills, whitelist de supported-params, reglas emit/consume por viz — vive en [Cross-filtering](/docs/build-workflow/cross-filtering). ## Checklist de composición - Cada `items[].visualization` referencia un id de visualization publicada existente. - `layout_mode` se elige deliberadamente: `fluid_grid` para exploración, `document` para reportes. - El título del dashboard es una pregunta, no una etiqueta de categoría. - El `param` del filtro matchea el nombre del parámetro declarado en el model de Malloy. - `granularity` matchea la resolución temporal de los datos subyacentes. ``` looky validate looky list dashboards ``` ---PAGE--- --- title: Scheduled Exports slug: docs/build-workflow/exports language: es description: Scheduleá exports a PDF de dashboards modo document para que corran automáticamente en un schedule cron. last_modified: "2026-06-11T14:17:03.873000+00:00" docs_section: build-workflow docs_summary: Definí jobs de export en content/exports/ para producir PDFs scheduleados desde dashboards document. --- ## Modo document y exports son la misma idea Un dashboard en `layout_mode: document` está diseñado para ser leído de arriba abajo, como un reporte. Renderiza en un layout fijo que mapea limpio a una página impresa o PDF. Los exports toman ese layout y lo entregan en un schedule — sin que nadie tenga que abrir la UI. La combinación de dashboards document y scheduled exports reemplaza el workflow manual de "screenshot y paste en slide". Definís el reporte una vez y aterriza donde tiene que ir, con data fresca, a tiempo. ## Dónde viven las definiciones de export Los exports son archivos YAML dentro de `content/exports/` en tu workspace: ``` / content/ exports/ sales_daily_pdf.yml fulfillment_weekly_pdf.yml ``` Cada archivo define un job de export: qué dashboard, qué schedule, qué parámetros y a dónde entregar el output. ## Working example ``` {% raw %}id: ec_business_daily_pdf enabled: true dashboard_id: ec_business_overview schedule: cron: "0 8 * * *" timezone: "America/New_York" params: cutoff_date: "{{today}}" destination: type: local_folder folder: "." filename: "business-overview-{{today}}.pdf" policy: on_missed_run: skip max_retries: 2 timeout_seconds: 180{% endraw %} ``` Campo por campo: - `id`: identifier único para este job de export. - `enabled`: seteá a `false` para pausar sin borrar. - `dashboard_id`: tiene que matchear el `id` de un dashboard publicado. El layout mode document es requerido para exports PDF. - `schedule.cron`: expresión cron estándar de cinco campos. `0 8 * * *` corre cada día a las 08:00. - `schedule.timezone`: nombre de timezone IANA. La expresión cron se evalúa en esta timezone. - `params`: valores de filtros de dashboard inyectados en tiempo de render. Matchean el campo `param` definido en el bloque `filters` del dashboard. - `destination.type`: `local_folder` guarda el PDF en la carpeta especificada en el host del export engine. - `destination.filename`: soporta template variables. `{{ '{{today}}' }}` se expande a la fecha actual en formato ISO. - `policy.on_missed_run`: `skip` saltea runs perdidos silenciosamente. Usá `backfill` si necesitás que los runs perdidos se ejecuten al recover. - `policy.timeout_seconds`: si el render del PDF lleva más que esto, el job se marca como failed. ## Enviar el reporte a suscriptores Un export puede entregar el PDF terminado directo a las personas — por email o WhatsApp — sin que nadie abra Looky. Agregá un bloque `subscribers` y listá a cada destinatario bajo el canal por el que querés alcanzarlo: ``` subscribers: email: - finance@acme.com - ops@acme.com whatsapp: - "+15551234567" ``` - `email`: direcciones de correo que reciben el PDF como adjunto. - `whatsapp`: números de teléfono en formato `+` que reciben el PDF por WhatsApp. Vos elegís el canal explícitamente — poné un correo bajo `email` y un teléfono bajo `whatsapp`. Los destinatarios no necesitan una cuenta de Looky, así que un reporte programado es una forma simple de mantener informados a los interesados. Ambas listas son opcionales; un export sin `subscribers` solo genera el PDF sin enviarlo a ningún lado. ## Template variables Tanto los valores de `params` como `destination.filename` soportan estas template variables: - `{{ '{{today}}' }}` — la fecha de hoy en la timezone del export (formato ISO: YYYY-MM-DD). - `{{ '{{yesterday}}' }}` — la fecha de ayer en la timezone del export. Esto mantiene en sync los nombres de archivo de export y los valores de filtro del dashboard sin configuración manual: ``` {% raw %}params: cutoff_date: "{{today}}" destination: filename: "sales-report-{{today}}.pdf"{% endraw %} ``` ## Ejemplos de cron schedule - `0 8 * * *` — cada día a las 08:00 - `0 8 * * 1` — cada lunes a las 08:00 - `0 8 1 * *` — primer día de cada mes a las 08:00 - `0 6,18 * * *` — dos veces al día a las 06:00 y 18:00 Siempre seteá un `timezone`. Un cron sin timezone corre en UTC, lo que produce fechas off-by-one cuando usás `{{ '{{today}}' }}` si tus usuarios están en otra timezone. ## Push y verificar exports Las definiciones de export se pushean con el resto del content del workspace: ``` looky validate looky diff looky push ``` Después del push, el export engine levanta el job nuevo en su próximo ciclo de scheduling. Para verificar que el job esté registrado, chequeá el status del workspace en la UI o corré `looky list exports`. ---PAGE--- --- title: Publish slug: docs/build-workflow/publish language: es description: Mover cambios de workspace al environment corriendo y validar el resultado efectivo ahí. last_modified: "2026-06-11T14:17:18.578000+00:00" docs_section: build-workflow docs_summary: Terminá con un camino de publish repetible y un paso de validación de runtime, no un supuesto del filesystem. --- ## El ciclo de vida de publish Publicar un workspace es un ritmo de cuatro pasos — leer el estado local, ver qué difiere, validar, push. Corré todos estos desde el root del workspace (o cualquier subdirectorio adentro): ``` looky status # confirmar root, instancia, billing, workspace, sync state looky diff # mostrar diferencias de archivos local-vs-server looky validate # correr el gate de validación looky push # publicar content (solo después de que validate esté limpio) ``` `push` corre el mismo gate de validación que `validate` y se rehúsa a publicar si algo falla — `validate` es la forma de correr ese gate sin publicar. ## Dos scopes de push — content vs settings `looky push` apunta a una de dos superficies distintas, nunca a las dos a la vez: - **Default (content push)** — publica `content/**` + `workspace.yml`. Gateado fuerte por la validación. Falla si tu configuración de runtime todavía no se deployó. - **`--settings` (config push)** — publica `runtime/**` + `secrets/**`. Valida las declaraciones de source y después escribe. Usalo en setup inicial, después de rotar credentials, o cada vez que cambies declaraciones de source. Orden típico de primera vez: `looky push --settings` primero para deployar sources + secrets, después `looky push` para content. ## Qué chequea la validación Cuando corrés `looky validate` o `looky push`: - **Pasada local.** El YAML parsea, los archivos referenciados existen, los IDs son únicos, `query` usa el separador `::`, los dashboards referencian visualizations reales, los exports referencian dashboards reales, las expresiones cron son válidas, el bloque `chart` de cada visualization pasa su schema tipado. - **Pasada server.** Los aliases de source son alcanzables, cada model Malloy compila, cada visualization publicada hace dry-run limpio contra las data sources configuradas. Si algún check falla, el comando sale no-cero e imprime el código de error, el path del archivo, y un mensaje legible. Los warnings **no** bloquean — aparecen en el output y dejan que el push proceda. ## Flags de validación - `--strict` — suma validación live-source por visualization. En Postgres, corre `EXPLAIN` contra la base live (un round-trip por viz, con timeout de statement de 10 segundos). En BigQuery, corre `estimateQueryCost` (gratis, sin bytes escaneados). Atrapa errores de dialecto y permisos faltantes que la pasada rápida default no puede. Usalo antes de un release; salteatelo para iteración rápida. Para validar sin publicar, usá `looky validate`. En push, el cliente es la fuente de verdad del scope de content — archivos presentes en el server pero ausentes localmente se borran como parte del push. ## Familias de error code Los errores y warnings de validación se taggean con un prefix de código estable. El prefix te dice qué gate se disparó y a qué familia de archivos concierne. - **`WS`***** — issues de workspace.yml (campos faltantes, shape inválido de identifier, billing account desconocido). - **`RT`***** — declaraciones de runtime / source (tipo de source faltante o no soportado, `credentials_file` faltante, `dsn` faltante para postgres, archivo de credenciales no encontrado en `secrets/`). Mayoritariamente warnings. - **`MD`***** — issues de archivo de model (referencia a alias de source faltante, archivo fuera de content/, archivo no legible). - **`VZ`***** — estructura YAML de visualization (id/title/type/query faltantes, referencia de query malformada, id duplicado). - **`VZ020`, `VZ021`** — violaciones de schema tipado de visualization: una propiedad `chart.*` que no está en el schema, un tipo equivocado, un valor fuera de rango. - **`DB`***** — issues de YAML de dashboard (campos faltantes, layout_mode no en `fluid_grid` / `document`, referencias a visualizations desconocidas). - **`EX`***** — issues de YAML de export (formato cron, referencia a dashboard desconocido). - **`MR`***** — errores de runtime reportados por el server. `MR000` falla de carga, `MR001` error de compile / parse de Malloy, `MR002` field no definido, `MR003` alias de source desconocido, `MR004` introspección de schema fallida, `MR005` query engine inalcanzable, `MR006` query engine timeouteó, `MR008` archivo de model faltante, `MR009` catch-all de precheck, `MR010` source inalcanzable desde la probe. ## Qué sube el push Un content push exitoso publica el set de archivos validado y reporta counts de archivos created / updated / unchanged / deleted. El disaster recovery es tu responsabilidad — mantené el workspace en git, o apoyate en el snapshot local `.bk//` que `looky pull` escribe cuando sobrescribe archivos locales. Un settings push exitoso publica `runtime/sources.runtime.yml` y los archivos de `secrets/` referenciados por él. La validación corre primero; si falla, el push imprime los issues y no se escribe nada. ## Verificación post-publish 1. Corré `looky list visualizations` y `looky list dashboards` — confirmá qué hay publicado realmente en el server. 2. Abrí `https://my.looky.studio` y confirmá que los dashboards renderizan. 3. Abrí al menos una página de detalle de visualization y confirmá que aparece data. La validación no chequea que los nombres de campo en `mapping` matcheen el resultado de la query; eso recién aparece cuando el chart efectivamente renderiza. 4. Si un dashboard renderiza en blanco o un chart muestra "no rows", chequeá la query de model subyacente en la página de detalle de la visualization — esa es la traza más directa. Un push exitoso solo significa que tus archivos fueron aceptados y publicados. No significa que cada chart renderice bien. Siempre confirmá visualmente después de cada push. ## Playbook de recovery de fallas - **La validación falló localmente (WS / RT / MD / VZ / DB / EX).** Leé el código de error y el path del archivo; arreglá el YAML; re-corré `looky validate`. - **La validación falló en la pasada server (MR* o VZ020).** El error menciona el model o visualization que lo disparó. Abrí ese archivo, arreglá el Malloy o spec de chart, re-corré `looky validate`. Si el error es `MR010` (source inalcanzable), confirmá que `looky push --settings` haya corrido exitosamente recientemente con las credentials actuales. Cuando el server reporta cualquier error, nada en el workspace se modifica. - **El push pasó pero el dashboard está mal en el sitio live.** Los archivos se publicaron bien pero un chart está fallando al renderizar. Chequeá nombres de campo en mapping, format keys, y el output real de la query en el viz detail. - **Necesitás deshacer un push.** Restaurá el contenido previo desde git o desde el directorio local `.bk//` que `looky pull` escribió, y volvé a pushear con `looky push`. ---PAGE--- --- title: Referencia slug: docs/reference language: es description: Material de referencia enfocado para preguntas recurrentes de implementación o debug. last_modified: "2026-06-11T14:17:51.065000+00:00" docs_section: reference docs_summary: Usá esta sección cuando ya conocés el tópico y necesitás guía operacional rápida. --- ## Quick reference del CLI Usá esta página como tu cheatsheet operacional. ``` # Auth y linkeo de root (cualquier subdirectorio bajo un linked root) looky login https://my.looky.studio looky whoami looky logout # Discovery y billing (correr exactamente desde ; el CLI rechaza subdirs) looky billing list looky billing use looky workspaces # Crear/pullear/borrar un workspace (correr desde /) looky pull looky create --name "My Workspace" --description "..." looky delete [--yes] # Loop de build/publish (correr desde cualquier lugar dentro del árbol del workspace) looky status looky validate looky diff looky push # Chequeos de runtime sources (dentro del árbol del workspace) looky sources diff looky sources list # Chequeos de contenido publicado (dentro del árbol del workspace) looky list visualizations looky list dashboards ``` ## Uso de comandos por fase ### Onboarding día cero `looky login` → `looky whoami` → `looky billing list/use` ### Empezar trabajo de workspace `looky pull` o `looky create` → `looky status` → `looky validate` ### Publicar seguro `looky diff` → `looky push` → `looky list dashboards/visualizations` ### Investigar mismatch de source `looky sources diff` y `looky sources list` ## Convenciones de placeholders usadas en la doc - ``: local root linkeado durante `looky login`. - ``: billing account activo seleccionado con `looky billing use`. - ``: una carpeta de workspace bajo el billing account. ## Entrypoint de troubleshooting Para fixes por síntoma, seguí con [Troubleshooting]({{ elemental_url_for_slug(slug='docs/reference/troubleshooting') }}). ---PAGE--- --- title: Troubleshooting slug: docs/reference/troubleshooting language: es description: Chequeos rápidos para problemas comunes de runtime, publicación y navegación en la doc o en el flow de builder. last_modified: "2026-06-11T14:17:55.783000+00:00" docs_section: reference docs_summary: Usá estos chequeos cuando el resultado runtime no matchea la intención autoreada. --- ## Errores de contexto de root, billing o workspace ### Síntomas El CLI reporta root inválido, workspace faltante, o mismatch de path. ### Secuencia de fix ``` looky login https://my.looky.studio looky whoami cd looky billing list looky billing use cd // looky status ``` Corré los comandos de billing desde `` y los comandos de workspace desde el root del workspace. ## La validación falla antes del push ### Síntomas `looky validate` reporta issues de model, visualization o dashboard. ### Orden de fix 1. Arreglá primero los issues de alias de source / runtime config. 2. Arreglá las definiciones de queries de model y los nombres. 3. Arreglá las referencias `query` y `mapping` de visualization. 4. Arreglá las referencias de items y filtros de dashboard. 5. Re-corré `looky validate` hasta que no queden errores bloqueantes. ## El push tuvo éxito pero el dashboard está mal o falta ### Síntomas El dashboard no es visible, está vacío, o es inconsistente con el output esperado en `my.looky.studio`. ### Secuencia de fix ``` looky status looky diff looky push looky list visualizations looky list dashboards ``` Después recargá la UI. Si el dashboard sigue fallando, confirmá que los ids de visualization referenciados en el YAML del dashboard matcheen exacto los ids de visualization publicados. ## Mismatch de source/runtime ### Síntomas Los models no pueden resolver tablas o el comportamiento del data source difiere inesperadamente. ### Secuencia de fix ``` looky sources diff looky sources list looky validate ``` Asegurate que los aliases usados en los models matcheen exacto los aliases definidos en `runtime/sources.runtime.yml`. No parches fallas de source por model. Corregí el runtime source config una vez, después mantené la lógica del model enfocada en la semántica analítica. ## Recuperarse de un push malo El disaster recovery es responsabilidad del caller. El camino forward más sano es arreglar el archivo localmente y volver a pushear. Para volver a un estado previo, necesitás ese estado en tu laptop — mantené el workspace en git, o apoyate en el snapshot local `.bk//` que `looky pull` escribe dentro de tu workspace local cada vez que sobrescribe un archivo. El `.bk/` local son archivos planos al lado de tu workspace — copialos de vuelta al lugar correspondiente y corré `looky push` para restaurar ese estado en el server. ## Leyendo códigos de error de validación Cada error o warning de validación lleva un prefix de código estable. El prefix te dice qué gate se disparó y a qué familia de archivos concierne: - **`WS`***** — issues en `workspace.yml` (fields faltantes, shape de identifier, referencia de billing-account). - **`RT`***** — issues en `runtime/sources.runtime.yml` (tipo de source faltante o no soportado, `credentials_file` faltante, `dsn` faltante para postgres/mysql, archivo de credenciales no encontrado en `secrets/` en disco). Frecuentemente warnings en vez de errores bloqueantes. - **`MD`***** — issues en archivos de model (alias de source desconocido, archivo de model fuera de `content/`, archivo no legible). - **`VZ`***** — issues estructurales del YAML de visualization (campos requeridos faltantes, referencia de query malformada, id duplicado). `VZ020` y `VZ021` específicamente flagean propiedades `chart.*` que violan el schema tipado para el viz type. - **`DB`***** — issues del YAML de dashboard (`layout_mode` fuera de `fluid_grid` / `document`, referencias a visualizations desconocidas, ids duplicados). - **`EX`***** — issues del YAML de export (formato cron, referencia a dashboard desconocido, ids duplicados). - **`MR`***** — errores levantados por el server al correr la validación contra el runtime live: - `MR000` — falló cargar visualizations / dashboards / sources. - `MR001` — error de compile / parse / sintaxis de Malloy, o cualquier otra falla aparecida por el query engine. La línea que sigue al código empieza con `Malloy service HTTP :` y lleva el error específico del engine. Mirá "Mensajes de error de engine bajo MR001" abajo para los comunes. - `MR002` — nombre de field o query referenciado en una query no está definido en el source Malloy. - `MR003` — el model referencia un alias de source desconocido. - `MR004` — la introspección de schema falló (no puede leer schema para una relación). - `MR005` — query engine inalcanzable. - `MR006` — query engine timeouteó — partí el workspace si es inusualmente grande. - `MR008` — archivo de model faltante o fuera de `content/`. - `MR009` — falla genérica de precheck. - `MR010` — source inalcanzable desde la probe de conectividad (chequeá que `looky push --settings` corrió con credentials actuales). Los códigos locales (WS / RT / MD / VZ / DB / EX) vienen del chequeo local que corre el CLI antes de contactar al server. Los códigos server (MR* y VZ020 / VZ021 emitidos por el server) vienen de la validación corrida contra el runtime live. Mirá [Publish](/docs/build-workflow/publish) para el flujo completo. ## Mensajes de error de engine bajo MR001 `MR001` reporta cualquier cosa que el query-engine rechaza, con prefix `Malloy service HTTP :`. El mensaje trailing identifica la causa específica y apunta al fix: - **`HTTP 400 — unsupported_model_shape`** — al model le falta el shape estricto que cada model de Looky necesita: el pragma `##! experimental.parameters` arriba del archivo, más paréntesis en la declaración del source (`source: name() is …`, incluso cuando el source no toma parámetros). Agregá los dos y re-validá. Mirá [Models](/docs/build-workflow/models). - **`HTTP 400 — unbound_param`** — el model usa un placeholder `@param` en SQL crudo que no tiene declaración matcheante en la signature del source. El error nombra el parámetro faltante; declaralo dentro de los paréntesis del source, ej. `p_date_from::date is null`. Tipos comunes: `date`, `string`, `number`, `boolean`. - **`HTTP 400 — missing_sources_config`** — este workspace todavía no tiene settings publicados. Corré `looky push --settings` una vez y validá de nuevo. Mirá [Publish](/docs/build-workflow/publish). - **`HTTP 400 — malformed_request`** — el CLI mandó un request body inválido al server. Esto es un bug de CLI, no un issue de model. Reintentá; si persiste, compartí el request id con tu administrador. - **`HTTP 500`** — una falla inesperada en la plataforma o en tu data source (errores de BigQuery / Postgres / MySQL, timeouts). Chequeá el request id, reintentá, y escalá si persiste. ---PAGE--- --- title: Diferencias entre adapters de source slug: docs/reference/bigquery-vs-postgres language: es description: "Referencia de divergencias de adapter: cada lugar donde BigQuery, Postgres y MySQL se comportan distinto en Looky — sources, parameter binding, NULL casting, dryRun, introspección y caching de conexión." last_modified: "2026-06-11T14:17:52.641000+00:00" docs_section: reference docs_summary: Divergencias de adapter en sources, parámetros, dialecto y paginación. --- ## Por qué existe esta página Looky soporta tres adapters de source: BigQuery, Postgres y MySQL. La mayor parte de la plataforma se comporta idéntica en los tres. Un puñado de áreas se comportan distinto — esas son las que tropiezan a autores de model y dashboard. Esta página lista cada una en términos observables (lo que ves, no cómo está implementado). ## Declaración de source #### BigQuery - Requeridos: `type: bigquery`, `project_id` (el proyecto de GCP que paga las queries), `credentials_file` (filename plano del JSON de service account dentro de `secrets/` — sin path). - Opcionales: `location` para datasets multi-region, `datasets` (requeridos en tiempo de introspección de schema). #### Postgres - Requeridos: `type: postgres`, `dsn` (URI libpq sin `user:password@`; lleva host, port, database, y cualquier flag de query-string libpq como `sslmode=require`, `application_name`, `connect_timeout`, …), `credentials_file` (filename de un JSON en `secrets/` con `{"user", "password"}`). - Opcionales: `name` (label cosmético), `schemas` (limita el scope de introspección; default es todos los schemas no-sistema). #### MySQL - Requeridos: `type: mysql`, `dsn` (URI `mysql://host:port/database` sin `user:password@` **y sin parámetros `?query`** — el port default a `3306`), `credentials_file` (filename de un JSON en `secrets/` con `{"user", "password"}`). - Opcionales: `name` (label cosmético), `schemas` (databases a introspeccionar; default es la database nombrada en el `dsn`). - Nota: las conexiones MySQL **no van encriptadas** — todavía no hay opción TLS, así que usá MySQL sobre una red de confianza. Mirá [Sources](/docs/build-workflow/sources) para ejemplos funcionando por adapter. ## Introspección de schema - **BigQuery** — requiere una lista explícita de datasets. Sin eso, la introspección no puede encontrar tablas. - **Postgres** — descubre tablas entre los schemas en tu lista `schemas` (o el smart default de cada schema no-sistema). No hace falta declaración de table-list. - **MySQL** — descubre tablas en la database nombrada en el `dsn` (o las databases en tu lista `schemas`). Las databases de sistema (`mysql`, `performance_schema`, `sys`, `information_schema`) quedan siempre excluidas. No hace falta declaración de table-list. Los mensajes de error en fallas de introspección se ven completamente distintos por adapter — cuando estés en duda, corré primero una pequeña query de test para confirmar que Looky puede llegar al source. ## Parameter binding #### Parámetros string, numéricos y array Se comportan idéntico en los tres adapters. No hace falta manejo especial. #### Parámetros date y timestamp - **BigQuery** — bindean nativos, sin trabajo extra. - **Postgres y MySQL** — el binding nativo falla con un error explícito en algunos shapes de query. Agregá un placeholder `@param` en el SQL subyacente de ese source así Looky sustituye el valor dentro del string SQL. Mirá [Soporte de Malloy](/docs/build-workflow/malloy-support) para un ejemplo trabajado. Tipos de filtro que producen parámetros date / timestamp: [cutoff_date](/docs/build-workflow/filters/cutoff-date), [date_range](/docs/build-workflow/filters/date-range), [date_range_preset](/docs/build-workflow/filters/date-range-preset). Planeá para el patrón `@param` cuando esos filtros manejen un model con backing en Postgres o MySQL. #### Valores NULL de parámetro Cada adapter escribe un null tipado distinto: BigQuery y MySQL requieren un cast explícito, Postgres usa un null tipado pelado. Looky maneja esto por vos — no hay nada que configurar — pero es la razón por la que un parámetro nullable malformado puede fallar en un adapter y silenciosamente funcionar en otro. #### Valores boolean (solo MySQL) MySQL no tiene un tipo boolean real — las columnas que parecen boolean vuelven como números (`0`/`1`). Para filtrar sobre una condición true/false, agregá un `cast(… as boolean)` explícito en el model. BigQuery y Postgres tienen booleans nativos y no necesitan ese cast. ## Ejecución de queries - **BigQuery** — expone una estimación de costo pre-flight gratis (en bytes escaneados) antes del run real. Útil para planear contra queries scan-heavy. - **Postgres y MySQL** — pre-flight valida el SQL plan (vía `EXPLAIN`) pero no estima costo. ## Filtros y cross-filtering El resolver de filtros y el mecanismo de pill de cross-filter son agnósticos del adapter. Las diferencias solo aparecen en el paso de parameter-binding descrito arriba. - Un filtro [select](/docs/build-workflow/filters/select) se comporta idéntico en los tres. - Los filtros de fecha aterrizan en la caveat de parámetro temporal para Postgres y MySQL — arreglá del lado del model con `@param`. - Los valores de pill de [Cross-filtering](/docs/build-workflow/cross-filtering) son típicamente strings o identifiers cortos, así que no se afectan. ## Paginación (grid & report_matrix) La paginación server-side usa el mismo protocolo en los tres adapters. Las características de costo difieren: - **BigQuery** — cada página sin cache es un scan facturado. Page size más grande + caching es más barato en total. Evitá grids sin bound en queries sin cache. - **Postgres y MySQL** — la latencia roundtrip de conexión domina; el costo depende mayormente del performance del indexed-scan de las tablas subyacentes. Asegurate que las columnas referenciadas en `order by` tengan índices apropiados. ## Recomendaciones de cache TTL La lógica del cache en sí es la misma en los tres adapters. La elección de TTL es editorial: - **BigQuery** — favorecé TTLs más largos para queries que escanean tablas grandes. Un TTL de 30 minutos en un dataset de batch diario es overkill; un TTL de 24 horas usualmente está bien. - **Postgres y MySQL** — el TTL es mayormente sobre freshness en vez de costo. El caching ayuda menos si la tabla subyacente es chica y bien indexada; considerá si el costo ahorrado supera el staleness introducido. ## Agregaciones y features de dialecto Looky no se ramifica en adapter para funciones agregadas o windowing. Cualquier cosa específica de dialecto (funciones ARRAY solo de BigQuery, `date_trunc` de Postgres en ciertos tipos, funciones de fecha solo de MySQL, etc.) aparece como un error de compilación de Malloy. El fix es del lado de Malloy — ajustá el model para usar un feature que todos los adapters de destino soportan, o protegé el model contra el adapter equivocado. ## Patrones de migración trabajados #### Un filtro de fecha que funciona en BigQuery pero rompe en Postgres o MySQL El model con binding nativo: ``` # funciona en BigQuery, frágil en Postgres / MySQL en algunos shapes ##! experimental.parameters source: orders( p_date_from::date is @2024-01-01, p_date_to::date is @2024-12-31 ) is bigquery.table('...') extend { view: revenue is { where: created_at::date >= p_date_from and created_at::date <= p_date_to aggregate: revenue is sum(sale_price) } } ``` Cambiá el source a `sql()` con placeholders para compatibilidad con Postgres / MySQL (este ejemplo usa sintaxis Postgres; en MySQL usá el equivalente `mysql.sql("""…""")` con casts del dialecto MySQL): ``` # funciona en Postgres (y BigQuery) ##! experimental.parameters source: orders( p_date_from::date is null, p_date_to::date is null ) is postgres.sql(""" select * from orders where (@date_from::date is null or created_at::date >= @date_from::date) and (@date_to::date is null or created_at::date <= @date_to::date) """) extend { view: revenue is { aggregate: revenue is sum(sale_price) } } ``` Cada placeholder `@param` tiene que tener una declaración matcheante en la signature del source; Looky sustituye el valor (o `NULL` tipado cuando está unset) antes del compile. #### Un grid que pagina bien en Postgres / MySQL pero hace explotar el scan budget de BigQuery Subí `pagination.page_size`, agregá un cache sidecar con TTL largo, y pre-agregá cuando sea posible. En BigQuery, paginar a través de millones de filas sin cache es caro — pre-resumí. ## Quick reference - **Auth de source** — BigQuery: archivo de service account. Postgres / MySQL: connection string + un secret `{"user","password"}`. - **Transporte** — BigQuery / Postgres: TLS disponible. MySQL: todavía sin encriptar. - **Introspección** — BigQuery: datasets explícitos requeridos. Postgres: schemas smart-default. MySQL: la database del DSN (o una lista `schemas`). - **Parámetros numéricos / string** — idéntico. - **Parámetros date / timestamp** — BigQuery: nativo. Postgres / MySQL: necesitan placeholder `@param` en SQL. - **Booleans** — nativos en BigQuery / Postgres. MySQL: numéricos — casteá explícitamente. - **Parámetros NULL** — manejados automáticamente; sin configuración. - **Pre-flight** — BigQuery: estimación de costo gratis. Postgres / MySQL: chequeo de SQL plan. - **Filtros & routing de cross-filter** — idéntico. - **Protocolo de paginación** — idéntico; las características de costo difieren. - **Agregaciones / features de dialecto** — delegado enteramente a Malloy; sin ramificaciones del lado de Looky. ---PAGE--- --- title: Diferencias entre adapters de source slug: docs/reference/source-adapters language: es description: "Referencia de divergencias de adapter: cada lugar donde BigQuery, Postgres y MySQL se comportan distinto en Looky — sources, parameter binding, NULL casting, dryRun, introspección y caching de conexión." last_modified: "2026-06-11T14:17:54.219000+00:00" docs_section: reference docs_summary: Divergencias de adapter en sources, parámetros, dialecto y paginación. --- ## Por qué existe esta página Looky soporta tres adapters de source: BigQuery, Postgres y MySQL. La mayor parte de la plataforma se comporta idéntica en los tres. Un puñado de áreas se comportan distinto — esas son las que tropiezan a autores de model y dashboard. Esta página lista cada una en términos observables (lo que ves, no cómo está implementado). ## Declaración de source #### BigQuery - Requeridos: `type: bigquery`, `project_id` (el proyecto de GCP que paga las queries), `credentials_file` (filename plano del JSON de service account dentro de `secrets/` — sin path). - Opcionales: `location` para datasets multi-region, `datasets` (requeridos en tiempo de introspección de schema). #### Postgres - Requeridos: `type: postgres`, `dsn` (URI libpq sin `user:password@`; lleva host, port, database, y cualquier flag de query-string libpq como `sslmode=require`, `application_name`, `connect_timeout`, …), `credentials_file` (filename de un JSON en `secrets/` con `{"user", "password"}`). - Opcionales: `name` (label cosmético), `schemas` (limita el scope de introspección; default es todos los schemas no-sistema). #### MySQL - Requeridos: `type: mysql`, `dsn` (URI `mysql://host:port/database` sin `user:password@` **y sin parámetros `?query`** — el port default a `3306`), `credentials_file` (filename de un JSON en `secrets/` con `{"user", "password"}`). - Opcionales: `name` (label cosmético), `schemas` (databases a introspeccionar; default es la database nombrada en el `dsn`). - Nota: las conexiones MySQL **no van encriptadas** — todavía no hay opción TLS, así que usá MySQL sobre una red de confianza. Mirá [Sources](/docs/build-workflow/sources) para ejemplos funcionando por adapter. ## Introspección de schema - **BigQuery** — requiere una lista explícita de datasets. Sin eso, la introspección no puede encontrar tablas. - **Postgres** — descubre tablas entre los schemas en tu lista `schemas` (o el smart default de cada schema no-sistema). No hace falta declaración de table-list. - **MySQL** — descubre tablas en la database nombrada en el `dsn` (o las databases en tu lista `schemas`). Las databases de sistema (`mysql`, `performance_schema`, `sys`, `information_schema`) quedan siempre excluidas. No hace falta declaración de table-list. Los mensajes de error en fallas de introspección se ven completamente distintos por adapter — cuando estés en duda, corré primero una pequeña query de test para confirmar que Looky puede llegar al source. ## Parameter binding #### Parámetros string, numéricos y array Se comportan idéntico en los tres adapters. No hace falta manejo especial. #### Parámetros date y timestamp - **BigQuery** — bindean nativos, sin trabajo extra. - **Postgres y MySQL** — el binding nativo falla con un error explícito en algunos shapes de query. Agregá un placeholder `@param` en el SQL subyacente de ese source así Looky sustituye el valor dentro del string SQL. Mirá [Soporte de Malloy](/docs/build-workflow/malloy-support) para un ejemplo trabajado. Tipos de filtro que producen parámetros date / timestamp: [cutoff_date](/docs/build-workflow/filters/cutoff-date), [date_range](/docs/build-workflow/filters/date-range), [date_range_preset](/docs/build-workflow/filters/date-range-preset). Planeá para el patrón `@param` cuando esos filtros manejen un model con backing en Postgres o MySQL. #### Valores NULL de parámetro Cada adapter escribe un null tipado distinto: BigQuery y MySQL requieren un cast explícito, Postgres usa un null tipado pelado. Looky maneja esto por vos — no hay nada que configurar — pero es la razón por la que un parámetro nullable malformado puede fallar en un adapter y silenciosamente funcionar en otro. #### Valores boolean (solo MySQL) MySQL no tiene un tipo boolean real — las columnas que parecen boolean vuelven como números (`0`/`1`). Para filtrar sobre una condición true/false, agregá un `cast(… as boolean)` explícito en el model. BigQuery y Postgres tienen booleans nativos y no necesitan ese cast. ## Ejecución de queries - **BigQuery** — expone una estimación de costo pre-flight gratis (en bytes escaneados) antes del run real. Útil para planear contra queries scan-heavy. - **Postgres y MySQL** — pre-flight valida el SQL plan (vía `EXPLAIN`) pero no estima costo. ## Filtros y cross-filtering El resolver de filtros y el mecanismo de pill de cross-filter son agnósticos del adapter. Las diferencias solo aparecen en el paso de parameter-binding descrito arriba. - Un filtro [select](/docs/build-workflow/filters/select) se comporta idéntico en los tres. - Los filtros de fecha aterrizan en la caveat de parámetro temporal para Postgres y MySQL — arreglá del lado del model con `@param`. - Los valores de pill de [Cross-filtering](/docs/build-workflow/cross-filtering) son típicamente strings o identifiers cortos, así que no se afectan. ## Paginación (grid & report_matrix) La paginación server-side usa el mismo protocolo en los tres adapters. Las características de costo difieren: - **BigQuery** — cada página sin cache es un scan facturado. Page size más grande + caching es más barato en total. Evitá grids sin bound en queries sin cache. - **Postgres y MySQL** — la latencia roundtrip de conexión domina; el costo depende mayormente del performance del indexed-scan de las tablas subyacentes. Asegurate que las columnas referenciadas en `order by` tengan índices apropiados. ## Recomendaciones de cache TTL La lógica del cache en sí es la misma en los tres adapters. La elección de TTL es editorial: - **BigQuery** — favorecé TTLs más largos para queries que escanean tablas grandes. Un TTL de 30 minutos en un dataset de batch diario es overkill; un TTL de 24 horas usualmente está bien. - **Postgres y MySQL** — el TTL es mayormente sobre freshness en vez de costo. El caching ayuda menos si la tabla subyacente es chica y bien indexada; considerá si el costo ahorrado supera el staleness introducido. ## Agregaciones y features de dialecto Looky no se ramifica en adapter para funciones agregadas o windowing. Cualquier cosa específica de dialecto (funciones ARRAY solo de BigQuery, `date_trunc` de Postgres en ciertos tipos, funciones de fecha solo de MySQL, etc.) aparece como un error de compilación de Malloy. El fix es del lado de Malloy — ajustá el model para usar un feature que todos los adapters de destino soportan, o protegé el model contra el adapter equivocado. ## Patrones de migración trabajados #### Un filtro de fecha que funciona en BigQuery pero rompe en Postgres o MySQL El model con binding nativo: ``` # funciona en BigQuery, frágil en Postgres / MySQL en algunos shapes ##! experimental.parameters source: orders( p_date_from::date is @2024-01-01, p_date_to::date is @2024-12-31 ) is bigquery.table('...') extend { view: revenue is { where: created_at::date >= p_date_from and created_at::date <= p_date_to aggregate: revenue is sum(sale_price) } } ``` Cambiá el source a `sql()` con placeholders para compatibilidad con Postgres / MySQL (este ejemplo usa sintaxis Postgres; en MySQL usá el equivalente `mysql.sql("""…""")` con casts del dialecto MySQL): ``` # funciona en Postgres (y BigQuery) ##! experimental.parameters source: orders( p_date_from::date is null, p_date_to::date is null ) is postgres.sql(""" select * from orders where (@date_from::date is null or created_at::date >= @date_from::date) and (@date_to::date is null or created_at::date <= @date_to::date) """) extend { view: revenue is { aggregate: revenue is sum(sale_price) } } ``` Cada placeholder `@param` tiene que tener una declaración matcheante en la signature del source; Looky sustituye el valor (o `NULL` tipado cuando está unset) antes del compile. #### Un grid que pagina bien en Postgres / MySQL pero hace explotar el scan budget de BigQuery Subí `pagination.page_size`, agregá un cache sidecar con TTL largo, y pre-agregá cuando sea posible. En BigQuery, paginar a través de millones de filas sin cache es caro — pre-resumí. ## Quick reference - **Auth de source** — BigQuery: archivo de service account. Postgres / MySQL: connection string + un secret `{"user","password"}`. - **Transporte** — BigQuery / Postgres: TLS disponible. MySQL: todavía sin encriptar. - **Introspección** — BigQuery: datasets explícitos requeridos. Postgres: schemas smart-default. MySQL: la database del DSN (o una lista `schemas`). - **Parámetros numéricos / string** — idéntico. - **Parámetros date / timestamp** — BigQuery: nativo. Postgres / MySQL: necesitan placeholder `@param` en SQL. - **Booleans** — nativos en BigQuery / Postgres. MySQL: numéricos — casteá explícitamente. - **Parámetros NULL** — manejados automáticamente; sin configuración. - **Pre-flight** — BigQuery: estimación de costo gratis. Postgres / MySQL: chequeo de SQL plan. - **Filtros & routing de cross-filter** — idéntico. - **Protocolo de paginación** — idéntico; las características de costo difieren. - **Agregaciones / features de dialecto** — delegado enteramente a Malloy; sin ramificaciones del lado de Looky.