Skip to content

Board Sizing

Dataface uses a three-layer sizing model. Understanding which layer owns what prevents most sizing surprises.


The Three Layers

Layer Where What it controls
Layout tile rows: / cols: / grid: entry The slot the chart renders into
Chart root charts.<name>.width / height Preferred width and fixed height requests from the chart itself
Chart style charts.<name>.style.aspect_ratio Aspect-ratio-derived height for this chart
Theme default style.charts.aspect_ratio in the face/theme cascade Global fallback

Priority

  1. An explicit height: on a layout tile always wins.
  2. If the tile has no explicit height, chart-root height: wins.
  3. Chart-style aspect_ratio: is used next (computes height = width / aspect_ratio).
  4. The theme default style.charts.aspect_ratio is the final fallback.

The layout owns each chart's final width. Chart-root width: contributes to the dashboard's intrinsic width, but does not force the final slot width.


Default Behavior

Width Rule

Dataface measures the layout's preferred width recursively:

  • Rows and tabs use their widest child.
  • Columns add their child widths and gaps.
  • Grids derive track widths from each item's span.
  • Nested dashboards repeat the same calculation.

The rendered dashboard shrinks to that intrinsic width and never exceeds style.board.width, which acts as the maximum. A dashboard with no charts keeps the maximum width.

That intrinsic width sets the board's proportions, not its final on-screen size. Viewed as an HTML page — dft serve, dft render --format html — the board scales to fill the window width and keeps its aspect ratio, so these measurements govern relative density (how much room each chart gets, how large type reads against its plot) rather than pixels in the browser.

After that outer width is chosen, existing layout allocation rules still apply. For example, items in a columns layout split the available width evenly: - In a cols layout with 2 items, each gets 50% - In a cols layout with 3 items, each gets 33.3%

Height Rule

Heights are derived from width ÷ aspect ratio for plot-style charts (bar, line, area, etc.). The global default aspect ratio is 1.5 (3:2), set in the base theme under style.charts.aspect_ratio. Override it in any face via style.charts.aspect_ratio or at the individual chart level with aspect_ratio:. Some chart types override this — for example, pie and arc default to 1.0 (square), and map types default to 1.2.

Exceptions — chart-root height is ignored for these types:

  • KPI — fixed compact height (renderer owns sizing)
  • Table — data-driven height based on actual row count
  • Callout — fixed compact height
  • Spark bar — fixed height

The aspect-ratio-derived height is clamped between min_height and max_height. The face/theme defaults are style.charts.min_height (150px) and style.charts.max_height (400px). Individual charts can override these clamps with per-chart style: {min_height: ..., max_height: ...} — see Chart height clamps below.

