Returns Distribution Histogram — D3.js

A histogram showing the distribution of financial returns (daily, weekly, or monthly) with a normal distribution overlay for comparison. This visualization is essential for risk analysis, allowing analysts to assess whether returns follow a normal distribution, identify fat tails indicating higher-than-expected extreme events, and measure asymmetry through skewness. Key statistics are displayed directly on the plot for quick interpretation.

Returns Distribution Histogram rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// histogram-returns-distribution: Returns Distribution Histogram
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 87/100 | Created: 2026-09-02

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

// --- Data: 252 daily returns (%) from a fixed-seed mixture -----------------
// 92% "calm" days ~ N(0.05, 1.05); 8% "shock" days ~ N(-2.1, 2.6) — the
// mixture produces the negative skew and fat left tail typical of equity
// return series without relying on an unseeded RNG.
let lcgState = 20260902;
function uniform() {
  lcgState = (1103515245 * lcgState + 12345) % 2147483648;
  return lcgState / 2147483648;
}
function gaussian() {
  const u1 = Math.max(uniform(), 1e-9);
  const u2 = uniform();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const n = 252;
const returns = [];
for (let i = 0; i < n; i++) {
  const shock = uniform() < 0.08;
  const value = shock ? -2.1 + gaussian() * 2.6 : 0.05 + gaussian() * 1.05;
  returns.push(value);
}

// --- Fitted statistics --------------------------------------------------
const meanVal = d3.mean(returns);
const stdVal = d3.deviation(returns);
const skew =
  d3.sum(returns, (d) => Math.pow((d - meanVal) / stdVal, 3)) / n;
const kurt =
  d3.sum(returns, (d) => Math.pow((d - meanVal) / stdVal, 4)) / n - 3;

// --- Histogram (density-normalized, equal-width bins) -----------------------
// Binned manually (rather than d3.bin()'s default "nice" thresholds) so every
// bin has the exact same width — d3.bin()'s rounded edge thresholds can leave
// the first/last bin narrower than the rest, which would distort a lone
// tail observation into a misleadingly tall density spike.
const [dataMin, dataMax] = d3.extent(returns);
const numBins = 28;
const binWidth = (dataMax - dataMin) / numBins;
const density = d3.range(numBins).map((i) => {
  const x0 = dataMin + i * binWidth;
  const x1 = x0 + binWidth;
  const count = returns.filter((v) => v >= x0 && (v < x1 || (i === numBins - 1 && v <= x1))).length;
  return { x0, x1, y: count / (n * binWidth) };
});

// --- Normal curve fitted to mean/std ----------------------------------------
function normalPdf(x) {
  return (
    Math.exp(-0.5 * Math.pow((x - meanVal) / stdVal, 2)) /
    (stdVal * Math.sqrt(2 * Math.PI))
  );
}
const curveX = d3.range(dataMin, dataMax, (dataMax - dataMin) / 200);
const curvePoints = curveX.map((x) => ({ x, y: normalPdf(x) }));

// --- Scales -------------------------------------------------------------
const x = d3.scaleLinear().domain([dataMin, dataMax]).nice().range([0, iw]);
const yMax = Math.max(d3.max(density, (d) => d.y), d3.max(curvePoints, (d) => d.y)) * 1.1;
const y = d3.scaleLinear().domain([0, yMax]).nice().range([ih, 0]);

// --- 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})`);

// --- Axes -----------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).tickFormat((d) => `${d}%`).ticks(10));
const yAxis = g.append("g").call(d3.axisLeft(y).ticks(6));
for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
  ax.selectAll("line").attr("stroke", t.grid);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// y-axis gridlines (subtle, matches "both axes for continuous/scatter-like" density plot)
g.append("g")
  .attr("class", "grid")
  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(""))
  .selectAll("line")
  .attr("stroke", t.grid);
g.select(".grid .domain").remove();

// --- Tail boundary reference lines (±2σ) ------------------------------------
const tailLo = meanVal - 2 * stdVal;
const tailHi = meanVal + 2 * stdVal;
for (const bound of [tailLo, tailHi]) {
  g.append("line")
    .attr("x1", x(bound)).attr("x2", x(bound))
    .attr("y1", 0).attr("y2", ih)
    .attr("stroke", t.inkSoft)
    .attr("stroke-width", 1.5)
    .attr("stroke-dasharray", "6,5")
    .attr("opacity", 0.6);
}

