{"spec_id":"histogram-density","library":"echarts","language":"javascript","code":"// anyplot.ai\n// histogram-density: Density Histogram\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Reaction times (ms) from a simulated cognitive-task experiment, n=600.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nfunction randomNormal() {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst sampleCount = 600;\nconst trueMean = 620;\nconst trueStd = 85;\nconst reactionTimes = Array.from(\n  { length: sampleCount },\n  () => trueMean + trueStd * randomNormal(),\n);\n\n// Sample statistics feed the theoretical PDF overlay (goodness-of-fit check).\nconst sampleMean = reactionTimes.reduce((a, b) => a + b, 0) / sampleCount;\nconst variance =\n  reactionTimes.reduce((a, b) => a + (b - sampleMean) ** 2, 0) / sampleCount;\nconst sampleStd = Math.sqrt(variance);\n\n// --- Binning: normalize so total bar area == 1 (density, not raw count) ----\nconst binCount = 20;\nconst minValue = Math.min(...reactionTimes);\nconst maxValue = Math.max(...reactionTimes);\nconst binWidth = (maxValue - minValue) / binCount;\nconst counts = new Array(binCount).fill(0);\nreactionTimes.forEach((value) => {\n  const idx = Math.min(binCount - 1, Math.floor((value - minValue) / binWidth));\n  counts[idx] += 1;\n});\n\nconst binCenters = counts.map((_, i) => minValue + (i + 0.5) * binWidth);\nconst binLabels = binCenters.map((c) => Math.round(c).toString());\nconst densities = counts.map((count) => count / (sampleCount * binWidth));\n\n// Theoretical normal PDF fit to the sample, evaluated at the same bin centers.\nconst normalPdf = binCenters.map((x) => {\n  const z = (x - sampleMean) / sampleStd;\n  return Math.exp(-0.5 * z * z) / (sampleStd * Math.sqrt(2 * Math.PI));\n});\n\n// Nearest-bin lookups drive the mean/±1σ annotations below.\nconst nearestBinIndex = (target) =>\n  binCenters.reduce(\n    (best, c, i) =>\n      Math.abs(c - target) < Math.abs(binCenters[best] - target) ? i : best,\n    0,\n  );\nconst meanBinLabel = binLabels[nearestBinIndex(sampleMean)];\nconst lowStdBinLabel = binLabels[nearestBinIndex(sampleMean - sampleStd)];\nconst highStdBinLabel = binLabels[nearestBinIndex(sampleMean + sampleStd)];\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"histogram-density · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Observed density\", \"Normal fit\"],\n    top: 62,\n    textStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  grid: { left: 100, right: 60, top: 120, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: binLabels,\n    name: \"Reaction Time (ms)\",\n    nameLocation: \"center\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Density\",\n    nameLocation: \"center\",\n    nameGap: 65,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => value.toFixed(3),\n    },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n    // Headroom above the tallest bar/curve keeps the \"Mean\" markLine label clear of the data.\n    max: (value) => value.max * 1.18,\n  },\n  series: [\n    {\n      name: \"Observed density\",\n      type: \"bar\",\n      data: densities,\n      barCategoryGap: \"0%\",\n      itemStyle: { color: t.palette[0] },\n      z: 2,\n      markArea: {\n        silent: true,\n        itemStyle: { color: t.palette[0], opacity: 0.12 },\n        label: { show: false },\n        data: [[{ xAxis: lowStdBinLabel }, { xAxis: highStdBinLabel }]],\n      },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"dotted\", width: 1.5 },\n        label: {\n          formatter: \"Mean\",\n          color: t.inkSoft,\n          fontSize: 13,\n          rotate: 0,\n          position: \"end\",\n          distance: 8,\n        },\n        data: [{ xAxis: meanBinLabel }],\n      },\n    },\n    {\n      name: \"Normal fit\",\n      type: \"line\",\n      data: normalPdf,\n      smooth: true,\n      symbol: \"none\",\n      itemStyle: { color: t.ink },\n      lineStyle: { color: t.ink, width: 3, type: \"dashed\" },\n      z: 3,\n    },\n  ],\n});\n"}