Skip to content

shadcn.io is an independent project built on shadcn/ui

Free

Hexbin chart

For x/y data too dense for a scatter plot. Hexagons are shaded by how many points they hold, or by the sum or mean of a field.

Charting library: TanStack Charts

Preview

Preview width

Installation

The command adds the example above to components/charts/ and, the first time, the chart kit to components/ui/charts/. Both are source files in your project from then on: read them, change them, delete what you don't use.

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Usage

Each row is one observation, with a number under xDataKey and one under yDataKey. Rows missing either are skipped.

<script lang="ts">
import {
  HexbinCells,
  HexbinChart,
  HexbinTooltip,
  HexbinXAxis,
  HexbinYAxis,
} from "@/components/ui/charts";

const sessions = [
  { minutes: 1.4, pages: 2 },
  { minutes: 2.2, pages: 3 },
  { minutes: 2.6, pages: 2 },
  { minutes: 6.8, pages: 7 },
  { minutes: 9.1, pages: 6 },
];
</script>

<HexbinChart
  ariaLabel="Session length and pages viewed"
  data={sessions}
  xDataKey="minutes"
  yDataKey="pages"
>
  <HexbinCells />
  <HexbinXAxis />
  <HexbinYAxis />
  <HexbinTooltip valueLabel="Sessions" />
</HexbinChart>

Anatomy

The cells always draw over the grid, whatever order you write the children in. Leave out the axes, tooltip or legend and they aren't drawn.

<HexbinChart {data} xDataKey="minutes" yDataKey="pages">
  <HexbinGrid />
  <HexbinCells />
  <HexbinXAxis />
  <HexbinYAxis />
  <HexbinTooltip valueLabel="Sessions" />
  <HexbinLegend title="Sessions" />
</HexbinChart>
  • HexbinChart: Holds the observations and sets up the x and y scales.
  • HexbinGrid: Dashed guide lines, horizontal unless you add vertical.
  • HexbinCells: The hexagons. binWidth sets their size, valueDataKey and reduce what their color shows.
  • HexbinXAxis: Tick labels along the bottom.
  • HexbinYAxis: Tick labels on the left.
  • HexbinTooltip: The hovered bin's position and value.
  • HexbinLegend: One swatch per color step and the range it covers, under the plot.

Bin size

binWidth on <HexbinCells> is the distance between neighboring hexagon centers, 22 px by default. Points are binned in pixels after the chart is sized, so a narrow chart puts more of them in each bin.

That keeps the hexagons a readable size on a phone. If you need the same bins at every width, aggregate the data yourself and use a heatmap.

Sums and averages

By default a bin's color is its count. Point valueDataKey at a field and each bin sums it, or averages it with reduce="mean". The tooltip then shows the value and the count.

Format the value with formatValue on both <HexbinTooltip> and <HexbinLegend> so they agree.

Preview width

Tooltip content

The default panel is titled with the bin's center, in the axes' formats, and lists its value. valueLabel renames that row and formatPosition rewrites the title.

For anything else, pass content. It gets the hovered bin with its x, y, count, value and the rows that fell in it, and renders inside the same panel.

Preview width

Customization

The kit is in your project, in components/ui/charts/: your framework's components over a shared core/. Change a default there and every chart picks it up.

class goes on the chart's container, so size and spacing are utility classes.

Fills come in steps, four by default, from --chart-scale-02 to --chart-scale-05. The cut points sit on a square-root scale, so a few crowded bins don't push the rest into the lightest step.

To choose them yourself, pass colors and thresholds (one fewer). gap sets the space between hexagons and stroke gives them an outline. The legend reads whatever you pass.

<HexbinCells
  colors={[
    "var(--chart-scale-02)",
    "var(--chart-scale-04)",
    "var(--chart-scale-05)",
  ]}
  gap={0}
  thresholds={[5, 20]}
/>

