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,
})
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
colorChannel<TDatum, ChartKey?>zValue sent to the chart color scale
keyChannel<TDatum, ChartKey>ID, x, y, x/y, indexStable scene and interaction identity
rnumber | Channel<TDatum, number?>3.5Raw radius value
rScale(value: number) => numberIdentityMaps each valid raw radius to pixels
fillstringResolved colorFinal constant fill override
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 color and the chart color scale for data-driven dot color.

Without an explicit key, dot tries a unique top-level or nested data.id, then x, y, and the x/y tuple. Supply key when positions can change while the same entity should reconcile across updates.

hexagon

hexagon draws a pointy-topped six-sided symbol.

ts
const mark = hexagon(bins, {
  x: 'x',
  y: 'y',
  color: 'count',
  r: 'count',
  rScale: radiusScale,
})
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
colorChannel<TDatum, ChartKey?>zValue sent to the chart color scale
keyChannel<TDatum, ChartKey>Top/nested id, indexStable identity
rnumber | Channel<TDatum, number?>6Raw circumradius
rScale(value: number) => numberIdentityMaps radius values to pixels
fillVisualChannel<TDatum, string>Resolved colorFinal fill override
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.