{"spec_id":"bar-permutation-importance","library":"muix","language":"javascript","code":"// anyplot.ai\n// bar-permutation-importance: Permutation Feature Importance Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n//# anyplot-orientation: landscape\n// anyplot.ai\n// bar-permutation-importance: Permutation Feature Importance Plot\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-08-26\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { BarPlot } from \"@mui/x-charts/BarChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Churn-prediction random forest, sklearn.inspection.permutation_importance\n// (n_repeats=10) — decrease in held-out accuracy when each feature is\n// shuffled. Sorted so the strongest driver renders at the top of the ranking.\nconst FEATURES = [\n  \"contract_type\",\n  \"tenure_months\",\n  \"monthly_charges\",\n  \"total_charges\",\n  \"tech_support\",\n  \"online_security\",\n  \"support_calls\",\n  \"internet_service\",\n  \"payment_method\",\n  \"paperless_billing\",\n  \"dependents\",\n  \"streaming_tv\",\n  \"partner\",\n  \"multiple_lines\",\n  \"senior_citizen\",\n];\nconst MEANS = [0.182, 0.146, 0.098, 0.071, 0.058, 0.049, 0.041, 0.033, 0.026, 0.019, 0.014, 0.009, 0.006, 0.003, -0.004];\nconst STDS  = [0.021, 0.018, 0.015, 0.014, 0.012, 0.011, 0.010, 0.009, 0.008, 0.007, 0.006, 0.006, 0.005, 0.005, 0.004];\n\n// --- Colour: sequential Imprint colormap (imprint_seq), one stop per bar ----\nfunction hexToRgb(hex) {\n  const int = parseInt(hex.slice(1), 16);\n  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n}\n\nfunction lerp(a, b, ratio) {\n  return Math.round(a + (b - a) * ratio);\n}\n\nfunction imprintSeqInterpolator(stops) {\n  const [low, high] = stops.map(hexToRgb);\n  return (position) => {\n    const ratio = Math.min(1, Math.max(0, position));\n    const [r, g, b] = [0, 1, 2].map((channel) => lerp(low[channel], high[channel], ratio));\n    return `rgb(${r}, ${g}, ${b})`;\n  };\n}\n\nconst seqColor = imprintSeqInterpolator(t.seq);\nconst MIN_MEAN = Math.min(...MEANS);\nconst MAX_MEAN = Math.max(...MEANS);\nconst BAR_COLORS = MEANS.map((v) => seqColor((v - MIN_MEAN) / (MAX_MEAN - MIN_MEAN)));\n\n// Horizontal ±1 SD whiskers across the shuffle repetitions, drawn with the\n// CartesianContext scales so they line up exactly with the bars underneath.\nfunction ErrorBars() {\n  const xScale = useXScale(\"x\");\n  const yScale = useYScale(\"y\");\n  if (!xScale || !yScale) return null;\n\n  const bw = yScale.bandwidth();\n  const cap = bw * 0.34;\n\n  return (\n    <g>\n      {FEATURES.map((feature, i) => {\n        const cy = yScale(feature) + bw / 2;\n        const xLo = xScale(MEANS[i] - STDS[i]);\n        const xHi = xScale(MEANS[i] + STDS[i]);\n        return (\n          <g key={feature} stroke={t.ink} strokeWidth={3} strokeOpacity={0.8} strokeLinecap=\"round\">\n            <line x1={xLo} y1={cy} x2={xHi} y2={cy} />\n            <line x1={xLo} y1={cy - cap} x2={xLo} y2={cy + cap} />\n            <line x1={xHi} y1={cy - cap} x2={xHi} y2={cy + cap} />\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width; // 1600 CSS px (landscape mount)\n  const H = window.ANYPLOT_SIZE.height; // 900 CSS px\n  const CHART_TOP = 84;\n\n  const title = \"Churn Prediction Model · bar-permutation-importance · javascript · muix · anyplot.ai\";\n  const titleSize = title.length > 67 ? Math.round(22 * 67 / title.length) : 22;\n\n  return (\n    <Box sx={{ position: \"relative\", width: W, height: H, bgcolor: t.pageBg }}>\n      {/* Title + subtitle */}\n      <Box sx={{ position: \"absolute\", top: 22, left: 56, right: 56 }}>\n        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 500, lineHeight: 1.25 }}>\n          {title}\n        </Typography>\n        <Typography sx={{ color: t.inkSoft, fontSize: 14, mt: 0.5 }}>\n          Decrease in held-out accuracy after shuffling each feature, ±1 SD across 10 repetitions\n        </Typography>\n      </Box>\n\n      {/* Chart */}\n      <Box sx={{ position: \"absolute\", top: CHART_TOP, left: 0, right: 0, bottom: 0 }}>\n        <ChartContainer\n          width={W}\n          height={H - CHART_TOP}\n          series={[\n            {\n              type: \"bar\",\n              id: \"importance\",\n              data: MEANS,\n              layout: \"horizontal\",\n              xAxisId: \"x\",\n              yAxisId: \"y\",\n              label: \"Permutation importance\",\n              valueFormatter: (v, ctx) => {\n                const i = ctx?.dataIndex ?? 0;\n                return `${v.toFixed(3)} ± ${STDS[i].toFixed(3)} accuracy`;\n              },\n            },\n          ]}\n          xAxis={[\n            {\n              id: \"x\",\n              scaleType: \"linear\",\n              min: -0.02,\n              max: 0.22,\n              label: \"Mean Decrease in Accuracy\",\n              labelStyle: { fontSize: 16, fill: t.ink },\n              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n              tickInterval: [0, 0.04, 0.08, 0.12, 0.16, 0.2],\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"y\",\n              scaleType: \"band\",\n              data: FEATURES,\n              colorMap: { type: \"ordinal\", values: FEATURES, colors: BAR_COLORS },\n              categoryGapRatio: 0.32,\n              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n              disableTicks: true,\n            },\n          ]}\n          margin={{ top: 14, right: 60, bottom: 64, left: 210 }}\n          sx={{\n            \"& .MuiChartsAxis-line\": { stroke: t.inkSoft },\n            \"& .MuiChartsGrid-line\": { stroke: t.grid },\n            \"& .MuiBarElement-root\": { stroke: t.pageBg, strokeWidth: 1.5 },\n          }}\n        >\n          <ChartsGrid vertical />\n          <BarPlot skipAnimation borderRadius={3} />\n          <ErrorBars />\n          <ChartsReferenceLine\n            x={0}\n            axisId=\"x\"\n            lineStyle={{ stroke: t.ink, strokeWidth: 2, strokeOpacity: 0.55 }}\n          />\n          <ChartsXAxis axisId=\"x\" />\n          <ChartsYAxis axisId=\"y\" />\n        </ChartContainer>\n      </Box>\n    </Box>\n  );\n}\n"}