TanStack
Mark Reference

Dot and Hexagon Marks

dot and hexagon place fixed-pixel symbols at scaled x/y values. Their radius does not change when a positional scale zooms or a chart resizes unless the application changes r or rScale.

ts
import { scaleSqrt } from 'd3-scale'
import { dot, hexagon } from '@tanstack/charts'

dot

ts
const mark = dot(rows, {
  x: 'date',
  y: 'value',
  z: 'series',
  r: 'population',
  rScale: radiusScale,
  key: 'id',
})
ts
function dot<TDatum>(
  source: Iterable<TDatum>,
  options?: DotOptions<TDatum>,
): ChartMark<TDatum, InferredX, InferredY>

Options

OptionTypeDefaultMeaning
idstringLayer-derivedStable mark ID
xChannel<TDatum, ChartValue?>Row indexHorizontal value
yChannel<TDatum, ChartValue?>Numeric datumVertical value
zChannel<TDatum, ChartKey?>No groupInteraction group and default fill color
keyChannel<TDatum, ChartKey>Unique id, then indexStable scene and interaction identity
rnumber | Channel<TDatum, number?>3.5Raw radius value
rScale(value: number) => numberIdentityMaps each valid raw radius to pixels
fillstringResolved z colorConstant fill
fillOpacitynumberSVG defaultFill opacity
strokestringNoneConstant stroke
strokeOpacitynumberSVG defaultStroke opacity
strokeWidthnumberSVG defaultStroke width

rScale is called only for finite, nonnegative raw radii. The mapped result must also be finite and nonnegative or the row is skipped.

Unlike hexagon, dot.fill and dot.stroke are constants. Use z and the chart color scale for data-driven dot color, or split data into multiple marks for independently styled layers.

hexagon

hexagon draws a pointy-topped six-sided symbol.

ts
const mark = hexagon(bins, {
  x: 'x',
  y: 'y',
  z: 'densityBand',
  r: 'count',
  rScale: radiusScale,
  fill: (bin) => densityColor(bin.count),
  key: 'id',
})
ts
function hexagon<TDatum>(
  source: Iterable<TDatum>,
  options?: HexagonOptions<TDatum>,
): ChartMark<TDatum, InferredX, InferredY>

Options

OptionTypeDefaultMeaning
idstringLayer-derivedStable mark ID
xChannel<TDatum, ChartValue?>Row indexHorizontal center
yChannel<TDatum, ChartValue?>Numeric datumVertical center
zChannel<TDatum, ChartKey?>No groupInteraction group and default paint
keyChannel<TDatum, ChartKey>Unique id, then indexStable identity
rnumber | Channel<TDatum, number?>6Raw circumradius
rScale(value: number) => numberIdentityMaps radius values to pixels
fillVisualChannel<TDatum, string>Resolved z colorFill per mark or row
fillOpacitynumberSVG defaultFill opacity
strokeVisualChannel<TDatum, string>NoneOptional stroke per mark or row
strokeOpacitynumberSVG defaultStroke opacity
strokeWidthnumberSVG defaultStroke width

The generated vertices begin at the top and proceed in 60-degree increments. The interaction point remains at the scaled center, and its color is the resolved fill.

Valid rows and points

Both marks skip a row when x or y is not a valid ChartValue, or when the final radius is negative, nonfinite, null, or undefined. A zero radius remains a valid interaction point even though it has no visible area.

Every valid row emits one ChartPoint with:

  • the original datum and row index
  • semantic channel values in xValue and yValue
  • scaled center coordinates in x and y
  • z as its group
  • fill paint as its interaction color

Radius scales

Pass a numeric scale factory to infer [0, maximum] from the radius channel. Configure its semantic pixel range inside the factory:

ts
const rScale = {
  scale: () => scaleSqrt().range([2, 18]),
}

An ordinary numeric mapper or configured scale instance remains valid when the application owns the complete mapping. The shared integration boundary is documented in Scales and D3.

Radius does not contribute to x/y guide margins. Add an explicit partial margin when large edge symbols must remain fully inside the SVG viewport.