{"spec_id":"box-horizontal","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// box-horizontal: Horizontal Box Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampling ------------------\nlet seed = 7;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randNormal(mean, sd) {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\nfunction quantile(sorted, q) {\n  const pos = (sorted.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return sorted[base + 1] !== undefined\n    ? sorted[base] + rest * (sorted[base + 1] - sorted[base])\n    : sorted[base];\n}\n\n// --- Data: API response-time distributions by endpoint (long labels ------\n// benefit from the horizontal orientation, per the spec's rationale) -------\nconst endpointSpecs = [\n  { label: \"User Authentication API\", mean: 45, sd: 8, n: 60 },\n  { label: \"Notification Dispatch Queue\", mean: 60, sd: 12, n: 70 },\n  { label: \"Search Indexing Service\", mean: 85, sd: 15, n: 65 },\n  { label: \"Payment Processing Gateway\", mean: 120, sd: 25, n: 55 },\n  { label: \"Image Upload Handler\", mean: 210, sd: 40, n: 50 },\n  { label: \"Legacy Report Generation\", mean: 380, sd: 60, n: 45 },\n];\n\nlet endpoints = endpointSpecs.map((spec) => {\n  const times = Array.from({ length: spec.n }, () =>\n    Math.max(1, randNormal(spec.mean, spec.sd))\n  ).sort((a, b) => a - b);\n\n  const q1 = quantile(times, 0.25);\n  const median = quantile(times, 0.5);\n  const q3 = quantile(times, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n\n  const inRange = times.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = times.filter((v) => v < lowerFence || v > upperFence);\n\n  return {\n    label: spec.label,\n    q1,\n    median,\n    q3,\n    whiskerMin: inRange[0],\n    whiskerMax: inRange[inRange.length - 1],\n    outliers,\n  };\n});\n\n// Sort by median (ascending) so the fastest endpoint reads first, top to bottom.\nendpoints = endpoints.sort((a, b) => a.median - b.median);\n\nconst allRangeValues = endpoints.flatMap((e) => [\n  e.whiskerMin,\n  e.whiskerMax,\n  ...e.outliers,\n]);\nconst dataMax = Math.max(...allRangeValues);\nconst xMax = Math.ceil((dataMax * 1.08) / 20) * 20;\n\n// --- Custom plugin: whiskers, caps, median line, outlier points ------------\nconst boxPlotExtras = {\n  id: \"boxPlotExtras\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const xScale = chart.scales.x;\n    ctx.save();\n    ctx.lineCap = \"round\";\n\n    endpoints.forEach((ep, i) => {\n      const bar = meta.data[i];\n      if (!bar) return;\n      const centerY = bar.y;\n      const capHalf = bar.height * 0.3;\n      const xWhiskerMin = xScale.getPixelForValue(ep.whiskerMin);\n      const xWhiskerMax = xScale.getPixelForValue(ep.whiskerMax);\n      const xQ1 = xScale.getPixelForValue(ep.q1);\n      const xQ3 = xScale.getPixelForValue(ep.q3);\n      const xMedian = xScale.getPixelForValue(ep.median);\n\n      // Whiskers\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(xWhiskerMin, centerY);\n      ctx.lineTo(xQ1, centerY);\n      ctx.moveTo(xQ3, centerY);\n      ctx.lineTo(xWhiskerMax, centerY);\n      ctx.stroke();\n\n      // Whisker caps\n      ctx.beginPath();\n      ctx.moveTo(xWhiskerMin, centerY - capHalf);\n      ctx.lineTo(xWhiskerMin, centerY + capHalf);\n      ctx.moveTo(xWhiskerMax, centerY - capHalf);\n      ctx.lineTo(xWhiskerMax, centerY + capHalf);\n      ctx.stroke();\n\n      // Median line — page-bg stroke for contrast against the saturated fill\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(xMedian, centerY - bar.height / 2);\n      ctx.lineTo(xMedian, centerY + bar.height / 2);\n      ctx.stroke();\n\n      // Outliers\n      const color = t.palette[i % t.palette.length];\n      ep.outliers.forEach((value) => {\n        const x = xScale.getPixelForValue(value);\n        ctx.beginPath();\n        ctx.arc(x, centerY, 6, 0, Math.PI * 2);\n        ctx.fillStyle = color;\n        ctx.fill();\n        ctx.lineWidth = 1.5;\n        ctx.strokeStyle = t.pageBg;\n        ctx.stroke();\n      });\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart: horizontal floating bars for Q1-Q3, custom plugin for the rest -\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: endpoints.map((e) => e.label),\n    datasets: [\n      {\n        label: \"Interquartile range\",\n        data: endpoints.map((e) => [e.q1, e.q3]),\n        backgroundColor: endpoints.map((_, i) => t.palette[i % t.palette.length]),\n        borderColor: t.ink,\n        borderWidth: 2,\n        borderRadius: 4,\n        barPercentage: 0.5,\n        categoryPercentage: 0.7,\n      },\n    ],\n  },\n  plugins: [boxPlotExtras],\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"box-horizontal · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        min: 0,\n        max: xMax,\n        title: {\n          display: true,\n          text: \"Response Time (ms)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        title: { display: true, text: \"Endpoint\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n    },\n  },\n});\n"}