Basic Heatmap — D3.js

A heatmap displaying values in a matrix format using color intensity. Each cell's color represents the magnitude of the value, making it easy to identify patterns, clusters, and outliers in two-dimensional data. Essential for visualizing correlations, frequencies, and relationships between variables.

Basic Heatmap rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// heatmap-basic: Basic Heatmap
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 92/100 | Updated: 2026-06-03
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 170, right: 180, bottom: 60, left: 220 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// Data — asset-class correlation matrix (symmetric, diagonal = 1), the canonical
// diverging-heatmap scenario from the spec's applications. Values are hand-tuned
// to reflect typical post-2020 regime correlations: equities cluster positive,
// long-duration bonds anti-correlate with risk assets, gold sits near-zero.
const assets = [
  "US Equities",
  "EU Equities",
  "EM Equities",
  "REITs",
  "High Yield",
  "IG Credit",
  "Treasuries",
  "Gold",
  "Oil",
  "USD Cash",
];

const corr = [
  [1.00,  0.84,  0.72,  0.68,  0.71,  0.42, -0.34,  0.08,  0.38, -0.28],
  [0.84,  1.00,  0.78,  0.62,  0.66,  0.45, -0.30,  0.12,  0.35, -0.31],
  [0.72,  0.78,  1.00,  0.55,  0.61,  0.38, -0.38,  0.18,  0.44, -0.42],
  [0.68,  0.62,  0.55,  1.00,  0.58,  0.48, -0.12,  0.10,  0.28, -0.18],
  [0.71,  0.66,  0.61,  0.58,  1.00,  0.66, -0.22,  0.05,  0.32, -0.20],
  [0.42,  0.45,  0.38,  0.48,  0.66,  1.00,  0.52,  0.14,  0.08,  0.06],
  [-0.34, -0.30, -0.38, -0.12, -0.22,  0.52,  1.00,  0.22, -0.18,  0.38],
  [0.08,  0.12,  0.18,  0.10,  0.05,  0.14,  0.22,  1.00,  0.26,  0.15],
  [0.38,  0.35,  0.44,  0.28,  0.32,  0.08, -0.18,  0.26,  1.00, -0.22],
  [-0.28, -0.31, -0.42, -0.18, -0.20,  0.06,  0.38,  0.15, -0.22,  1.00],
];

// SVG mount
const svg = d3.select("#container").append("svg")
  .attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// Scales
const x = d3.scaleBand().domain(assets).range([0, iw]).padding(0.04);
const y = d3.scaleBand().domain(assets).range([0, ih]).padding(0.04);

// Imprint diverging colormap — three stops, midpoint already theme-adaptive
const color = d3.scaleSequential(d3.interpolateRgbBasis(t.div)).domain([-1, 1]);

// Cells (flattened)
const cells = [];
for (let i = 0; i < assets.length; i++) {
  for (let j = 0; j < assets.length; j++) {
    cells.push({ row: assets[i], col: assets[j], value: corr[i][j] });
  }
}

// Per-cell text color picked by WCAG contrast ratio against the actual cell
// fill — keeps annotation contrast uniform across light/dark themes. The
// diverging midpoint flips with the theme, so a fixed |r| threshold would
// under-serve saturated cells in one theme or the other.
const relLum = (hex) => {
  const c = d3.color(hex).rgb();
  const f = (v) => {
    const u = v / 255;
    return u <= 0.03928 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4);
  };
  return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
};
const contrast = (a, b) => {
  const la = relLum(a);
  const lb = relLum(b);
  return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
};
const textOn = (fill) =>
  contrast(fill, t.ink) >= contrast(fill, t.pageBg) ? t.ink : t.pageBg;

// Cell rectangles — diagonal gets a soft accent stroke so the eye lands on
// the unit-correlation backbone first (DE-03 storytelling).
g.selectAll("rect.cell").data(cells).join("rect")
  .attr("class", "cell")
  .attr("x", (d) => x(d.col))
  .attr("y", (d) => y(d.row))
  .attr("width", x.bandwidth())
  .attr("height", y.bandwidth())
  .attr("fill", (d) => color(d.value))
  .attr("stroke", (d) => (d.row === d.col ? t.ink : "none"))
  .attr("stroke-width", (d) => (d.row === d.col ? 1.6 : 0));