In a cols layout, all items share the same height (the tallest item's height).


Unequal Widths with Nesting

Use nested lists as a shorthand for nested boards. A nested list automatically inherits the layout type (rows or cols) of its parent.

50% / 25% / 25% Split

To make the first chart take up half the width (50%) and the next two share the remaining half (25% each), nest them in a list:

source: examples_db
title: "Asymmetric Layout"

queries:
  sales_by_category:
    sql: |
      SELECT category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC
  sales_summary:
    sql: |
      SELECT SUM(revenue) AS revenue, SUM(units_sold) AS units_sold, COUNT(DISTINCT category) AS category_count FROM ecommerce_orders

charts:
  main_chart:
    query: sales_by_category
    type: bar
    title: "Main Chart (50%)"
    x: category
    y: revenue
  kpi1:
    query: sales_summary
    type: kpi
    label: "Side Chart 1 (25%)"
    value: revenue
  kpi2:
    query: sales_summary
    type: kpi
    label: "Side Chart 2 (25%)"
    value: units_sold

# 50% / 25% / 25% split using nested cols
cols:
  - main_chart
  - cols:
      - kpi1
      - kpi2
Asymmetric Layout 020k40k60k80k100k120k140k160k180k200kElectronicsAccessoriesToolsMain Chart (50%) 440k Side Chart 1 (25%) 15k Side Chart 2 (25%) Data as of 16:49 UTC on 5 Aug 2026 made with dataface

Complex Nesting

source: examples_db
title: "Complex Dashboard"

queries:
  sales_summary:
    sql: |
      SELECT SUM(revenue) AS revenue, SUM(units_sold) AS units_sold, COUNT(DISTINCT category) AS category_count FROM ecommerce_orders
  sales_by_category:
    sql: |
      SELECT category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC
  sales_by_product:
    sql: |
      SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
  sales_by_date_category:
    sql: |
      SELECT date, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date, category ORDER BY date, category

charts:
  kpi1:
    query: sales_summary
    type: kpi
    label: "Revenue"
    value: revenue
  kpi2:
    query: sales_summary
    type: kpi
    label: "Units Sold"
    value: units_sold
  mini1:
    query: sales_by_category
    type: bar
    title: "Mini Chart 1"
    x: category
    y: revenue
  mini2:
    query: sales_by_product
    type: bar
    title: "Mini Chart 2"
    x: product
    y: units_sold
  main:
    query: sales_by_date_category
    type: line
    title: "Main Analysis"
    x: date
    y: revenue
    color: category

rows:
  - cols:
      - kpi1
      - kpi2
      - cols:
          - mini1
          - mini2
  - main
Complex Dashboard 440k Revenue 15k Units Sold 0100k200kElectronicsAccessoriesToolsMini Chart 101k2kWidget BWidget CGadget YWidget AGadget XTool ZTool AGadget ZMini Chart 2 1 Jan20242 Jan3 Jan4 Jan5 Jan6 Jan7 Jan8 Jan9 Jan10 Jan11 Jan12 Jan13 Jan14 Jan15 Jan16 Jan17 Jan18 Jan19 Jan20 Jan21 Jan22 Jan23 Jan24 Jan25 Jan26 Jan27 Jan28 Jan29 Jan30 Jan05k10k15k20kMain AnalysisElectronicsAccessoriesTools Data as of 16:49 UTC on 5 Aug 2026 made with dataface

Authoring Chart Size

Chart root holds the two fixed-geometry fields:

  • height: <px> — exact pixel height. Wins over aspect_ratio and theme defaults. Bypasses min_height / max_height clamps.
  • width: <px> — preferred width used to measure the dashboard. Does not override the final layout slot width.

style: holds aspect-ratio sizing fields (and all paint — colors, fonts, marks):

  • style.aspect_ratio: <float> — shape without a fixed pixel size. height = width / aspect_ratio.
  • style.min_height: <px> — floor for the computed height.
  • style.max_height: <px> — ceiling for the computed height.

height and width belong at chart root, not in style: — putting them there raises a validation error.

Choosing between chart-root height and a layout tile height

# Chart-root height — the chart requests 400px;
# the layout honours it unless the tile has its own height.
charts:
  inventory_overview:
    query: inventory
    type: bar
    x: category
    y: count
    height: 400

# Layout tile height — the slot is fixed at 600px regardless of the chart inside.
rows:
  - height: 600
    rows:
      - inventory_overview

Use chart-root height when the chart itself has a natural size independent of its context (e.g. a detail chart that always needs 400px to be readable).

Use a layout tile height when you want the tile — and everything in that row — to be a fixed size (e.g. a KPI row that must be 120px tall).

Rejected shape

# REJECTED — height/width under style: is not supported
charts:
  bad:
    type: bar
    style:
      height: 400      # ← raises a validation error; move height to chart root

Explicit Sizing

Chart height

Set height on a chart to fix its pixel height explicitly. This bypasses aspect-ratio sizing and ignores min_height / max_height clamps.

charts:
  revenue:
    type: bar
    query: revenue_by_month
    x: month
    y: revenue
    height: 500   # fixed at 500px

Set aspect_ratio to control the shape without fixing an exact pixel size:

charts:
  revenue:
    type: bar
    query: revenue_by_month
    x: month
    y: revenue
    style:
      aspect_ratio: 2.5   # wider-than-default; height = width / 2.5

Chart height clamps

min_height and max_height cap the aspect-ratio-derived height. Face/theme defaults cascade from style.charts.min_height and style.charts.max_height. Override them per chart under style::

charts:
  stock_levels:
    type: bar
    query: stock_levels
    x: product_name
    y: stock_quantity
    style:
      aspect_ratio: 1.5
      min_height: 80    # floor for this chart only; overrides face/theme default
      max_height: 200   # ceiling for this chart only; overrides face/theme default

Explicit chart.height bypasses min_height / max_height — when you set an exact pixel height, the clamps do not apply.

Height on Rows

Set explicit height on row items:

rows:
  - height: "120px"
    cols:
      - kpi1
      - kpi2
      - kpi3

  - main_chart  # Takes remaining height

Grid Sizing

In grid layouts, use width and height on items:

grid:
  columns: 24
  items:
    - item: kpi1
      width: 8      # 8 of 24 columns
    - item: kpi2
      width: 8
    - item: main_chart
      width: 16
      height: 2     # 2 rows tall
    - item: sidebar
      width: 8
      height: 2

Sizing Patterns

KPI Row + Main Chart

rows:
  - height: "100px"
    cols: [kpi1, kpi2, kpi3]
  - main_chart
cols:
  - cols:           # 66% main content
      - chart1
      - chart2
  - sidebar_chart   # 33% sidebar
rows:
  - height: "80px"
    text: "# Dashboard Title"
  - main_content    # Flexible middle
  - height: "60px"
    text: "Footer text"

Configuration

Theme defaults (face/theme cascade)

These keys set the theme default layer — they apply to every chart that does not provide a chart-root override.

Key Default Description
style.charts.aspect_ratio 1.5 Global width:height ratio for plot-style charts
style.charts.min_height 150 Minimum chart height (px) when aspect-ratio drives sizing
style.charts.max_height 400 Maximum chart height (px) when aspect-ratio drives sizing
style.charts.<type>.aspect_ratio Per-type override (e.g. pie: 1.0, map: 1.2)

height and width cannot be set in the theme — they are per-chart explicit overrides only (see below).

Chart-root fields (per chart, under charts.<name>:)

Field Description
height Exact pixel height. Wins over aspect_ratio and theme. Bypasses min_height/max_height.
width Preferred width in pixels. Contributes to the dashboard's intrinsic width; the chart still fills its final layout slot.

Per-chart style fields (under charts.<name>.style: or in the theme under style.charts.*)

Field Description
aspect_ratio Shape without a fixed pixel size. height = width / aspect_ratio.
min_height Floor for this chart only; overrides theme style.charts.min_height.
max_height Ceiling for this chart only; overrides theme style.charts.max_height.

aspect_ratio, min_height, and max_height are valid both at chart level (style: {aspect_ratio: 2.0}) and in the theme (style.charts.aspect_ratio). height and width are chart-root-only — they are not valid under style:.


Card Gap

By default, cards (charts and content blocks) render edge-to-edge — no gap between them. Set card_gap: true at the board root to add spacing between every card on the board:

title: "Spaced-Out Dashboard"
card_gap: true

rows:
  - revenue
  - orders

The actual pixel value comes from the theme (style.board.card_gap), not from the board — card_gap is a per-board on/off switch, the theme owns how big the gap is. card_gap is root-only: setting it on a nested board raises a validation error, because spacing is a whole-board layout decision, not a per-section one.