{"spec_id":"shap-summary","library":"echarts","language":"javascript","code":"// anyplot.ai\n// shap-summary: SHAP Summary Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG + Box-Muller) ----------------------------------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\nconst 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};\nconst clip = (v, lo, hi) => Math.min(hi, Math.max(lo, v));\n\n// --- Data: SHAP explanations for a house-price model ------------------------\n// Each feature has a raw generator and a shap() function of its own\n// min-max-normalized value (0=low, 1=high) that defines its effect on the\n// predicted price. Floor Level is deliberately non-linear: both very low and\n// very high floors hurt the price, mid floors help it.\nconst N_SAMPLES = 260;\n\nconst featureDefs = [\n  { label: \"Living Area (sqm)\", gen: () => clip(100 + randNormal() * 32, 32, 210), shap: (n) => 42000 * (n - 0.5) * 2 },\n  { label: \"School Rating\", gen: () => clip(5.5 + randNormal() * 2.2, 1, 10), shap: (n) => 27000 * (n - 0.5) * 2 },\n  { label: \"Distance to Center (km)\", gen: () => clip(8 + randNormal() * 4.5, 0.4, 26), shap: (n) => -22000 * (n - 0.5) * 2 },\n  { label: \"Property Age (yrs)\", gen: () => clip(30 + randNormal() * 22, 0, 90), shap: (n) => -16000 * (n - 0.5) * 2 },\n  { label: \"Floor Level\", gen: () => Math.round(clip(9 + randNormal() * 5.5, 0, 19)), shap: (n) => 11500 * (1 - 4 * (n - 0.5) * (n - 0.5)) - 5750 },\n  { label: \"Crime Rate Index\", gen: () => clip(48 + randNormal() * 19, 2, 98), shap: (n) => -8800 * (n - 0.5) * 2 },\n  { label: \"Bathrooms\", gen: () => Math.round(clip(1.8 + randNormal() * 0.9, 1, 4)), shap: (n) => 6200 * (n - 0.5) * 2 },\n  { label: \"Energy Rating\", gen: () => Math.round(clip(4 + randNormal() * 1.8, 1, 7)), shap: (n) => 4600 * (n - 0.5) * 2 },\n  { label: \"Has Balcony\", gen: () => (rand() < 0.55 ? 1 : 0), shap: (n) => 2100 * (n - 0.5) * 2 },\n];\n\nconst rawValues = featureDefs.map((f) => Array.from({ length: N_SAMPLES }, f.gen));\n\nconst normValues = rawValues.map((vals) => {\n  const lo = Math.min(...vals);\n  const hi = Math.max(...vals);\n  const span = hi - lo || 1;\n  return vals.map((v) => (v - lo) / span);\n});\n\nconst shapValues = featureDefs.map((f, fi) =>\n  normValues[fi].map((n) => f.shap(n) + randNormal() * (Math.abs(f.shap(n)) * 0.4 + 900))\n);\n\nconst meanAbsShap = shapValues.map((vals) => vals.reduce((a, v) => a + Math.abs(v), 0) / vals.length);\n\n// Most important feature at the top: rank descending, then reverse so index 0\n// (bottom of the value-axis) is the least important feature.\nconst displayOrder = featureDefs\n  .map((_, i) => i)\n  .sort((a, b) => meanAbsShap[b] - meanAbsShap[a])\n  .reverse();\n\nconst featureLabels = displayOrder.map((i) => featureDefs[i].label);\nconst rowIndices = featureLabels.map((_, i) => i);\n\n// --- Vertical jitter (binned beeswarm approximation) ------------------------\nconst jitter = (vals, rowHalfWidth) => {\n  const nBins = 50;\n  const lo = Math.min(...vals);\n  const hi = Math.max(...vals);\n  const binWidth = (hi - lo) / nBins || 1;\n  const counts = new Array(nBins).fill(0);\n  const step = 0.035;\n  return vals.map((v) => {\n    const b = clip(Math.floor((v - lo) / binWidth), 0, nBins - 1);\n    const k = counts[b]++;\n    const dir = k % 2 === 0 ? 1 : -1;\n    const mag = Math.ceil(k / 2);\n    return clip(dir * mag * step, -rowHalfWidth, rowHalfWidth);\n  });\n};\n\nconst points = [];\ndisplayOrder.forEach((fi, row) => {\n  const offsets = jitter(shapValues[fi], 0.42);\n  shapValues[fi].forEach((x, si) => {\n    points.push([x, row + offsets[si], normValues[fi][si]]);\n  });\n});\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"shap-summary · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  grid: { left: 24, right: 150, top: 90, bottom: 70, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    name: \"SHAP value (impact on predicted price, USD)\",\n    nameLocation: \"middle\",\n    nameGap: 36,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    min: -0.5,\n    max: featureLabels.length - 0.5,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      customValues: rowIndices,\n      // Bold + slightly larger label draws the eye to the single most impactful\n      // feature (topmost row) to sharpen the importance-ordering story.\n      formatter: (v) => (v === rowIndices.length - 1 ? `{top|${featureLabels[v]}}` : featureLabels[v]),\n      rich: { top: { color: t.ink, fontWeight: \"bold\", fontSize: 15 } },\n    },\n    axisTick: { show: false, customValues: rowIndices },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { customValues: rowIndices, lineStyle: { color: t.grid } },\n  },\n  visualMap: {\n    type: \"continuous\",\n    dimension: 2,\n    min: 0,\n    max: 1,\n    calculable: false,\n    right: 16,\n    top: 320,\n    itemWidth: 18,\n    itemHeight: 260,\n    text: [\"High\", \"Low\"],\n    textGap: 10,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    inRange: { color: [\"#4467A3\", t.pageBg, \"#AE3030\"] },\n  },\n  graphic: {\n    elements: [\n      {\n        type: \"text\",\n        right: 4,\n        top: 288,\n        style: { text: \"Feature value\", fill: t.inkSoft, fontSize: 12, fontWeight: 500, align: \"right\" },\n      },\n    ],\n  },\n  series: [\n    {\n      type: \"scatter\",\n      data: points,\n      symbolSize: 6,\n      itemStyle: { opacity: 0.58 },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: { show: false },\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        data: [{ xAxis: 0 }],\n      },\n    },\n  ],\n});\n"}