{"spec_id":"box-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// box-basic: Basic Box Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Test scores (%) across 5 classes — Imprint palette positions 1-5, canonical\n// order (abstract class labels carry no semantic color cue).\nfunction makeLcg(seed) {\n  let state = seed % 2147483647;\n  if (state <= 0) state += 2147483646;\n  return function uniform() {\n    state = (state * 16807) % 2147483647;\n    return (state - 1) / 2147483646;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal(mean, std) {\n  const u1 = rand();\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 classes = [\n  { name: \"Class A\", n: 65, mean: 76, std: 9, extra: [32, 99] },\n  { name: \"Class B\", n: 72, mean: 83, std: 6, extra: [] },\n  { name: \"Class C\", n: 60, mean: 71, std: 11, extra: [28] },\n  { name: \"Class D\", n: 88, mean: 85, std: 5.5, extra: [58, 99.5] },\n  { name: \"Class E\", n: 68, mean: 79, std: 8.5, extra: [] },\n];\n\nfunction percentile(sorted, p) {\n  const idx = (sorted.length - 1) * p;\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\nconst categoryNames = classes.map((c) => c.name);\nconst boxData = [];\nconst outlierData = [];\nconst allScores = [];\nconst iqrByClass = [];\nconst upperWhiskerByClass = [];\n\nclasses.forEach((cls, catIndex) => {\n  const scores = [];\n  for (let i = 0; i < cls.n; i++) {\n    scores.push(Math.round(Math.min(100, Math.max(0, randNormal(cls.mean, cls.std))) * 10) / 10);\n  }\n  cls.extra.forEach((v) => scores.push(v));\n  scores.sort((a, b) => a - b);\n  allScores.push(...scores);\n\n  const q1 = percentile(scores, 0.25);\n  const median = percentile(scores, 0.5);\n  const q3 = percentile(scores, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n\n  const inliers = scores.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = scores.filter((v) => v < lowerFence || v > upperFence);\n\n  iqrByClass.push(iqr);\n  upperWhiskerByClass.push(inliers[inliers.length - 1]);\n\n  const color = t.palette[catIndex];\n  boxData.push({\n    value: [inliers[0], q1, median, q3, inliers[inliers.length - 1]],\n    itemStyle: { color: t.elevatedBg, borderColor: color, borderWidth: 3 },\n  });\n  outliers.forEach((v) => {\n    outlierData.push({\n      value: [catIndex, v],\n      itemStyle: { color: color, opacity: 0.85, borderColor: t.pageBg, borderWidth: 1 },\n    });\n  });\n});\n\n// Data storytelling: call out the class with the widest spread (largest IQR)\n// with a bolder border plus an annotation, and add an ECharts markLine\n// referencing the overall mean across all classes.\nconst widestIdx = iqrByClass.indexOf(Math.max(...iqrByClass));\nboxData[widestIdx].itemStyle.borderWidth = 5;\nconst overallMean = allScores.reduce((sum, v) => sum + v, 0) / allScores.length;\nconst calloutData = [\n  {\n    value: [widestIdx, upperWhiskerByClass[widestIdx]],\n    label: {\n      show: true,\n      formatter: `Widest spread (IQR ${iqrByClass[widestIdx].toFixed(1)})`,\n      position: \"top\",\n      color: t.ink,\n      fontSize: 14,\n      fontWeight: 600,\n    },\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: \"box-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 24, fontWeight: 500 },\n  },\n  grid: { left: 90, right: 60, top: 100, bottom: 80 },\n  xAxis: {\n    type: \"category\",\n    data: categoryNames,\n    boundaryGap: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Test Score (%)\",\n    nameLocation: \"middle\",\n    nameGap: 55,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 100,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Score distribution\",\n      type: \"boxplot\",\n      data: boxData,\n      boxWidth: [24, 60],\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        label: {\n          formatter: `Overall mean: ${overallMean.toFixed(1)}%`,\n          color: t.inkSoft,\n          fontSize: 13,\n          position: \"insideEndTop\",\n        },\n        data: [{ yAxis: overallMean }],\n      },\n    },\n    {\n      name: \"Outliers\",\n      type: \"scatter\",\n      data: outlierData,\n      symbolSize: 13,\n    },\n    {\n      name: \"Spread callout\",\n      type: \"scatter\",\n      data: calloutData,\n      symbolSize: 0,\n      silent: true,\n      tooltip: { show: false },\n    },\n  ],\n});\n"}