TanStack
Mark Reference

Waffle Marks

waffleY and waffleX divide nonnegative source values into equal visual units. They pack directly inside the final chart bounds, so no positional scales or application-owned cell expansion are required.

ts
import { waffleX, waffleY } from '@tanstack/charts/waffle'

const mark = waffleY(rows, {
  y: 'share',
  color: 'category',
  unit: 0.01,
  round: true,
  gap: 2,
  radius: 2,
})

Both marks are also exported from @tanstack/charts and @tanstack/charts/universal.

Orientation

waffleY treats y as each source row's contribution. Units advance left-to-right, then bottom-to-top. waffleX transposes the contract: x contains the contribution and units advance bottom-to-top, then left-to-right.

ts
function waffleY<TDatum>(
  source: Iterable<TDatum>,
  options: WaffleYOptions<TDatum>,
): ChartMark<TDatum, ChartKey, number, never, never>

function waffleX<TDatum>(
  source: Iterable<TDatum>,
  options: WaffleXOptions<TDatum>,
): ChartMark<TDatum, number, ChartKey, never, never>

Options

WaffleOptions<TDatum> contains the shared identity, unit, paint, state, and motion fields. WaffleYOptions<TDatum> adds y and columns; WaffleXOptions<TDatum> adds x and rows.

OptionTypeDefaultMeaning
yChannel<TDatum, number?>Required by YContribution encoded by waffleY
xChannel<TDatum, number?>Required by XContribution encoded by waffleX
unitnumber1Semantic value represented by one complete cell
roundbooleanfalseRound cumulative unit boundaries before allocating cells
columnsnumberResponsiveFixed cells per row for waffleY
rowsnumberResponsiveFixed cells per column for waffleX
gapnumber1Empty pixels between complete cells
radiusnumberNoneCorner radius for complete cells
idstringLayer-derivedStable mark ID
zChannel<TDatum, ChartKey?>No groupInteraction group; color fallback when omitted
colorChannel<TDatum, ChartKey?>zValue sent to the chart color scale
keyChannel<TDatum, ChartKey>InferredStable source-row identity
fill, strokeVisualChannel<TDatum, string>Resolved colorPer-row paint overrides
fillOpacity, strokeOpacity, and strokeWidthnumberRenderer defaultCell presentation
statesreadonly ChartMarkState[]NoneFocus-driven rectangle styles applied to every source tile
motionChartMarkMotionOptions<TDatum>['motion']NonePer-tile motion policy

unit must be positive and finite. Contributions must be nonnegative and finite; nullish and nonfinite channel values are omitted. Fixed columns or rows must be positive integers, and gap must be nonnegative.

Unit boundaries

The mark allocates each row against cumulative values. With unit: 0.01, a complete cell represents one percentage point. With round: true, cumulative boundaries are rounded, so the complete allocation preserves the rounded total without independently rounding every category.

When round is false, a category boundary may divide one cell. Each category receives its exact fractional rectangle and the adjacent fragments meet without an added gap. radius applies only to complete cells.

Responsive packing

Without columns or rows, the mark chooses a square-cell grid from the final plot bounds after margins and legends resolve. It may change the number of rows or columns when the chart resizes while preserving source order and keys. Set columns on waffleY or rows on waffleX when the grid count is part of the chart's meaning, such as a fixed ten-by-ten percentage display.

Source identity and interaction

Cell expansion is internal. Each visible source row contributes one interaction point, and every complete or fractional tile for that row references the same original datum and datum index. The quantitative point value remains the row's contribution; cumulative start and end values are exposed as its interval.

Color-domain inference uses the original source rows, including a category that rounds to zero visible cells. This keeps an explicitly meaningful category in a legend without manufacturing a rendered interaction point.