{"spec_id":"bar-spine","library":"muix","language":"javascript","code":"// anyplot.ai\n// bar-spine: Spine Plot for Two-Variable Proportions\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\nimport { BarChart } from \"@mui/x-charts/BarChart\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\nconst INK_MUTED = window.ANYPLOT_THEME === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\n// Segment labels: a redundant text cue on top of stacking position so the\n// Retained/Churned split doesn't rely on the green/red hue alone. Fixed\n// (theme-independent) fill colors are picked per series for contrast against\n// that series' fixed data color, not the page theme.\nconst MIN_LABEL_HEIGHT = 22; // px; hide the label if a segment can't fit one line of text\nconst segmentLabel = (item, context) =>\n  context.bar.height < MIN_LABEL_HEIGHT ? null : `${item.value}%`;\nconst LABEL_FILL = { retained: \"#1A1A17\", churned: \"#FAF8F1\" };\nconst labelSlotProps = (ownerState) => ({\n  style: { fill: LABEL_FILL[ownerState.seriesId], fontSize: 12, fontWeight: 600 },\n});\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Customer counts per subscription tier, split into churned vs. retained.\n// Bar WIDTH encodes each tier's marginal share of the customer base; segment\n// HEIGHT encodes the conditional churn/retention split within that tier.\nconst tiers = [\n  { name: \"Free\", churned: 1664, retained: 3536 },\n  { name: \"Basic\", churned: 651, retained: 2449 },\n  { name: \"Pro\", churned: 216, retained: 1584 },\n  { name: \"Enterprise\", churned: 42, retained: 658 },\n];\n\nconst totals = tiers.map((d) => d.churned + d.retained);\nconst grandTotal = totals.reduce((sum, v) => sum + v, 0);\nconst churnPct = tiers.map((d, i) => Math.round((d.churned / totals[i]) * 1000) / 10);\nconst retainPct = churnPct.map((pct) => Math.round((100 - pct) * 10) / 10);\n\n// --- Layout: variable-width columns, each a single 100%-stacked bar --------\nconst PAD_H = 32;\nconst PAD_V = 28;\nconst GAP = 10;\nconst AXIS_LABEL_W = 26; // rotated \"Share of customers\" caption, drawn outside the chart SVGs\nconst TICK_W = 52; // reserved for the shared y-axis tick numbers, drawn only on the first column\nconst TITLE_H = 34;\nconst LEGEND_H = 28;\nconst CAPTION_H = 24;\n\nconst rowWidth = size.width - PAD_H * 2;\nconst barAreaWidth = rowWidth - AXIS_LABEL_W - TICK_W;\nconst barWidths = totals.map((total) => Math.round((barAreaWidth * total) / grandTotal));\nbarWidths[barWidths.length - 1] += barAreaWidth - barWidths.reduce((sum, w) => sum + w, 0);\nconst chartWidths = barWidths.map((w, i) => (i === 0 ? TICK_W + w : w));\nconst chartHeight = size.height - PAD_V * 2 - TITLE_H - LEGEND_H - CAPTION_H - GAP * 3;\n\nconst title = \"Customer Churn by Subscription Tier · bar-spine · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.round(18 * (title.length > 67 ? 67 / title.length : 1));\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div\n      style={{\n        width: size.width,\n        height: size.height,\n        padding: `${PAD_V}px ${PAD_H}px`,\n        boxSizing: \"border-box\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: GAP,\n        fontFamily: \"system-ui, sans-serif\",\n      }}\n    >\n      <div style={{ height: TITLE_H, fontSize: titleFontSize, fontWeight: 600, color: t.ink }}>\n        {title}\n      </div>\n\n      <div\n        style={{\n          height: LEGEND_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 20,\n          fontSize: 13,\n          color: t.inkSoft,\n        }}\n      >\n        <span style={{ display: \"flex\", alignItems: \"center\", gap: 6 }}>\n          <span style={{ width: 11, height: 11, background: t.palette[0], display: \"inline-block\" }} />\n          Retained\n        </span>\n        <span style={{ display: \"flex\", alignItems: \"center\", gap: 6 }}>\n          <span style={{ width: 11, height: 11, background: t.palette[4], display: \"inline-block\" }} />\n          Churned\n        </span>\n        <span style={{ marginLeft: \"auto\" }}>n = {grandTotal.toLocaleString(\"en-US\")} customers</span>\n      </div>\n\n      <div style={{ display: \"flex\", height: chartHeight }}>\n        <div\n          style={{\n            width: AXIS_LABEL_W,\n            height: chartHeight,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            flexShrink: 0,\n          }}\n        >\n          <span\n            style={{\n              display: \"inline-block\",\n              transform: \"rotate(-90deg)\",\n              whiteSpace: \"nowrap\",\n              fontSize: 13,\n              color: t.inkSoft,\n            }}\n          >\n            Share of customers\n          </span>\n        </div>\n        {tiers.map((tier, i) => (\n          <BarChart\n            key={tier.name}\n            width={chartWidths[i]}\n            height={chartHeight}\n            skipAnimation\n            margin={{ top: 8, right: 0, bottom: 26, left: i === 0 ? TICK_W : 0 }}\n            xAxis={[\n              {\n                scaleType: \"band\",\n                data: [tier.name],\n                categoryGapRatio: 0,\n                tickLabelStyle: { fontSize: 13 },\n                disableTicks: true,\n                disableLine: true,\n              },\n            ]}\n            yAxis={[\n              {\n                min: 0,\n                max: 100,\n                disableTicks: i !== 0,\n                disableLine: i !== 0,\n                valueFormatter: i === 0 ? (v) => `${v}%` : () => \"\",\n                tickLabelStyle: { fontSize: 12 },\n              },\n            ]}\n            series={[\n              {\n                id: \"retained\",\n                data: [retainPct[i]],\n                label: \"Retained\",\n                stack: \"total\",\n                color: t.palette[0],\n              },\n              {\n                id: \"churned\",\n                data: [churnPct[i]],\n                label: \"Churned\",\n                stack: \"total\",\n                color: t.palette[4],\n              },\n            ]}\n            barLabel={segmentLabel}\n            slotProps={{ legend: { hidden: true }, barLabel: labelSlotProps }}\n          />\n        ))}\n      </div>\n\n      <div style={{ height: CAPTION_H, fontSize: 12, fontStyle: \"italic\", color: INK_MUTED }}>\n        Bar width ∝ tier size · segment height = conditional churn/retention rate within tier\n      </div>\n    </div>\n  );\n}\n"}