Docs / Build Workflow

Visualization — grid (alias: table)

When to use grid

Grid is the right choice when the audience needs to read individual records, export data, or verify the detail behind a summary. Type identifiers grid and table map to the same renderer; either is accepted.

Use a chart-style viz (bar, line) when the question is about a comparison or a trend rather than the raw rows. Use report_matrix when the records need grouping with subtotals or are designed for PDF export.

Mapping

Mapping for grid is minimal — the columns come from the query result.

  • mapping.columns — optional. Array of column names. The subset to display, in the order given. Use this to drop noisy columns or to force a specific column order without changing the query. When omitted, every column in the query result is rendered, in query order.
mapping:
  columns:
    - order_date
    - category
    - brand
    - country
    - status
    - revenue

grid block

Grid options live under the top-level grid block (alias table is also accepted). Grid does not have a chart block.

Column display

  • grid.column_widths — object mapping column name to a fixed width ("120px"), proportional ("25%"), or numeric (pixel) value.
  • grid.frozen_columns — number of leftmost columns to freeze, or an array of column names. Useful when the grid scrolls horizontally and the audience needs the row identifier always visible.
  • grid.nowrap_columns — array of column names that should never wrap; overflow shows ellipsis.
  • grid.labels — object mapping column name to display label (overrides the raw column name in the header).

Composite cells

grid.composite_columns renders one cell as multiple lines drawn from other fields:

grid:
  composite_columns:
    customer:
      lines:
        - field: name
          class: font-semibold
        - field: city
          prefix: "📍 "
          show_empty: false

Comparison cells

  • grid.comparison_columns — array of column names to render as up / down / dash trend indicator next to the value. Useful for delta columns where the audience needs the direction at a glance.

Formats

  • grid.column_formats — object mapping column name to a format key.
  • grid.formats — object mapping format key to a pattern. Two-step indirection lets you reuse the same pattern across many columns.

Cross-filter

  • grid.cross_filter — boolean, default true. Set to false to suppress click-to-filter for this grid.

Pagination

Pagination options live under the top-level pagination block:

  • pagination.page_size — integer. Rows per page. Default 25. Pick larger when the audience does data-export work; smaller when scanning is the typical use.
  • pagination.column_page_size — integer. When the visible columns exceed this, a horizontal column-pager kicks in. Default 8. Aliases: columns_per_page, columns_page_size.

Pagination is server-side: changing pages re-runs the underlying query with the new page parameters. Cost characteristics differ by adapter — see Source adapter differences.

format

The grid.column_formats + grid.formats pair is the primary way to format columns. The root format field acts as a fallback for any column not covered. Use the indirection pattern when the same number style applies to many columns:

grid:
  column_formats:
    revenue: currency
    avg_order_value: currency
    refunds: currency
    item_count: integer
  formats:
    currency: "$#,##0.00"
    integer: "#,##0"

Cross-filter behavior

  • Clicking a cell (when the column is configured as clickable) cross-filters the rest of the dashboard by the column name and clicked value.
  • The clicked field must be declared as a parameter in at least one model used by the dashboard, or the click is silently ignored.
  • The top-level emphasis block can declaratively highlight a row matching a related cross-filter value.
  • Disable per viz with chart.cross_filter: false.

See Cross-filtering for the full mechanism.

Worked examples

Order detail with frozen first column, currency formats, and a comparison column:

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

Customer roster with composite cells:

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

Common pitfalls

  • The grid takes up too much horizontal space. Drop columns from mapping.columns or set explicit narrower widths in grid.column_widths.
  • Column widths do not stick. Make sure the column names in grid.column_widths match the field names in the query result exactly.
  • Sorting is per-page only. Server-side pagination means client-side sorts only see one page. To sort across all rows, sort in the Malloy query.
  • The header label is wrong. Set grid.labels to override the raw column name (or rename in the Malloy query).
  • The comparison indicator points the wrong way. The renderer derives direction from the sign of the value. Encode "good" deltas with positive sign and "bad" with negative.
  • Cross-filter clicks have no effect. The clicked column must be declared as a parameter in at least one model used by the dashboard.
  • Grid expected to group rows with subtotals. Grid is flat; use report_matrix for grouping.