TanStack
Mark Reference

Bar and Rect Marks

Bar marks encode a numeric interval against a categorical or positional channel. Rect marks encode independent x and y intervals and are the general primitive for heatmaps, interval blocks, and cells.

ts
import { barX, barY, cell, group, rect, stack } from '@tanstack/charts'
import { scaleBand } from '@tanstack/charts-scales/band'

barY

barY draws vertical bars from y1 to y2 at x.

ts
const mark = barY(rows, {
  x: 'category',
  y: 'value',
})
ts
function barY<TDatum>(
  source: Iterable<TDatum>,
  options?: BarYOptions<TDatum>,
): ChartMark<TDatum, InferredX, number>

Options

OptionTypeDefaultMeaning
idstringLayer-derivedStable mark ID
xChannel<TDatum, ChartValue?>Row indexBar category or center
yChannel<TDatum, number?>Numeric datumLength; implicitly stacked at each x
y1number | Channel<TDatum, number?>Implicit stack startExplicit baseline endpoint
y2number | Channel<TDatum, number?>Implicit stack endExplicit value endpoint; takes precedence over y
zChannel<TDatum, ChartKey?>No groupGroup identity; color fallback when omitted
colorChannel<TDatum, ChartKey?>zIndependent value sent to the chart color scale
keyChannel<TDatum, ChartKey>Top/nested id, x, then indexStable scene and interaction identity
fillVisualChannel<TDatum, string>Resolved colorFinal bar paint override
fillOpacitynumberSVG defaultFill opacity
layoutGroupLayout | StackLayoutImplicit diverging stackConfigures grouping or stack order/offset
insetnumber0Pixels removed from both categorical edges
maxThicknessnumberUnboundedMaximum painted width after grouping and inset
radiusnumberNoneSVG rectangle corner radius
statesreadonly ChartMarkState[]NoneFocus-driven presentation overrides

The interaction point is at the group-band center and the y2/y endpoint. Its semantic xValue is x and its yValue is the value endpoint.

barX

barX draws horizontal bars from x1 to x2 at y.

ts
const mark = barX(rows, {
  x: 'value',
  y: 'category',
})
ts
function barX<TDatum>(
  source: Iterable<TDatum>,
  options?: BarXOptions<TDatum>,
): ChartMark<TDatum, number, InferredY>

Its options transpose barY:

OptionTypeDefaultMeaning
idstringLayer-derivedStable mark ID
xChannel<TDatum, number?>Numeric datumLength; implicitly stacked at each y
x1number | Channel<TDatum, number?>Implicit stack startExplicit baseline endpoint
x2number | Channel<TDatum, number?>Implicit stack endExplicit value endpoint; takes precedence
yChannel<TDatum, ChartValue?>Row indexBar category or center
zChannel<TDatum, ChartKey?>No groupGroup identity; color fallback when omitted
colorChannel<TDatum, ChartKey?>zIndependent color-scale value
keyChannel<TDatum, ChartKey>Top/nested id, y, then indexStable identity
fillVisualChannel<TDatum, string>Resolved colorFinal bar paint override
fillOpacitynumberSVG defaultFill opacity
layoutGroupLayout | StackLayoutImplicit diverging stackConfigures grouping or stack order/offset
insetnumber0Pixels removed from both categorical edges
maxThicknessnumberUnboundedMaximum painted height after grouping/inset
radiusnumberNoneCorner radius
statesreadonly ChartMarkState[]NoneFocus-driven presentation overrides

The interaction point is at the x2/x endpoint and group-band center.

Bar bandwidth

With a band scale on the categorical axis, bars use its responsive bandwidth. With a nonband scale, the mark estimates width from the smallest distance between distinct mapped positions and uses 80 percent of that distance. A single-position fallback is capped at 48 pixels.

For predictable categorical bars, use a configured band scale and set its padding. Scale setup and ownership are documented in Scales.

inset is applied after band or inferred layout and is clamped to at least zero. A sufficiently large inset produces a zero-width or zero-height bar rather than negative geometry.

maxThickness caps the painted width for barY or height for barX in final chart pixels. The mark applies the cap after grouped-band layout and inset, then centers the narrower bar in its resolved band. Narrow responsive bands keep their natural size. Negative finite values clamp to zero; nonfinite values do not cap the bar. Inline-state inset overrides remain absolute, but their resolved geometry still honors the cap.

Grouped bars

Grouping is explicit:

ts
barY(rows, {
  x: 'quarter',
  y: 'revenue',
  color: 'region',
  layout: group(),
})

group() creates a secondary band scale inside the primary categorical band. Use group({ padding: 0.2 }) for the common spacing control, or pass group({ scale }) when subgroup order is fixed application state. GroupOptions is the reusable configuration shape for those padding and scale controls.

An explicit z supplies subgroup identity. If z is omitted, a discrete color channel may supply identity after grouped geometry has been selected. When both are present, z controls placement and interaction grouping while color remains independent. A continuous color channel cannot infer series.

The mark throws when:

  • the scale has no bandwidth method
  • a grouped row has a null effective group value
  • a group value is outside the group-scale domain or maps to a nonfinite position

Repeated positions stack by default. layout: group() is the explicit opt-in to side-by-side geometry.

Stacked bars

The single value channel is a length. Repeated categorical positions stack automatically, with positive and negative values diverging from zero:

ts
barY(rows, {
  x: 'quarter',
  y: 'revenue',
  color: 'region',
})

Use z when series identity differs from color. Add layout: stack() only when the default stack needs a configured order or offset:

ts
barY(rows, {
  x: 'quarter',
  y: 'revenue',
  color: 'segment',
  layout: stack({
    order: ['Core', 'Services'],
    offset: 'normalize',
  }),
})

