{"spec_id":"cat-box-strip","library":"echarts","language":"javascript","code":"// anyplot.ai\n// cat-box-strip: Box Plot with Strip Overlay\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller gaussian -------------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussian(mean, std) {\n  const u1 = Math.max(lcg(), 1e-12);\n  const u2 = lcg();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// --- Box-plot summary stats (Tukey whiskers, 1.5x IQR fence) ---------------\nfunction boxStats(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const quantile = (p) => {\n    const pos = (sorted.length - 1) * p;\n    const lo = Math.floor(pos);\n    const hi = Math.ceil(pos);\n    return lo === hi\n      ? sorted[lo]\n      : sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);\n  };\n  const q1 = quantile(0.25);\n  const median = quantile(0.5);\n  const q3 = quantile(0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const withinFence = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  const whiskerMin = withinFence.length ? withinFence[0] : sorted[0];\n  const whiskerMax = withinFence.length\n    ? withinFence[withinFence.length - 1]\n    : sorted[sorted.length - 1];\n  return [whiskerMin, q1, median, q3, whiskerMax];\n}\n\n// --- Data: marathon finish times (minutes) by runner age group -------------\nconst ageGroups = [\"20-29\", \"30-39\", \"40-49\", \"50-59\", \"60+\"];\nconst meansByGroup = [245, 252, 268, 288, 315];\nconst stdsByGroup = [28, 30, 32, 34, 36];\nconst countsByGroup = [58, 65, 62, 45, 34];\n\nconst rawByGroup = ageGroups.map((_, i) => {\n  const values = [];\n  for (let j = 0; j < countsByGroup[i]; j++) {\n    values.push(Math.max(150, gaussian(meansByGroup[i], stdsByGroup[i])));\n  }\n  return values;\n});\n\nconst boxData = rawByGroup.map(boxStats);\nconst medians = boxData.map((d) => d[2]);\nconst medianDelta = Math.round(medians[medians.length - 1] - medians[0]);\n\n// Strip overlay: every individual runner, jittered around its category slot\nconst stripPoints = [];\nrawByGroup.forEach((values, catIndex) => {\n  values.forEach((v) => {\n    const jitter = (lcg() - 0.5) * 0.72;\n    stripPoints.push([catIndex + jitter, v]);\n  });\n});\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: \"cat-box-strip · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Distribution\", \"Runners\"],\n    top: 50,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: { left: 100, right: 60, top: 110, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: ageGroups,\n    name: \"Runner Age Group\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    scale: true,\n    name: \"Finish Time (minutes)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Distribution\",\n      type: \"boxplot\",\n      data: boxData,\n      boxWidth: [20, 40],\n      itemStyle: {\n        color: \"transparent\",\n        borderColor: t.palette[0],\n        borderWidth: 2.5,\n      },\n      markLine: {\n        silent: true,\n        symbol: [\"none\", \"none\"],\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        label: {\n          show: true,\n          color: t.inkSoft,\n          fontSize: 12,\n          position: \"middle\",\n          formatter: `Median +${medianDelta} min (20-29 → 60+)`,\n        },\n        data: [\n          [\n            { coord: [ageGroups[0], medians[0]] },\n            { coord: [ageGroups[ageGroups.length - 1], medians[medians.length - 1]] },\n          ],\n        ],\n      },\n      z: 2,\n    },\n    {\n      name: \"Runners\",\n      type: \"scatter\",\n      data: stripPoints,\n      symbolSize: 7,\n      itemStyle: { color: t.palette[0], opacity: 0.4 },\n      z: 3,\n      silent: true,\n    },\n  ],\n});\n"}