{"spec_id":"bar-diverging-likert","library":"muix","language":"javascript","code":"// anyplot.ai\n// bar-diverging-likert: Likert Scale Diverging Bar Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-24\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME === \"dark\" ? \"dark\" : \"light\";\n// `muted` isn't part of window.ANYPLOT_TOKENS — derive it per\n// default-style-guide.md \"Theme-adaptive Chrome\" (the semantic-anchor role\n// fits the Likert \"Neutral\" bucket: soft-contrast, sits behind the data).\nconst MUTED = THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data (in-memory, deterministic) — employee engagement survey, 5-point Likert ---\nconst RESPONSES = [\n  {\n    question: \"I understand my role's impact\",\n    strongly_disagree: 2,\n    disagree: 5,\n    neutral: 8,\n    agree: 45,\n    strongly_agree: 40,\n  },\n  {\n    question: \"I receive regular recognition\",\n    strongly_disagree: 8,\n    disagree: 17,\n    neutral: 20,\n    agree: 35,\n    strongly_agree: 20,\n  },\n  {\n    question: \"I have growth opportunities\",\n    strongly_disagree: 10,\n    disagree: 22,\n    neutral: 23,\n    agree: 30,\n    strongly_agree: 15,\n  },\n  {\n    question: \"My manager gives useful feedback\",\n    strongly_disagree: 5,\n    disagree: 12,\n    neutral: 18,\n    agree: 40,\n    strongly_agree: 25,\n  },\n  {\n    question: \"I can share ideas openly\",\n    strongly_disagree: 4,\n    disagree: 11,\n    neutral: 15,\n    agree: 42,\n    strongly_agree: 28,\n  },\n  {\n    question: \"Changes are communicated well\",\n    strongly_disagree: 12,\n    disagree: 25,\n    neutral: 20,\n    agree: 28,\n    strongly_agree: 15,\n  },\n  {\n    question: \"My workload is manageable\",\n    strongly_disagree: 18,\n    disagree: 27,\n    neutral: 15,\n    agree: 28,\n    strongly_agree: 12,\n  },\n  {\n    question: \"I'd recommend this company\",\n    strongly_disagree: 3,\n    disagree: 7,\n    neutral: 12,\n    agree: 38,\n    strongly_agree: 40,\n  },\n];\n\n// Sort by net agreement (agree + strongly_agree − disagree − strongly_disagree),\n// descending so the strongest agreement lands at the top of the band axis.\nconst SORTED = [...RESPONSES].sort((a, b) => {\n  const netA = a.agree + a.strongly_agree - a.disagree - a.strongly_disagree;\n  const netB = b.agree + b.strongly_agree - b.disagree - b.strongly_disagree;\n  return netB - netA;\n});\n\nconst questions = SORTED.map((d) => d.question);\n\n// Diverging construction: split \"neutral\" evenly across the zero midpoint so\n// bars extend left (disagreement) and right (agreement) from a shared centerline.\n// Series listed closest-to-zero first within each sign — @mui/x-charts stacks\n// negative and positive values independently (d3's stackOffsetDiverging), with\n// the earliest series in a sign group nearest the baseline.\nconst negNeutral = SORTED.map((d) => -d.neutral / 2);\nconst negDisagree = SORTED.map((d) => -d.disagree);\nconst negStronglyDisagree = SORTED.map((d) => -d.strongly_disagree);\nconst posNeutral = SORTED.map((d) => d.neutral / 2);\nconst posAgree = SORTED.map((d) => d.agree);\nconst posStronglyAgree = SORTED.map((d) => d.strongly_agree);\n\n// Symmetric axis extent so both diverging halves read against the same scale.\nconst maxExtent = Math.max(\n  ...SORTED.map((d) => d.strongly_disagree + d.disagree + d.neutral / 2),\n  ...SORTED.map((d) => d.agree + d.strongly_agree + d.neutral / 2),\n);\nconst axisBound = Math.ceil((maxExtent * 1.08) / 5) * 5;\n\n// Diverging red <-> muted <-> blue mirrors imprint_div's own red <-> neutral <-> blue\n// construction (default-style-guide.md \"Semantic exception\" / \"Continuous Data\") —\n// the Likert scale is ordinal data with a meaningful midpoint, same as the spec's\n// own suggested \"red-to-blue\" scheme.\nconst COLOR_STRONGLY_DISAGREE = t.palette[4]; // \"#AE3030\" matte red\nconst COLOR_DISAGREE = t.palette[6]; // \"#954477\" rose\nconst COLOR_NEUTRAL = MUTED;\nconst COLOR_AGREE = t.palette[5]; // \"#2ABCCD\" cyan\nconst COLOR_STRONGLY_AGREE = t.palette[2]; // \"#4467A3\" blue\n\n// Neutral is split into two invisible halves for the diverging layout — label\n// the true (unsplit) share once, on the right half, rather than showing the\n// halved value twice.\nconst MIN_LABEL_WIDTH = 30;\nconst barLabel = (item, context) => {\n  if (item.value === null || context.bar.width < MIN_LABEL_WIDTH) return null;\n  if (item.seriesId === \"neutral_left\") return null;\n  if (item.seriesId === \"neutral_right\")\n    return `${SORTED[item.dataIndex].neutral}%`;\n  return `${Math.abs(item.value)}%`;\n};\n\nconst title = \"bar-diverging-likert · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.round(\n  22 * (title.length > 67 ? 67 / title.length : 1),\n);\n\nconst LEGEND_ITEMS = [\n  { label: \"Strongly Disagree\", color: COLOR_STRONGLY_DISAGREE },\n  { label: \"Disagree\", color: COLOR_DISAGREE },\n  { label: \"Neutral\", color: COLOR_NEUTRAL },\n  { label: \"Agree\", color: COLOR_AGREE },\n  { label: \"Strongly Agree\", color: COLOR_STRONGLY_AGREE },\n];\n\nexport default function Chart() {\n  const width = window.ANYPLOT_SIZE.width;\n  const titleHeight = 56;\n  const legendHeight = 44;\n  const chartHeight = window.ANYPLOT_SIZE.height - titleHeight - legendHeight;\n\n  return (\n    <div\n      style={{\n        width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography\n        align=\"center\"\n        style={{\n          fontSize: titleFontSize,\n          fontWeight: 500,\n          height: titleHeight,\n          lineHeight: `${titleHeight}px`,\n        }}\n      >\n        {title}\n      </Typography>\n      <BarChart\n        width={width}\n        height={chartHeight}\n        layout=\"horizontal\"\n        skipAnimation\n        series={[\n          {\n            id: \"neutral_left\",\n            data: negNeutral,\n            stack: \"likert\",\n            color: COLOR_NEUTRAL,\n          },\n          {\n            id: \"disagree\",\n            data: negDisagree,\n            label: \"Disagree\",\n            stack: \"likert\",\n            color: COLOR_DISAGREE,\n          },\n          {\n            id: \"strongly_disagree\",\n            data: negStronglyDisagree,\n            label: \"Strongly Disagree\",\n            stack: \"likert\",\n            color: COLOR_STRONGLY_DISAGREE,\n          },\n          {\n            id: \"neutral_right\",\n            data: posNeutral,\n            label: \"Neutral\",\n            stack: \"likert\",\n            color: COLOR_NEUTRAL,\n          },\n          {\n            id: \"agree\",\n            data: posAgree,\n            label: \"Agree\",\n            stack: \"likert\",\n            color: COLOR_AGREE,\n          },\n          {\n            id: \"strongly_agree\",\n            data: posStronglyAgree,\n            label: \"Strongly Agree\",\n            stack: \"likert\",\n            color: COLOR_STRONGLY_AGREE,\n          },\n        ]}\n        barLabel={barLabel}\n        xAxis={[\n          {\n            min: -axisBound,\n            max: axisBound,\n            valueFormatter: (value) => `${Math.abs(value)}%`,\n            tickLabelStyle: { fontSize: 14 },\n            label: \"% of Responses\",\n            labelStyle: { fontSize: 15 },\n          },\n        ]}\n        yAxis={[\n          {\n            scaleType: \"band\",\n            data: questions,\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n        margin={{ left: 260, right: 40, top: 10, bottom: 64 }}\n        slots={{ legend: () => null }}\n        sx={{\n          // Dark diverging segments (red/rose/blue) need light label text to clear\n          // WCAG AA contrast; the lighter segments (muted neutral, cyan agree) keep\n          // the theme-default ink fill from BarLabel's styled `text.primary`.\n          [[\"strongly_disagree\", \"disagree\", \"strongly_agree\"]\n            .map((id) => `& .MuiBarLabel-series-${id}`)\n            .join(\", \")]: {\n            fill: \"#FFFFFF\",\n          },\n        }}\n      >\n        <ChartsReferenceLine\n          x={0}\n          lineStyle={{ stroke: t.ink, strokeWidth: 1.5 }}\n        />\n      </BarChart>\n      <div\n        style={{\n          height: legendHeight,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: 24,\n        }}\n      >\n        {LEGEND_ITEMS.map((item) => (\n          <div\n            key={item.label}\n            style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}\n          >\n            <span\n              style={{\n                width: 14,\n                height: 14,\n                borderRadius: 3,\n                background: item.color,\n              }}\n            />\n            <Typography style={{ fontSize: 14 }}>{item.label}</Typography>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"}