{"spec_id":"box-horizontal","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// box-horizontal: Horizontal Box Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 96/100 | Created: 2026-09-02\n\n// The core bundle (no highcharts-more) has no \"boxplot\" series type, so the box\n// is built from two stacked \"bar\" series (an invisible floor up to Q1, then a\n// visible box from Q1 to Q3, colorByPoint from the Imprint palette). Whiskers\n// and the median are drawn as two extra core \"line\" series (null-separated\n// segments, one per category) rather than custom chart.renderer shapes — a\n// series defined after the box series always layers on top, which is what a\n// median mark sitting inside the box needs. chart.type: \"bar\" inverts the\n// axes so the category axis reads vertically — the whole point of this spec.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nlet seed = 7;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\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 * std;\n}\nfunction quartile(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}\nfunction boxStats(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const q1 = quartile(sorted, 0.25);\n  const median = quartile(sorted, 0.5);\n  const q3 = quartile(sorted, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const inRange = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerLow: inRange[0],\n    whiskerHigh: inRange[inRange.length - 1],\n    outliers: sorted.filter((v) => v < lowerFence || v > upperFence),\n  };\n}\n\n// Long job-title labels are exactly where a horizontal box plot earns its\n// keep — they'd collide or need rotation on a vertical category axis.\nconst roles = [\n  { name: \"VP of Engineering\", mean: 210, std: 24, n: 22 },\n  { name: \"Data Science Team Lead\", mean: 156, std: 19, n: 38 },\n  { name: \"Senior Software Engineer\", mean: 145, std: 17, n: 60 },\n  { name: \"Product Marketing Manager\", mean: 118, std: 15, n: 45 },\n  { name: \"UX Research Specialist\", mean: 98, std: 12, n: 40 },\n  { name: \"Customer Success Associate\", mean: 68, std: 9, n: 70 },\n  { name: \"Junior Financial Analyst\", mean: 62, std: 8, n: 55 },\n];\nconst statsByRole = {};\nconst allSalaries = [];\nroles.forEach((r) => {\n  const salaries = Array.from({ length: r.n }, () =>\n    Math.max(35, randNormal(r.mean, r.std))\n  );\n  statsByRole[r.name] = boxStats(salaries);\n  allSalaries.push(...salaries);\n});\n// Pooled-median reference line — the storytelling device that lets a reader\n// see at a glance which roles sit above/below the org-wide typical salary.\nconst overallMedian = quartile(\n  [...allSalaries].sort((a, b) => a - b),\n  0.5\n);\n\n// Sort by median so the highest-paid role reads first — easier comparison,\n// per the spec's note on ordering categories by median value.\nconst sortedRoles = [...roles].sort(\n  (a, b) => statsByRole[b.name].median - statsByRole[a.name].median\n);\nconst categories = sortedRoles.map((r) => r.name);\n\n// Whiskers (box edge → fence) and caps, one null-separated \"line\" series for\n// every category so a single series draws all the disconnected segments.\n// Every entry (including gaps) carries an explicit x so a mixed array of real\n// points and null-y gap markers can't be re-bucketed by an auto pointStart/\n// pointInterval index — only the y:null gaps break the connecting line.\nconst capHalf = 0.13;\nconst whiskerData = categories.flatMap((name, i) => {\n  const s = statsByRole[name];\n  return [\n    { x: i, y: s.q3 }, { x: i, y: s.whiskerHigh }, { x: i, y: null },\n    { x: i - capHalf, y: s.whiskerHigh }, { x: i + capHalf, y: s.whiskerHigh }, { x: i, y: null },\n    { x: i, y: s.q1 }, { x: i, y: s.whiskerLow }, { x: i, y: null },\n    { x: i - capHalf, y: s.whiskerLow }, { x: i + capHalf, y: s.whiskerLow }, { x: i, y: null },\n  ];\n});\n// Median line spans the same half-width as the box itself (matches the\n// pointPadding/groupPadding below), drawn as its own series so it renders on\n// top of the box fill instead of being covered by it.\nconst boxHalf = 0.24;\nconst medianData = categories.flatMap((name, i) => [\n  { x: i - boxHalf, y: statsByRole[name].median },\n  { x: i + boxHalf, y: statsByRole[name].median },\n  { x: i, y: null },\n]);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"bar\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginRight: 50, // room for the rightmost value-axis tick label, which sits at the plot edge in an inverted chart\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"box-horizontal · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories,\n    reversed: true, // index 0 (highest median) reads at the top, not the bottom\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    min: 0,\n    maxPadding: 0.06, // keeps the last tick label from touching the canvas edge\n    reversedStacks: false, // keep series[0] (invisible floor) at the base of the stack\n    title: { text: \"Annual Salary ($K)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"{value}K\" },\n    gridLineColor: t.grid,\n    plotLines: [\n      {\n        value: overallMedian,\n        color: t.inkSoft,\n        width: 1.5,\n        dashStyle: \"Dash\",\n        zIndex: 5,\n        label: {\n          text: `Org-wide median: $${overallMedian.toFixed(0)}K`,\n          rotation: 0,\n          align: \"left\",\n          verticalAlign: \"top\",\n          x: 8,\n          y: 4,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n    ],\n  },\n  legend: { enabled: false },\n  tooltip: { outside: false },\n  plotOptions: {\n    bar: {\n      stacking: \"normal\",\n      pointPadding: 0.12,\n      groupPadding: 0.18,\n      borderRadius: 0,\n      animation: false,\n    },\n    series: { animation: false },\n  },\n  series: [\n    {\n      name: \"Floor\",\n      data: categories.map((name) => +statsByRole[name].q1.toFixed(1)),\n      color: \"transparent\",\n      borderWidth: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Interquartile range\",\n      data: categories.map((name) => +(statsByRole[name].q3 - statsByRole[name].q1).toFixed(1)),\n      colorByPoint: true,\n      borderColor: t.inkSoft,\n      borderWidth: 1.25,\n      borderRadius: 2,\n      showInLegend: false,\n      tooltip: {\n        pointFormatter: function () {\n          const s = statsByRole[categories[this.index]];\n          return (\n            `<b>${categories[this.index]}</b><br/>` +\n            `Max: $${s.whiskerHigh.toFixed(0)}K<br/>Q3: $${s.q3.toFixed(0)}K<br/>` +\n            `Median: $${s.median.toFixed(0)}K<br/>Q1: $${s.q1.toFixed(0)}K<br/>` +\n            `Min: $${s.whiskerLow.toFixed(0)}K`\n          );\n        },\n      },\n    },\n    {\n      type: \"line\",\n      name: \"Whiskers\",\n      data: whiskerData,\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      linecap: \"round\",\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      name: \"Median\",\n      data: medianData,\n      color: t.pageBg,\n      lineWidth: 3,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"scatter\",\n      name: \"Outliers\",\n      data: sortedRoles.flatMap((r, i) =>\n        statsByRole[r.name].outliers.map((value) => ({\n          x: i,\n          y: +value.toFixed(1),\n          color: t.palette[i % t.palette.length],\n        }))\n      ),\n      marker: { radius: 6.5, symbol: \"circle\", lineWidth: 1, lineColor: t.pageBg, fillOpacity: 0.85 },\n      showInLegend: false,\n      tooltip: { pointFormat: \"Outlier: <b>${point.y:.0f}K</b>\" },\n    },\n  ],\n});\n"}