// Value annotations — weight bumps to 700 on strong correlations so the
// equity cluster and treasury anti-correlation block read as figures, not
// uniform texture.
g.selectAll("text.value").data(cells).join("text")
  .attr("class", "value")
  .attr("x", (d) => x(d.col) + x.bandwidth() / 2)
  .attr("y", (d) => y(d.row) + y.bandwidth() / 2)
  .attr("text-anchor", "middle")
  .attr("dominant-baseline", "central")
  .style("font-size", "22px")
  .style("font-weight", (d) => (Math.abs(d.value) > 0.5 ? 700 : 500))
  .attr("fill", (d) => textOn(color(d.value)))
  .text((d) => d.value.toFixed(2));

// Column labels — rotated above the matrix
const xAxis = g.append("g")
  .attr("transform", "translate(0,0)")
  .call(d3.axisTop(x).tickSize(0).tickPadding(14));
xAxis.select(".domain").remove();
xAxis.selectAll("text")
  .attr("fill", t.inkSoft)
  .style("font-size", "24px")
  .style("font-weight", "500")
  .attr("transform", "rotate(-38)")
  .style("text-anchor", "start");

// Row labels — left of the matrix
const yAxis = g.append("g").call(d3.axisLeft(y).tickSize(0).tickPadding(14));
yAxis.select(".domain").remove();
yAxis.selectAll("text")
  .attr("fill", t.inkSoft)
  .style("font-size", "24px")
  .style("font-weight", "500");

// Title
svg.append("text")
  .attr("x", width / 2).attr("y", 60)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "38px")
  .style("font-weight", "600")
  .text("heatmap-basic · javascript · d3 · anyplot.ai");

// Colorbar — vertical gradient strip + axis
const cbWidth = 38;
const cbHeight = ih;
const cbX = iw + 70;

const defs = svg.append("defs");
const grad = defs.append("linearGradient")
  .attr("id", "cbgrad")
  .attr("x1", "0%").attr("y1", "0%")
  .attr("x2", "0%").attr("y2", "100%");
const nStops = 32;
for (let i = 0; i <= nStops; i++) {
  const u = i / nStops;
  grad.append("stop")
    .attr("offset", `${u * 100}%`)
    .attr("stop-color", color(1 - 2 * u));
}

g.append("rect")
  .attr("x", cbX).attr("y", 0)
  .attr("width", cbWidth).attr("height", cbHeight)
  .attr("fill", "url(#cbgrad)")
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 0.6);

const cbScale = d3.scaleLinear().domain([1, -1]).range([0, cbHeight]);
const cbAxis = g.append("g")
  .attr("transform", `translate(${cbX + cbWidth}, 0)`)
  .call(
    d3.axisRight(cbScale)
      .tickValues([-1, -0.5, 0, 0.5, 1])
      .tickFormat(d3.format("+.1f"))
      .tickSize(8),
  );
cbAxis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "22px");
cbAxis.selectAll("line").attr("stroke", t.inkSoft);
cbAxis.select(".domain").attr("stroke", t.inkSoft);

// Colorbar caption
g.append("text")
  .attr("x", cbX + cbWidth / 2).attr("y", -22)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft)
  .style("font-size", "22px")
  .style("font-weight", "500")
  .text("Pearson r");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/heatmap-basic/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "heatmap-basic",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/heatmap-basic/javascript/d3",
  "hub": "https://anyplot.ai/heatmap-basic",
  "code_json": "https://api.anyplot.ai/specs/heatmap-basic/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/heatmap-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/heatmap-basic/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/heatmap-basic/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/heatmap-basic/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/heatmap-basic/javascript/d3/plot-dark.html",
  "quality_score": 92.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Heatmap on anyplot.ai.

Other implementations