{"spec_id":"dumbbell-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// dumbbell-basic: Basic Dumbbell Chart\n// Library: muix 7.29.1 | JavaScript 22.23.0\n// Quality: 89/100 | Created: 2026-06-30\n//# anyplot-orientation: landscape\n// anyplot.ai\n// dumbbell-basic: Basic Dumbbell Chart\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-30\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Employee satisfaction (0–10) before/after a wellness program.\n// Includes a wide range of outcomes — one regression, one standout gain.\nconst RAW = [\n  { dept: \"Finance\",     before: 5.4, after: 5.9 },  // +0.5 modest gain\n  { dept: \"Legal\",       before: 5.7, after: 5.5 },  // -0.2 regression\n  { dept: \"Marketing\",   before: 6.0, after: 7.6 },  // +1.6\n  { dept: \"Sales\",       before: 6.2, after: 6.7 },  // +0.5\n  { dept: \"Operations\",  before: 6.5, after: 9.0 },  // +2.5 ← largest gain\n  { dept: \"Engineering\", before: 6.8, after: 7.7 },  // +0.9\n  { dept: \"HR\",          before: 7.1, after: 8.4 },  // +1.3\n  { dept: \"Design\",      before: 7.4, after: 8.0 },  // +0.6\n];\n\n// Sort ascending by before-score to reveal baseline-vs-gain pattern\nconst DATA = [...RAW].sort((a, b) => a.before - b.before);\n\nconst departments  = DATA.map(d => d.dept);\nconst beforeScores = DATA.map(d => d.before);\nconst afterScores  = DATA.map(d => d.after);\nconst gaps         = DATA.map(d => +(d.after - d.before).toFixed(1));\nconst N            = DATA.length;\n\n// Index of the department with the largest gain (focal point)\nconst maxGapIdx = gaps.indexOf(Math.max(...gaps));\n\nconst beforeData = beforeScores.map((x, i) => ({ id: i,       x, y: i }));\nconst afterData  = afterScores.map( (x, i) => ({ id: i + 100, x, y: i }));\n\n// Rendered inside ChartContainer so it can access the D3 coordinate scales.\n// Placed before <ScatterPlot /> so lines sit below the dots.\nfunction DumbbellOverlay() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g>\n      {/* Connecting lines — highlight the biggest-gain row with amber */}\n      {DATA.map((d, i) => {\n        const x1 = xScale(d.before) ?? 0;\n        const x2 = xScale(d.after)  ?? 0;\n        const y  = yScale(i) ?? 0;\n        const isHighlight = i === maxGapIdx;\n        return (\n          <line\n            key={i}\n            x1={x1} y1={y}\n            x2={x2} y2={y}\n            stroke={isHighlight ? t.amber : t.inkSoft}\n            strokeWidth={isHighlight ? 5 : 2.5}\n            strokeOpacity={isHighlight ? 0.9 : 0.35}\n          />\n        );\n      })}\n\n      {/* Gap labels — showing change magnitude to the right of each dumbbell */}\n      {DATA.map((d, i) => {\n        const rightX = Math.max(xScale(d.before) ?? 0, xScale(d.after) ?? 0);\n        const y      = yScale(i) ?? 0;\n        const g      = gaps[i];\n        const isHighlight  = i === maxGapIdx;\n        const isRegression = g < 0;\n        const label = g > 0 ? `+${g.toFixed(1)}` : g.toFixed(1);\n        return (\n          <text\n            key={`gap-${i}`}\n            x={rightX + 14}\n            y={y + 5}\n            fontSize={isHighlight ? 13 : 12}\n            fill={isHighlight ? t.amber : t.inkSoft}\n            fontFamily=\"sans-serif\"\n            fontWeight={isHighlight ? \"700\" : isRegression ? \"600\" : \"400\"}\n            fontStyle={isRegression ? \"italic\" : \"normal\"}\n          >\n            {label}\n          </text>\n        );\n      })}\n\n      {/* Callout annotation for the focal-point row */}\n      {(() => {\n        const i  = maxGapIdx;\n        const x1 = xScale(beforeScores[i]) ?? 0;\n        const x2 = xScale(afterScores[i])  ?? 0;\n        const y  = yScale(i) ?? 0;\n        const xMid = (x1 + x2) / 2;\n        return (\n          <text\n            x={xMid}\n            y={y - 16}\n            textAnchor=\"middle\"\n            fontSize={12}\n            fill={t.amber}\n            fontFamily=\"sans-serif\"\n            fontWeight=\"600\"\n          >\n            largest gain\n          </text>\n        );\n      })()}\n    </g>\n  );\n}\n\nconst TITLE = \"dumbbell-basic · javascript · muix · anyplot.ai\";\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      skipAnimation\n      series={[\n        {\n          type: \"scatter\",\n          id: \"before\",\n          label: \"Before Program\",\n          data: beforeData,\n          color: t.palette[0],\n          markerSize: 10,\n        },\n        {\n          type: \"scatter\",\n          id: \"after\",\n          label: \"After Program\",\n          data: afterData,\n          color: t.palette[1],\n          markerSize: 10,\n        },\n      ]}\n      xAxis={[{\n        id: \"xAxis\",\n        min: 4.5,\n        max: 9.8,\n        label: \"Employee Satisfaction Score (0 – 10)\",\n        tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        labelStyle: { fontSize: 16, fill: t.ink },\n      }]}\n      yAxis={[{\n        id: \"yAxis\",\n        min: -0.5,\n        max: N - 0.5,\n        tickMinStep: 1,\n        valueFormatter: (v) => departments[Math.round(v)] ?? \"\",\n        tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n      }]}\n      margin={{ left: 130, right: 80, top: 70, bottom: 110 }}\n    >\n      <ChartsGrid vertical />\n      <DumbbellOverlay />\n      <ScatterPlot />\n      <ChartsXAxis axisId=\"xAxis\" />\n      {/* disableLine removes the left spine for a cleaner open-chart look */}\n      <ChartsYAxis axisId=\"yAxis\" disableLine />\n      <ChartsLegend\n        position={{ vertical: \"bottom\", horizontal: \"middle\" }}\n        direction=\"row\"\n      />\n      <text\n        x={W / 2}\n        y={42}\n        textAnchor=\"middle\"\n        fontSize={22}\n        fontFamily=\"sans-serif\"\n        fontWeight=\"500\"\n        fill={t.ink}\n      >\n        {TITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}