{"spec_id":"ice-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// ice-basic: Individual Conditional Expectation (ICE) Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-17\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst N_HOUSES = 90;\nconst N_GRID = 60;\nconst SQFT_MIN = 800;\nconst SQFT_MAX = 3200;\nconst RANGE = SQFT_MAX - SQFT_MIN;\n\nconst grid = Array.from(\n  { length: N_GRID },\n  (_, i) => SQFT_MIN + (i / (N_GRID - 1)) * RANGE\n);\n\n// Each house gets its own base price, slope and curvature — a stand-in for a\n// GradientBoostingRegressor's individual conditional expectation curve. A\n// minority of houses plateau (renovation-capped neighborhoods) or dip\n// (oversized-for-block penalty) instead of climbing monotonically, so the\n// fan demonstrates the interaction/subgroup-detection use case, not just\n// varying slope magnitude.\nconst iceCurves = [];\nconst observedSqft = [];\nlet highlightIndex = -1;\nfor (let h = 0; h < N_HOUSES; h++) {\n  const basePrice = 140000 + rand() * 90000;\n  const pricePerSqft = 90 + rand() * 60;\n  const curvature = -6 + rand() * 12; // diminishing vs. accelerating returns\n  const noise = (rand() - 0.5) * 12000;\n  observedSqft.push(SQFT_MIN + rand() * RANGE);\n\n  const isPlateau = h % 12 === 4;\n  const isDip = h % 15 === 9;\n  if (isDip && highlightIndex === -1) highlightIndex = h;\n\n  const curve = grid.map((sqft) => {\n    const dx = sqft - SQFT_MIN;\n    if (isPlateau) {\n      // Price growth caps past ~55% of the range — a subgroup where extra\n      // square footage stops adding value (e.g. a HOA size cap).\n      const cappedDx = Math.min(dx, RANGE * 0.55);\n      const price = basePrice + pricePerSqft * cappedDx + curvature * 0.001 * cappedDx * cappedDx + noise;\n      return [sqft, Math.round(price)];\n    }\n    let price = basePrice + pricePerSqft * dx + curvature * 0.001 * dx * dx + noise;\n    if (isDip) {\n      // A localized dip around the upper-middle of the range — a subgroup\n      // where oversized homes read as \"too big for the block\" to buyers.\n      const dipCenter = RANGE * 0.68;\n      const dipWidth = RANGE * 0.22;\n      const dipDepth = pricePerSqft * RANGE * 0.4;\n      price -= dipDepth * Math.exp(-((dx - dipCenter) ** 2) / (2 * dipWidth * dipWidth));\n    }\n    return [sqft, Math.round(price)];\n  });\n  iceCurves.push(curve);\n}\nconst highlightCurve = iceCurves[highlightIndex];\n\n// Partial dependence (PDP) — the average of all ICE curves at each grid point.\nconst pdp = grid.map((sqft, gi) => {\n  const avg = iceCurves.reduce((sum, curve) => sum + curve[gi][1], 0) / N_HOUSES;\n  return [sqft, Math.round(avg)];\n});\n\nconst allPrices = iceCurves.flat().map((p) => p[1]);\nconst priceMin = Math.min(...allPrices);\nconst priceMax = Math.max(...allPrices);\nconst priceRange = priceMax - priceMin;\n// Keep the rug band well clear of the axis line and tick labels below it.\nconst rugY = priceMin - priceRange * 0.05;\nconst rug = observedSqft.map((sqft) => [sqft, rugY]);\n\n// --- Custom marker: a short vertical tick for the rug plot ------------------\nHighcharts.SVGRenderer.prototype.symbols.rugtick = (x, y, w, h) => [\n  \"M\", x + w / 2, y,\n  \"L\", x + w / 2, y + h,\n];\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"House Price by Square Footage · ice-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"18px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Predicted price vs. square footage — one curve per house, average in bold\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: { text: \"Square Footage\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    min: SQFT_MIN,\n    max: SQFT_MAX,\n  },\n  yAxis: {\n    title: { text: \"Predicted House Price\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter() {\n        return \"$\" + Math.round(this.value / 1000) + \"k\";\n      },\n    },\n    min: priceMin - priceRange * 0.14,\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    style: { color: t.ink },\n    formatter() {\n      return `${Math.round(this.x)} sq ft<br/><b>$${Math.round(this.y).toLocaleString()}</b>`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false, marker: { enabled: false } },\n    line: { enableMouseTracking: false },\n  },\n  series: [\n    ...iceCurves.map((curve, i) => ({\n      type: \"line\",\n      name: i === 0 ? \"Individual houses (ICE)\" : undefined,\n      showInLegend: i === 0,\n      data: curve,\n      color: \"rgba(0, 158, 115, 0.18)\",\n      lineWidth: 1.25,\n    })),\n    {\n      type: \"line\",\n      name: \"Divergent house (price dip)\",\n      data: highlightCurve,\n      color: \"#4467A3\",\n      lineWidth: 2.5,\n      dashStyle: \"ShortDash\",\n      enableMouseTracking: true,\n      zIndex: 4,\n    },\n    {\n      type: \"line\",\n      name: \"Average effect (PDP)\",\n      data: pdp,\n      color: t.ink,\n      lineWidth: 4,\n      enableMouseTracking: true,\n      zIndex: 5,\n    },\n    {\n      type: \"scatter\",\n      name: \"Observed sq ft (rug)\",\n      data: rug,\n      color: t.inkSoft,\n      marker: {\n        enabled: true,\n        symbol: \"rugtick\",\n        radius: 7,\n        fillColor: \"none\",\n        lineColor: t.inkSoft,\n        lineWidth: 2,\n      },\n      enableMouseTracking: false,\n    },\n  ],\n});\n"}