{"spec_id":"box-horizontal","library":"muix","language":"javascript","code":"// anyplot.ai\n// box-horizontal: Horizontal Box Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"box-horizontal · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 56;\n\n// --- Data (in-memory, deterministic LCG — no seeded RNG in the browser) -----\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rand, mean, stdDev) {\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 * stdDev;\n}\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 base + 1 < sorted.length\n    ? sorted[base] + rest * (sorted[base + 1] - sorted[base])\n    : sorted[base];\n}\n\n// Annual base salary ($k) by job title — long titles are exactly the case\n// this orientation exists for, since they still read cleanly on the y-axis.\nconst roles = [\n  { title: \"Senior Software Engineer\", mean: 152, std: 16, size: 13 },\n  { title: \"Data Scientist\", mean: 138, std: 15, size: 12 },\n  { title: \"Product Manager\", mean: 134, std: 19, size: 11 },\n  { title: \"DevOps Engineer\", mean: 121, std: 13, size: 12 },\n  { title: \"UX/UI Designer\", mean: 104, std: 14, size: 11 },\n  { title: \"Marketing Analyst\", mean: 79, std: 11, size: 11 },\n  { title: \"Customer Success Manager\", mean: 73, std: 9, size: 11 },\n];\n\nconst rand = lcg(42);\n\n// A couple of roles get one deliberate, deterministic extreme salary on top\n// of the random draws (a rare senior hire, a below-band junior contract) so\n// the outlier marker is demonstrated on more than one row.\nconst injectedOutliers = { \"Senior Software Engineer\": 206, \"Marketing Analyst\": 43 };\n\nconst boxStats = roles\n  .map(({ title, mean, std, size }) => {\n    const salaries = Array.from({ length: size }, () => Math.max(28, randomNormal(rand, mean, std)));\n    if (title in injectedOutliers) salaries.push(injectedOutliers[title]);\n    salaries.sort((a, b) => a - b);\n\n    const q1 = quantile(salaries, 0.25);\n    const median = quantile(salaries, 0.5);\n    const q3 = quantile(salaries, 0.75);\n    const iqr = q3 - q1;\n    const lowerFence = q1 - 1.5 * iqr;\n    const upperFence = q3 + 1.5 * iqr;\n    const inliers = salaries.filter((v) => v >= lowerFence && v <= upperFence);\n    const outliers = salaries.filter((v) => v < lowerFence || v > upperFence);\n\n    return {\n      title,\n      q1,\n      median,\n      q3,\n      whiskerLow: inliers.length ? Math.min(...inliers) : q1,\n      whiskerHigh: inliers.length ? Math.max(...inliers) : q3,\n      outliers,\n    };\n  })\n  .sort((a, b) => b.median - a.median); // highest-paid role first (top row)\n\nconst categories = boxStats.map((s) => s.title);\nconst allValues = boxStats.flatMap((s) => [s.q1, s.median, s.q3, s.whiskerLow, s.whiskerHigh, ...s.outliers]);\nconst valuePad = (Math.max(...allValues) - Math.min(...allValues)) * 0.1;\nconst valueMin = Math.max(0, Math.floor(Math.min(...allValues) - valuePad));\nconst valueMax = Math.ceil(Math.max(...allValues) + valuePad);\n\n// --- Box-and-whisker overlay -------------------------------------------------\n// The community package (7.29.1) has no BoxPlot component — a custom SVG\n// layer positioned via the chart's own band/linear scale hooks reproduces it\n// while staying entirely within the community ChartContainer surface.\nfunction BoxWhiskers() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const bandwidth = yScale.bandwidth();\n  const boxHeight = bandwidth * 0.56;\n\n  return (\n    <g>\n      {boxStats.map((s, i) => {\n        const center = yScale(s.title) + bandwidth / 2;\n        const top = center - boxHeight / 2;\n        const bottom = center + boxHeight / 2;\n        const color = t.palette[i % t.palette.length];\n\n        return (\n          <g key={s.title}>\n            <line\n              x1={xScale(s.whiskerLow)}\n              x2={xScale(s.q1)}\n              y1={center}\n              y2={center}\n              stroke={color}\n              strokeWidth={2}\n              strokeLinecap=\"round\"\n            />\n            <line\n              x1={xScale(s.q3)}\n              x2={xScale(s.whiskerHigh)}\n              y1={center}\n              y2={center}\n              stroke={color}\n              strokeWidth={2}\n              strokeLinecap=\"round\"\n            />\n            <line\n              x1={xScale(s.whiskerLow)}\n              x2={xScale(s.whiskerLow)}\n              y1={top}\n              y2={bottom}\n              stroke={color}\n              strokeWidth={2}\n              strokeLinecap=\"round\"\n            />\n            <line\n              x1={xScale(s.whiskerHigh)}\n              x2={xScale(s.whiskerHigh)}\n              y1={top}\n              y2={bottom}\n              stroke={color}\n              strokeWidth={2}\n              strokeLinecap=\"round\"\n            />\n            <rect\n              x={xScale(s.q1)}\n              y={top}\n              width={Math.max(1, xScale(s.q3) - xScale(s.q1))}\n              height={boxHeight}\n              rx={4}\n              ry={4}\n              fill={color}\n              fillOpacity={0.28}\n              stroke={color}\n              strokeWidth={2.5}\n            />\n            <line\n              x1={xScale(s.median)}\n              x2={xScale(s.median)}\n              y1={top}\n              y2={bottom}\n              stroke={color}\n              strokeWidth={3.5}\n              strokeLinecap=\"round\"\n            />\n            {s.outliers.map((v, j) => (\n              <circle key={j} cx={xScale(v)} cy={center} r={6.5} fill={t.pageBg} stroke={color} strokeWidth={2} />\n            ))}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT;\n\n  return (\n    <div style={{ width: window.ANYPLOT_SIZE.width, height: window.ANYPLOT_SIZE.height }}>\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          lineHeight: `${TITLE_HEIGHT}px`,\n          paddingLeft: 24,\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={chartHeight}\n        series={[]}\n        skipAnimation\n        margin={{ top: 24, right: 60, bottom: 64, left: 320 }}\n        xAxis={[\n          {\n            id: \"salary\",\n            scaleType: \"linear\",\n            min: valueMin,\n            max: valueMax,\n            label: \"Annual Salary ($k)\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"role\",\n            scaleType: \"band\",\n            data: categories,\n            label: \"Job Title\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n      >\n        <ChartsGrid vertical sx={{ \"& .MuiChartsGrid-line\": { opacity: 0.2, strokeDasharray: \"2 5\" } }} />\n        <BoxWhiskers />\n        <ChartsXAxis axisId=\"salary\" />\n        <ChartsYAxis axisId=\"role\" slotProps={{ axisLabel: { x: -190 } }} />\n      </ChartContainer>\n    </div>\n  );\n}\n"}