Where a part renders content of its own, you pass it in. In Svelte:

  • HexbinTooltip: {#snippet content({ bin, index })}

Loading state

Pass status="loading" and a patch of skeleton hexagons shimmers in the plot, while the axes and grid stay put. The pointer is ignored until you switch to "ready", then the real bins sweep in.

There's no separate placeholder. With no rows yet, pass an empty data array along with xDomain and yDomain, so the axes don't change when the data lands.

Preview width

Animation

The hexagons grow in as a wave from left to right. The wave takes animationDuration (900 ms) to cross the plot and each hexagon 450 ms to settle.

New data doesn't replay it. Change revealSignature when you want the wave again, say after a filter changes. animate={false} and reduced motion both draw the bins at rest.

Theming

Every color in the kit is one of your theme's tokens or a mix of them, except --chart-positive, a fixed green. A preset or the .dark class restyles the chart, and overriding a token in your CSS changes it everywhere.

The default fills are mixes of --chart-1 into --muted, so changing --chart-1 recolors the whole scale. The hovered hexagon is redrawn slightly larger with a --chart-foreground outline.

TokenDefaultPaints
--chart-1your theme'sthe first series
--chart-scale-05--chart-1the high end of a color scale
--chart-grid--bordergrid lines
--chart-label--muted-foregroundtick labels
--chart-tooltip-background--popoverthe tooltip panel
globals.css
:root {
  --chart-grid: var(--muted);
  --chart-label: var(--foreground);
}

Accessibility

The chart is announced as an image named by ariaLabel ("Hexbin chart" by default). The bins carry no text, so put the pattern a reader should notice in ariaDescription.

Each bin takes keyboard focus in turn, left to right, and shows its tooltip as it does on hover. On a touch screen, tap a bin.

Tab
Moves focus to the chart, on its leftmost bin.
Arrow Right
Moves to the next bin to the right. ArrowDown does the same.
Arrow Left
Moves to the previous bin. ArrowUp does the same.
Home
Jumps to the leftmost bin.
End
Jumps to the rightmost bin.

API reference

Svelte takes the same props as React, with class for className. Render props are snippets, listed below by their names. Keep data in $state.raw so the chart sees each new array.

HexbinChart

PropType
datareadonly SpatialRow[]

The observations to bin: one row per point. Rows without a finite x or y are left out.

xDataKeystring

Key of the horizontal value in each row.

yDataKeystring

Key of the vertical value in each row.

xDomain?readonly [number, number]

The x range shown. Defaults to the data's, rounded out to nice values.

yDomain?readonly [number, number]

The y range shown. Defaults to the data's, rounded out to nice values.

aspectRatio?string

Aspect ratio of the chart as "width / height".

Default
"16 / 9"
margin?Partial<SpatialMargin>

Space around the plot in pixels. Any side you omit keeps its default.

Type
Partial<SpatialMargin>
animate?boolean

Play the entrance animation.

Default
true
animationDuration?number

Length of the entrance sweep in milliseconds: the time from the first mark starting to the last one.

Default
900
revealSignature?string

Changing this value replays the entrance animation.

status?ChartStatus

"loading" draws a pulsing skeleton of the chart's shape in place of its marks (the axes, grid and land stay) and ignores the pointer; switching to "ready" plays the entrance.

Default
"ready"
loadingLabel?string

Text shown centered over the chart, with a shimmer, while status is "loading". Nothing is shown when it is unset or empty.

class?string

Additional class name for the chart container.

children?ReactNode

The chart's carriers: its layers, axes, grid, tooltip and legend.

ariaLabel?string

Accessible label of the chart.

ariaDescription?string

Longer accessible description of the chart.

HexbinCells

PropType
binWidth?number

Horizontal distance between neighbouring bin centers, in pixels. Bins are laid out at the chart's size, so a narrower chart groups more observations per bin.

Default
22
valueDataKey?string

Key of a value to combine per bin (with reduce) instead of counting observations.

reduce?HexbinReduce

How a bin's observations combine into its value: their number, or the sum or mean of valueDataKey.

Default
"count", or "sum" when `valueDataKey` is set
colors?readonly string[]

The fills from the lowest values to the highest.

Default
the sequential scale `var(--chart-scale-02)` … `var(--chart-scale-05)`
thresholds?readonly number[]

The values at which each fill after the first starts (one fewer than colors). Defaults to equal steps up to the largest bin.

gap?number

Gap between neighbouring hexagons, in pixels.

Default
1.5
stroke?string

Border color of the hexagons.

fadeOnHover?boolean

Dim the layer's other marks while one is hovered or focused.

Default
true
inactiveOpacity?number

Opacity of the other marks while one is hovered or focused.

Default
0.35

HexbinGrid

PropType
horizontal?boolean

Show horizontal grid lines, at the y axis's ticks.

Default
true
vertical?boolean

Show vertical grid lines, at the x axis's ticks.

Default
false
stroke?string

Color of the grid lines.

Default
"var(--chart-grid)"
strokeDasharray?string

Dash pattern of the grid lines.

Default
"4 4"

HexbinXAxis

PropType
numTicks?number

Target number of tick labels.

Default
5
formatValue?(value: number) => string

Formats a tick value into its label. Defaults to the number, thousands abbreviated (2k).

Type
(value: number) => string
label?string

A title for the axis, drawn beside its labels.

HexbinYAxis

PropType
numTicks?number

Target number of tick labels.

Default
5
formatValue?(value: number) => string

Formats a tick value into its label. Defaults to the number, thousands abbreviated (2k).

Type
(value: number) => string
label?string

A title for the axis, drawn beside its labels.

HexbinTooltip

PropType
formatPosition?(x: number, y: number) => string

Formats the bin's x and y values for the default title.

Type
(x: number, y: number) => string
content?Snippet<[{ bin, index }]>

Custom tooltip content. Replaces the default title and rows, inside the same panel.

Type
Snippet<[{ bin, index }]>
formatValue?(value: number) => string

Formats the value row of the default content.

Type
(value: number) => string
Default
the number with thousands separators
valueLabel?string

The label of the value row of the default content.

class?string

Additional class name for the tooltip panel.

panelStyle?CSSProperties

Inline styles merged onto the tooltip panel.

backgroundColor?string

Background color of the tooltip panel. Overrides the theme's popover color.

HexbinLegend

PropType
title?string

A title before the legend's items.

formatValue?(value: number) => string

Formats the values the legend shows.

Type
(value: number) => string
Default
the number, thousands abbreviated (`2k`)
align?"start" | "center" | "end"

Where the legend sits in the row under the plot.

Type
"start" | "center" | "end"
Default
"center"
class?string

Additional class name for the legend.

Examples

4 examples on Svelte, each live with its code and its own install command.