{"spec_id":"shap-summary","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// shap-summary: SHAP Summary Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (1664525 * seed + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_SAMPLES = 180;\n\nfunction zscore(values) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;\n  const std = Math.sqrt(variance);\n  return values.map((v) => (v - mean) / std);\n}\n\nfunction minMaxNorm(values) {\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  return values.map((v) => (v - min) / (max - min));\n}\n\n// Raw feature values for a synthetic gradient-boosted house-price model\nconst rawFeatures = {\n  \"Living Area (sqft)\": Array.from({ length: N_SAMPLES }, () => 1450 + randNormal() * 480),\n  Bathrooms: Array.from({ length: N_SAMPLES }, () => 2 + randNormal() * 0.9),\n  \"House Age (years)\": Array.from({ length: N_SAMPLES }, () => 28 + randNormal() * 16),\n  \"Garage Spaces\": Array.from({ length: N_SAMPLES }, () => 1.6 + randNormal() * 0.8),\n  \"Lot Size (acres)\": Array.from({ length: N_SAMPLES }, () => 0.35 + randNormal() * 0.18),\n  \"Distance to Downtown (km)\": Array.from({ length: N_SAMPLES }, () => 12 + randNormal() * 7),\n  \"Walk Score\": Array.from({ length: N_SAMPLES }, () => 55 + randNormal() * 22),\n};\nconst featureNames = Object.keys(rawFeatures);\n\nconst zFeatures = {};\nfeatureNames.forEach((name) => {\n  zFeatures[name] = zscore(rawFeatures[name]);\n});\n\n// SHAP-like contribution per feature, in $1,000s of predicted price.\n// \"House Age\" is modeled as non-linear: both historic and brand-new homes\n// command a premium over mid-age housing stock.\nconst shapValues = {};\nfeatureNames.forEach((name) => {\n  shapValues[name] = zFeatures[name].map((zi) => {\n    const noise = randNormal() * 4;\n    switch (name) {\n      case \"Living Area (sqft)\":\n        return 26 * zi + noise;\n      case \"Bathrooms\":\n        return 14 * zi + noise;\n      case \"House Age (years)\":\n        return 9 * zi ** 2 - 6 + noise * 0.8;\n      case \"Garage Spaces\":\n        return 8 * zi + noise * 0.7;\n      case \"Lot Size (acres)\":\n        return 7 * zi + noise * 0.7;\n      case \"Distance to Downtown (km)\":\n        return -11 * zi + noise;\n      case \"Walk Score\":\n        return 6 * zi + noise * 0.8;\n      default:\n        return noise;\n    }\n  });\n});\n\n// Rank features by mean absolute SHAP value — most important at the top.\nconst meanAbsShap = {};\nfeatureNames.forEach((name) => {\n  const vals = shapValues[name];\n  meanAbsShap[name] = vals.reduce((a, b) => a + Math.abs(b), 0) / vals.length;\n});\nconst orderedFeatures = [...featureNames].sort((a, b) => meanAbsShap[b] - meanAbsShap[a]);\nconst categories = orderedFeatures;\n\nconst normFeatures = {};\nfeatureNames.forEach((name) => {\n  normFeatures[name] = minMaxNorm(rawFeatures[name]);\n});\n\n// --- Color mapping ------------------------------------------------------------\n// The `coloraxis` module (colorAxis + automatic legend gradient) lives in\n// modules/coloraxis.js, which isn't loaded — only the core bundle is. Interpolate\n// each point's fill from the Imprint imprint_seq gradient by hand instead, and\n// draw a matching colorbar with the core SVG renderer.\nfunction hexToRgb(hex) {\n  const v = parseInt(hex.slice(1), 16);\n  return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n}\nconst seqLow = hexToRgb(t.seq[0]);\nconst seqHigh = hexToRgb(t.seq[1]);\nfunction valueColor(ratio) {\n  const [r, g, b] = seqLow.map((c, i) => Math.round(c + (seqHigh[i] - c) * ratio));\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\n// Beeswarm points: one dot per sample per feature, jittered around its row.\nconst points = [];\norderedFeatures.forEach((name, rowIndex) => {\n  const shap = shapValues[name];\n  const norm = normFeatures[name];\n  shap.forEach((value, j) => {\n    const jitter = (rand() - 0.5) * 0.62;\n    points.push({\n      x: value,\n      y: rowIndex + jitter,\n      color: valueColor(norm[j]),\n      custom: { feature: name, featureValue: rawFeatures[name][j] },\n    });\n  });\n});\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    marginRight: 170,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load: function () {\n        // Manual colorbar (see \"Color mapping\" note above) mirroring the\n        // imprint_seq gradient used for the point fills.\n        const chart = this;\n        const barWidth = 26;\n        const barX = chart.plotLeft + chart.plotWidth + 46;\n        const barY = chart.plotTop;\n        const barHeight = chart.plotHeight;\n\n        // Fragment-url paint servers (linearGradient defs) don't resolve on the\n        // harness's about:blank document, so the bar is built from many thin\n        // interpolated bands instead of a single gradient fill.\n        const bandCount = 60;\n        const bandHeight = barHeight / bandCount;\n        for (let i = 0; i < bandCount; i++) {\n          const frac = (i + 0.5) / bandCount; // 0 = bottom (low), 1 = top (high)\n          chart.renderer\n            .rect(barX, barY + barHeight - (i + 1) * bandHeight, barWidth, bandHeight + 0.5)\n            .attr({ fill: valueColor(frac) })\n            .add();\n        }\n        chart.renderer\n          .rect(barX, barY, barWidth, barHeight)\n          .attr({ fill: \"none\", stroke: t.inkSoft, \"stroke-width\": 1 })\n          .add();\n\n        chart.renderer\n          .text(\"High\", barX + barWidth + 10, barY + 14)\n          .css({ color: t.inkSoft, fontSize: \"14px\" })\n          .add();\n        chart.renderer\n          .text(\"Low\", barX + barWidth + 10, barY + barHeight)\n          .css({ color: t.inkSoft, fontSize: \"14px\" })\n          .add();\n        chart.renderer\n          .text(\"Feature value\", barX + barWidth + 58, barY + barHeight / 2)\n          .attr({ rotation: 90, align: \"center\" })\n          .css({ color: t.inkSoft, fontSize: \"16px\" })\n          .add();\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"shap-summary · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Gradient-boosted house-price model · 180 samples\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    headerFormat: \"\",\n    pointFormat:\n      \"<b>{point.custom.feature}</b><br/>Feature value: {point.custom.featureValue:.2f}<br/>SHAP value: {point.x:.2f}\",\n  },\n  xAxis: {\n    title: {\n      text: \"SHAP value (impact on predicted price, $1,000s)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [{ value: 0, color: t.inkSoft, width: 1.5, dashStyle: \"ShortDash\", zIndex: 3 }],\n  },\n  yAxis: {\n    categories,\n    reversed: true,\n    tickPositions: categories.map((_, i) => i),\n    minPadding: 0.09,\n    maxPadding: 0.09,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: null },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    // Faint focal-point band on the most important feature (row 0) so the\n    // ranking hierarchy reads at a glance, not just via row order.\n    plotBands: [{ from: -0.5, to: 0.5, color: t.elevatedBg, zIndex: 0 }],\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: { radius: 5, lineWidth: 0.5, lineColor: t.pageBg, fillOpacity: 0.75 },\n    },\n  },\n  series: [{ name: \"SHAP values\", showInLegend: false, data: points }],\n});\n"}