{"spec_id":"bar-permutation-importance","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-permutation-importance: Permutation Feature Importance Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 110, bottom: 100, left: 270 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: permutation importance for a churn-prediction model -------------\n// (feature, mean decrease in ROC-AUC across n_repeats=10 shuffles, std dev)\nconst data = [\n  { feature: \"monthly_charges\", mean: 0.182, std: 0.014 },\n  { feature: \"tenure_months\", mean: 0.156, std: 0.012 },\n  { feature: \"contract_type\", mean: 0.124, std: 0.011 },\n  { feature: \"tech_support_calls\", mean: 0.089, std: 0.009 },\n  { feature: \"internet_service_fiber\", mean: 0.071, std: 0.008 },\n  { feature: \"total_charges\", mean: 0.058, std: 0.01 },\n  { feature: \"payment_method_echeck\", mean: 0.041, std: 0.007 },\n  { feature: \"paperless_billing\", mean: 0.026, std: 0.006 },\n  { feature: \"num_dependents\", mean: 0.018, std: 0.005 },\n  { feature: \"multiple_lines\", mean: 0.012, std: 0.005 },\n  { feature: \"streaming_tv\", mean: 0.007, std: 0.004 },\n  { feature: \"senior_citizen\", mean: 0.004, std: 0.004 },\n  { feature: \"phone_service\", mean: 0.001, std: 0.003 },\n  { feature: \"gender\", mean: -0.003, std: 0.003 },\n]; // pre-sorted by mean, descending — highest importance at top\n\nconst labelize = (s) =>\n  s\n    .split(\"_\")\n    .map((w) => w[0].toUpperCase() + w.slice(1))\n    .join(\" \");\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales --------------------------------------------------------------------\nconst y = d3\n  .scaleBand()\n  .domain(data.map((d) => d.feature))\n  .range([0, ih])\n  .padding(0.3);\n\nconst x = d3\n  .scaleLinear()\n  .domain([d3.min(data, (d) => d.mean - d.std), d3.max(data, (d) => d.mean + d.std)])\n  .nice()\n  .range([0, iw]);\n\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain(d3.extent(data, (d) => d.mean));\n\n// --- Reference line at x = 0 ----------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(0))\n  .attr(\"x2\", x(0))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"5,4\");\n\n// --- Bars ------------------------------------------------------------------------\nconst bars = g.selectAll(\".bar\").data(data).join(\"g\").attr(\"class\", \"bar\");\n\nbars\n  .append(\"rect\")\n  .attr(\"x\", (d) => Math.min(x(0), x(d.mean)))\n  .attr(\"y\", (d) => y(d.feature))\n  .attr(\"width\", (d) => Math.abs(x(d.mean) - x(0)))\n  .attr(\"height\", y.bandwidth())\n  .attr(\"fill\", (d) => color(d.mean))\n  .attr(\"stroke\", (d, i) => (i === 0 ? t.ink : \"none\"))\n  .attr(\"stroke-width\", (d, i) => (i === 0 ? 2 : 0));\n\n// --- Error bars (horizontal, showing shuffle variability) -----------------------\nconst capHalf = Math.min(10, y.bandwidth() * 0.35);\nbars.each(function (d) {\n  const cy = y(d.feature) + y.bandwidth() / 2;\n  const errG = d3.select(this);\n  errG\n    .append(\"line\")\n    .attr(\"x1\", x(d.mean - d.std))\n    .attr(\"x2\", x(d.mean + d.std))\n    .attr(\"y1\", cy)\n    .attr(\"y2\", cy)\n    .attr(\"stroke\", t.ink)\n    .attr(\"stroke-width\", 2);\n  for (const xv of [d.mean - d.std, d.mean + d.std]) {\n    errG\n      .append(\"line\")\n      .attr(\"x1\", x(xv))\n      .attr(\"x2\", x(xv))\n      .attr(\"y1\", cy - capHalf)\n      .attr(\"y2\", cy + capHalf)\n      .attr(\"stroke\", t.ink)\n      .attr(\"stroke-width\", 2);\n  }\n});\n\n// --- Axes --------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6).tickFormat(d3.format(\".2f\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat(labelize).tickSize(0));\n\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").attr(\"dx\", \"-4px\");\nyAxis.select(\".domain\").remove();\n\n// Emphasize the top-ranked feature (the story: which feature matters most)\nyAxis\n  .selectAll(\"text\")\n  .filter((d, i) => i === 0)\n  .attr(\"fill\", t.ink)\n  .style(\"font-weight\", \"700\");\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Mean Decrease in ROC-AUC (Permutation Importance)\");\n\n// --- Title ---------------------------------------------------------------------\nconst title = \"Customer Churn Model · bar-permutation-importance · javascript · d3 · anyplot.ai\";\nconst titleSize = title.length > 67 ? Math.max(20, Math.round((22 * 67) / title.length)) : 22;\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n\n// --- Color legend (sequential gradient encoding importance magnitude) ----------\nconst legendW = 220;\nconst legendX = width - margin.right - legendW;\nconst legendY = 88;\nconst gradientId = \"importance-gradient\";\nconst defs = svg.append(\"defs\");\nconst gradient = defs.append(\"linearGradient\").attr(\"id\", gradientId).attr(\"x1\", \"0%\").attr(\"x2\", \"100%\");\ngradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\ngradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendW)\n  .attr(\"height\", 12)\n  .attr(\"fill\", `url(#${gradientId})`);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY - 8)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Lower importance\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW)\n  .attr(\"y\", legendY - 8)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Higher importance\");\n"}