{"spec_id":"heatmap-calendar","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// heatmap-calendar: Basic Calendar Heatmap\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 86/100 | Created: 2026-07-23\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Daily website visits for 2023, with a weekly seasonality (weekday traffic\n// higher than weekend) plus a data-collection outage in August (missing dates).\nlet lcgState = 42;\nfunction nextRandom() {\n  lcgState = (lcgState * 1103515245 + 12345) % 2147483648;\n  return lcgState / 2147483648;\n}\n\nconst WEEKDAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\nconst MONTH_LABELS = [\n  \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n  \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\",\n];\nconst START = Date.UTC(2023, 0, 1); // Jan 1 2023 — a Sunday\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst TOTAL_DAYS = 365;\n// Monday-indexed weekday (0=Mon..6=Sun) of the first day, so weeks align to\n// Monday-start columns matching the Mon-Sun y-axis labels.\nconst firstMondayIndex = (new Date(START).getUTCDay() + 6) % 7;\n\n// The core Highcharts bundle has no heatmap/colorAxis-mapping module (see\n// prompts/library/highcharts.md), so each cell's fill is computed by hand —\n// a linear interpolation across the two-stop imprint_seq gradient.\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nconst seqLow = hexToRgb(t.seq[0]);\nconst seqHigh = hexToRgb(t.seq[1]);\nfunction valueToColor(value, min, max) {\n  const frac = max > min ? (value - min) / (max - min) : 0;\n  const rgb = seqLow.map((c, i) => Math.round(c + (seqHigh[i] - c) * frac));\n  return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;\n}\n\nconst cells = [];\nconst missingCells = [];\nlet minVisits = Infinity;\nlet maxVisits = -Infinity;\n\nfor (let i = 0; i < TOTAL_DAYS; i++) {\n  const date = new Date(START + i * DAY_MS);\n  const mondayIndex = (date.getUTCDay() + 6) % 7; // 0=Mon .. 6=Sun\n  const weekIndex = Math.floor((i + firstMondayIndex) / 7);\n\n  // Simulated data-collection outage: 12 days in mid-August with no records.\n  const isOutage = i >= 215 && i < 227;\n\n  if (isOutage) {\n    missingCells.push({ x: weekIndex, y: mondayIndex });\n    continue;\n  }\n\n  const weekdayBoost = mondayIndex < 5 ? 1.3 : 0.6; // weekdays busier than weekends\n  const seasonal = 1 + 0.35 * Math.sin((i / TOTAL_DAYS) * 2 * Math.PI + 1.2);\n  const noise = 0.75 + nextRandom() * 0.5;\n  const visits = Math.round(220 * weekdayBoost * seasonal * noise);\n\n  minVisits = Math.min(minVisits, visits);\n  maxVisits = Math.max(maxVisits, visits);\n  cells.push({ x: weekIndex, y: mondayIndex, value: visits, date: date.toISOString().slice(0, 10) });\n}\ncells.forEach((c) => {\n  c.color = valueToColor(c.value, minVisits, maxVisits);\n});\n\nconst weekCount = Math.floor((TOTAL_DAYS - 1 + firstMondayIndex) / 7) + 1;\n\n// Tick position (week index) for the first day of each month, for the top axis.\nconst monthTicks = [];\nfor (let m = 0; m < 12; m++) {\n  const first = Date.UTC(2023, m, 1);\n  const daysSinceStart = Math.round((first - START) / DAY_MS);\n  const mondayIndex = (new Date(first).getUTCDay() + 6) % 7;\n  const weekIndex = Math.floor((daysSinceStart + firstMondayIndex) / 7);\n  monthTicks.push({ value: weekIndex, label: MONTH_LABELS[m] });\n}\n\n// Peak day — called out with a renderer-drawn callout (see drawPeakCallout),\n// since core Highcharts has no annotations module.\nconst peakCell = cells.reduce((best, c) => (c.value > best.value ? c : best), cells[0]);\n\n// Tight vertical band: 7 rows at ~30px pitch, matching the ~28px week-column\n// pitch, so cells read as square rather than stretched across the canvas.\nconst MARGIN_TOP = 110;\nconst BAND_HEIGHT = 210;\nconst MARGIN_BOTTOM = 900 - MARGIN_TOP - BAND_HEIGHT;\n\n// --- Chart -------------------------------------------------------------------\nconst title = \"heatmap-calendar · javascript · highcharts · anyplot.ai\";\n\n// The core Highcharts bundle has no annotations/colorAxis module, so the\n// gradient legend bar and the peak-day callout are drawn with the SVG\n// renderer directly against the built chart — an idiom specific to\n// Highcharts' rendering engine rather than a generic scatter overlay.\nfunction drawColorLegend(chart) {\n  const r = chart.renderer;\n  const x0 = chart.plotLeft;\n  const y0 = chart.plotTop + chart.plotHeight + 34;\n  const barWidth = 220;\n  const barHeight = 14;\n\n  r.text(\"Visits / day\", x0, y0 - 10)\n    .css({ color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n\n  r.rect(x0, y0, 16, 16, 2).attr({ fill: t.grid, \"stroke-width\": 0 }).add();\n  r.text(\"No data\", x0 + 24, y0 + 13)\n    .css({ color: t.inkSoft, fontSize: \"13px\" })\n    .add();\n\n  const gx = x0 + 120;\n  r.rect(gx, y0, barWidth, barHeight, 3)\n    .attr({\n      fill: {\n        linearGradient: { x1: 0, y1: 0, x2: 1, y2: 0 },\n        stops: [\n          [0, t.seq[0]],\n          [1, t.seq[1]],\n        ],\n      },\n      \"stroke-width\": 0,\n    })\n    .add();\n  r.text(String(minVisits), gx, y0 + barHeight + 16)\n    .css({ color: t.inkSoft, fontSize: \"12px\" })\n    .add();\n  r.text(String(maxVisits), gx + barWidth - 20, y0 + barHeight + 16)\n    .css({ color: t.inkSoft, fontSize: \"12px\" })\n    .add();\n}\n\nfunction drawPeakCallout(chart) {\n  const px = chart.xAxis[0].toPixels(peakCell.x, false);\n  const py = chart.yAxis[0].toPixels(peakCell.y, false);\n  const below = peakCell.y <= 1; // keep clear of the month-label axis on top rows\n  const labelY = below ? py + 46 : py - 46;\n\n  chart.renderer\n    .label(`Peak: ${peakCell.value} visits (${peakCell.date})`, px, labelY, \"callout\", px, py)\n    .attr({\n      fill: t.elevatedBg,\n      stroke: t.inkSoft,\n      \"stroke-width\": 1,\n      padding: 6,\n      r: 4,\n      zIndex: 6,\n    })\n    .css({ color: t.ink, fontSize: \"12px\", fontWeight: \"600\" })\n    .add();\n}\n\nHighcharts.chart(\n  \"container\",\n  {\n    chart: {\n      type: \"scatter\",\n      backgroundColor: \"transparent\",\n      animation: false,\n      style: { fontFamily: \"inherit\" },\n      marginLeft: 60,\n      marginRight: 40,\n      marginTop: MARGIN_TOP,\n      marginBottom: MARGIN_BOTTOM,\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n      text: title,\n      align: \"left\",\n      style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    },\n    subtitle: {\n      text: \"Daily website visits, 2023 — outage gap in August shown as empty cells\",\n      align: \"left\",\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    xAxis: {\n      min: -0.7,\n      max: weekCount - 0.3,\n      startOnTick: false,\n      endOnTick: false,\n      lineWidth: 0,\n      gridLineWidth: 0,\n      tickWidth: 0,\n      opposite: true,\n      tickPositions: monthTicks.map((m) => m.value),\n      labels: {\n        style: { color: t.inkSoft, fontSize: \"14px\" },\n        formatter() {\n          const tick = monthTicks.find((m) => m.value === this.value);\n          return tick ? tick.label : \"\";\n        },\n      },\n    },\n    yAxis: {\n      title: { text: null },\n      min: -0.5,\n      max: 6.5,\n      startOnTick: false,\n      endOnTick: false,\n      tickPositions: [0, 1, 2, 3, 4, 5, 6],\n      reversed: true,\n      lineWidth: 0,\n      gridLineWidth: 0,\n      tickWidth: 0,\n      labels: {\n        style: { color: t.inkSoft, fontSize: \"14px\" },\n        formatter() {\n          return WEEKDAY_LABELS[this.value] ?? \"\";\n        },\n      },\n    },\n    legend: { enabled: false },\n    tooltip: { enabled: false },\n    plotOptions: {\n      series: { animation: false, enableMouseTracking: false },\n      scatter: {\n        marker: { symbol: \"square\", radius: 13, states: { hover: { enabled: false } } },\n      },\n    },\n    series: [\n      { name: \"Visits\", data: cells, showInLegend: false },\n      { name: \"No data\", data: missingCells, color: t.grid, showInLegend: false },\n    ],\n  },\n  function (chart) {\n    drawColorLegend(chart);\n    drawPeakCallout(chart);\n  }\n);\n"}