TanStack
Mark Reference

Dodge Layouts

dodgeY preserves each dot's scaled x position and derives a collision-free y position in final plot pixels. dodgeX transposes the layout: y is preserved and x is derived.

ts
import { dodgeY } from '@tanstack/charts/dodge'
import { dot } from '@tanstack/charts/dot'

dot(rows, {
  x: 'value',
  key: 'id',
  r: 4,
  layout: dodgeY({
    anchor: 'middle',
    padding: 1,
  }),
})

The layouts and createDotLayout are also exported from @tanstack/charts and @tanstack/charts/universal.

Signatures

ts
function dodgeY(options?: {
  anchor?: 'top' | 'middle' | 'bottom'
  padding?: number
}): DodgeYLayout

function dodgeX(options?: {
  anchor?: 'left' | 'middle' | 'right'
  padding?: number
}): DodgeXLayout

function createDotLayout(options: {
  axis: 'x' | 'y'
  anchor: ChartValue
  resolve(context: {
    chart: ChartBounds
    measuredPositions: readonly number[]
    radii: readonly number[]
  }): readonly number[]
}): DotLayout

dodgeY defaults to bottom; dodgeX defaults to left. padding is the empty pixel distance between neighboring circle edges and defaults to 1. It must be finite and nonnegative.

The public type surface also includes CreateDotLayoutOptions, DotLayout, DotLayoutResolveContext, DodgeOptions, DodgeXAnchor, DodgeYAnchor, DodgeXOptions, and DodgeYOptions.

Custom layouts

Use createDotLayout when placement depends on final plot bounds but is not a dodge. The resolver receives the preserved channel's scaled pixel positions and the final radii in materialized valid-row order. Return one finite cross-axis pixel position per materialized row. dot validates the result and retains the authored anchor as the derived interaction-point value.

ts
const rowLayout = createDotLayout({
  axis: 'y',
  anchor: 'rows',
  resolve: ({ chart, measuredPositions, radii }) =>
    measuredPositions.map(
      (_position, index) => chart.y + radii[index] + index * 12,
    ),
})

Scale ownership

The measured channel remains an ordinary semantic chart value. A dodgeY dot contributes only x scale values, so its chart definition needs an x scale but no y scale. A dodgeX dot needs only a y scale.

ts
defineChart({
  marks: [
    dot(rows, {
      x: 'economy',
      layout: dodgeY({ anchor: 'middle' }),
    }),
  ],
  x: { scale: scaleLinear().domain([5, 50]) },
})

Do not configure the generated channel: y with dodgeY, or x with dodgeX. The generated interaction-point value is the logical anchor while its x and y fields contain the actual laid-out pixel center.

Collision and radius

Collision distance is the sum of both final dot radii and padding. Configure r and rScale once on dot; the layout receives the same resolved radii used for rendering.

Placement is synchronous and deterministic. Edge anchors choose the nearest valid inward position. Middle anchors choose the valid position with the smallest absolute displacement. Fixed-radius rows retain source order; variable-radius rows are placed largest first with source order as the stable tie-breaker.

Identity, facets, and grouping

Every rendered point retains its source datum, source index, key, group, state, and motion policy. Supply key when the measured channel contains duplicates.

Each facet resolves its child dodge layout against that cell's final scales and bounds. z and color affect ordinary dot grouping and paint; they do not create separate collision lanes. Use facets when groups need independent swarm bounds.