{"spec_id":"raincloud-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// raincloud-basic: Basic Raincloud Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 96/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Reaction time (ms) in a cognitive task, measured under increasing caffeine\n// dosage. \"Medium Dose\" is a deliberate two-component mixture (responders vs.\n// non-responders) to demonstrate why the cloud (KDE) catches shapes a plain\n// box plot would hide.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst categories = [\"Placebo\", \"Low Dose\", \"Medium Dose\", \"High Dose\", \"Very High Dose\"];\nconst sampleSizes = [70, 65, 90, 60, 55];\n\nconst rawData = [\n  Array.from({ length: sampleSizes[0] }, () => randNormal(520, 42)),\n  Array.from({ length: sampleSizes[1] }, () => randNormal(478, 38)),\n  Array.from({ length: sampleSizes[2] }, () =>\n    rand() < 0.55 ? randNormal(430, 26) : randNormal(505, 30)\n  ),\n  Array.from({ length: sampleSizes[3] }, () => randNormal(452, 48)),\n  Array.from({ length: sampleSizes[4] }, () => randNormal(498, 58)),\n];\n\n// --- Statistics helpers -------------------------------------------------\nfunction quantile(sorted, p) {\n  const idx = p * (sorted.length - 1);\n  const lo = Math.floor(idx);\n  const hi = Math.ceil(idx);\n  if (lo === hi) return sorted[lo];\n  return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);\n}\n\nfunction stdDev(arr) {\n  const mean = arr.reduce((a, b) => a + b, 0) / arr.length;\n  const variance = arr.reduce((a, b) => a + (b - mean) ** 2, 0) / (arr.length - 1);\n  return Math.sqrt(variance);\n}\n\nconst GRID_SIZE = 80;\nconst CLOUD_HEIGHT = 0.45;\nconst BOX_HALF_HEIGHT = 0.07;\nconst RAIN_CENTER_OFFSET = 0.3;\nconst RAIN_JITTER_HALF_WIDTH = 0.08;\n\nconst boxStats = [];\nconst violinData = [];\nconst rainPoints = [];\nlet globalMin = Infinity;\nlet globalMax = -Infinity;\n\nrawData.forEach((data, i) => {\n  const sorted = [...data].sort((a, b) => a - b);\n  const q1 = quantile(sorted, 0.25);\n  const median = quantile(sorted, 0.5);\n  const q3 = quantile(sorted, 0.75);\n  const iqr = q3 - q1;\n  const whiskerLow = Math.max(sorted[0], q1 - 1.5 * iqr);\n  const whiskerHigh = Math.min(sorted[sorted.length - 1], q3 + 1.5 * iqr);\n  boxStats.push({ whiskerLow, q1, median, q3, whiskerHigh });\n\n  const bw = 0.9 * Math.min(stdDev(data), iqr / 1.34) * Math.pow(data.length, -0.2);\n  const gridMin = sorted[0] - 3 * bw;\n  const gridMax = sorted[sorted.length - 1] + 3 * bw;\n  const grid = Array.from(\n    { length: GRID_SIZE },\n    (_, k) => gridMin + ((gridMax - gridMin) * k) / (GRID_SIZE - 1)\n  );\n  const density = grid.map((gx) => {\n    let sum = 0;\n    for (const xi of data) {\n      const u = (gx - xi) / bw;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum / (data.length * bw * Math.sqrt(2 * Math.PI));\n  });\n  const maxDensity = Math.max(...density);\n  violinData.push(grid.map((gx, k) => [gx, density[k] / maxDensity]));\n\n  const jittered = data.map((value) => [\n    value,\n    i - RAIN_CENTER_OFFSET + (rand() * 2 - 1) * RAIN_JITTER_HALF_WIDTH,\n  ]);\n  rainPoints.push(jittered);\n\n  globalMin = Math.min(globalMin, gridMin);\n  globalMax = Math.max(globalMax, gridMax);\n});\n\nconst xPad = (globalMax - globalMin) * 0.04;\nconst xMin = Math.floor((globalMin - xPad) / 25) * 25;\nconst xMax = Math.ceil((globalMax + xPad) / 25) * 25;\n// Integer bounds so the yAxis interval:1 ticks land exactly on category rows.\nconst yMin = -1;\nconst yMax = categories.length;\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Series builders ------------------------------------------------------\nconst cloudSeries = {\n  name: \"cloud\",\n  type: \"custom\",\n  coordinateSystem: \"cartesian2d\",\n  data: categories.map((_, i) => i),\n  renderItem: (params, api) => {\n    const i = params.dataIndex;\n    const base = i + BOX_HALF_HEIGHT;\n    const topPoints = violinData[i].map(([gx, d]) => api.coord([gx, base + d * CLOUD_HEIGHT]));\n    const basePoints = violinData[i].map(([gx]) => api.coord([gx, base])).reverse();\n    const color = t.palette[i % t.palette.length];\n    return {\n      type: \"polygon\",\n      shape: { points: topPoints.concat(basePoints) },\n      style: { fill: color, opacity: 0.5, stroke: color, lineWidth: 1.5 },\n    };\n  },\n  z: 2,\n};\n\nconst boxSeries = {\n  name: \"box\",\n  type: \"custom\",\n  coordinateSystem: \"cartesian2d\",\n  data: categories.map((_, i) => i),\n  renderItem: (params, api) => {\n    const i = params.dataIndex;\n    const s = boxStats[i];\n    const color = t.palette[i % t.palette.length];\n    const top = i + BOX_HALF_HEIGHT;\n    const bottom = i - BOX_HALF_HEIGHT;\n    const q1Top = api.coord([s.q1, top]);\n    const q3Bottom = api.coord([s.q3, bottom]);\n    const whiskerLowStart = api.coord([s.whiskerLow, i]);\n    const whiskerLowEnd = api.coord([s.q1, i]);\n    const whiskerHighStart = api.coord([s.q3, i]);\n    const whiskerHighEnd = api.coord([s.whiskerHigh, i]);\n    const capLowTop = api.coord([s.whiskerLow, i + BOX_HALF_HEIGHT * 0.5]);\n    const capLowBottom = api.coord([s.whiskerLow, i - BOX_HALF_HEIGHT * 0.5]);\n    const capHighTop = api.coord([s.whiskerHigh, i + BOX_HALF_HEIGHT * 0.5]);\n    const capHighBottom = api.coord([s.whiskerHigh, i - BOX_HALF_HEIGHT * 0.5]);\n    const medianTop = api.coord([s.median, top]);\n    const medianBottom = api.coord([s.median, bottom]);\n    const line = (p1, p2) => ({\n      type: \"line\",\n      shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },\n      style: { stroke: color, lineWidth: 2 },\n    });\n    return {\n      type: \"group\",\n      children: [\n        line(whiskerLowStart, whiskerLowEnd),\n        line(whiskerHighStart, whiskerHighEnd),\n        line(capLowTop, capLowBottom),\n        line(capHighTop, capHighBottom),\n        {\n          type: \"rect\",\n          shape: {\n            x: q1Top[0],\n            y: q1Top[1],\n            width: q3Bottom[0] - q1Top[0],\n            height: q3Bottom[1] - q1Top[1],\n          },\n          style: { fill: color, opacity: 0.85, stroke: t.ink, lineWidth: 1.5 },\n        },\n        {\n          type: \"line\",\n          shape: { x1: medianTop[0], y1: medianTop[1], x2: medianBottom[0], y2: medianBottom[1] },\n          style: { stroke: t.pageBg, lineWidth: 2.5 },\n        },\n      ],\n    };\n  },\n  z: 3,\n};\n\nconst rainSeries = categories.map((_, i) => ({\n  name: `rain-${i}`,\n  type: \"scatter\",\n  data: rainPoints[i],\n  symbolSize: 9,\n  itemStyle: { color: t.palette[i % t.palette.length], opacity: 0.6 },\n  z: 4,\n}));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"raincloud-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: { left: 60, right: 60, top: 100, bottom: 80, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    name: \"Reaction Time (ms)\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.ink, fontSize: 15 },\n    min: xMin,\n    max: xMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14, showMinLabel: false, showMaxLabel: false },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    min: yMin,\n    max: yMax,\n    interval: 1,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 15,\n      formatter: (val) =>\n        Math.abs(val - Math.round(val)) < 1e-6 && categories[Math.round(val)]\n          ? categories[Math.round(val)]\n          : \"\",\n    },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [cloudSeries, boxSeries, ...rainSeries],\n});\n"}