SHAP Summary Plot — Apache ECharts

A SHAP (SHapley Additive exPlanations) summary plot displaying the distribution of SHAP values for each feature, ordered by mean absolute SHAP value (importance). Each dot represents a sample, positioned horizontally by its SHAP value and colored by the feature's value (typically low=blue to high=red). This visualization is essential for machine learning interpretability, showing both feature importance and the direction and magnitude of feature effects on model predictions.

SHAP Summary Plot rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// shap-summary: SHAP Summary Plot
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-09-09

//# anyplot-orientation: landscape
const t = window.ANYPLOT_TOKENS;

// --- Deterministic PRNG (LCG + Box-Muller) ----------------------------------
let seed = 42;
const rand = () => {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
};
const randNormal = () => {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
};
const clip = (v, lo, hi) => Math.min(hi, Math.max(lo, v));

// --- Data: SHAP explanations for a house-price model ------------------------
// Each feature has a raw generator and a shap() function of its own
// min-max-normalized value (0=low, 1=high) that defines its effect on the
// predicted price. Floor Level is deliberately non-linear: both very low and
// very high floors hurt the price, mid floors help it.
const N_SAMPLES = 260;

const featureDefs = [
  { label: "Living Area (sqm)", gen: () => clip(100 + randNormal() * 32, 32, 210), shap: (n) => 42000 * (n - 0.5) * 2 },
  { label: "School Rating", gen: () => clip(5.5 + randNormal() * 2.2, 1, 10), shap: (n) => 27000 * (n - 0.5) * 2 },
  { label: "Distance to Center (km)", gen: () => clip(8 + randNormal() * 4.5, 0.4, 26), shap: (n) => -22000 * (n - 0.5) * 2 },
  { label: "Property Age (yrs)", gen: () => clip(30 + randNormal() * 22, 0, 90), shap: (n) => -16000 * (n - 0.5) * 2 },
  { 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 },
  { label: "Crime Rate Index", gen: () => clip(48 + randNormal() * 19, 2, 98), shap: (n) => -8800 * (n - 0.5) * 2 },
  { label: "Bathrooms", gen: () => Math.round(clip(1.8 + randNormal() * 0.9, 1, 4)), shap: (n) => 6200 * (n - 0.5) * 2 },
  { label: "Energy Rating", gen: () => Math.round(clip(4 + randNormal() * 1.8, 1, 7)), shap: (n) => 4600 * (n - 0.5) * 2 },
  { label: "Has Balcony", gen: () => (rand() < 0.55 ? 1 : 0), shap: (n) => 2100 * (n - 0.5) * 2 },
];

const rawValues = featureDefs.map((f) => Array.from({ length: N_SAMPLES }, f.gen));

const normValues = rawValues.map((vals) => {
  const lo = Math.min(...vals);
  const hi = Math.max(...vals);
  const span = hi - lo || 1;
  return vals.map((v) => (v - lo) / span);
});

const shapValues = featureDefs.map((f, fi) =>
  normValues[fi].map((n) => f.shap(n) + randNormal() * (Math.abs(f.shap(n)) * 0.4 + 900))
);

const meanAbsShap = shapValues.map((vals) => vals.reduce((a, v) => a + Math.abs(v), 0) / vals.length);

// Most important feature at the top: rank descending, then reverse so index 0
// (bottom of the value-axis) is the least important feature.
const displayOrder = featureDefs
  .map((_, i) => i)
  .sort((a, b) => meanAbsShap[b] - meanAbsShap[a])
  .reverse();

const featureLabels = displayOrder.map((i) => featureDefs[i].label);
const rowIndices = featureLabels.map((_, i) => i);

// --- Vertical jitter (binned beeswarm approximation) ------------------------
const jitter = (vals, rowHalfWidth) => {
  const nBins = 50;
  const lo = Math.min(...vals);
  const hi = Math.max(...vals);
  const binWidth = (hi - lo) / nBins || 1;
  const counts = new Array(nBins).fill(0);
  const step = 0.035;
  return vals.map((v) => {
    const b = clip(Math.floor((v - lo) / binWidth), 0, nBins - 1);
    const k = counts[b]++;
    const dir = k % 2 === 0 ? 1 : -1;
    const mag = Math.ceil(k / 2);
    return clip(dir * mag * step, -rowHalfWidth, rowHalfWidth);
  });
};

const points = [];
displayOrder.forEach((fi, row) => {
  const offsets = jitter(shapValues[fi], 0.42);
  shapValues[fi].forEach((x, si) => {
    points.push([x, row + offsets[si], normValues[fi][si]]);
  });
});

// --- Init --------------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option --------------------------------------------------------------------
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  title: {
    text: "shap-summary · javascript · echarts · anyplot.ai",
    left: "center",
    textStyle: { color: t.ink, fontSize: 22 },
  },
  grid: { left: 24, right: 150, top: 90, bottom: 70, containLabel: true },
  xAxis: {
    type: "value",
    name: "SHAP value (impact on predicted price, USD)",
    nameLocation: "middle",
    nameGap: 36,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  yAxis: {
    type: "value",
    min: -0.5,
    max: featureLabels.length - 0.5,
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      customValues: rowIndices,
      // Bold + slightly larger label draws the eye to the single most impactful
      // feature (topmost row) to sharpen the importance-ordering story.
      formatter: (v) => (v === rowIndices.length - 1 ? `{top|${featureLabels[v]}}` : featureLabels[v]),
      rich: { top: { color: t.ink, fontWeight: "bold", fontSize: 15 } },
    },
    axisTick: { show: false, customValues: rowIndices },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { customValues: rowIndices, lineStyle: { color: t.grid } },
  },
  visualMap: {
    type: "continuous",
    dimension: 2,
    min: 0,
    max: 1,
    calculable: false,
    right: 16,
    top: 320,
    itemWidth: 18,
    itemHeight: 260,
    text: ["High", "Low"],
    textGap: 10,
    textStyle: { color: t.inkSoft, fontSize: 14 },
    inRange: { color: ["#4467A3", t.pageBg, "#AE3030"] },
  },
  graphic: {
    elements: [
      {
        type: "text",
        right: 4,
        top: 288,
        style: { text: "Feature value", fill: t.inkSoft, fontSize: 12, fontWeight: 500, align: "right" },
      },
    ],
  },
  series: [
    {
      type: "scatter",
      data: points,
      symbolSize: 6,
      itemStyle: { opacity: 0.58 },
      markLine: {
        silent: true,
        symbol: "none",
        label: { show: false },
        lineStyle: { color: t.inkSoft, type: "dashed", width: 1.5 },
        data: [{ xAxis: 0 }],
      },
    },
  ],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/shap-summary/echarts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "shap-summary",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/shap-summary/javascript/echarts",
  "hub": "https://anyplot.ai/shap-summary",
  "code_json": "https://api.anyplot.ai/specs/shap-summary/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/shap-summary",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/shap-summary/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/shap-summary/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/shap-summary/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/shap-summary/javascript/echarts/plot-dark.html",
  "quality_score": 93.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of SHAP Summary Plot on anyplot.ai.

Other implementations