{"spec_id":"slope-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// slope-basic: Basic Slope Chart (Slopegraph)\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 94/100 | Updated: 2026-07-26\n\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: employee satisfaction score (0-100) by department, 2023 vs 2025 -\nconst PERIODS = [\"2023\", \"2025\"];\nconst departments = [\n  { name: \"Customer Support\", start: 52, end: 46 },\n  { name: \"Sales\", start: 57, end: 51 },\n  { name: \"Design\", start: 47, end: 55 },\n  { name: \"Marketing\", start: 65, end: 60 },\n  { name: \"Finance\", start: 61, end: 68 },\n  { name: \"Product\", start: 79, end: 72 },\n  { name: \"HR\", start: 70, end: 77 },\n  { name: \"Operations\", start: 74, end: 82 },\n  { name: \"Engineering\", start: 83, end: 90 },\n];\n\n// Direction semantics — profit/up/gain -> brand green, loss/down -> matte red\nconst IMPROVED = t.palette[0];\nconst DECLINED = t.palette[4];\n\n// Slugified id (no spaces) so the per-series CSS class selectors below stay valid.\nconst slug = (name) => name.replace(/\\s+/g, \"-\");\n\nconst series = departments.map((d) => ({\n  id: slug(d.name),\n  data: [d.start, d.end],\n  label: d.name,\n  color: d.end >= d.start ? IMPROVED : DECLINED,\n  showMark: true,\n  curve: \"linear\",\n}));\n\n// The biggest riser and biggest decliner get a bolder stroke + a delta\n// callout — a focal point so the eye isn't spread evenly across all 9\n// equally-weighted lines.\nconst biggestRiser = departments.reduce((best, d) => (d.end - d.start > best.end - best.start ? d : best));\nconst biggestDecliner = departments.reduce((best, d) => (d.end - d.start < best.end - best.start ? d : best));\n\n// Direct endpoint labels — name + value on BOTH columns so a reader never\n// has to trace color/slope back to the left label to identify a department.\nfunction EndpointLabels() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const xStart = xScale(\"2023\");\n  const xEnd = xScale(\"2025\");\n\n  return (\n    <g fontFamily=\"'Roboto','Helvetica Neue',Arial,sans-serif\" fontSize={14}>\n      {departments.map((d) => (\n        <g key={d.name}>\n          <text\n            x={xStart - 16}\n            y={yScale(d.start)}\n            textAnchor=\"end\"\n            dominantBaseline=\"middle\"\n            fill={t.ink}\n          >\n            {d.name} · {d.start}\n          </text>\n          <text\n            x={xEnd + 16}\n            y={yScale(d.end)}\n            textAnchor=\"start\"\n            dominantBaseline=\"middle\"\n            fill={t.ink}\n          >\n            {d.end} · {d.name}\n          </text>\n        </g>\n      ))}\n    </g>\n  );\n}\n\n// Delta callouts for the single biggest riser and biggest decliner — a small\n// chip near the line's midpoint so the standout changes read at a glance.\nfunction DeltaAnnotations() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const xMid = (xScale(\"2023\") + xScale(\"2025\")) / 2;\n\n  const callouts = [\n    { d: biggestRiser, color: IMPROVED, dy: -22 },\n    { d: biggestDecliner, color: DECLINED, dy: 22 },\n  ].map(({ d, color, dy }) => {\n    const delta = d.end - d.start;\n    return {\n      key: d.name,\n      label: `${delta > 0 ? \"+\" : \"\"}${delta}`,\n      color,\n      x: xMid,\n      y: yScale((d.start + d.end) / 2) + dy,\n    };\n  });\n\n  return (\n    <g fontFamily=\"'Roboto','Helvetica Neue',Arial,sans-serif\" fontSize={15} fontWeight={700}>\n      {callouts.map((c) => (\n        <g key={c.key}>\n          <rect x={c.x - 22} y={c.y - 14} width={44} height={22} rx={11} fill={t.pageBg} stroke={c.color} strokeWidth={1.5} />\n          <text x={c.x} y={c.y - 3} textAnchor=\"middle\" dominantBaseline=\"middle\" fill={c.color}>\n            {c.label}\n          </text>\n        </g>\n      ))}\n    </g>\n  );\n}\n\nconst TITLE = \"Employee Satisfaction by Department · slope-basic · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.round(22 * Math.min(1, 67 / TITLE.length));\nconst TITLE_H = 70;\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: t.pageBg,\n        boxSizing: \"border-box\",\n        fontFamily: \"'Roboto','Helvetica Neue',Arial,sans-serif\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          paddingLeft: 28,\n          paddingRight: 28,\n        }}\n      >\n        <span style={{ color: t.ink, fontSize: TITLE_FONT_SIZE, fontWeight: 500, letterSpacing: 0.3 }}>\n          {TITLE}\n        </span>\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: 24 }}>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <svg width={24} height={12} style={{ overflow: \"visible\" }}>\n              <line x1={1} y1={6} x2={23} y2={6} stroke={IMPROVED} strokeWidth={3} strokeLinecap=\"round\" />\n              <circle cx={12} cy={6} r={3} fill={t.pageBg} stroke={IMPROVED} strokeWidth={2} />\n            </svg>\n            <span style={{ color: t.inkSoft, fontSize: 15 }}>Improved</span>\n          </div>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <svg width={24} height={12} style={{ overflow: \"visible\" }}>\n              <line x1={1} y1={6} x2={23} y2={6} stroke={DECLINED} strokeWidth={3} strokeLinecap=\"round\" />\n              <circle cx={12} cy={6} r={3} fill={t.pageBg} stroke={DECLINED} strokeWidth={2} />\n            </svg>\n            <span style={{ color: t.inkSoft, fontSize: 15 }}>Declined</span>\n          </div>\n        </div>\n      </div>\n\n      <LineChart\n        width={width}\n        height={height - TITLE_H}\n        skipAnimation\n        margin={{ left: 210, right: 200, top: 50, bottom: 30 }}\n        xAxis={[\n          {\n            scaleType: \"point\",\n            data: PERIODS,\n            position: \"top\",\n            disableLine: true,\n            disableTicks: true,\n            tickLabelStyle: { fontSize: 16, fontWeight: 500, fill: t.ink },\n          },\n        ]}\n        yAxis={[\n          {\n            min: 35,\n            max: 100,\n            disableLine: true,\n            disableTicks: true,\n            tickLabelInterval: () => false,\n          },\n        ]}\n        series={series}\n        slotProps={{ legend: { hidden: true } }}\n        sx={{\n          \"& .MuiLineElement-root\": { strokeWidth: 3 },\n          \"& .MuiMarkElement-root\": { strokeWidth: 2 },\n          [`& .MuiLineElement-series-${slug(biggestRiser.name)}`]: { strokeWidth: 5 },\n          [`& .MuiLineElement-series-${slug(biggestDecliner.name)}`]: { strokeWidth: 5 },\n          [`& .MuiMarkElement-series-${slug(biggestRiser.name)}`]: { strokeWidth: 3 },\n          [`& .MuiMarkElement-series-${slug(biggestDecliner.name)}`]: { strokeWidth: 3 },\n        }}\n      >\n        <EndpointLabels />\n        <DeltaAnnotations />\n      </LineChart>\n    </div>\n  );\n}\n"}