{"spec_id":"violin-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// violin-basic: Basic Violin Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic, LCG-seeded) ----------------------------\n// Test-score distributions (0-100 scale) across four course sections, each\n// shaped to show a distinct distribution: right-skewed, bimodal, moderate\n// spread, and tightly clustered.\n\nfunction makeLCG(seed) {\n  let state = seed >>> 0;\n  return function rng() {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction randNormal(rng) {\n  let u1 = 0;\n  while (u1 === 0) u1 = rng();\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nfunction clip(value, lo, hi) {\n  return Math.min(hi, Math.max(lo, value));\n}\n\nconst rng = makeLCG(42);\nconst sampleSize = 180;\n\nconst courses = [\"Intro Stats\", \"Data Structures\", \"Algorithms\", \"Capstone\"];\n\nfunction generateScores(centerFn, noiseStd) {\n  const scores = [];\n  for (let i = 0; i < sampleSize; i++) {\n    scores.push(clip(centerFn() + randNormal(rng) * noiseStd, 50, 100));\n  }\n  return scores;\n}\n\nconst scoresByCourse = [\n  generateScores(() => 55 + 45 * Math.pow(rng(), 2.3), 2), // right-skewed\n  generateScores(() => (rng() < 0.52 ? 65 : 89), 3.2), // bimodal\n  generateScores(() => 78, 8), // moderate spread\n  generateScores(() => 90, 3), // narrow cluster\n];\n\n// --- Stats + kernel density estimation --------------------------------------\n\nfunction quantile(sorted, q) {\n  const pos = q * (sorted.length - 1);\n  const lo = Math.floor(pos);\n  const hi = Math.ceil(pos);\n  const frac = pos - lo;\n  return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;\n}\n\nfunction computeStats(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  return {\n    min: sorted[0],\n    q1: quantile(sorted, 0.25),\n    median: quantile(sorted, 0.5),\n    q3: quantile(sorted, 0.75),\n    max: sorted[sorted.length - 1],\n  };\n}\n\nfunction silvermanBandwidth(values, stats) {\n  const n = values.length;\n  const mean = values.reduce((sum, v) => sum + v, 0) / n;\n  const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (n - 1);\n  const std = Math.sqrt(variance);\n  const iqr = stats.q3 - stats.q1;\n  const spread = iqr > 0 ? Math.min(std, iqr / 1.34) : std;\n  return 0.9 * spread * Math.pow(n, -0.2);\n}\n\nfunction gaussianKDE(values, grid, bandwidth) {\n  const norm = 1 / (values.length * bandwidth * Math.sqrt(2 * Math.PI));\n  return grid.map((x) => {\n    let sum = 0;\n    for (const v of values) {\n      const u = (x - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum * norm;\n  });\n}\n\nconst gridN = 120;\n\nconst boxStats = scoresByCourse.map(computeStats);\nconst violinProfiles = scoresByCourse.map((values, i) => {\n  const stats = boxStats[i];\n  const bandwidth = silvermanBandwidth(values, stats);\n  // Scope the grid to this category's own data range (padded by 3 bandwidths)\n  // instead of a shared fixed range, so tight distributions taper to zero\n  // within their real extent rather than streaking to the axis floor.\n  const gridMin = Math.max(50, stats.min - 3 * bandwidth);\n  const gridMax = Math.min(100, stats.max + 3 * bandwidth);\n  const grid = Array.from(\n    { length: gridN },\n    (_, j) => gridMin + (j * (gridMax - gridMin)) / (gridN - 1),\n  );\n  const density = gaussianKDE(values, grid, bandwidth);\n  return { grid, density, maxDensity: Math.max(...density) };\n});\n\n// --- Layout constants ---------------------------------------------------\nconst violinHalfWidth = 0.42;\nconst boxHalfWidth = 0.09;\nconst capHalfWidth = boxHalfWidth * 0.6;\n\n// --- renderItem: mirrored density polygon -----------------------------------\nfunction renderViolin(params, api) {\n  const idx = params.dataIndex;\n  const profile = violinProfiles[idx];\n  const points = [];\n  for (let j = 0; j < profile.grid.length; j++) {\n    const w = violinHalfWidth * (profile.density[j] / profile.maxDensity);\n    points.push(api.coord([idx - w, profile.grid[j]]));\n  }\n  for (let j = profile.grid.length - 1; j >= 0; j--) {\n    const w = violinHalfWidth * (profile.density[j] / profile.maxDensity);\n    points.push(api.coord([idx + w, profile.grid[j]]));\n  }\n  return {\n    type: \"polygon\",\n    shape: { points },\n    style: api.style(),\n  };\n}\n\n// --- renderItem: box (quartiles) + median line + whiskers -------------------\nfunction renderBox(params, api) {\n  const idx = params.dataIndex;\n  const s = boxStats[idx];\n  const topLeft = api.coord([idx - boxHalfWidth, s.q3]);\n  const bottomRight = api.coord([idx + boxHalfWidth, s.q1]);\n  const medianL = api.coord([idx - boxHalfWidth, s.median]);\n  const medianR = api.coord([idx + boxHalfWidth, s.median]);\n  const whiskerHighTop = api.coord([idx, s.max]);\n  const whiskerHighBottom = api.coord([idx, s.q3]);\n  const whiskerLowTop = api.coord([idx, s.q1]);\n  const whiskerLowBottom = api.coord([idx, s.min]);\n  const capHighL = api.coord([idx - capHalfWidth, s.max]);\n  const capHighR = api.coord([idx + capHalfWidth, s.max]);\n  const capLowL = api.coord([idx - capHalfWidth, s.min]);\n  const capLowR = api.coord([idx + capHalfWidth, s.min]);\n\n  const whiskerStyle = { stroke: t.inkSoft, lineWidth: 1.5 };\n  return {\n    type: \"group\",\n    children: [\n      { type: \"line\", shape: { x1: whiskerHighTop[0], y1: whiskerHighTop[1], x2: whiskerHighBottom[0], y2: whiskerHighBottom[1] }, style: whiskerStyle },\n      { type: \"line\", shape: { x1: whiskerLowTop[0], y1: whiskerLowTop[1], x2: whiskerLowBottom[0], y2: whiskerLowBottom[1] }, style: whiskerStyle },\n      { type: \"line\", shape: { x1: capHighL[0], y1: capHighL[1], x2: capHighR[0], y2: capHighR[1] }, style: whiskerStyle },\n      { type: \"line\", shape: { x1: capLowL[0], y1: capLowL[1], x2: capLowR[0], y2: capLowR[1] }, style: whiskerStyle },\n      {\n        type: \"rect\",\n        shape: {\n          x: topLeft[0],\n          y: topLeft[1],\n          width: bottomRight[0] - topLeft[0],\n          height: bottomRight[1] - topLeft[1],\n        },\n        style: { fill: t.elevatedBg, stroke: t.ink, lineWidth: 1.5 },\n      },\n      { type: \"line\", shape: { x1: medianL[0], y1: medianL[1], x2: medianR[0], y2: medianR[1] }, style: { stroke: t.ink, lineWidth: 2.5 } },\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: \"violin-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (params) => {\n      const s = boxStats[params.dataIndex];\n      return [\n        `<b>${courses[params.dataIndex]}</b>`,\n        `Max: ${s.max.toFixed(1)}`,\n        `Q3: ${s.q3.toFixed(1)}`,\n        `Median: ${s.median.toFixed(1)}`,\n        `Q1: ${s.q1.toFixed(1)}`,\n        `Min: ${s.min.toFixed(1)}`,\n      ].join(\"<br/>\");\n    },\n  },\n  grid: { left: 110, right: 60, top: 110, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    min: -0.6,\n    max: courses.length - 1 + 0.6,\n    name: \"Course Section\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: {\n      customValues: courses.map((_, i) => i),\n      formatter: (value) => courses[Math.round(value)],\n      color: t.inkSoft,\n      fontSize: 14,\n    },\n    axisTick: { customValues: courses.map((_, i) => i) },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: 50,\n    max: 100,\n    name: \"Test Score (%)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"custom\",\n      name: \"Distribution\",\n      renderItem: renderViolin,\n      coordinateSystem: \"cartesian2d\",\n      clip: true,\n      data: courses.map((_, i) => ({\n        value: [i, boxStats[i].median],\n        itemStyle: { color: t.palette[i], borderColor: t.palette[i], borderWidth: 1.5, opacity: 0.6 },\n      })),\n      encode: { x: 0, y: 1 },\n    },\n    {\n      type: \"custom\",\n      name: \"Quartiles\",\n      renderItem: renderBox,\n      coordinateSystem: \"cartesian2d\",\n      clip: true,\n      data: courses.map((_, i) => [i, boxStats[i].median]),\n      encode: { x: 0, y: 1 },\n    },\n  ],\n});\n"}