{"spec_id":"ice-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// ice-basic: Individual Conditional Expectation (ICE) Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-17\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// House price predictions from a GradientBoostingRegressor as square footage\n// varies, for 70 individual houses. Each house has its own latent \"quality\"\n// level that governs how quickly the size effect on price saturates -- higher\n// quality builds keep appreciating with size, modest builds plateau sooner.\n// That divergence is exactly what ICE reveals and an averaged PDP curve hides.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst N_OBSERVATIONS = 70;\nconst N_GRID_POINTS = 60;\nconst SQFT_MIN = 800;\nconst SQFT_MAX = 3600;\n\nconst featureGrid = Array.from({ length: N_GRID_POINTS }, (_, i) =>\n  SQFT_MIN + (i / (N_GRID_POINTS - 1)) * (SQFT_MAX - SQFT_MIN),\n);\n\nconst houses = Array.from({ length: N_OBSERVATIONS }, () => ({\n  basePrice: 140000 + lcg() * 60000,\n  quality: lcg(),\n  amplitude: 220000 + lcg() * 120000,\n}));\n\nfunction predictPrice(house, sqft) {\n  const decay = 0.0025 - house.quality * 0.0016; // higher quality -> slower saturation\n  const sizeEffect = house.amplitude * (1 - Math.exp(-decay * (sqft - SQFT_MIN)));\n  return house.basePrice + sizeEffect;\n}\n\nconst iceCurves = houses.map((house) => featureGrid.map((sqft) => predictPrice(house, sqft)));\nconst pdpCurve = featureGrid.map((_, gridIndex) => {\n  const total = iceCurves.reduce((sum, curve) => sum + curve[gridIndex], 0);\n  return total / iceCurves.length;\n});\n\nfunction hexToRgba(hex, alpha) {\n  const value = parseInt(hex.slice(1), 16);\n  const r = (value >> 16) & 255;\n  const g = (value >> 8) & 255;\n  const b = value & 255;\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// Color-code the ICE bundle by house quality (terciles) so the divergence\n// between modest and premium builds -- the interaction the spec calls out --\n// is visible directly in the fan of lines, not just in the average PDP curve.\nconst QUALITY_TIERS = [\n  { label: \"Lower-quality houses (ICE)\", test: (q) => q < 1 / 3 },\n  { label: \"Mid-quality houses (ICE)\", test: (q) => q < 2 / 3 },\n  { label: \"Higher-quality houses (ICE)\", test: () => true },\n];\n\nfunction tierIndexFor(quality) {\n  return QUALITY_TIERS.findIndex((tier) => tier.test(quality));\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -------------------------------------------------------------------\n// Group by tier (not raw house order) so the chart's first dataset -- and\n// first legend swatch -- is always the tier-0 bundle in the mandated\n// #009E73 brand green.\nconst houseIndicesByTier = QUALITY_TIERS.map((_, tierIndex) =>\n  houses.map((house, i) => i).filter((i) => tierIndexFor(houses[i].quality) === tierIndex),\n);\n\nconst iceDatasets = houseIndicesByTier.flatMap((indices, tierIndex) =>\n  indices.map((i, j) => ({\n    label: j === 0 ? QUALITY_TIERS[tierIndex].label : \"\",\n    data: featureGrid.map((sqft, gridIndex) => ({ x: sqft, y: iceCurves[i][gridIndex] })),\n    borderColor: hexToRgba(t.palette[tierIndex], 0.22),\n    borderWidth: 1.5,\n    pointRadius: 0,\n    fill: false,\n    tension: 0.3,\n  })),\n);\n\nconst pdpDataset = {\n  label: \"Average effect (PDP)\",\n  data: featureGrid.map((sqft, gridIndex) => ({ x: sqft, y: pdpCurve[gridIndex] })),\n  borderColor: t.ink,\n  borderWidth: 4,\n  pointRadius: 0,\n  fill: false,\n  tension: 0.3,\n};\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [...iceDatasets, pdpDataset],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"ice-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          pointStyle: \"line\",\n          filter: (item) => item.text !== \"\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: SQFT_MIN,\n        max: SQFT_MAX,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Square Footage\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => `$${(value / 1000).toFixed(0)}k`,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Predicted Sale Price\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}