{"spec_id":"violin-box","library":"echarts","language":"javascript","code":"// anyplot.ai\n// violin-box: Violin Plot with Embedded Box Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Commute time (minutes) by transport mode. Public Transit is generated as a\n// mix of a direct-route trip and a slower transfer-route trip, producing a\n// bimodal distribution the violin's KDE reveals but the box plot alone hides.\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\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 GROUPS = [\"Car\", \"Bike\", \"Public Transit\"];\nconst N = 200;\n\nconst carTimes = Array.from({ length: N }, () => Math.max(2, randNormal(28, 7)));\nconst bikeTimes = Array.from({ length: N }, () => Math.max(2, randNormal(22, 5)));\nconst transitTimes = Array.from({ length: N }, () =>\n  Math.max(2, rand() < 0.6 ? randNormal(28, 4) : randNormal(48, 6))\n);\nconst dataByGroup = [carTimes, bikeTimes, transitTimes];\n\n// --- Stats helpers ------------------------------------------------------------\nfunction quantile(sorted, p) {\n  const idx = p * (sorted.length - 1);\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\nfunction boxStats(values) {\n  const sorted = [...values].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 inFence = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = sorted.filter((v) => v < lowerFence || v > upperFence);\n  return { min: inFence[0], q1, median, q3, max: inFence[inFence.length - 1], outliers };\n}\n\n// Each violin's KDE is evaluated over its OWN local range (data extent ±3\n// bandwidths), not the shared axis range — otherwise the gaussian kernel's\n// near-zero-but-nonzero tail stretches every violin into a thin needle all the\n// way to the tallest group's max, even for groups with a much smaller range.\nfunction kde(values, gridN, axisMin) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;\n  const std = Math.sqrt(variance);\n  const bandwidth = 1.06 * std * Math.pow(values.length, -0.2);\n  const localMin = Math.max(axisMin, Math.min(...values) - 3 * bandwidth);\n  const localMax = Math.max(...values) + 3 * bandwidth;\n  const points = [];\n  for (let i = 0; i <= gridN; i++) {\n    const y = localMin + (i / gridN) * (localMax - localMin);\n    let sum = 0;\n    for (const v of values) {\n      const u = (y - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    points.push({ y, density: sum / (values.length * bandwidth * Math.sqrt(2 * Math.PI)) });\n  }\n  return points;\n}\n\nconst gridMin = 0;\nconst kdeByGroup = dataByGroup.map((vals) => kde(vals, 120, gridMin));\nconst kdeMax = Math.max(...kdeByGroup.flat().map((p) => p.y));\nconst gridMax = Math.ceil(kdeMax / 10) * 10;\nconst statsByGroup = dataByGroup.map(boxStats);\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Custom renderers ----------------------------------------------------------\nfunction renderViolin(params, api) {\n  const idx = api.value(0);\n  const points = kdeByGroup[idx];\n  const maxDensity = Math.max(...points.map((p) => p.density));\n  const bandWidth = api.size([1, 0])[0];\n  const halfWidthPx = bandWidth * 0.36;\n\n  const left = points.map((p) => {\n    const [x, y] = api.coord([idx, p.y]);\n    return [x - halfWidthPx * (p.density / maxDensity), y];\n  });\n  const right = points\n    .slice()\n    .reverse()\n    .map((p) => {\n      const [x, y] = api.coord([idx, p.y]);\n      return [x + halfWidthPx * (p.density / maxDensity), y];\n    });\n\n  return {\n    type: \"polygon\",\n    shape: { points: left.concat(right) },\n    style: { fill: t.palette[0], opacity: 0.32, stroke: t.palette[0], lineWidth: 2 },\n  };\n}\n\nfunction renderBox(params, api) {\n  const idx = api.value(0);\n  const s = statsByGroup[idx];\n  const bandWidth = api.size([1, 0])[0];\n  const boxHalfPx = bandWidth * 0.11;\n\n  const centerCoord = api.coord([idx, s.median]);\n  const cx = centerCoord[0];\n  const [, yMin] = api.coord([idx, s.min]);\n  const [, yQ1] = api.coord([idx, s.q1]);\n  const [, yMedian] = api.coord([idx, s.median]);\n  const [, yQ3] = api.coord([idx, s.q3]);\n  const [, yMax] = api.coord([idx, s.max]);\n\n  const whisker = {\n    type: \"polyline\",\n    shape: { points: [[cx, yMin], [cx, yQ1], [cx, yQ3], [cx, yMax]] },\n    style: { stroke: t.ink, lineWidth: 2, fill: \"none\" },\n  };\n  const capMin = {\n    type: \"line\",\n    shape: { x1: cx - boxHalfPx * 0.6, y1: yMin, x2: cx + boxHalfPx * 0.6, y2: yMin },\n    style: { stroke: t.ink, lineWidth: 2 },\n  };\n  const capMax = {\n    type: \"line\",\n    shape: { x1: cx - boxHalfPx * 0.6, y1: yMax, x2: cx + boxHalfPx * 0.6, y2: yMax },\n    style: { stroke: t.ink, lineWidth: 2 },\n  };\n  const box = {\n    type: \"rect\",\n    shape: { x: cx - boxHalfPx, y: yQ3, width: boxHalfPx * 2, height: yQ1 - yQ3 },\n    style: { fill: t.pageBg, stroke: t.ink, lineWidth: 2 },\n  };\n  const medianLine = {\n    type: \"line\",\n    shape: { x1: cx - boxHalfPx, y1: yMedian, x2: cx + boxHalfPx, y2: yMedian },\n    style: { stroke: t.ink, lineWidth: 3 },\n  };\n\n  return { type: \"group\", children: [whisker, capMin, capMax, box, medianLine] };\n}\n\nconst outlierData = statsByGroup.flatMap((s, idx) => s.outliers.map((v) => [idx, v]));\n\n// --- Option ---------------------------------------------------------------------\nconst title = \"Commute Time by Transport Mode · violin-box · javascript · echarts · anyplot.ai\";\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    left: \"center\",\n    top: 30,\n    textStyle: { color: t.ink, fontSize: 19, fontWeight: 500 },\n  },\n  grid: { left: 110, right: 80, top: 130, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: GROUPS,\n    axisLabel: { color: t.inkSoft, fontSize: 16 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Commute Time (minutes)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: gridMin,\n    max: gridMax,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"custom\",\n      name: \"Distribution\",\n      renderItem: renderViolin,\n      data: GROUPS.map((_, idx) => idx),\n      encode: { x: 0 },\n      z: 2,\n      silent: true,\n    },\n    {\n      type: \"custom\",\n      name: \"Quartiles\",\n      renderItem: renderBox,\n      data: GROUPS.map((_, idx) => idx),\n      encode: { x: 0 },\n      z: 3,\n      silent: true,\n    },\n    {\n      type: \"scatter\",\n      name: \"Outliers\",\n      data: outlierData,\n      symbolSize: 8,\n      itemStyle: { color: t.ink, opacity: 0.75 },\n      z: 4,\n    },\n  ],\n});\n\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}