{"spec_id":"scatter-color-mapped","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-color-mapped: Color-Mapped Scatter Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 190, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Urban heat island study: surface temperature recorded at monitoring\n// stations scattered around a city center.\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst stationCount = 180;\nconst stations = [];\nfor (let i = 0; i < stationCount; i++) {\n  const distanceEast = (nextRandom() - 0.5) * 24;\n  const distanceNorth = (nextRandom() - 0.5) * 24;\n  const distanceFromCenter = Math.sqrt(distanceEast ** 2 + distanceNorth ** 2);\n  const surfaceTemp = 34 - distanceFromCenter * 0.55 + (nextRandom() - 0.5) * 3;\n  stations.push({ distanceEast, distanceNorth, surfaceTemp });\n}\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(stations, (d) => d.distanceEast)).nice().range([0, iw]);\nconst y = d3.scaleLinear().domain(d3.extent(stations, (d) => d.distanceNorth)).nice().range([ih, 0]);\nconst [tempMin, tempMax] = d3.extent(stations, (d) => d.surfaceTemp);\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([tempMin, tempMax]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (both axes, L-shaped: skip the domain-extreme ticks so the\n// gridlines don't double up with the axis lines and form a full box-frame) --\nconst [xDomainMin, xDomainMax] = x.domain();\nconst [yDomainMin, yDomainMax] = y.domain();\n\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(x.ticks(8).filter((d) => d > xDomainMin && d < xDomainMax))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid);\n\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(8).filter((d) => d > yDomainMin && d < yDomainMax))\n  .join(\"line\")\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"stroke\", t.grid);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(8));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Distance East of City Center (km)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Distance North of City Center (km)\");\n\n// --- Urban-core focal annotation (data storytelling) -----------------------\n// A dashed reference ring around the city center draws the eye to where the\n// heat-island effect should be strongest, before the points render on top.\nconst coreRadiusKm = 6;\nconst coreRx = Math.abs(x(coreRadiusKm) - x(0));\nconst coreRy = Math.abs(y(0) - y(coreRadiusKm));\ng.append(\"ellipse\")\n  .attr(\"cx\", x(0))\n  .attr(\"cy\", y(0))\n  .attr(\"rx\", coreRx)\n  .attr(\"ry\", coreRy)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"5,4\")\n  .attr(\"opacity\", 0.45);\n\ng.append(\"text\")\n  .attr(\"x\", x(0))\n  .attr(\"y\", y(0) - coreRy - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Urban core\");\n\n// --- Points -----------------------------------------------------------------\n// Warmest stations (top quartile) render larger and more opaque, creating a\n// focal point that surfaces the heat-island clustering near the city center.\nconst hotThreshold = tempMin + (tempMax - tempMin) * 0.75;\ng.selectAll(\"circle.station\")\n  .data(stations)\n  .join(\"circle\")\n  .attr(\"class\", \"station\")\n  .attr(\"cx\", (d) => x(d.distanceEast))\n  .attr(\"cy\", (d) => y(d.distanceNorth))\n  .attr(\"r\", (d) => (d.surfaceTemp >= hotThreshold ? 12 : 8))\n  .attr(\"fill\", (d) => color(d.surfaceTemp))\n  .attr(\"fill-opacity\", (d) => (d.surfaceTemp >= hotThreshold ? 0.95 : 0.75))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Colorbar (Imprint sequential scale) -----------------------------------\nconst barWidth = 22;\nconst barX = iw + 55;\n\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"temperature-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"y2\", \"0%\");\nd3.range(0, 1.0001, 0.1).forEach((stop) => {\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${stop * 100}%`)\n    .attr(\"stop-color\", color(tempMin + stop * (tempMax - tempMin)));\n});\n\ng.append(\"rect\")\n  .attr(\"x\", barX)\n  .attr(\"y\", 0)\n  .attr(\"width\", barWidth)\n  .attr(\"height\", ih)\n  .attr(\"fill\", \"url(#temperature-gradient)\");\n\nconst barScale = d3.scaleLinear().domain([tempMin, tempMax]).range([ih, 0]);\nconst barAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(${barX + barWidth},0)`)\n  .call(\n    d3\n      .axisRight(barScale)\n      .ticks(6)\n      .tickFormat((d) => `${d.toFixed(0)}°`),\n  );\nbarAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nbarAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nbarAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", barX + barWidth + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text(\"Surface Temperature (°C)\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-color-mapped · javascript · d3 · anyplot.ai\");\n"}