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, defaulttrue. Set tofalseto suppress click-to-filter for this grid.
Pagination
Pagination options live under the top-level pagination block:
pagination.page_size— integer. Rows per page. Default25. 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. Default8. 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
emphasisblock 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.columnsor set explicit narrower widths ingrid.column_widths. - Column widths do not stick. Make sure the column names in
grid.column_widthsmatch 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.labelsto 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.