{"spec_id":"histogram-kde","library":"muix","language":"javascript","code":"// anyplot.ai\n// histogram-kde: Histogram with KDE Overlay\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 89/100 | Created: 2026-08-05\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG + Box-Muller — the browser has no seeded Math.random.\nlet seed = 42;\nfunction lcg() {\n  seed = (Math.imul(1664525, seed) + 1013904223) >>> 0;\n  return seed / 0x100000000;\n}\nfunction randn() {\n  const u1 = Math.max(lcg(), 1e-10);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Daily portfolio returns (%): most days are calm fluctuations around a small\n// positive drift; a minority are downside-shock days, giving the distribution\n// a left-skewed, fat tail that binning alone tends to obscure.\nconst N = 500;\nconst returns = Array.from({ length: N }, () => {\n  const isShockDay = lcg() < 0.09;\n  const mean = isShockDay ? -2.6 : 0.12;\n  const sd = isShockDay ? 2.4 : 0.85;\n  return mean + randn() * sd;\n});\n\n// --- Histogram (density-scaled so bars and KDE share the same y-axis) -------\nconst dataMin = Math.min(...returns);\nconst dataMax = Math.max(...returns);\nconst pad = (dataMax - dataMin) * 0.04;\nconst X_MIN = dataMin - pad;\nconst X_MAX = dataMax + pad;\n\nconst BIN_COUNT = 30;\nconst binWidth = (dataMax - dataMin) / BIN_COUNT;\nconst binCounts = new Array(BIN_COUNT).fill(0);\nreturns.forEach((v) => {\n  const idx = Math.min(BIN_COUNT - 1, Math.floor((v - dataMin) / binWidth));\n  binCounts[idx] += 1;\n});\nconst histogramDensity = binCounts.map((c) => c / (N * binWidth));\n\n// --- Gaussian KDE, evaluated on a fine continuous grid -----------------------\nconst sampleMean = returns.reduce((s, v) => s + v, 0) / N;\nconst sampleStd = Math.sqrt(\n  returns.reduce((s, v) => s + (v - sampleMean) ** 2, 0) / (N - 1)\n);\n// Silverman's rule of thumb for bandwidth.\nconst bandwidth = 1.06 * sampleStd * N ** (-1 / 5);\n\nfunction kernelDensity(x) {\n  const sum = returns.reduce((acc, v) => {\n    const u = (x - v) / bandwidth;\n    return acc + Math.exp(-0.5 * u * u);\n  }, 0);\n  return sum / (N * bandwidth * Math.sqrt(2 * Math.PI));\n}\n\nconst KDE_POINTS = 240;\nconst kdeX = Array.from(\n  { length: KDE_POINTS },\n  (_, i) => X_MIN + (i / (KDE_POINTS - 1)) * (X_MAX - X_MIN)\n);\nconst kdeDensity = kdeX.map(kernelDensity);\n\nconst Y_MAX = Math.max(...histogramDensity, ...kdeDensity) * 1.15;\n\n// Imprint palette colors\nconst BRAND = t.palette[0]; // #009E73 — histogram bars (first categorical series)\nconst BLUE = t.palette[2]; // #4467A3 — KDE curve, contrasts against the green bars\n\n// Title sizing (scale down for longer-than-67-char titles)\nconst TITLE = \"histogram-kde · javascript · muix · anyplot.ai\";\nconst titleSize = Math.max(11, Math.round(22 * (67 / TITLE.length)));\n\n// --- Histogram bars, drawn as SVG rects mapped onto the shared linear x-axis -\n// A MUI X `<BarPlot>` needs a band-scale x-axis, which cannot share the same\n// continuous axis as the KDE's fine-grained line — so the bars are positioned\n// directly from the drawing-area coordinate map instead.\nfunction HistogramBars() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (v) => left + ((v - X_MIN) / (X_MAX - X_MIN)) * width;\n  const toY = (d) => top + (1 - d / Y_MAX) * height;\n  const binPx = (binWidth / (X_MAX - X_MIN)) * width;\n  return (\n    <g>\n      {histogramDensity.map((d, i) => {\n        const bx = toX(dataMin + i * binWidth);\n        const by = toY(d);\n        return (\n          <rect\n            key={i}\n            x={bx + 0.5}\n            y={by}\n            width={Math.max(0, binPx - 1)}\n            height={Math.max(0, top + height - by)}\n            fill={BRAND}\n            fillOpacity={0.5}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Main component (default-exported — the harness mounts it) --------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const TITLE_H = 56;\n  const LEGEND_H = 34;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        overflow: \"hidden\",\n      }}\n    >\n      <Typography\n        sx={{\n          fontSize: titleSize,\n          fontWeight: 500,\n          color: t.ink,\n          pt: \"16px\",\n          px: \"40px\",\n          pb: 0,\n          lineHeight: 1.2,\n          fontFamily: FONT,\n        }}\n      >\n        {TITLE}\n      </Typography>\n\n      <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"20px\", px: \"40px\", pt: \"6px\" }}>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n          <Box sx={{ width: 16, height: 12, bgcolor: BRAND, opacity: 0.5, borderRadius: \"2px\" }} />\n          <Typography sx={{ fontSize: 15, color: t.inkSoft, fontFamily: FONT }}>\n            Observed density\n          </Typography>\n        </Box>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n          <Box sx={{ width: 16, height: 3, bgcolor: BLUE, borderRadius: \"2px\" }} />\n          <Typography sx={{ fontSize: 15, color: t.inkSoft, fontFamily: FONT }}>\n            KDE estimate\n          </Typography>\n        </Box>\n      </Box>\n\n      <ChartContainer\n        width={width}\n        height={height - TITLE_H - LEGEND_H}\n        series={[\n          {\n            type: \"line\",\n            id: \"kde\",\n            data: kdeDensity,\n            label: \"KDE estimate\",\n            color: BLUE,\n            showMark: false,\n          },\n        ]}\n        xAxis={[\n          {\n            data: kdeX,\n            scaleType: \"linear\",\n            min: X_MIN,\n            max: X_MAX,\n            label: \"Daily Return (%)\",\n            valueFormatter: (v) => `${v.toFixed(1)}%`,\n            labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft, fontFamily: FONT },\n          },\n        ]}\n        yAxis={[\n          {\n            min: 0,\n            max: Y_MAX,\n            label: \"Density\",\n            labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft, fontFamily: FONT },\n          },\n        ]}\n        margin={{ top: 44, right: 40, bottom: 80, left: 130 }}\n        skipAnimation\n      >\n        <ChartsGrid horizontal sx={{ \"& line\": { stroke: t.grid, strokeWidth: 0.8 } }} />\n        <HistogramBars />\n        <LinePlot skipAnimation slotProps={{ line: { sx: { strokeWidth: 3.5 } } }} />\n        {/* Mean reference line — calls out where the shock-day left tail pulls\n            the average below the 0% no-change point the bulk of days cluster near. */}\n        <ChartsReferenceLine\n          x={sampleMean}\n          label={`Mean ${sampleMean.toFixed(2)}%`}\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n          labelStyle={{ fontSize: 13, fill: t.inkSoft, fontFamily: FONT }}\n        />\n        <ChartsXAxis />\n        {/* Explicit axisLabel x offset — the default offset formula assumes short\n            tick labels and clips against our 4-char decimal density values. */}\n        <ChartsYAxis slotProps={{ axisLabel: { x: -72 } }} />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}