The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Histogram with KDE Overlay in Python, R, Julia and JavaScript.
A histogram with kernel density estimate (KDE) overlay combines discrete binning with continuous density estimation to visualize the distribution of continuous data. The histogram bars show frequency counts in each bin while the smooth KDE curve reveals the underlying probability density, making it easier to perceive the true shape of the distribution without binning artifacts.

// anyplot.ai
// histogram-kde: Histogram with KDE Overlay
// Library: muix 9.16.0 | JavaScript 22.23.3
// Quality: 87/100 | Updated: 2026-10-24
import { ChartsContainer } from "@mui/x-charts/ChartsContainer";
import { LinePlot } from "@mui/x-charts/LineChart";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { useDrawingArea } from "@mui/x-charts/hooks";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
const t = window.ANYPLOT_TOKENS;
// Constants
const FONT = "system-ui, -apple-system, sans-serif";
const TITLE = "histogram-kde · javascript · muix · anyplot.ai";
const BIN_COUNT = 30;
const KDE_POINTS = 240;
const X_PAD_RATIO = 0.04;
const Y_HEADROOM_RATIO = 0.15;
const SILVERMAN_FACTOR = 1.06;
const BRAND = t.palette[0];
const BLUE = t.palette[2];
// Columns: set these to your data's column names
const DAILY_RETURN = "Daily return (%)"; // role: values
// Data: replace this block with your own records
type KdeRow = {
[DAILY_RETURN]: number;
};
const data: KdeRow[] = [];
let seed = 42 >>> 0;
const sampleSize = 500;
for (let i = 0; i < sampleSize; i++) {
seed = (Math.imul(1664525, seed) + 1013904223) >>> 0;
const isShockDay = seed / 0x100000000 < 0.09;
seed = (Math.imul(1664525, seed) + 1013904223) >>> 0;
const u1 = Math.max(seed / 0x100000000, 1e-10);
seed = (Math.imul(1664525, seed) + 1013904223) >>> 0;
const u2 = seed / 0x100000000;
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
const mean = isShockDay ? -2.6 : 0.12;
const sd = isShockDay ? 2.4 : 0.85;
data.push({
[DAILY_RETURN]: Number((mean + z * sd).toFixed(4)),
});
}
// Plot: reads only data and the constants above
const returns = data.map((r) => r[DAILY_RETURN]);
const n = returns.length;
const dataMin = Math.min(...returns);
const dataMax = Math.max(...returns);
const pad = (dataMax - dataMin) * X_PAD_RATIO;
const xMin = dataMin - pad;
const xMax = dataMax + pad;
const binWidth = (dataMax - dataMin) / BIN_COUNT;
const binCounts = new Array(BIN_COUNT).fill(0);
for (const v of returns) {
const idx = Math.min(BIN_COUNT - 1, Math.floor((v - dataMin) / binWidth));
binCounts[idx] += 1;
}
const histogramDensity = binCounts.map((c) => c / (n * binWidth));
const sampleMean = returns.reduce((s, v) => s + v, 0) / n;
const sampleStd = Math.sqrt(
returns.reduce((s, v) => s + (v - sampleMean) ** 2, 0) / (n - 1),
);
const bandwidth = SILVERMAN_FACTOR * sampleStd * Math.pow(n, -0.2);
const kdeX = Array.from(
{ length: KDE_POINTS },
(_, i) => xMin + (i / (KDE_POINTS - 1)) * (xMax - xMin),
);
const kdeDensity = kdeX.map((x) => {
let sum = 0;
for (const v of returns) {
const u = (x - v) / bandwidth;
sum += Math.exp(-0.5 * u * u);
}
return sum / (n * bandwidth * Math.sqrt(2 * Math.PI));
});
const yMax = Math.max(...histogramDensity, ...kdeDensity) * (1 + Y_HEADROOM_RATIO);
const unitMatch = DAILY_RETURN.match(/\(([^)]+)\)/);
const unitSuffix = unitMatch ? unitMatch[1] : "";
const SUBTITLE = `${DAILY_RETURN} (n = ${n}) · Mean ${sampleMean.toFixed(2)}${unitSuffix}, SD ${sampleStd.toFixed(2)}${unitSuffix}, Silverman KDE bandwidth h = ${bandwidth.toFixed(2)}${unitSuffix}`;
function HistogramBarsLayer() {
const { left, top, width, height } = useDrawingArea();
const binPx = (binWidth / (xMax - xMin)) * width;
return (
<g>
{histogramDensity.map((d, i) => {
const bx =
left + ((dataMin + i * binWidth - xMin) / (xMax - xMin)) * width;
const by = top + (1 - d / yMax) * height;
return (
<rect
key={i}
x={bx + 0.5}
y={by}
width={binPx - 1}
height={top + height - by}
fill={BRAND}
fillOpacity={0.5}
/>
);
})}
</g>
);
}
export default function Chart() {
const { width, height } = window.ANYPLOT_SIZE;
const HEADER_H = 108;
return (
<Box
sx={{
width,
height,
bgcolor: t.pageBg,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<Typography
sx={{
fontSize: 22,
fontWeight: 600,
color: t.ink,
pt: "14px",
px: "40px",
pb: 0,
lineHeight: 1.2,
fontFamily: FONT,
}}
>
{TITLE}
</Typography>
<Typography
sx={{
fontSize: 15,
fontWeight: 400,
color: t.inkSoft,
px: "40px",
pt: "4px",
pb: 0,
lineHeight: 1.2,
fontFamily: FONT,
}}
>
{SUBTITLE}
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "24px",
px: "40px",
pt: "8px",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Box
sx={{
width: 16,
height: 12,
bgcolor: BRAND,
opacity: 0.5,
borderRadius: "2px",
}}
/>
<Typography
sx={{ fontSize: 14, fontWeight: 600, color: t.ink, fontFamily: FONT }}
>
Observed density ({BIN_COUNT} bins)
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Box
sx={{ width: 18, height: 3.5, bgcolor: BLUE, borderRadius: "2px" }}
/>
<Typography
sx={{ fontSize: 14, fontWeight: 600, color: t.ink, fontFamily: FONT }}
>
Gaussian KDE estimate
</Typography>
</Box>
</Box>
<ChartsContainer
width={width}
height={height - HEADER_H}
series={[
{
type: "line",
id: "kde",
data: kdeDensity,
label: "KDE estimate",
color: BLUE,
showMark: false,
},
]}
xAxis={[
{
id: "return-axis",
data: kdeX,
scaleType: "linear",
min: xMin,
max: xMax,
height: 60,
label: DAILY_RETURN,
valueFormatter: (v: number) => `${v.toFixed(1)}${unitSuffix}`,
labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },
tickLabelStyle: { fontSize: 14, fill: t.inkSoft, fontFamily: FONT },
},
]}
yAxis={[
{
id: "density-axis",
min: 0,
max: yMax,
width: 96,
label: "Probability Density",
valueFormatter: (v: number) => v.toFixed(2),
labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },
tickLabelStyle: { fontSize: 14, fill: t.inkSoft, fontFamily: FONT },
},
]}
margin={{ top: 24, right: 40, bottom: 20, left: 20 }}
skipAnimation
>
<ChartsGrid
horizontal
sx={{ "& line": { stroke: t.grid, strokeWidth: 0.8 } }}
/>
<HistogramBarsLayer />
<LinePlot
skipAnimation
slotProps={{ line: { sx: { strokeWidth: 3.5 } } }}
/>
<ChartsReferenceLine
x={sampleMean}
label={`Mean ${sampleMean.toFixed(2)}${unitSuffix}`}
labelAlign="start"
lineStyle={{
stroke: t.inkSoft,
strokeDasharray: "6 4",
strokeWidth: 1.5,
}}
labelStyle={{
fontSize: 14,
fontWeight: 600,
fill: t.inkSoft,
fontFamily: FONT,
}}
/>
<ChartsXAxis axisId="return-axis" />
<ChartsYAxis axisId="density-axis" />
</ChartsContainer>
</Box>
);
}
Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/histogram-kde/muix/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-kde",
"language": "javascript",
"library": "muix",
"page": "https://anyplot.ai/histogram-kde/javascript/muix",
"hub": "https://anyplot.ai/histogram-kde",
"code_json": "https://api.anyplot.ai/specs/histogram-kde/muix/code",
"spec_json": "https://api.anyplot.ai/specs/histogram-kde",
"render_light_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-kde/javascript/muix/plot-light.png",
"render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-kde/javascript/muix/plot-dark.png",
"interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-kde/javascript/muix/plot-light.html",
"interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-kde/javascript/muix/plot-dark.html",
"quality_score": 87.0,
"license": "MIT",
"guide": "https://anyplot.ai/llms.txt"
}Part of Histogram with KDE Overlay on anyplot.ai.