{"spec_id":"boxen-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// boxen-basic: Basic Boxen Plot (Letter-Value Plot)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-01\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME === \"dark\" ? \"dark\" : \"light\";\nconst INK_MUTED = THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\"; // Imprint \"muted\" anchor — theme-adaptive, not in ANYPLOT_TOKENS\n\n// --- Deterministic PRNG (LCG + Box-Muller) -----------------------------------\nfunction makeLcg(seed) {\n  let state = seed;\n  return function lcg() {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nfunction randNormal(rand) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: response times (ms) per API endpoint, right-skewed --------------\nconst endpoints = [\n  { name: \"/api/search\", mu: 4.6, sigma: 0.45 },\n  { name: \"/api/checkout\", mu: 5.0, sigma: 0.35 },\n  { name: \"/api/orders\", mu: 4.3, sigma: 0.55 },\n  { name: \"/api/users\", mu: 3.9, sigma: 0.3 },\n];\nconst N = 2000;\nconst rand = makeLcg(42);\nconst datasets = endpoints.map(({ mu, sigma }) => {\n  const values = [];\n  for (let i = 0; i < N; i++) {\n    values.push(Math.exp(mu + sigma * randNormal(rand)));\n  }\n  values.sort((a, b) => a - b);\n  return values;\n});\n\n// --- Letter-value statistics --------------------------------------------------\n// 4 nested levels (quartiles -> 32nds) is enough to reveal tail shape without\n// crowding the legend; deeper levels would add boxes too thin to read at this size.\nconst LEVEL_META = [\n  { label: \"Quartiles (25–75%)\", widthFrac: 0.55, opacity: 1.0 },\n  { label: \"Eighths (12.5–87.5%)\", widthFrac: 0.42, opacity: 0.75 },\n  { label: \"Sixteenths (6.25–93.75%)\", widthFrac: 0.3, opacity: 0.55 },\n  { label: \"32nds (3.1–96.9%)\", widthFrac: 0.2, opacity: 0.4 },\n];\nconst LEVEL_COUNT = LEVEL_META.length;\n\nfunction letterValues(sorted) {\n  const n = sorted.length;\n  const medianDepth = (n + 1) / 2;\n  const median =\n    n % 2 === 0\n      ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2\n      : sorted[Math.floor(medianDepth) - 1];\n  const levels = [];\n  let depth = medianDepth;\n  for (let k = 0; k < LEVEL_COUNT; k++) {\n    depth = (Math.floor(depth) + 1) / 2;\n    const lowIdx = Math.max(0, Math.floor(depth) - 1);\n    const highIdx = Math.min(n - 1, n - Math.floor(depth));\n    levels.push({ low: sorted[lowIdx], high: sorted[highIdx] });\n  }\n  return { median, levels };\n}\nconst stats = datasets.map(letterValues);\n\n// --- Custom-series renderItem implementations --------------------------------\nfunction renderBox(params, api) {\n  const categoryIndex = api.value(0);\n  const low = api.value(1);\n  const high = api.value(2);\n  const widthFrac = api.value(3);\n  const bandWidth = api.size([1, 0])[0];\n  const boxWidth = bandWidth * widthFrac;\n  const lowPoint = api.coord([categoryIndex, low]);\n  const highPoint = api.coord([categoryIndex, high]);\n  return {\n    type: \"rect\",\n    shape: {\n      x: lowPoint[0] - boxWidth / 2,\n      y: highPoint[1],\n      width: boxWidth,\n      height: Math.max(lowPoint[1] - highPoint[1], 1),\n    },\n    style: api.style(),\n  };\n}\nfunction renderMedian(params, api) {\n  const categoryIndex = api.value(0);\n  const value = api.value(1);\n  const widthFrac = api.value(2);\n  const bandWidth = api.size([1, 0])[0];\n  const boxWidth = bandWidth * widthFrac;\n  const center = api.coord([categoryIndex, value]);\n  const thickness = 4;\n  return {\n    type: \"rect\",\n    shape: {\n      x: center[0] - boxWidth / 2,\n      y: center[1] - thickness / 2,\n      width: boxWidth,\n      height: thickness,\n    },\n    style: api.style(),\n  };\n}\nfunction renderOutlier(params, api) {\n  const categoryIndex = api.value(0);\n  const value = api.value(1);\n  const jitter = api.value(2);\n  const bandWidth = api.size([1, 0])[0];\n  const center = api.coord([categoryIndex, value]);\n  return {\n    type: \"circle\",\n    shape: { cx: center[0] + jitter * bandWidth * 0.28, cy: center[1], r: 4 },\n    style: api.style(),\n  };\n}\n\n// --- Series data ---------------------------------------------------------------\n// Boxes are pushed narrowest-first so the widest (quartile) box paints last and\n// sits on top, producing the classic stepped/nested letter-value silhouette.\nconst boxSeries = [];\nfor (let levelIdx = LEVEL_COUNT - 1; levelIdx >= 0; levelIdx--) {\n  const meta = LEVEL_META[levelIdx];\n  boxSeries.push({\n    name: meta.label,\n    type: \"custom\",\n    renderItem: renderBox,\n    itemStyle: { color: t.palette[0], opacity: meta.opacity },\n    data: stats.map((s, catIdx) => [\n      catIdx,\n      s.levels[levelIdx].low,\n      s.levels[levelIdx].high,\n      meta.widthFrac,\n    ]),\n    tooltip: {\n      formatter: (p) =>\n        `${endpoints[p.value[0]].name}<br/>${meta.label}: ${p.value[1].toFixed(0)}–${p.value[2].toFixed(0)} ms`,\n    },\n  });\n}\nconst medianSeries = {\n  name: \"Median\",\n  type: \"custom\",\n  renderItem: renderMedian,\n  itemStyle: { color: t.ink },\n  data: stats.map((s, catIdx) => [catIdx, s.median, LEVEL_META[0].widthFrac]),\n  tooltip: {\n    formatter: (p) => `${endpoints[p.value[0]].name}<br/>Median: ${p.value[1].toFixed(0)} ms`,\n  },\n};\nconst outlierPoints = [];\ndatasets.forEach((sorted, catIdx) => {\n  const outerLevel = stats[catIdx].levels[LEVEL_COUNT - 1];\n  sorted.forEach((value) => {\n    if (value < outerLevel.low || value > outerLevel.high) {\n      outlierPoints.push([catIdx, value, rand() * 2 - 1]);\n    }\n  });\n});\nconst outlierSeries = {\n  name: \"Outliers\",\n  type: \"custom\",\n  renderItem: renderOutlier,\n  itemStyle: { color: INK_MUTED, opacity: 0.6 },\n  data: outlierPoints,\n  tooltip: {\n    formatter: (p) => `${endpoints[p.value[0]].name}<br/>${p.value[1].toFixed(0)} ms`,\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: \"boxen-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    top: 52,\n    data: [...LEVEL_META.map((m) => m.label), \"Median\", \"Outliers\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    itemWidth: 16,\n    itemHeight: 12,\n  },\n  tooltip: { trigger: \"item\" },\n  grid: { left: 110, right: 60, top: 140, bottom: 80 },\n  xAxis: {\n    type: \"category\",\n    data: endpoints.map((e) => e.name),\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    name: \"Response time (ms)\",\n    nameLocation: \"middle\",\n    nameGap: 65,\n    nameTextStyle: { color: t.inkSoft, fontSize: 15 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [...boxSeries, medianSeries, outlierSeries],\n});\n"}