{"spec_id":"box-horizontal","library":"echarts","language":"javascript","code":"// anyplot.ai\n// box-horizontal: Horizontal Box Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Annual salary distributions by job title — long category labels are exactly\n// where the horizontal orientation earns its keep (no rotated x-axis text).\nfunction makeLcg(seed) {\n  let state = seed % 2147483647;\n  if (state <= 0) state += 2147483646;\n  return function uniform() {\n    state = (state * 16807) % 2147483647;\n    return (state - 1) / 2147483646;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal(mean, std) {\n  const u1 = rand();\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\nfunction median(sorted) {\n  const mid = Math.floor(sorted.length / 2);\n  return sorted.length % 2 === 0\n    ? (sorted[mid - 1] + sorted[mid]) / 2\n    : sorted[mid];\n}\n\nconst roles = [\n  {\n    name: \"Customer Support Representative\",\n    n: 20,\n    mean: 48,\n    std: 6,\n    extra: [],\n  },\n  { name: \"Marketing Specialist\", n: 18, mean: 62, std: 9, extra: [] },\n  { name: \"UX Researcher\", n: 16, mean: 88, std: 11, extra: [42] },\n  { name: \"Data Scientist\", n: 17, mean: 118, std: 15, extra: [] },\n  { name: \"Product Manager\", n: 18, mean: 125, std: 18, extra: [72] },\n  { name: \"Software Engineer\", n: 22, mean: 132, std: 20, extra: [214] },\n];\n\nconst withSalaries = roles.map((role) => {\n  const salaries = [];\n  for (let i = 0; i < role.n; i++) {\n    salaries.push(\n      Math.round(Math.max(28, randNormal(role.mean, role.std)) * 10) / 10,\n    );\n  }\n  role.extra.forEach((v) => salaries.push(v));\n  salaries.sort((a, b) => a - b);\n  return { name: role.name, salaries };\n});\n\n// Sort by median ascending so the axis reads low-to-high bottom-to-top —\n// per the spec's \"sort by median for easier comparison\" guidance.\nwithSalaries.sort((a, b) => median(a.salaries) - median(b.salaries));\n\nconst categoryNames = withSalaries.map((r) => r.name);\nconst rawSource = withSalaries.map((r) => r.salaries);\nconst overallMedian = median(\n  withSalaries.flatMap((r) => r.salaries).sort((a, b) => a - b),\n);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\n// Quartiles, whiskers (1.5*IQR) and outliers are computed by ECharts' own\n// built-in \"boxplot\" dataset transform (registered with the boxplot chart,\n// no extra import needed) rather than reimplemented by hand. The transform\n// yields two result sets: boxData (dataset[1]) and outliers (dataset[2]).\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"box-horizontal · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 24, fontWeight: 500 },\n  },\n  dataset: [\n    { source: rawSource },\n    {\n      fromDatasetIndex: 0,\n      transform: {\n        type: \"boxplot\",\n        config: {\n          itemNameFormatter: (params) => categoryNames[params.value],\n        },\n      },\n    },\n    { fromDatasetIndex: 1, fromTransformResult: 1 },\n  ],\n  grid: { left: 40, right: 70, top: 100, bottom: 90, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    name: \"Annual Salary ($1,000s)\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    data: categoryNames,\n    boundaryGap: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      name: \"Salary distribution\",\n      type: \"boxplot\",\n      datasetIndex: 1,\n      encode: { x: [1, 2, 3, 4, 5], y: 0 },\n      // colorBy:\"data\" + boxplot's own stroke-only visualDrawType makes\n      // ECharts cycle the Imprint palette across the box *borders* per\n      // category automatically, while the fill stays a constant elevated\n      // surface — the hollow-box look, driven by the library itself rather\n      // than per-item itemStyle bookkeeping.\n      colorBy: \"data\",\n      boxWidth: [16, 32],\n      itemStyle: { color: t.elevatedBg, borderWidth: 3 },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        label: {\n          color: t.ink,\n          fontSize: 13,\n          formatter: `Overall median: $${Math.round(overallMedian)}k`,\n          position: \"insideEndTop\",\n        },\n        data: [{ xAxis: overallMedian }],\n      },\n    },\n    {\n      name: \"Outliers\",\n      type: \"scatter\",\n      datasetIndex: 2,\n      encode: { x: 1, y: 0 },\n      symbolSize: 13,\n      itemStyle: {\n        color: (params) =>\n          t.palette[categoryNames.indexOf(params.value[0]) % t.palette.length],\n        opacity: 0.85,\n        borderColor: t.pageBg,\n        borderWidth: 1,\n      },\n    },\n  ],\n});\n"}