{"spec_id":"heatmap-calendar","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nheatmap-calendar: Basic Calendar Heatmap\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-07-23\n\"\"\"\n\nimport os\nimport sys\nimport xml.etree.ElementTree as ET\nfrom datetime import datetime, timedelta\n\n\n# This file is named pygal.py, so `import pygal` would resolve to it; drop the\n# script's own directory from sys.path so the installed pygal package wins.\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — daily coding activity (commits) across calendar year 2025\nnp.random.seed(42)\nstart_date = datetime(2025, 1, 1)\nend_date = datetime(2025, 12, 31)\nn_days = (end_date - start_date).days + 1\nday_dates = [start_date + timedelta(days=i) for i in range(n_days)]\nweekdays = np.array([d.weekday() for d in day_dates])\n\nbase = np.random.choice([0, 0, 1, 2, 3], size=n_days, p=[0.30, 0.20, 0.25, 0.15, 0.10])\nspikes = np.random.randint(5, 15, size=n_days)\ncommits = np.where(weekdays >= 5, 0, base)\ncommits = np.where(np.random.random(n_days) < 0.05, spikes, commits)\ncommits = np.where(np.random.random(n_days) < 0.25, 0, commits)\n\nactive = commits[commits > 0]\nlo, hi = int(active.min()), int(active.max())\n\n# Imprint sequential colormap (brand green → blue) split into 4 activity bins\nN_BINS = 4\nbrand_rgb = np.array([0x00, 0x9E, 0x73])\nblue_rgb = np.array([0x44, 0x67, 0xA3])\nbin_rgb = np.round(brand_rgb + (blue_rgb - brand_rgb) * np.linspace(0, 1, N_BINS)[:, None]).astype(int)\nBIN_COLORS = [f\"#{r:02X}{g:02X}{b:02X}\" for r, g, b in bin_rgb]\n\n# No-activity cells get a subtle neutral tint (blend of page background + muted ink)\nbg_rgb = np.array([int(PAGE_BG[i : i + 2], 16) for i in (1, 3, 5)])\nmuted_rgb = np.array([int(INK_MUTED[i : i + 2], 16) for i in (1, 3, 5)])\nempty_rgb = np.round(bg_rgb + (muted_rgb - bg_rgb) * 0.30).astype(int)\nEMPTY_COLOR = f\"#{empty_rgb[0]:02X}{empty_rgb[1]:02X}{empty_rgb[2]:02X}\"\n\nbin_idx = np.clip(((commits - lo) / max(hi - lo, 1) * N_BINS).astype(int), 0, N_BINS - 1)\ncell_colors = [BIN_COLORS[b] if v > 0 else EMPTY_COLOR for v, b in zip(commits, bin_idx, strict=True)]\n\n# Week column per day (Monday-start) + first week each month appears in\nfirst_monday = start_date - timedelta(days=start_date.weekday())\nweek_idx = np.array([(d - first_monday).days // 7 for d in day_dates])\nn_weeks = int(week_idx.max()) + 1\n\nmonth_starts = {}\nfor d, w in zip(day_dates, week_idx, strict=True):\n    if d.day <= 7:\n        month_starts.setdefault((d.year, d.month), w)\n\n# Longest streak of consecutive active days via run-length grouping\nactive_flag = (commits > 0).astype(int)\ngroup_id = np.cumsum(np.diff(active_flag, prepend=0) != 0)\nrun_lengths = np.bincount(group_id[active_flag == 1])\nlongest_streak = int(run_lengths.max()) if run_lengths.size else 0\n\nWEEKDAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\nMONTH_LABELS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Title with required language token (48 chars < 67-char baseline → default fontsize)\ntitle = \"heatmap-calendar · python · pygal · anyplot.ai\"\ntitle_font_size = 66\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\"#009E73\",),\n    title_font_size=title_font_size,\n    font_family=\"DejaVu Sans, Arial, sans-serif\",\n)\n\n# Landscape canvas — canonical 3200×1800 for the wide 53-week grid\nCANVAS_W, CANVAS_H = 3200, 1800\n\nchart = pygal.Bar(\n    width=CANVAS_W,\n    height=CANVAS_H,\n    style=custom_style,\n    title=title,\n    show_legend=False,\n    show_x_labels=False,\n    show_y_labels=False,\n    show_x_guides=False,\n    show_y_guides=False,\n    margin=40,\n    margin_top=150,\n    no_data_text=\"\",\n)\n\n# No data added — pygal renders title + background; render_tree() gives the SVG to extend\nsvg_root = chart.render_tree()\n\nSVG_NS = \"{http://www.w3.org/2000/svg}\"\nfor plot_group in list(svg_root.iter(f\"{SVG_NS}g\")):\n    cls = plot_group.attrib.get(\"class\", \"\")\n    if cls.startswith(\"plot\"):\n        plot_group.clear()\n        plot_group.set(\"class\", cls)\n\n# Grid geometry — cell size is width-constrained by n_weeks columns\nLEFT, RIGHT = 190, 70\navail_w = CANVAS_W - LEFT - RIGHT\ngap_ratio = 0.16\ncell = avail_w / (n_weeks + (n_weeks - 1) * gap_ratio)\ngap = cell * gap_ratio\ngrid_w = n_weeks * cell + (n_weeks - 1) * gap\ngrid_h = 7 * cell + 6 * gap\n\nfs = max(40, int(cell * 0.62))\nfs_legend = max(36, int(cell * 0.56))\nfs_stats = max(44, int(cell * 0.62))\nlegend_cell = cell * 1.5\n\n# Vertically center the whole block (month row + grid + legend + stats) in the\n# space below the chart's own title, so the previous title-to-grid gap collapses\n# into an even top/bottom margin instead of one lopsided empty band.\nmonth_row_h = cell * 1.05\nlegend_block_h = cell * 1.3 + legend_cell + fs_legend * 1.1\nstats_block_h = cell * 1.0 + fs_stats * 1.35 * 2\nblock_h = month_row_h + grid_h + legend_block_h + stats_block_h\n\nTOP, BOTTOM = 190, 60\nvpad = max(30, (CANVAS_H - TOP - BOTTOM - block_h) / 2)\n\nx0 = LEFT + (avail_w - grid_w) / 2\ny0 = TOP + vpad + month_row_h\n\ncalendar = ET.SubElement(svg_root, \"g\", {\"class\": \"calendar-heatmap\"})\nFONT = \"DejaVu Sans, Arial, sans-serif\"\n\n# Weekday labels\nfor i, label in enumerate(WEEKDAY_LABELS):\n    node = ET.SubElement(\n        calendar,\n        \"text\",\n        {\n            \"x\": str(x0 - gap * 2),\n            \"y\": str(y0 + i * (cell + gap) + cell * 0.72),\n            \"text-anchor\": \"end\",\n            \"fill\": INK,\n            \"style\": f\"font-size:{fs}px;font-weight:bold;font-family:{FONT};\",\n        },\n    )\n    node.text = label\n\n# Calendar cells\nfor w, wd, color, day, n in zip(week_idx, weekdays, cell_colors, day_dates, commits, strict=True):\n    x = x0 + w * (cell + gap)\n    y = y0 + wd * (cell + gap)\n    cell_rect = ET.SubElement(\n        calendar,\n        \"rect\",\n        {\n            \"x\": str(x),\n            \"y\": str(y),\n            \"width\": str(cell),\n            \"height\": str(cell),\n            \"rx\": str(cell * 0.14),\n            \"ry\": str(cell * 0.14),\n            \"fill\": color,\n        },\n    )\n    # pygal's reactive tooltip JS targets its own chart markup, not hand-drawn\n    # SVG; a native <title> child gives every cell a browser hover tooltip.\n    tooltip_node = ET.SubElement(cell_rect, \"title\")\n    tooltip_node.text = f\"{day.strftime('%b %d, %Y')}: {int(n)} commits\"\n\n# Month labels (skip any that would collide with the right edge)\nright_bound = x0 + grid_w - cell * 2.2\nfor (_, month), w in month_starts.items():\n    mx = x0 + w * (cell + gap)\n    if mx > right_bound:\n        continue\n    node = ET.SubElement(\n        calendar,\n        \"text\",\n        {\n            \"x\": str(mx),\n            \"y\": str(y0 - gap * 2.2),\n            \"fill\": INK,\n            \"style\": f\"font-size:{fs}px;font-weight:bold;font-family:{FONT};\",\n        },\n    )\n    node.text = MONTH_LABELS[month - 1]\n\n# Color scale legend\nlegend_gap = cell * 0.35\nlegend_colors = [EMPTY_COLOR, *BIN_COLORS]\nedges = [round(lo + i * (hi - lo) / N_BINS) for i in range(N_BINS + 1)]\nlegend_labels = [\"0\"] + [f\"{edges[i]}-{edges[i + 1]}\" if i < N_BINS - 1 else f\"{edges[i]}+\" for i in range(N_BINS)]\nlw_total = len(legend_colors) * legend_cell + (len(legend_colors) - 1) * legend_gap\nlx = x0 + grid_w / 2 - lw_total / 2\nly = y0 + grid_h + cell * 1.3\n\nless_node = ET.SubElement(\n    calendar,\n    \"text\",\n    {\n        \"x\": str(lx - legend_gap * 2),\n        \"y\": str(ly + legend_cell * 0.7),\n        \"text-anchor\": \"end\",\n        \"fill\": INK,\n        \"style\": f\"font-size:{fs_legend}px;font-weight:bold;font-family:{FONT};\",\n    },\n)\nless_node.text = \"Less\"\n\nmore_node = ET.SubElement(\n    calendar,\n    \"text\",\n    {\n        \"x\": str(lx + len(legend_colors) * (legend_cell + legend_gap)),\n        \"y\": str(ly + legend_cell * 0.7),\n        \"text-anchor\": \"start\",\n        \"fill\": INK,\n        \"style\": f\"font-size:{fs_legend}px;font-weight:bold;font-family:{FONT};\",\n    },\n)\nmore_node.text = \"More\"\n\nfor i, (color, label) in enumerate(zip(legend_colors, legend_labels, strict=True)):\n    bx = lx + i * (legend_cell + legend_gap)\n    ET.SubElement(\n        calendar,\n        \"rect\",\n        {\n            \"x\": str(bx),\n            \"y\": str(ly),\n            \"width\": str(legend_cell),\n            \"height\": str(legend_cell),\n            \"rx\": str(legend_cell * 0.14),\n            \"ry\": str(legend_cell * 0.14),\n            \"fill\": color,\n        },\n    )\n    label_node = ET.SubElement(\n        calendar,\n        \"text\",\n        {\n            \"x\": str(bx + legend_cell / 2),\n            \"y\": str(ly + legend_cell + fs_legend * 0.9),\n            \"text-anchor\": \"middle\",\n            \"fill\": INK_MUTED,\n            \"style\": f\"font-size:{max(38, int(fs_legend * 0.78))}px;font-family:{FONT};\",\n        },\n    )\n    label_node.text = label\n\n# Summary statistics\ntotal_commits = int(commits.sum())\nn_active_days = int((commits > 0).sum())\navg_per_active_day = total_commits / max(n_active_days, 1)\ncx = x0 + grid_w / 2\nstats_y = ly + legend_cell + fs_legend + cell * 1.1\n\nheadline_node = ET.SubElement(\n    calendar,\n    \"text\",\n    {\n        \"x\": str(cx),\n        \"y\": str(stats_y),\n        \"text-anchor\": \"middle\",\n        \"fill\": INK,\n        \"style\": f\"font-size:{fs_stats}px;font-weight:bold;font-family:{FONT};\",\n    },\n)\nheadline_node.text = f\"{total_commits} commits · {n_active_days} active days\"\n\ndetail_node = ET.SubElement(\n    calendar,\n    \"text\",\n    {\n        \"x\": str(cx),\n        \"y\": str(stats_y + fs_stats * 1.35),\n        \"text-anchor\": \"middle\",\n        \"fill\": INK_MUTED,\n        \"style\": f\"font-size:{int(fs_stats * 0.82)}px;font-family:{FONT};\",\n    },\n)\ndetail_node.text = f\"Longest streak: {longest_streak} days · {avg_per_active_day:.1f} commits/active day\"\n\n# Save\nsvg_bytes = ET.tostring(svg_root, xml_declaration=True, encoding=\"utf-8\")\ncairosvg.svg2png(bytestring=svg_bytes, write_to=f\"plot-{THEME}.png\", output_width=CANVAS_W, output_height=CANVAS_H)\n\nhtml_page = (\n    f'<!DOCTYPE html><html><head><meta charset=\"utf-8\">'\n    f\"<title>heatmap-calendar · python · pygal · anyplot.ai</title></head>\"\n    f'<body style=\"margin:0;background:{PAGE_BG};display:flex;'\n    f'justify-content:center;align-items:center;min-height:100vh;\">'\n    f\"{svg_bytes.decode('utf-8')}\"\n    f\"</body></html>\"\n)\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html_page)\n"}