{"spec_id":"box-notched","library":"echarts","language":"javascript","code":"// anyplot.ai\n// box-notched: Notched Box Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny LCG so the browser (no seeded Math.random) still reproduces the same\n// salary samples on every render.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rng, mean, std) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nfunction quantile(sortedValues, p) {\n  const pos = (sortedValues.length - 1) * p;\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 computeBoxStats(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const n = sorted.length;\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  const notchHalfWidth = (1.57 * iqr) / Math.sqrt(n);\n  return {\n    n,\n    q1,\n    median,\n    q3,\n    whiskerLow: inliers.length ? inliers[0] : sorted[0],\n    whiskerHigh: inliers.length ? inliers[inliers.length - 1] : sorted[n - 1],\n    notchLow: median - notchHalfWidth,\n    notchHigh: median + notchHalfWidth,\n    outliers,\n  };\n}\n\nconst departments = [\n  { name: \"Engineering\", n: 60, mean: 95000, std: 12000 },\n  { name: \"Product\", n: 55, mean: 88000, std: 11000 },\n  { name: \"Sales\", n: 70, mean: 82000, std: 15000 },\n  { name: \"Support\", n: 50, mean: 65000, std: 9000 },\n  { name: \"Finance\", n: 45, mean: 91000, std: 10000 },\n];\n\nconst rng = makeLcg(42);\nconst boxStats = departments.map((dept, idx) => {\n  const salaries = Array.from({ length: dept.n }, () => randomNormal(rng, dept.mean, dept.std));\n  return { idx, name: dept.name, ...computeBoxStats(salaries) };\n});\n\nconst boxSeriesData = boxStats.map((s) => [\n  s.idx,\n  s.whiskerLow,\n  s.q1,\n  s.notchLow,\n  s.median,\n  s.notchHigh,\n  s.q3,\n  s.whiskerHigh,\n]);\n\nconst outlierData = boxStats.flatMap((s) =>\n  s.outliers.map((value) => ({\n    value: [s.name, value],\n    itemStyle: { color: t.palette[s.idx % t.palette.length] },\n  })),\n);\n\n// --- Custom renderer: notched box + whiskers --------------------------------\n// ECharts' built-in \"boxplot\" series has no notch support, so the box +\n// whiskers + notch geometry is drawn by hand with a \"custom\" series, following\n// the pixel-space offset pattern from ECharts' own custom-boxplot example\n// (offsets computed via api.size, not in data space, so the shape stays\n// crisp regardless of axis scale).\nfunction renderNotchedBox(params, api) {\n  const category = api.value(0);\n  const whiskerLow = api.value(1);\n  const q1 = api.value(2);\n  const notchLow = api.value(3);\n  const median = api.value(4);\n  const notchHigh = api.value(5);\n  const q3 = api.value(6);\n  const whiskerHigh = api.value(7);\n\n  const categoryWidth = api.size([1, 0])[0];\n  const halfWidth = categoryWidth * 0.3;\n  const notchWidth = halfWidth * 0.45;\n  const capWidth = halfWidth * 0.4;\n\n  const coordAt = (value) => api.coord([category, value]);\n  const cx = coordAt(median)[0];\n  const yWhiskerLow = coordAt(whiskerLow)[1];\n  const yQ1 = coordAt(q1)[1];\n  const yNotchLow = coordAt(notchLow)[1];\n  const yMedian = coordAt(median)[1];\n  const yNotchHigh = coordAt(notchHigh)[1];\n  const yQ3 = coordAt(q3)[1];\n  const yWhiskerHigh = coordAt(whiskerHigh)[1];\n\n  const x0 = cx - halfWidth;\n  const x1 = cx + halfWidth;\n  const xN0 = cx - notchWidth;\n  const xN1 = cx + notchWidth;\n\n  const color = t.palette[category % t.palette.length];\n  const lineStyle = { stroke: color, lineWidth: 2 };\n\n  return {\n    type: \"group\",\n    children: [\n      { type: \"line\", shape: { x1: cx, y1: yQ3, x2: cx, y2: yWhiskerHigh }, style: lineStyle },\n      {\n        type: \"line\",\n        shape: { x1: cx - capWidth, y1: yWhiskerHigh, x2: cx + capWidth, y2: yWhiskerHigh },\n        style: lineStyle,\n      },\n      { type: \"line\", shape: { x1: cx, y1: yQ1, x2: cx, y2: yWhiskerLow }, style: lineStyle },\n      {\n        type: \"line\",\n        shape: { x1: cx - capWidth, y1: yWhiskerLow, x2: cx + capWidth, y2: yWhiskerLow },\n        style: lineStyle,\n      },\n      {\n        type: \"polygon\",\n        shape: {\n          points: [\n            [x0, yQ3],\n            [x1, yQ3],\n            [x1, yNotchHigh],\n            [xN1, yMedian],\n            [x1, yNotchLow],\n            [x1, yQ1],\n            [x0, yQ1],\n            [x0, yNotchLow],\n            [xN0, yMedian],\n            [x0, yNotchHigh],\n          ],\n        },\n        style: { fill: color, fillOpacity: 0.75, stroke: color, lineWidth: 2 },\n      },\n      {\n        type: \"line\",\n        shape: { x1: xN0, y1: yMedian, x2: xN1, y2: yMedian },\n        style: { stroke: t.ink, lineWidth: 2.5 },\n      },\n    ],\n  };\n}\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: \"box-notched · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  grid: { left: 130, right: 60, top: 130, bottom: 90 },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) =>\n      Array.isArray(p.value) && p.value.length === 8\n        ? `${departments[p.value[0]].name}<br/>Q3: $${Math.round(p.value[6]).toLocaleString()}<br/>Median: $${Math.round(p.value[4]).toLocaleString()}<br/>Q1: $${Math.round(p.value[2]).toLocaleString()}`\n        : `Salary: $${Math.round(p.value[1]).toLocaleString()}`,\n  },\n  xAxis: {\n    type: \"category\",\n    data: departments.map((d) => d.name),\n    name: \"Department\",\n    nameLocation: \"middle\",\n    nameGap: 50,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 15 },\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: \"Annual Salary (USD)\",\n    nameLocation: \"middle\",\n    nameGap: 90,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (v) => `$${Math.round(v / 1000)}k`,\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"custom\",\n      name: \"Salary Distribution\",\n      renderItem: renderNotchedBox,\n      data: boxSeriesData,\n      encode: { x: 0, y: [1, 2, 3, 4, 5, 6, 7] },\n      z: 10,\n    },\n    {\n      type: \"scatter\",\n      name: \"Outliers\",\n      data: outlierData,\n      symbolSize: 10,\n      itemStyle: { borderColor: t.pageBg, borderWidth: 1.5 },\n      z: 20,\n    },\n  ],\n});\n"}