2D histogram
Overview
A 2D histogram takes pairs of samples (x, y), bins them along both axes, and colors each cell by how many samples fell in it, or by an aggregate of a third value. It shows the joint distribution of two variables: where they cluster, how they correlate, and where the outliers are, even when a scatter plot of the same points would be one solid blob.
Holochart bins the samples on the CPU and draws the grid as one textured quad with a colorscale lookup, so large grids stay cheap and zooming or panning only updates uniforms.
Pick a different chart when:
- you have a few hundred points and each one matters: use a scatter;
- you want smooth density levels rather than cells, or an overlay on top of points: use a 2D density contour;
- the values are already on a grid (one
zper cell, not samples): use a heatmap; - only one variable matters: use a histogram.
Minimal example
import { createChart } from '@mk7s/holochart';
const x = [1.2, 2.4, 2.1, 3.3, 2.8, 1.9, 2.2, 3.1];
const y = [0.8, 1.9, 1.6, 2.9, 2.2, 1.1, 1.8, 2.5];
const chart = createChart(document.getElementById('chart')!, {
data: [{ type: 'histogram2d', x, y }],
});Without bin settings, Holochart picks bins like Plotly: a nice round size (1, 2 or 5 × 10ⁿ) from the spread of the samples, with the size shrinking as n^0.25 as the sample count grows. The cells are counted, and a colorbar on the right maps counts to colors. The live example bins 4,000 correlated samples:
histogram2d/basicData format
xandy: the samples, one pair per sample. Numbers, dates (date strings or ms on a date axis) and categories all work, independently per axis. Only the firstmin(x.length, y.length)pairs are used; a trace without both is hidden.- A sample with a missing or non-numeric coordinate (
null,NaN) falls in no bin. z(ormarker.colorwhen there is noz): one value per sample, aggregated per cell byhistfunc. Without either, cells are counted.histfunc:'count'(default), or'sum','avg','min'and'max'of thezvalues in each cell. Empty cells ofavg,minandmaxhave no value and draw nothing.histnorm:''(raw values),'percent'or'probability'(share of the total),'density'(value divided by the cell area) or'probability density'(both: the grid integrates to 1).
import { createChart } from '@mk7s/holochart';
// Mean delivery time (z) per distance and parcel weight bin.
const distance = [3, 12, 7, 25, 18, 4, 30, 9];
const weight = [1.2, 0.4, 3.1, 2.2, 0.8, 4.5, 1.9, 2.7];
const minutes = [22, 41, 35, 70, 52, 30, 85, 38];
createChart(document.getElementById('chart')!, {
data: [
{
type: 'histogram2d',
x: distance,
y: weight,
z: minutes,
histfunc: 'avg',
xbins: { size: 10 },
colorbar: { title: { text: 'avg minutes' } },
},
],
});Bins
nbinsx/nbinsy: the maximum number of bins per axis. The size is still rounded to a nice value, so you may get fewer bins than asked.xbins/ybins{ start, end, size }: explicit bins. Any part you leave out is automatic. On a date axissizeis in milliseconds, or'M<n>'for calendar months ('M1'monthly,'M3'quarterly,'M12'yearly);startandendare dates. On a category axis there is one bin per category by default, andsizecounts categories.bingroup: traces with the samebingroupget the same x bins and the same y bins, so their cells line up (for example two panels you want to compare cell by cell).xbingroupandybingroupshare one direction only, and also pair with 1Dhistogramtraces of the samebingroup, which keeps marginal histograms aligned with the grid.
import { createChart } from '@mk7s/holochart';
const before = { x: [1, 2, 2, 3, 4, 4, 5], y: [2, 3, 3, 4, 4, 5, 6] };
const after = { x: [2, 3, 3, 4, 5, 6, 6], y: [1, 2, 3, 3, 4, 4, 5] };
// Shared bins: the cells of both panels have the same edges.
createChart(document.getElementById('chart')!, {
data: [
{ type: 'histogram2d', ...before, bingroup: 'ab', coloraxis: 'coloraxis' },
{ type: 'histogram2d', ...after, bingroup: 'ab', coloraxis: 'coloraxis', xaxis: 'x2' },
],
layout: { xaxis: { domain: [0, 0.48] }, xaxis2: { domain: [0.52, 1] } },
});Variations
Cell labels on a category axis
texttemplate writes a label in every non-empty cell, with %{z}, %{x} and %{y} (bin centers) and optional d3 formats (%{z:.1f}). With the default textfont.size: 'auto', labels take the largest size at which they fit the cells, and without textfont.color they are black or white, whichever contrasts with the cell. Here x is a category axis (one bin per weekday) and y uses explicit 3-hour bins.
histogram2d/texttemplateGaps between cells
xgap and ygap leave that many pixels of background between cells, a tiled look that suits coarse grids. nbinsx / nbinsy cap the number of bins, and colorscale replaces the default scale. Gaps only apply to flat cells (zsmooth: false).
histogram2d/gapsSmoothing
zsmooth: 'best' interpolates bilinearly between cell centers in data space, which stays exact for uneven bins (month bins, for example); 'fast' interpolates by cell index, which is the same for equal bins. Both run in the fragment shader: no extra geometry, and zooming stays a uniform update. The two panels share one coloraxis, so one colorbar describes both.
histogram2d/smoothDensity with a titled colorbar
histnorm: 'probability density' divides each cell by the total and by its area, so the grid integrates to 1 and grids with different bin sizes compare fairly. zhoverformat and colorbar.tickformat keep the small numbers readable.
histogram2d/densityDates and month bins
On a date axis, month-sized bins ('M1', 'M3', 'M12') follow the calendar, so every cell is one month even though months differ in length. Pair them with zsmooth: 'best' if you smooth.
import { createChart } from '@mk7s/holochart';
const opened = ['2024-01-03', '2024-01-19', '2024-02-11', '2024-03-02', '2024-03-28'];
const hoursToClose = [4, 30, 12, 7, 52];
createChart(document.getElementById('chart')!, {
data: [
{
type: 'histogram2d',
x: opened,
y: hoursToClose,
xbins: { start: '2024-01-01', end: '2024-04-01', size: 'M1' },
ybins: { size: 12 },
},
],
layout: { xaxis: { type: 'date' } },
});Styling
- Colorscale.
colorscale(a name, a list of colors, or[position, color]stops), with thez-lettered range attributes:zauto,zmin,zmax,zmid(a symmetric automatic domain for diverging data) andreversescale.autocolorscaleis off by default, socolorscaleis used as given. See colors and colorscales. - Default look. In the default
holocharttemplate, 2D histograms use the neon plasma sequential scale, whose dark end still shows on the dark background. Plotly's own default isRdBu, which you get withlayout.template: 'plotly-classic'(themes and templates). - Colorbar.
showscale(defaulttrue) andcolorbar(title,tickformat,len, …). To share one scale and one colorbar across traces, setcoloraxis: 'coloraxis'on each and stylelayout.coloraxis. - Cells.
xgap/ygap(px, flat cells only),zsmooth(false,'fast','best') and traceopacity. - Labels.
texttemplateandtextfont(family,sizeor'auto',color,weight,style); the font defaults tolayout.font. - Legend.
showlegenddefaults tofalse: the colorbar describes the trace. Set it totruefor a legend entry (a swatch of the scale's middle color). - Order. Like other heatmap-like traces, a 2D histogram draws below bars and scatter traces in the same subplot;
zorderchanges that.
Interactivity
Hover. The cell under the pointer shows its bin ranges and value, for example
x: 0 - 4,y: 10 - 12,z: 37. Ranges are rounded to the precision the bins need; on a category axis the category name is shown.xhoverformat,yhoverformatandzhoverformatformat the values, andhovertemplatetakes%{x}and%{y}(the range labels) and%{z}:tsimport { createChart } from '@mk7s/holochart'; createChart(document.getElementById('chart')!, { data: [ { type: 'histogram2d', x: [1, 2, 2, 3, 3, 3], y: [4, 5, 5, 6, 6, 7], hovertemplate: 'size %{x}<br>latency %{y} ms<br>%{z} requests<extra></extra>', }, ], });Markers and lines drawn over the grid win the
closesthover, as in Plotly.Events.
hoverandclickpoints carryxandy(the cell center),z,pointNumber(the cell index, row-major) andpointNumbers, the indices of the samples in that cell, for drill-down:tschart.on('click', (e) => console.log(e.points[0]?.pointNumbers?.length, 'samples'));Zoom and pan. Drag to zoom, double-click to reset. The grid is not rebinned while you zoom.
Selection. Box and lasso selection don't select cells, as in Plotly.
Performance notes
- Binning runs on the CPU in the calc step. For numeric bins of equal size, finding a sample's bin is O(1), about 10 ms per million samples; month bins and uneven bins use a binary search.
- The grid is one textured quad per trace plus a small colorscale lookup texture, whatever the number of cells. Zoom and pan only set uniforms; style edits (colorscale,
zmin/zmax,zsmooth, gaps, opacity) set uniforms or swap the lookup texture, with no rebinning. - Grids of more than 4096 × 4096 cells are not drawn (a warning is logged). Pick coarser bins.
- Cell labels are text, which is typeset asynchronously; thousands of labels take longer to appear than the grid. Keep
texttemplatefor coarse grids. - Aggregating samples on the GPU (for well beyond a million samples) is not implemented yet. See the performance guide.
Accessibility notes
- Screen readers: the chart is a
<canvas>; the hidden description (see the accessibility guide) says how many samples were binned into how many bins and names the fullest cell with its bin ranges, and its data table lists the non-empty cells (x range, y range, value). - Keyboard: Tab moves into the plot area; the arrow keys then move a cell cursor along the row (← / →) and the column (↑ / ↓), showing each cell's hover label. See the keys by chart family. The script-tag build leaves these stops out for now.
- Color: use a sequential scale that is monotonic in lightness (the default, Viridis, Cividis) so that "more" reads as "brighter" without relying on hue, and keep the colorbar visible. For coarse grids,
texttemplate: '%{z}'puts the exact values in the cells.
Attribute reference
See the histogram2d attribute reference for every attribute, its type, and its default. Shared color axes are under coloraxis in the layout reference.
Related charts
- 2D density contour: the same binning drawn as contour levels, for smooth shapes and overlays on scatter plots
- Scatter: each sample as a point, for small data or outliers
- Heatmap: values already on a grid
Plotly migration notes
- Attribute names and defaults match Plotly's
histogram2d:x,y,z,marker.color,histfunc,histnorm,nbinsx/nbinsy,xbins/ybins,bingroup/xbingroup/ybingroup,xgap/ygap,zsmooth, the colorscale attributes,texttemplate,textfont, the hover formats andhovertemplate. Automatic bins follow Plotly's rules, so Plotly figures carry over. - The default colorscale comes from the template: the neon plasma scale in the default look,
RdBuwithtemplate: 'plotly-classic'. zsmooth: 'fast'interpolates between cell centers on the GPU; Plotly's'fast'stretches an image of the cells, so edges look slightly different.- A click point's
pointNumberis a flat cell index (row-major) rather than Plotly's[row, col]pair;pointNumbers(the samples in the cell) matches. - Not supported: selection (Plotly doesn't select 2D histogram cells either),
xcalendar/ycalendar(non-Gregorian calendars), and grids larger than 4096 × 4096 cells.