{"spec_id":"heatmap-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-basic: Basic Heatmap\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-08-20\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: dashboard page views by day of week and hour of day -------------\n// The core Highcharts bundle has no heatmap/colorAxis-mapping module loaded,\n// so the grid is drawn cell-by-cell with the SVG renderer instead.\nconst DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\nconst HOURS = Array.from({ length: 24 }, (_, h) => h);\nconst HOUR_LABELS = HOURS.map((h) => (h === 0 ? '12am' : h < 12 ? `${h}am` : h === 12 ? '12pm' : `${h - 12}pm`));\nconst N_ROWS = DAYS.length;\nconst N_COLS = HOURS.length;\n\n// Deterministic LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\n// Two-Gaussian daily curve — a morning login peak and a late-afternoon peak,\n// shifted and damped on weekends to mimic a B2B SaaS dashboard's usage rhythm.\nfunction gaussian(x, center, width) {\n  return Math.exp(-((x - center) ** 2) / (2 * width * width));\n}\nfunction hourlyBase(hour, isWeekend) {\n  const morningPeak = isWeekend ? 11 : 10;\n  const afternoonPeak = isWeekend ? 14 : 16;\n  const morning = gaussian(hour, morningPeak, 2.2);\n  const afternoon = gaussian(hour, afternoonPeak, 3);\n  return 0.04 + 0.75 * morning + (isWeekend ? 0.3 : 0.65) * afternoon;\n}\n\nconst PEAK_VIEWS = 3200;\nconst VIEWS = DAYS.map((_, row) => {\n  const isWeekend = row >= 5;\n  const dayFactor = isWeekend ? 0.4 : 1.0;\n  return HOURS.map((hour) => {\n    const base = hourlyBase(hour, isWeekend) * dayFactor;\n    const noise = 0.85 + rand() * 0.3;\n    return Math.round(base * PEAK_VIEWS * noise);\n  });\n});\n\nlet minViews = Infinity;\nlet maxViews = -Infinity;\nVIEWS.forEach((row) =>\n  row.forEach((v) => {\n    if (v < minViews) minViews = v;\n    if (v > maxViews) maxViews = v;\n  })\n);\n\n// --- Color: imprint_seq — single-polarity data (page views, always >= 0) ---\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nconst SEQ_LO = hexToRgb(t.seq[0]); // #009E73\nconst SEQ_HI = hexToRgb(t.seq[1]); // #4467A3\n\nfunction lerp(a, b, f) {\n  return a + (b - a) * f;\n}\nfunction viewsRgb(value) {\n  const f = (value - minViews) / (maxViews - minViews);\n  return [Math.round(lerp(SEQ_LO[0], SEQ_HI[0], f)), Math.round(lerp(SEQ_LO[1], SEQ_HI[1], f)), Math.round(lerp(SEQ_LO[2], SEQ_HI[2], f))];\n}\nfunction viewsFill(value) {\n  const [red, green, blue] = viewsRgb(value);\n  return `rgb(${red},${green},${blue})`;\n}\n\n// --- Title (fontsize scaled off the 67-char baseline) ----------------------\nconst TITLE_TEXT = 'Dashboard Page Views · heatmap-basic · javascript · highcharts · anyplot.ai';\nconst TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);\n\n// Fixed chart geometry (landscape canvas, harness-guaranteed 1600x900 CSS px)\n// — a single source of truth for the margin, the grid, and the invisible\n// hover layer below, so everything lines up without a runtime resync.\nconst CHART_MARGIN = [130, 210, 100, 90]; // [top, right, bottom, left]\nconst CELL_W = (window.ANYPLOT_SIZE.width - CHART_MARGIN[1] - CHART_MARGIN[3]) / N_COLS;\nconst CELL_H = (window.ANYPLOT_SIZE.height - CHART_MARGIN[0] - CHART_MARGIN[2]) / N_ROWS;\nconst MARKER_RADIUS = Math.max(Math.min(CELL_W, CELL_H) / 2 - 2, 3);\n\nconst drawn = [];\nfunction clearDrawn() {\n  drawn.forEach((el) => {\n    try {\n      el.destroy();\n    } catch (_err) {\n      // already removed\n    }\n  });\n  drawn.length = 0;\n}\n\nfunction drawAll() {\n  const chart = this;\n  clearDrawn();\n  const r = chart.renderer;\n\n  const cellW = chart.plotWidth / N_COLS;\n  const cellH = chart.plotHeight / N_ROWS;\n\n  // Grid cells — full N_ROWS x N_COLS matrix, no masking needed (unlike a\n  // symmetric correlation matrix, this data has no mirror redundancy).\n  for (let row = 0; row < N_ROWS; row++) {\n    for (let col = 0; col < N_COLS; col++) {\n      const value = VIEWS[row][col];\n      const x = chart.plotLeft + col * cellW;\n      const y = chart.plotTop + row * cellH;\n      drawn.push(\n        r\n          .rect(x + 0.5, y + 0.5, cellW - 1, cellH - 1, 1)\n          .attr({ fill: viewsFill(value), stroke: 'none', zIndex: 2 })\n          .add()\n      );\n    }\n  }\n\n  // Row labels (left, right-aligned against the grid).\n  DAYS.forEach((lbl, row) => {\n    const cy = chart.plotTop + (row + 0.5) * cellH + 5;\n    drawn.push(\n      r\n        .text(lbl, chart.plotLeft - 14, cy)\n        .attr({ align: 'right', zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '15px' })\n        .add()\n    );\n  });\n\n  // Column labels — every 3rd hour to avoid crowding 24 narrow columns.\n  HOUR_LABELS.forEach((lbl, col) => {\n    if (col % 3 !== 0) return;\n    const cx = chart.plotLeft + (col + 0.5) * cellW;\n    drawn.push(\n      r\n        .text(lbl, cx, chart.plotTop + chart.plotHeight + 24)\n        .attr({ align: 'center', zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '14px' })\n        .add()\n    );\n  });\n\n  // Vertical colorbar in the freed right margin.\n  const barLeft = chart.plotLeft + chart.plotWidth + 55;\n  const barTop = chart.plotTop + 10;\n  const barWidth = 26;\n  const barHeight = chart.plotHeight - 20;\n  const segments = 50;\n  const segH = barHeight / segments;\n\n  for (let i = 0; i < segments; i++) {\n    const value = maxViews - ((maxViews - minViews) * i) / (segments - 1);\n    drawn.push(\n      r\n        .rect(barLeft, barTop + i * segH, barWidth, segH + 0.5)\n        .attr({ fill: viewsFill(value), zIndex: 2 })\n        .add()\n    );\n  }\n  drawn.push(\n    r\n      .rect(barLeft, barTop, barWidth, barHeight)\n      .attr({ fill: 'none', stroke: t.inkSoft, 'stroke-width': 1, zIndex: 2 })\n      .add()\n  );\n  [\n    [maxViews, 0],\n    [minViews, 1],\n  ].forEach(([value, frac]) => {\n    drawn.push(\n      r\n        .text(Math.round(value).toLocaleString(), barLeft + barWidth + 10, barTop + frac * barHeight + 5)\n        .attr({ align: 'left', zIndex: 2 })\n        .css({ color: t.inkSoft, fontSize: '13px' })\n        .add()\n    );\n  });\n  drawn.push(\n    r\n      .text('Page views', barLeft, barTop - 16)\n      .attr({ align: 'left', zIndex: 2 })\n      .css({ color: t.inkSoft, fontSize: '14px', fontWeight: '500' })\n      .add()\n  );\n}\n\n// Invisible scatter layer aligned to each drawn cell so hovering exposes a\n// real Highcharts tooltip — the core bundle has no heatmap/colorAxis module,\n// but a matched-axis scatter series recovers native hover interactivity\n// without disturbing the hand-drawn grid above it.\nconst cellPoints = [];\nfor (let row = 0; row < N_ROWS; row++) {\n  for (let col = 0; col < N_COLS; col++) {\n    cellPoints.push({ x: col, y: row, views: VIEWS[row][col], dayLabel: DAYS[row], hourLabel: HOUR_LABELS[col] });\n  }\n}\n\nHighcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    margin: CHART_MARGIN,\n    events: { load: drawAll, redraw: drawAll },\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE_TEXT,\n    style: { color: t.ink, fontSize: TITLE_FS + 'px', fontWeight: '600' },\n  },\n  subtitle: {\n    text: 'Simulated hourly logins to a B2B analytics dashboard across one week',\n    style: { color: t.inkSoft, fontSize: '14px' },\n  },\n  xAxis: { visible: false, min: -0.5, max: N_COLS - 0.5 },\n  yAxis: { visible: false, gridLineWidth: 0, min: -0.5, max: N_ROWS - 0.5, reversed: true },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    borderRadius: 6,\n    style: { color: t.ink, fontSize: '13px' },\n    formatter: function () {\n      const p = this.point;\n      return `<b>${p.dayLabel} ${p.hourLabel}</b><br/>${p.views.toLocaleString()} views`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      enableMouseTracking: true,\n      stickyTracking: false,\n      marker: {\n        enabled: true,\n        symbol: 'circle',\n        radius: MARKER_RADIUS,\n        fillColor: 'rgba(0,0,0,0.001)',\n        lineWidth: 0,\n        states: { hover: { enabled: false } },\n      },\n    },\n  },\n  series: [\n    {\n      type: 'scatter',\n      name: 'Page views',\n      data: cellPoints,\n    },\n  ],\n});\n"}