{"spec_id":"scatter-color-mapped","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-color-mapped: Color-Mapped Scatter Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated sensor readings across a monitoring grid ---------------\n// Temperature drifts cooler to the north and warmer to the east, plus noise.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst n = 180;\nconst readings = [];\nfor (let i = 0; i < n; i++) {\n  const distanceEast = rand() * 100;\n  const distanceNorth = rand() * 100;\n  const temperature =\n    24 - 0.09 * distanceNorth + 0.03 * distanceEast + (rand() - 0.5) * 3;\n  readings.push({ x: distanceEast, y: distanceNorth, temperature });\n}\n\nconst temperatures = readings.map((r) => r.temperature);\nconst tempMin = Math.min(...temperatures);\nconst tempMax = Math.max(...temperatures);\n\n// --- Color mapping: imprint_seq (brand green -> blue), single-polarity data\nfunction hexToRgb(hex) {\n  const value = parseInt(hex.slice(1), 16);\n  return [(value >> 16) & 255, (value >> 8) & 255, value & 255];\n}\nconst seqLow = hexToRgb(t.seq[0]);\nconst seqHigh = hexToRgb(t.seq[1]);\nconst inkRgb = hexToRgb(t.ink);\n\nfunction colorForTemperature(value) {\n  const ratio = (value - tempMin) / (tempMax - tempMin);\n  const r = Math.round(seqLow[0] + ratio * (seqHigh[0] - seqLow[0]));\n  const g = Math.round(seqLow[1] + ratio * (seqHigh[1] - seqLow[1]));\n  const b = Math.round(seqLow[2] + ratio * (seqHigh[2] - seqLow[2]));\n  return `rgba(${r}, ${g}, ${b}, 0.78)`;\n}\n\nconst pointColors = readings.map((r) => colorForTemperature(r.temperature));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Colorbar sizing: measure the tick labels up front so the layout's\n// right padding hugs the colorbar instead of leaving guessed-at dead space ---\nconst colorbarFont = \"14px sans-serif\";\nconst measureCtx = canvas.getContext(\"2d\");\nmeasureCtx.font = colorbarFont;\nconst tickLabels = [`${tempMax.toFixed(1)}°C`, `${tempMin.toFixed(1)}°C`];\nconst maxTickWidth = Math.max(\n  ...tickLabels.map((label) => measureCtx.measureText(label).width),\n);\nconst barGap = 20;\nconst barWidth = 24;\nconst tickLabelGap = 8;\nconst axisLabelGap = 18;\nconst rightMargin = 14;\nconst axisLabelOffset = barGap + barWidth + tickLabelGap + maxTickWidth + axisLabelGap;\n// colorbarSpace reserves layout.padding.right; it must include barGap because\n// barX is measured from chartArea.right (= canvasWidth - colorbarSpace), not\n// from canvasWidth itself, or the rotated label's anchor lands past the edge.\nconst colorbarSpace = barGap + axisLabelOffset + rightMargin;\n\n// --- Colorbar plugin: draws the imprint_seq gradient as a reference scale ---\nconst colorbarPlugin = {\n  id: \"colorbar\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const { top, bottom, right } = chartArea;\n    const barX = right + barGap;\n\n    const gradient = ctx.createLinearGradient(0, bottom, 0, top);\n    gradient.addColorStop(0, t.seq[0]);\n    gradient.addColorStop(1, t.seq[1]);\n\n    ctx.save();\n    ctx.fillStyle = gradient;\n    ctx.fillRect(barX, top, barWidth, bottom - top);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(barX, top, barWidth, bottom - top);\n\n    ctx.fillStyle = t.ink;\n    ctx.font = colorbarFont;\n    ctx.textBaseline = \"middle\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(tickLabels[0], barX + barWidth + tickLabelGap, top);\n    ctx.fillText(tickLabels[1], barX + barWidth + tickLabelGap, bottom);\n\n    ctx.translate(barX + axisLabelOffset, (top + bottom) / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = \"center\";\n    ctx.fillText(\"Temperature (°C)\", 0, 0);\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Sensor reading\",\n        data: readings,\n        backgroundColor: pointColors,\n        borderColor: `rgba(${inkRgb[0]}, ${inkRgb[1]}, ${inkRgb[2]}, 0.35)`,\n        borderWidth: 1,\n        pointRadius: 7.5,\n        pointHoverRadius: 7.5,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: colorbarSpace } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"scatter-color-mapped · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        title: { display: true, text: \"Distance East (km)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft, width: 1.5 },\n      },\n      y: {\n        title: { display: true, text: \"Distance North (km)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft, width: 1.5 },\n      },\n    },\n  },\n  plugins: [colorbarPlugin],\n});\n"}