{"spec_id":"cat-box-strip","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// cat-box-strip: Box Plot with Strip Overlay\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------\nlet lcgState = 42;\nfunction nextRandom() {\n  lcgState = (lcgState * 1103515245 + 12345) % 2147483648;\n  return lcgState / 2147483648;\n}\nfunction nextGaussian() {\n  const u1 = Math.max(nextRandom(), 1e-9);\n  const u2 = nextRandom();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\nfunction quantile(sortedValues, p) {\n  const idx = p * (sortedValues.length - 1);\n  const lower = Math.floor(idx);\n  const upper = Math.ceil(idx);\n  if (lower === upper) return sortedValues[lower];\n  return (\n    sortedValues[lower] +\n    (sortedValues[upper] - sortedValues[lower]) * (idx - lower)\n  );\n}\n\nconst categories = [\"Low Light\", \"Medium Light\", \"High Light\", \"Full Sun\"];\nconst means = [12, 22, 34, 41];\nconst sds = [3, 4, 5, 6];\nconst sampleSize = 80;\n\nconst groupValues = categories.map((_, i) => {\n  const values = [];\n  for (let j = 0; j < sampleSize; j++) {\n    values.push(Math.max(1, means[i] + sds[i] * nextGaussian()));\n  }\n  return values;\n});\n\nconst stats = groupValues.map((values) => {\n  const sorted = [...values].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 lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const withinLower = sorted.filter((v) => v >= lowerFence);\n  const withinUpper = sorted.filter((v) => v <= upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerMin: withinLower.length ? withinLower[0] : sorted[0],\n    whiskerMax: withinUpper.length\n      ? withinUpper[withinUpper.length - 1]\n      : sorted[sorted.length - 1],\n  };\n});\n\nconst globalMin = Math.min(...groupValues.flat());\nconst globalMax = Math.max(...groupValues.flat());\nconst medianEpsilon = (globalMax - globalMin) * 0.01;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart (floating bars for box/whisker/median, scatter for strip) --------\nconst whiskerDataset = {\n  type: \"bar\",\n  label: \"Whisker range\",\n  data: stats.map((s, i) => ({ x: i, y: [s.whiskerMin, s.whiskerMax] })),\n  backgroundColor: (ctx) =>\n    hexToRgba(t.palette[ctx.dataIndex % t.palette.length], 0.5),\n  barThickness: 6,\n  borderWidth: 0,\n  grouped: false,\n};\n\nconst boxDataset = {\n  type: \"bar\",\n  label: \"Interquartile range\",\n  data: stats.map((s, i) => ({ x: i, y: [s.q1, s.q3] })),\n  backgroundColor: (ctx) =>\n    hexToRgba(t.palette[ctx.dataIndex % t.palette.length], 0.35),\n  borderColor: (ctx) => t.palette[ctx.dataIndex % t.palette.length],\n  borderWidth: 2,\n  borderSkipped: false,\n  barThickness: 90,\n  grouped: false,\n};\n\nconst medianDataset = {\n  type: \"bar\",\n  label: \"Median\",\n  data: stats.map((s, i) => ({\n    x: i,\n    y: [s.median - medianEpsilon, s.median + medianEpsilon],\n  })),\n  backgroundColor: t.ink,\n  borderWidth: 0,\n  borderSkipped: false,\n  barThickness: 90,\n  grouped: false,\n};\n\nconst stripDatasets = categories.map((category, i) => ({\n  type: \"scatter\",\n  label: category,\n  data: groupValues[i].map((value) => ({\n    x: i + (nextRandom() - 0.5) * 0.36,\n    y: value,\n  })),\n  backgroundColor: hexToRgba(t.palette[i % t.palette.length], 0.45),\n  pointRadius: 3.5,\n  pointHoverRadius: 3.5,\n  pointBorderWidth: 1,\n  pointBorderColor: t.pageBg,\n}));\n\n// Whisker end caps: short horizontal strokes at whiskerMin/whiskerMax so the\n// range reads unambiguously as a box-plot whisker rather than a plain bar.\nconst capDataset = {\n  type: \"scatter\",\n  label: \"Whisker caps\",\n  data: stats.flatMap((s, i) => [\n    { x: i, y: s.whiskerMin },\n    { x: i, y: s.whiskerMax },\n  ]),\n  pointStyle: \"line\",\n  pointRotation: 0,\n  pointRadius: 20,\n  pointBorderColor: (ctx) =>\n    t.palette[Math.floor(ctx.dataIndex / 2) % t.palette.length],\n  pointBorderWidth: 2.5,\n  showLine: false,\n};\n\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    datasets: [\n      whiskerDataset,\n      boxDataset,\n      medianDataset,\n      ...stripDatasets,\n      capDataset,\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"cat-box-strip · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.6,\n        max: categories.length - 1 + 0.6,\n        ticks: {\n          stepSize: 1,\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) =>\n            categories[value] !== undefined ? categories[value] : \"\",\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Light Condition\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Plant Height (cm)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n});\n"}