{"spec_id":"heatmap-calendar","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-calendar: Basic Calendar Heatmap\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-07-23\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG — reproducible daily activity without a seeded global RNG\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\nconst rng = makeLcg(42);\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];\n\n// One full year of daily coding activity (GitHub-style contribution counts).\n// A 9-day trip in August has no commits at all — those dates are simply\n// omitted from the series, leaving the calendar cell empty (neutral).\nconst YEAR = 2023;\nconst TOTAL_DAYS = 365;\nconst VACATION_START_DAY = 215; // day-of-year index (0-based), ~early August\nconst VACATION_LENGTH = 9;\n\nconst firstDay = new Date(YEAR, 0, 1);\nconst firstWeekdayIdx = (firstDay.getDay() + 6) % 7; // Mon=0 .. Sun=6\n\nconst points = [];\nconst monthLabelByWeek = new Map();\nlet lastMonth = -1;\nlet maxValue = 0;\n\nfor (let i = 0; i < TOTAL_DAYS; i += 1) {\n  const date = new Date(YEAR, 0, 1 + i);\n  const weekdayIdx = (date.getDay() + 6) % 7;\n  const weekIndex = Math.floor((i + firstWeekdayIdx) / 7);\n  const month = date.getMonth();\n\n  if (month !== lastMonth) {\n    if (!monthLabelByWeek.has(weekIndex)) {\n      monthLabelByWeek.set(weekIndex, MONTH_LABELS[month]);\n    }\n    lastMonth = month;\n  }\n\n  const onVacation = i >= VACATION_START_DAY && i < VACATION_START_DAY + VACATION_LENGTH;\n  if (onVacation) continue;\n\n  const isWeekend = weekdayIdx >= 5;\n  let commits = isWeekend ? 1 + rng() * 3 : 3 + rng() * 7;\n  if (i % 37 === 17) commits += 10 + rng() * 8; // occasional release-day spike\n  commits = Math.round(commits);\n\n  maxValue = Math.max(maxValue, commits);\n  points.push({\n    id: `d${i}`,\n    x: weekIndex,\n    y: WEEKDAY_LABELS[weekdayIdx],\n    z: commits,\n  });\n}\n\nconst weekCount = Math.max(...points.map((p) => p.x)) + 1;\nconst weekIndices = Array.from({ length: weekCount }, (_, i) => i);\n\n// Custom marker: rounded rectangle cells (calendar days) instead of the\n// default circles, sized from the band scales so they tile the week/weekday\n// grid with a small gap between neighbours (from each axis's categoryGapRatio).\nfunction SquareCell(props) {\n  const { series, xScale, yScale, colorGetter, color } = props;\n  const xIsBand = typeof xScale.bandwidth === \"function\";\n  const yIsBand = typeof yScale.bandwidth === \"function\";\n  const cellW = xIsBand ? xScale.bandwidth() : 10;\n  const cellH = yIsBand ? yScale.bandwidth() : 10;\n\n  return (\n    <g>\n      {series.data.map((pt, i) => {\n        const cx = (xScale(pt.x) ?? 0) + (xIsBand ? cellW / 2 : 0);\n        const cy = (yScale(pt.y) ?? 0) + (yIsBand ? cellH / 2 : 0);\n        return (\n          <rect\n            key={pt.id}\n            x={cx - cellW / 2}\n            y={cy - cellH / 2}\n            width={cellW}\n            height={cellH}\n            rx={Math.min(4, cellW * 0.18)}\n            fill={colorGetter ? colorGetter(i) : color}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleHeight = 56;\n\n  // 53 weeks x 7 weekdays is a much wider grid than the 16:9 mount, so a\n  // literal square cell would leave most of the canvas empty. Instead give\n  // the grid a deliberate, moderately tall row height (a \"brick\" cell rather\n  // than a pixel-perfect square) that consumes most of the space below the\n  // title, so the calendar+legend block fills the canvas tightly instead of\n  // floating in a taller centered box.\n  const LEFT_MARGIN = 68;\n  const RIGHT_MARGIN = 28;\n  const GAP_RATIO = 0.18;\n  const TOP_MARGIN = 40;\n  const GRID_HEIGHT = 560;\n  const LEGEND_SPACE = 56;\n  const chartInnerHeight = TOP_MARGIN + GRID_HEIGHT + LEGEND_SPACE;\n  // ContinuousColorLegend right-aligns flush to the chart's own width (not\n  // the margin box), so its max-value label can kiss the true canvas edge —\n  // give the chart a slightly narrower width than the mount so that label\n  // always has breathing room.\n  const LEGEND_EDGE_BUFFER = 36;\n  const chartWidth = width - LEGEND_EDGE_BUFFER;\n\n  return (\n    <Box sx={{ width, height, bgcolor: t.pageBg, display: \"flex\", flexDirection: \"column\" }}>\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 22,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          pt: \"16px\",\n          height: titleHeight,\n          fontFamily: \"inherit\",\n        }}\n      >\n        heatmap-calendar · javascript · muix · anyplot.ai\n      </Typography>\n      <Box sx={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"flex-start\" }}>\n        <ScatterChart\n          width={chartWidth}\n          height={chartInnerHeight}\n          skipAnimation\n          disableVoronoi\n          series={[\n            {\n              id: \"activity\",\n              type: \"scatter\",\n              data: points,\n              label: \"Commits per day\",\n              xAxisId: \"week\",\n              yAxisId: \"weekday\",\n              zAxisId: \"activity\",\n            },\n          ]}\n          xAxis={[\n            { id: \"week\", scaleType: \"band\", data: weekIndices, categoryGapRatio: GAP_RATIO },\n            {\n              id: \"month\",\n              scaleType: \"band\",\n              data: weekIndices,\n              categoryGapRatio: GAP_RATIO,\n              valueFormatter: (weekIdx) => monthLabelByWeek.get(weekIdx) ?? \"\",\n              tickLabelStyle: { fontSize: 16, fill: t.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"weekday\",\n              scaleType: \"band\",\n              data: WEEKDAY_LABELS,\n              categoryGapRatio: GAP_RATIO,\n              tickLabelStyle: { fontSize: 16, fill: t.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          zAxis={[\n            {\n              id: \"activity\",\n              min: 0,\n              max: maxValue,\n              colorMap: { type: \"continuous\", min: 0, max: maxValue, color: [t.seq[0], t.seq[1]] },\n            },\n          ]}\n          topAxis=\"month\"\n          bottomAxis={null}\n          leftAxis=\"weekday\"\n          rightAxis={null}\n          margin={{ top: TOP_MARGIN, right: RIGHT_MARGIN, bottom: LEGEND_SPACE, left: LEFT_MARGIN }}\n          slots={{ scatter: SquareCell }}\n          slotProps={{ legend: { hidden: true } }}\n        >\n          <ChartsText\n            text=\"Commits per day\"\n            x={chartWidth - 8}\n            y={chartInnerHeight - LEGEND_SPACE + 8}\n            style={{\n              fontSize: 14,\n              fill: t.inkSoft,\n              fontFamily: \"inherit\",\n              textAnchor: \"end\",\n              dominantBaseline: \"hanging\",\n            }}\n          />\n          <ContinuousColorLegend\n            axisId=\"activity\"\n            axisDirection=\"z\"\n            position={{ horizontal: \"right\", vertical: \"bottom\" }}\n            direction=\"row\"\n            length=\"30%\"\n            thickness={12}\n            labelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: \"inherit\" }}\n          />\n        </ScatterChart>\n      </Box>\n    </Box>\n  );\n}\n"}