{"spec_id":"box-grouped","library":"echarts","language":"javascript","code":"// anyplot.ai\n// box-grouped: Grouped Box Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Simple LCG + Box-Muller so the sample data is reproducible without Math.random().\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// NOTE: echarts.dataTool.prepareBoxplotData is unavailable here -- the harness\n// loads only the core `echarts` bundle, not the separate dataTool extension\n// that mounts it, so quartiles/fences are computed by hand below.\nfunction quantile(sorted, q) {\n  const pos = (sorted.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return sorted[base + 1] !== undefined\n    ? sorted[base] + rest * (sorted[base + 1] - sorted[base])\n    : sorted[base];\n}\n\nfunction boxStats(values) {\n  const sorted = values.slice().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 inliers = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = sorted.filter((v) => v < lowerFence || v > upperFence);\n  return {\n    whiskerLow: inliers.length ? inliers[0] : q1,\n    q1,\n    median,\n    q3,\n    whiskerHigh: inliers.length ? inliers[inliers.length - 1] : q3,\n    outliers,\n  };\n}\n\nconst N_SAMPLES = 45;\nconst departments = [\"Engineering\", \"Sales\", \"Marketing\", \"Support\"];\nconst levels = [\"Junior\", \"Mid\", \"Senior\"];\nconst levelBaseline = { Junior: 60, Mid: 73, Senior: 84 };\nconst levelSpread = { Junior: 16, Mid: 11, Senior: 6 };\nconst deptOffset = { Engineering: 4, Sales: -3, Marketing: 1, Support: -6 };\nconst middleLevelIndex = Math.floor((levels.length - 1) / 2);\n\nconst categories = [];\nconst boxItems = [];\nconst outlierItems = [];\n// Dashed line connecting each department's Junior->Mid->Senior medians, so the\n// \"scores rise and tighten with seniority\" trend is visible at a glance\n// instead of something the viewer has to piece together box-by-box.\nconst medianTrend = [];\n\ndepartments.forEach((dept, i) => {\n  levels.forEach((level, j) => {\n    const samples = [];\n    for (let k = 0; k < N_SAMPLES; k++) {\n      const raw = randNormal(\n        levelBaseline[level] + deptOffset[dept],\n        levelSpread[level],\n      );\n      samples.push(Math.min(100, Math.max(0, raw)));\n    }\n    const stats = boxStats(samples);\n    const catIndex = categories.length;\n    categories.push(j === middleLevelIndex ? dept : \"\");\n    boxItems.push({\n      name: `${level} · ${dept}`,\n      value: [\n        stats.whiskerLow,\n        stats.q1,\n        stats.median,\n        stats.q3,\n        stats.whiskerHigh,\n      ],\n      itemStyle: {\n        color: t.elevatedBg,\n        borderColor: t.palette[j],\n        borderWidth: 2.5,\n      },\n    });\n    medianTrend.push(stats.median);\n    stats.outliers.forEach((v) => {\n      outlierItems.push({\n        name: `${level} · ${dept} outlier`,\n        value: [catIndex, v],\n        itemStyle: {\n          color: t.palette[j],\n          borderColor: t.pageBg,\n          borderWidth: 1,\n        },\n      });\n    });\n  });\n  // Gap tick between department groups so the side-by-side boxes read as clusters.\n  // NaN values (rather than a bare null item) keep echarts' box-plot data\n  // preprocessing happy while still making data.hasValue() skip the render.\n  if (i < departments.length - 1) {\n    categories.push(\"\");\n    boxItems.push({ value: [NaN, NaN, NaN, NaN, NaN] });\n    medianTrend.push(null);\n  }\n});\n\nconst title = \"box-grouped · javascript · echarts · anyplot.ai\";\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    subtext: \"Scores rise and tighten from Junior to Senior in every department\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  legend: {\n    data: levels,\n    top: 92,\n    itemWidth: 18,\n    itemHeight: 14,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (params) => {\n      if (params.seriesType === \"boxplot\") {\n        const [low, q1, median, q3, high] = params.data.value;\n        return [\n          `<strong>${params.name}</strong>`,\n          `Max: ${high.toFixed(1)}`,\n          `Q3: ${q3.toFixed(1)}`,\n          `Median: ${median.toFixed(1)}`,\n          `Q1: ${q1.toFixed(1)}`,\n          `Min: ${low.toFixed(1)}`,\n        ].join(\"<br/>\");\n      }\n      if (params.seriesName === \"Outliers\") {\n        return `<strong>${params.name}</strong><br/>Score: ${params.data.value[1].toFixed(1)}`;\n      }\n      return params.name;\n    },\n  },\n  grid: { left: 130, right: 70, top: 158, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: categories,\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 0 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Performance Score (0-100)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 100,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    ...levels.map((level, j) => ({\n      name: level,\n      type: \"scatter\",\n      data: [],\n      symbol: \"rect\",\n      symbolSize: [16, 14],\n      itemStyle: { color: t.palette[j] },\n    })),\n    {\n      name: \"Distribution\",\n      type: \"boxplot\",\n      data: boxItems,\n      boxWidth: [\"45%\", \"68%\"],\n    },\n    {\n      name: \"Outliers\",\n      type: \"scatter\",\n      data: outlierItems,\n      symbolSize: 12,\n      z: 3,\n    },\n    {\n      name: \"Median trend\",\n      type: \"line\",\n      data: medianTrend,\n      connectNulls: false,\n      symbol: \"none\",\n      silent: true,\n      z: 1,\n      lineStyle: { color: t.inkSoft, width: 1.5, type: \"dashed\", opacity: 0.6 },\n      tooltip: { show: false },\n    },\n  ],\n});\n"}