// --- Histogram bars: green for the bulk, matte red beyond ±2σ --------------
g.selectAll("rect.bar")
  .data(density)
  .join("rect")
  .attr("class", "bar")
  .attr("x", (d) => x(d.x0) + 1)
  .attr("y", (d) => y(d.y))
  .attr("width", (d) => Math.max(x(d.x1) - x(d.x0) - 2, 0))
  .attr("height", (d) => ih - y(d.y))
  .attr("fill", (d) => {
    const center = (d.x0 + d.x1) / 2;
    return center < tailLo || center > tailHi ? t.palette[4] : t.palette[0];
  })
  .attr("opacity", 0.9);

// --- Fitted normal curve ----------------------------------------------------
const line = d3.line().x((d) => x(d.x)).y((d) => y(d.y)).curve(d3.curveBasis);
g.append("path")
  .datum(curvePoints)
  .attr("fill", "none")
  .attr("stroke", t.palette[2])
  .attr("stroke-width", 3)
  .attr("d", line);

// --- Axis titles --------------------------------------------------------
g.append("text")
  .attr("x", iw / 2).attr("y", ih + 60)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "17px")
  .text("Daily Return (%)");
g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2).attr("y", -62)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "17px")
  .text("Density");

// --- Legend -----------------------------------------------------------------
const legend = [
  { label: "Daily returns", swatch: t.palette[0] },
  { label: "|z| > 2σ (tail)", swatch: t.palette[4] },
  { label: "Normal fit", swatch: t.palette[2], line: true },
];
const legendG = g.append("g").attr("transform", `translate(${iw - 230}, 6)`);
legend.forEach((item, i) => {
  const row = legendG.append("g").attr("transform", `translate(0, ${i * 26})`);
  if (item.line) {
    row.append("line").attr("x1", 0).attr("x2", 18).attr("y1", 8).attr("y2", 8)
      .attr("stroke", item.swatch).attr("stroke-width", 3);
  } else {
    row.append("rect").attr("width", 18).attr("height", 14).attr("y", 1).attr("fill", item.swatch);
  }
  row.append("text").attr("x", 26).attr("y", 12)
    .attr("fill", t.inkSoft).style("font-size", "14px")
    .text(item.label);
});

// --- Statistics callout box --------------------------------------------------
const statLines = [
  `mean  ${meanVal >= 0 ? "+" : ""}${meanVal.toFixed(2)}%`,
  `std       ${stdVal.toFixed(2)}%`,
  `skew    ${skew.toFixed(2)}`,
  `kurt     ${kurt.toFixed(2)}`,
];
const boxW = 210;
const boxH = 24 * statLines.length + 24;
const boxG = g.append("g").attr("transform", `translate(12, 12)`);
boxG.append("rect")
  .attr("width", boxW).attr("height", boxH)
  .attr("fill", t.elevatedBg)
  .attr("stroke", t.grid)
  .attr("rx", 6);
boxG.append("text")
  .attr("x", 16).attr("y", 26)
  .attr("fill", t.ink).style("font-size", "15px").style("font-weight", "600")
  .text("Fitted statistics");
statLines.forEach((line, i) => {
  boxG.append("text")
    .attr("x", 16).attr("y", 52 + i * 22)
    .attr("fill", t.inkSoft).style("font-size", "14px")
    .style("font-family", "monospace")
    .text(line);
});

// --- Title ------------------------------------------------------------------
svg.append("text")
  .attr("x", width / 2).attr("y", 52)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "22px").style("font-weight", "600")
  .text("histogram-returns-distribution · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/histogram-returns-distribution/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": "histogram-returns-distribution",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/histogram-returns-distribution/javascript/d3",
  "hub": "https://anyplot.ai/histogram-returns-distribution",
  "code_json": "https://api.anyplot.ai/specs/histogram-returns-distribution/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/histogram-returns-distribution",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-returns-distribution/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-returns-distribution/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-returns-distribution/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-returns-distribution/javascript/d3/plot-dark.html",
  "quality_score": 87.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Returns Distribution Histogram on anyplot.ai.

Other implementations