order accepts input order, ascending or descending absolute totals, inside-out streamgraph order, or an explicit series list. reverse reverses the resolved order. offset accepts diverging (default), normalize, center, or wiggle.

ts
type StackOrder =
  'input' | 'ascending' | 'descending' | 'inside-out' | readonly ChartKey[]
type StackOffset = 'diverging' | 'normalize' | 'center' | 'wiggle'

interface StackAnchor {
  series: ChartKey
  fraction?: number
}

interface StackOptions {
  order?: StackOrder
  offset?: StackOffset
  reverse?: boolean
  anchor?: StackAnchor
}

interface StackLayout extends StackOptions {
  readonly type: 'stack'
}

inside-out uses each series peak and total to balance layers above and below the stream. wiggle is intended for nonnegative streamgraph values. After the wiggle offset is complete, Charts translates the whole stack so its global minimum start is zero. It does not independently rebase each position.

Use anchor when ordered, nonnegative category counts should diverge around a point inside one series. This Likert layout places the midpoint of Neutral at zero:

ts
barX(counts, {
  x: 'count',
  y: 'question',
  z: 'response',
  color: 'response',
  layout: stack({
    order: ['Strongly Disagree', 'Disagree', 'Neutral', 'Agree'],
    anchor: { series: 'Neutral', fraction: 0.5 },
  }),
})

fraction defaults to 0.5; zero selects the series start and one selects its end. Anchor layout zero-imputes missing position/series cells before translating the completed stack, but it emits no synthetic rows. The anchor series may be absent at a position. It must still appear in the resolved series order, which an explicit order can establish. Anchors reject negative lengths and cannot be combined with normalize, center, or wiggle offsets.

Positions and series retain first-seen order. Missing position/series pairs contribute zero to layout without creating synthetic rows or interaction points.

Supplying y1 or y2 opts out of implicit stacking and treats the channels as authored endpoints. The same contract is transposed for barX.

Bar baselines and invalid rows

Implicit stacks include zero in inferred quantitative domains. Explicit endpoints contribute their authored bounds. The configured scale still owns its semantic domain.

Rows are skipped when their category, baseline, or endpoint is invalid. Negative and reversed intervals are supported because geometry uses the minimum mapped endpoint and absolute length.

bandX and bandY

bandX paints the complete plot height at each x value. bandY paints the complete plot width at each y value. They are ordinary marks used for categorical backgrounds and focus presentation:

ts
whenFocused(
  bandX(rows, {
    x: 'category',
    fill: '#64748b',
    fillOpacity: 0.14,
    inset: -6,
  }),
  { match: 'x' },
)

BandXOptions and BandYOptions provide the positional channel, z, color, key, fill, fillOpacity, inset, and radius. bandX.width and bandY.height can replace scale or inferred bandwidth with an explicit nonnegative scene-pixel size, which is useful for a one-pixel focus cursor. A negative inset expands the resolved band. The helpers are also available from @tanstack/charts/band.

rect

rect draws one independent x/y interval per valid row:

ts
const mark = rect(events, {
  x1: 'start',
  x2: 'end',
  y: 'lane',
  z: 'status',
})
ts
function rect<TDatum>(
  source: Iterable<TDatum>,
  options: RectOptions<TDatum>,
): ChartMark<
  TDatum,
  InferredPointX,
  InferredPointY,
  InferredScaleX,
  InferredScaleY
>

Options

OptionTypeDefaultMeaning
idstringLayer-derivedStable mark ID
xChannel<TDatum, ChartValue?>Row indexX center/category and preferred semantic focus value
x1Channel<TDatum, ChartValue?>x, or row index when x is absentFirst x endpoint
x2Channel<TDatum, ChartValue?>xSecond x endpoint
yChannel<TDatum, ChartValue?>Numeric datumY center/category and preferred semantic focus value
y1Channel<TDatum, ChartValue?>yFirst y endpoint
y2Channel<TDatum, ChartValue?>ySecond y endpoint
zChannel<TDatum, ChartKey?>No groupInteraction group
colorChannel<TDatum, ChartKey?>zValue sent to the chart color scale
keyChannel<TDatum, ChartKey>Top/nested id, x/y tuple, then indexStable identity
fillstringResolved colorFinal constant fill override
fillOpacitynumberSVG defaultFill opacity
strokestringNoneConstant stroke
strokeWidthnumberSVG defaultStroke width
insetnumber0.75Pixels removed from all four edges
radiusnumberNoneCorner radius
statesreadonly ChartMarkState[]NoneFocus-driven presentation overrides

Both endpoints must be valid chart values. Endpoint order may be reversed.

When two semantic endpoints are equal and the resolved scale has bandwidth, the rect spans that complete band. Otherwise it spans the mapped endpoint distance. This lets x: 'column', y: 'row' create a heatmap cell without manually deriving boundaries.

The interaction coordinate is the geometric center before inset. Semantic point values use a valid x and y. An omitted x defaults to the row index; an invalid x falls back to x2. An omitted or invalid y falls back to y2 unless the datum itself is numeric. Scale typing still includes all interval endpoints, so a heterogeneous interval remains honest without widening interaction callbacks unnecessarily.

cell

cell is rect without explicit endpoint options:

ts
const mark = cell(rows, {
  x: 'weekday',
  y: 'week',
  z: 'bucket',
  fillOpacity: 0.9,
})
ts
type CellOptions<TDatum> = Omit<RectOptions<TDatum>, 'x1' | 'x2' | 'y1' | 'y2'>

Both axes normally use band scales. cell shares rect rendering, defaults, focus behavior, and class names.