{"spec_id":"raincloud-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// raincloud-basic: Basic Raincloud Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: reaction times (ms) across a 4-arm dosage trial -----------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function random() {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst random = makeLcg(42);\n\nfunction randomNormal(mean, std) {\n  const u1 = Math.max(random(), 1e-9);\n  const u2 = random();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst groups = [\n  // A few attention-lapse (slow) and anticipatory (fast) trials beyond the\n  // whisker fences, as seen in real reaction-time data.\n  { name: \"Placebo\", mean: 430, std: 50, n: 136, outliers: [138, 148, 612, 628] },\n  { name: \"Low Dose\", mean: 400, std: 50, n: 140 },\n  { name: \"Medium Dose\", mean: 360, std: 48, n: 140 },\n  // ~18% of subjects are non-responders whose reaction time stays near the\n  // placebo level, producing a genuinely bimodal distribution.\n  { name: \"High Dose\", mean: 305, std: 40, n: 140, mixture: { weight: 0.18, mean: 415, std: 35 } },\n];\n\nconst categories = groups.map((group, index) => {\n  const generated = Array.from({ length: group.n }, () => {\n    const raw =\n      group.mixture && random() < group.mixture.weight\n        ? randomNormal(group.mixture.mean, group.mixture.std)\n        : randomNormal(group.mean, group.std);\n    return Math.max(120, raw);\n  });\n  const values = generated.concat(group.outliers ?? []).sort((a, b) => a - b);\n  return { name: group.name, index, values };\n});\n\n// --- Statistics helpers ------------------------------------------------------\nfunction quantile(sortedValues, q) {\n  const pos = (sortedValues.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return sortedValues[base + 1] !== undefined\n    ? sortedValues[base] + rest * (sortedValues[base + 1] - sortedValues[base])\n    : sortedValues[base];\n}\n\nfunction gaussianKde(values, gridPoints, bandwidth) {\n  const norm = values.length * bandwidth * Math.sqrt(2 * Math.PI);\n  return gridPoints.map((x) => {\n    let sum = 0;\n    for (const v of values) {\n      const u = (x - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum / norm;\n  });\n}\n\n// --- Cloud (half-violin) and rain (jittered strip) series ------------------\nconst CLOUD_HEIGHT = 0.32;\nconst RAIN_CENTER = 0.2;\nconst RAIN_JITTER = 0.08;\nconst BOX_HALF_HEIGHT = 0.08;\n\nconst cloudSeries = categories.map((cat) => {\n  const q1 = quantile(cat.values, 0.25);\n  const q3 = quantile(cat.values, 0.75);\n  const mean = cat.values.reduce((sum, v) => sum + v, 0) / cat.values.length;\n  const std = Math.sqrt(cat.values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / cat.values.length);\n  const bandwidth = 0.9 * Math.min(std, (q3 - q1) / 1.34) * cat.values.length ** -0.2;\n  const min = cat.values[0] - 2 * bandwidth;\n  const max = cat.values[cat.values.length - 1] + 2 * bandwidth;\n  const steps = 60;\n  const grid = Array.from({ length: steps + 1 }, (_, i) => min + ((max - min) * i) / steps);\n  const density = gaussianKde(cat.values, grid, bandwidth);\n  const maxDensity = Math.max(...density);\n  return {\n    type: \"area\",\n    name: `${cat.name} distribution`,\n    data: grid.map((x, i) => [x, cat.index + (density[i] / maxDensity) * CLOUD_HEIGHT]),\n    threshold: cat.index,\n    color: t.palette[cat.index % t.palette.length],\n    fillOpacity: 0.35,\n    lineWidth: 1.5,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    zIndex: 1,\n  };\n});\n\nconst rainSeries = categories.map((cat) => ({\n  type: \"scatter\",\n  name: `${cat.name} observations`,\n  data: cat.values.map((v) => [v, cat.index - RAIN_CENTER + (random() - 0.5) * 2 * RAIN_JITTER]),\n  color: t.palette[cat.index % t.palette.length],\n  opacity: 0.5,\n  marker: { radius: 2.5, symbol: \"circle\", lineWidth: 0 },\n  enableMouseTracking: false,\n  showInLegend: false,\n  zIndex: 2,\n}));\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load: function drawBoxPlots() {\n        const chart = this;\n        const xAxis = chart.xAxis[0];\n        const yAxis = chart.yAxis[0];\n        const capHalf = BOX_HALF_HEIGHT * 0.5;\n\n        categories.forEach((cat) => {\n          const values = cat.values;\n          const q1 = quantile(values, 0.25);\n          const median = quantile(values, 0.5);\n          const q3 = quantile(values, 0.75);\n          const iqr = q3 - q1;\n          const lowerFence = q1 - 1.5 * iqr;\n          const upperFence = q3 + 1.5 * iqr;\n          const withinFence = values.filter((v) => v >= lowerFence && v <= upperFence);\n          const whiskerMin = withinFence[0];\n          const whiskerMax = withinFence[withinFence.length - 1];\n          const color = t.palette[cat.index % t.palette.length];\n\n          const yMid = yAxis.toPixels(cat.index);\n          const yTop = yAxis.toPixels(cat.index + BOX_HALF_HEIGHT);\n          const yBottom = yAxis.toPixels(cat.index - BOX_HALF_HEIGHT);\n          const yCapTop = yAxis.toPixels(cat.index + capHalf);\n          const yCapBottom = yAxis.toPixels(cat.index - capHalf);\n          const xQ1 = xAxis.toPixels(q1);\n          const xQ3 = xAxis.toPixels(q3);\n          const xMedian = xAxis.toPixels(median);\n          const xWhiskerMin = xAxis.toPixels(whiskerMin);\n          const xWhiskerMax = xAxis.toPixels(whiskerMax);\n\n          chart.renderer\n            .path([\"M\", xWhiskerMin, yMid, \"L\", xQ1, yMid])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 1.5, zIndex: 3 })\n            .add();\n          chart.renderer\n            .path([\"M\", xQ3, yMid, \"L\", xWhiskerMax, yMid])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 1.5, zIndex: 3 })\n            .add();\n          chart.renderer\n            .path([\"M\", xWhiskerMin, yCapTop, \"L\", xWhiskerMin, yCapBottom])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 1.5, zIndex: 3 })\n            .add();\n          chart.renderer\n            .path([\"M\", xWhiskerMax, yCapTop, \"L\", xWhiskerMax, yCapBottom])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 1.5, zIndex: 3 })\n            .add();\n          chart.renderer\n            .rect(Math.min(xQ1, xQ3), yTop, Math.abs(xQ3 - xQ1), yBottom - yTop)\n            .attr({ fill: t.pageBg, stroke: color, \"stroke-width\": 2, zIndex: 4 })\n            .add();\n          chart.renderer\n            .path([\"M\", xMedian, yTop, \"L\", xMedian, yBottom])\n            .attr({ stroke: color, \"stroke-width\": 2.5, zIndex: 5 })\n            .add();\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"raincloud-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    title: { text: \"Reaction Time (ms)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: null },\n    min: -0.5,\n    max: categories.length - 1 + 0.5,\n    startOnTick: false,\n    endOnTick: false,\n    tickPositions: categories.map((cat) => cat.index),\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    tickWidth: 1,\n    tickLength: 6,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter: function formatCategoryLabel() {\n        return categories[this.value] ? categories[this.value].name : \"\";\n      },\n    },\n  },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [...cloudSeries, ...rainSeries],\n});\n"}