{"spec_id":"density-rug","library":"echarts","language":"javascript","code":"// anyplot.ai\n// density-rug: Density Plot with Rug Marks\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1103515245 * state + 12345) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction gaussianSample() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Reaction times (ms) from a two-speed mixture of responders — a KDE reveals\n// the bimodal shape that a plain histogram bin width could easily hide, and\n// the rug preserves exactly which trials produced each reading.\nconst sampleSize = 180;\nconst reactionTimesMs = [];\nfor (let i = 0; i < sampleSize; i++) {\n  const isQuickResponder = rand() < 0.55;\n  const mean = isQuickResponder ? 320 : 480;\n  const std = isQuickResponder ? 35 : 50;\n  reactionTimesMs.push(mean + gaussianSample() * std);\n}\n\n// --- Kernel density estimate --------------------------------------------------\nconst n = reactionTimesMs.length;\nconst meanMs = reactionTimesMs.reduce((sum, v) => sum + v, 0) / n;\nconst variance =\n  reactionTimesMs.reduce((sum, v) => sum + (v - meanMs) ** 2, 0) / (n - 1);\nconst stdMs = Math.sqrt(variance);\nconst bandwidth = 1.06 * stdMs * Math.pow(n, -1 / 5); // Silverman's rule of thumb\n\nfunction densityAt(x) {\n  const sum = reactionTimesMs.reduce((acc, xi) => {\n    const u = (x - xi) / bandwidth;\n    return acc + Math.exp(-0.5 * u * u) / Math.sqrt(2 * Math.PI);\n  }, 0);\n  return sum / (n * bandwidth);\n}\n\nconst minObs = Math.min(...reactionTimesMs);\nconst maxObs = Math.max(...reactionTimesMs);\nconst gridStart = minObs - 3 * bandwidth;\nconst gridEnd = maxObs + 3 * bandwidth;\nconst gridSteps = 220;\nconst densityPoints = [];\nfor (let i = 0; i <= gridSteps; i++) {\n  const x = gridStart + ((gridEnd - gridStart) * i) / gridSteps;\n  densityPoints.push([x, densityAt(x)]);\n}\nconst maxDensity = Math.max(...densityPoints.map((p) => p[1]));\n\n// Reserve a band below zero (never rendered as a labelled tick) for the rug.\n// Ticks are shorter than the band and jittered vertically within it so a\n// dense cluster of nearby x-values scatters into visible texture instead of\n// merging into a solid block.\nconst rugBandTop = 0;\nconst rugBandBottom = -maxDensity * 0.16;\nconst yAxisMin = rugBandBottom * 1.1;\nconst yAxisMax = maxDensity * 1.15;\n\nconst rugTickHeight = (rugBandTop - rugBandBottom) * 0.55;\nconst rugJitterRange = rugBandTop - rugBandBottom - rugTickHeight;\nconst jitterRand = makeLcg(7);\nconst rugTickTops = reactionTimesMs.map(() => -jitterRand() * rugJitterRange);\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: \"density-rug · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 28, fontWeight: 500 },\n  },\n  legend: {\n    top: 46,\n    data: [\"KDE density\", \"Individual trials (rug)\"],\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 110, right: 60, top: 110, bottom: 90 },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n  },\n  xAxis: {\n    type: \"value\",\n    name: \"Reaction time (ms)\",\n    nameLocation: \"middle\",\n    nameGap: 46,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: gridStart,\n    max: gridEnd,\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => Math.round(value).toString(),\n    },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Density\",\n    nameLocation: \"middle\",\n    nameGap: 80,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: yAxisMin,\n    max: yAxisMax,\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (value) => (value < 0 ? \"\" : value.toFixed(4)),\n    },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      name: \"KDE density\",\n      type: \"line\",\n      data: densityPoints,\n      smooth: true,\n      symbol: \"none\",\n      lineStyle: { color: t.palette[0], width: 3.5 },\n      areaStyle: { color: t.palette[0], opacity: 0.25 },\n      z: 2,\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        animation: false,\n        lineStyle: { color: t.grid, width: 1 },\n        label: { show: false },\n        data: [{ yAxis: 0 }],\n      },\n    },\n    {\n      name: \"Individual trials (rug)\",\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      data: reactionTimesMs,\n      itemStyle: { color: t.palette[0], opacity: 0.3 },\n      z: 3,\n      renderItem: (params, api) => {\n        const value = api.value(0);\n        const top = rugTickTops[params.dataIndex];\n        const bottom = top - rugTickHeight;\n        const topPx = api.coord([value, top]);\n        const bottomPx = api.coord([value, bottom]);\n        return {\n          type: \"line\",\n          shape: {\n            x1: topPx[0],\n            y1: topPx[1],\n            x2: bottomPx[0],\n            y2: bottomPx[1],\n          },\n          style: {\n            stroke: t.palette[0],\n            lineWidth: 2,\n            opacity: 0.3,\n          },\n        };\n      },\n    },\n  ],\n});\n"}