{"spec_id":"heatmap-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nheatmap-basic: Basic Heatmap\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-28\n\"\"\"\n\nimport importlib\nimport os\nimport re\nimport sys\n\nimport numpy as np\n\n\n# Import pygal avoiding name collision with this filename\n_cwd = sys.path[0]\nsys.path[:] = [p for p in sys.path if p != _cwd]\n_pygal = importlib.import_module(\"pygal\")\n_Style = importlib.import_module(\"pygal.style\").Style\n_cairosvg = importlib.import_module(\"cairosvg\")\nsys.path.insert(0, _cwd)\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — Monthly website traffic (thousands) across content categories\nnp.random.seed(42)\n\nmonths = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\nbase_traffic = {\n    \"Tech\": [120, 115, 130, 135, 140, 125, 110, 105, 145, 155, 160, 150],\n    \"Science\": [85, 80, 90, 95, 88, 82, 78, 75, 92, 98, 95, 88],\n    \"Health\": [95, 110, 105, 100, 90, 85, 80, 82, 115, 120, 108, 130],\n    \"Finance\": [140, 135, 150, 145, 130, 125, 120, 118, 155, 160, 165, 173],\n    \"Sports\": [70, 65, 80, 85, 95, 100, 105, 110, 90, 75, 72, 68],\n    \"Travel\": [60, 55, 75, 90, 110, 130, 140, 148, 105, 80, 65, 58],\n    \"Food\": [88, 82, 85, 90, 95, 92, 98, 100, 88, 92, 105, 115],\n    \"Culture\": [72, 68, 78, 82, 85, 88, 75, 70, 80, 90, 95, 92],\n}\n\ncategories = sorted(base_traffic.keys(), key=lambda c: sum(base_traffic[c]) / len(base_traffic[c]))\n\nmatrix = []\nfor cat in categories:\n    row = [max(40, v + np.random.randint(-5, 6)) for v in base_traffic[cat]]\n    matrix.append(row)\n\nall_vals = [v for row in matrix for v in row]\nvmin, vmax = min(all_vals), max(all_vals)\n\n# Imprint sequential colormap: #009E73 (brand green) → #4467A3 (blue)\n# Precompute all cell colors to avoid duplicating interpolation logic\n_R0, _G0, _B0 = 0x00, 0x9E, 0x73  # #009E73\n_R1, _G1, _B1 = 0x44, 0x67, 0xA3  # #4467A3\n\ncell_hex = {}\ncell_rgb = {}\nfor row_i in range(len(categories)):\n    for col_j in range(len(months)):\n        t = max(0.0, min(1.0, (matrix[row_i][col_j] - vmin) / (vmax - vmin)))\n        rc = int(_R0 + (_R1 - _R0) * t)\n        gc = int(_G0 + (_G1 - _G0) * t)\n        bc = int(_B0 + (_B1 - _B0) * t)\n        cell_hex[(row_i, col_j)] = f\"#{rc:02x}{gc:02x}{bc:02x}\"\n        cell_rgb[(row_i, col_j)] = (rc, gc, bc)\n\n# Colorbar gradient (60 stops, top = vmax, bottom = vmin)\nN_CB = 60\ncb_hex = []\nfor s in range(N_CB):\n    t = max(0.0, min(1.0, 1.0 - s / (N_CB - 1)))\n    rc = int(_R0 + (_R1 - _R0) * t)\n    gc = int(_G0 + (_G1 - _G0) * t)\n    bc = int(_B0 + (_B1 - _B0) * t)\n    cb_hex.append(f\"#{rc:02x}{gc:02x}{bc:02x}\")\n\n# Plot\ntitle = \"heatmap-basic · python · pygal · anyplot.ai\"\ntitle_font_size = max(44, round(66 * 67 / len(title))) if len(title) > 67 else 66\n\nIMPRINT_COLORS = (\n    \"#009E73\",\n    \"#C475FD\",\n    \"#4467A3\",\n    \"#BD8233\",\n    \"#AE3030\",\n    \"#2ABCCD\",\n    \"#954477\",\n    \"#99B314\",\n    \"#009E73\",\n    \"#C475FD\",\n    \"#4467A3\",\n    \"#BD8233\",\n)\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=IMPRINT_COLORS,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    font_family=\"sans-serif\",\n)\n\nchart = _pygal.HorizontalStackedBar(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    title=title,\n    show_legend=False,\n    print_values=True,\n    value_font_size=36,\n    show_y_guides=False,\n    show_x_guides=False,\n    show_x_labels=False,\n    margin_top=200,\n    margin_bottom=100,\n    margin_left=300,\n    margin_right=450,\n    range=(0, 12),\n    spacing=0,\n    rounded_bars=1,\n)\nchart.x_labels = categories\n\nfor col_j, month in enumerate(months):\n    series_data = [\n        {\"value\": 1, \"color\": cell_hex[(row_i, col_j)], \"formatter\": lambda x, v=matrix[row_i][col_j]: str(v)}\n        for row_i in range(len(categories))\n    ]\n    chart.add(month, series_data)\n\n# Render SVG via pygal\nsvg = chart.render(is_unicode=True)\n\n# Post-process SVG: brightness-adaptive text colors for value annotations\noverlay_match = re.search(\n    r'(<g[^>]*class=\"plot text-overlay\">)(.*?)(</g>\\s*<g[^>]*class=\"plot tooltip-overlay\")', svg, re.DOTALL\n)\nif overlay_match:\n    overlay_content = overlay_match.group(2)\n    for serie_match in re.finditer(r'(<g class=\"series serie-(\\d+)[^\"]*\">)(.*?)(</g>)', overlay_content, re.DOTALL):\n        col_j = int(serie_match.group(2))\n        serie_content = serie_match.group(3)\n        value_texts = sorted(\n            [\n                (float(m.group(2)), m)\n                for m in re.finditer(\n                    r'(<text\\s+text-anchor=\"middle\"\\s+x=\"[^\"]+\"\\s+y=\"([^\"]+)\"\\s+class=\"value\">)(\\d+)(</text>)',\n                    serie_content,\n                )\n            ],\n            key=lambda x: x[0],\n            reverse=True,\n        )\n        new_serie = serie_content\n        for rank, (_, m) in enumerate(value_texts):\n            rc, gc, bc = cell_rgb.get((rank, col_j), (200, 200, 200))\n            brightness = (rc * 299 + gc * 587 + bc * 114) / 1000\n            txt_color = \"#ffffff\" if brightness < 140 else \"#1a1a1a\"\n            old_tag = m.group(0)\n            new_tag = m.group(1).replace('class=\"value\"', f'class=\"value\" fill=\"{txt_color}\"') + m.group(3) + m.group(4)\n            new_serie = new_serie.replace(old_tag, new_tag, 1)\n        overlay_content = overlay_content.replace(serie_match.group(3), new_serie, 1)\n    svg = svg[: overlay_match.start(2)] + overlay_content + svg[overlay_match.end(2) :]\n\n# Inject month headers, axis titles, colorbar, and peak annotations before </svg>\nW, H = 2400, 2400\n\ntransform_match = re.search(r\"translate\\(([^,]+),\\s*([^)]+)\\)\", svg)\nplot_x = float(transform_match.group(1)) if transform_match else 400\nplot_y = float(transform_match.group(2)) if transform_match else 200\n\nbg_rects = re.findall(r'<rect[^>]*width=\"([^\"]+)\"[^>]*height=\"([^\"]+)\"[^>]*class=\"background\"', svg)\nif len(bg_rects) >= 2:\n    plot_w, plot_h = float(bg_rects[1][0]), float(bg_rects[1][1])\nelse:\n    plot_w, plot_h = 1720.0, 2100.0\n\ngrid_left = plot_x\ngrid_top = plot_y\ngrid_right = plot_x + plot_w\ngrid_bottom = plot_y + plot_h\ncell_w = plot_w / len(months)\ncell_h = plot_h / len(categories)\n\nextra = []\n\n# Subtitle\nextra.append(\n    f'<text x=\"{W / 2:.0f}\" y=\"{grid_top - 55:.0f}\" text-anchor=\"middle\" fill=\"{INK_SOFT}\" '\n    f'style=\"font-size:30px;font-family:sans-serif\">'\n    f\"Monthly website traffic (thousands of visits) by content category</text>\"\n)\n\n# Month column headers\nfor col_j, mn in enumerate(months):\n    mx = grid_left + col_j * cell_w + cell_w / 2\n    extra.append(\n        f'<text x=\"{mx:.0f}\" y=\"{grid_top - 18:.0f}\" text-anchor=\"middle\" fill=\"{INK}\" '\n        f'style=\"font-size:32px;font-weight:600;font-family:sans-serif\">{mn}</text>'\n    )\n\n# X-axis title\nextra.append(\n    f'<text x=\"{(grid_left + grid_right) / 2:.0f}\" y=\"{grid_bottom + 52:.0f}\" '\n    f'text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:40px;font-weight:bold;font-family:sans-serif\">Month</text>'\n)\n\n# Y-axis title (rotated)\nmid_y = (grid_top + grid_bottom) / 2\nextra.append(\n    f'<text x=\"{grid_left - 230:.0f}\" y=\"{mid_y:.0f}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:40px;font-weight:bold;font-family:sans-serif\" '\n    f'transform=\"rotate(-90,{grid_left - 230:.0f},{mid_y:.0f})\">Content Category</text>'\n)\n\n# Colorbar\ncb_x = grid_right + 50\ncb_w = 36\ncb_top = grid_top + 18\ncb_bot = grid_bottom - 18\ncb_h = cb_bot - cb_top\nfor s in range(N_CB):\n    sy = cb_top + cb_h * s / N_CB\n    extra.append(\n        f'<rect x=\"{cb_x:.0f}\" y=\"{sy:.1f}\" width=\"{cb_w}\" height=\"{cb_h / N_CB + 1:.1f}\" fill=\"{cb_hex[s]}\"/>'\n    )\nextra.append(\n    f'<rect x=\"{cb_x:.0f}\" y=\"{cb_top:.0f}\" width=\"{cb_w}\" height=\"{cb_h:.0f}\" '\n    f'fill=\"none\" stroke=\"{INK_MUTED}\" stroke-width=\"1.5\"/>'\n)\nfor frac, label_val in [(0.0, vmax), (0.5, (vmin + vmax) / 2), (1.0, vmin)]:\n    ty = cb_top + cb_h * frac\n    extra.append(\n        f'<text x=\"{cb_x + cb_w + 10:.0f}\" y=\"{ty + 10:.0f}\" fill=\"{INK_SOFT}\" '\n        f'style=\"font-size:28px;font-family:sans-serif\">{label_val:.0f}</text>'\n    )\nextra.append(\n    f'<text x=\"{cb_x + cb_w / 2:.0f}\" y=\"{cb_top - 16:.0f}\" text-anchor=\"middle\" '\n    f'fill=\"{INK}\" style=\"font-size:30px;font-weight:bold;font-family:sans-serif\">Visits (k)</text>'\n)\n\n# Peak annotations — amber highlight for seasonal storytelling\nfor cat_name, col_j, label in [(\"Finance\", 11, \"Year-end peak\"), (\"Travel\", 7, \"Summer peak\")]:\n    row_i = categories.index(cat_name)\n    svg_row = len(categories) - 1 - row_i\n    ay = grid_top + svg_row * cell_h + cell_h * 0.82\n    if col_j == len(months) - 1:\n        ax = grid_right - 10\n        anchor = \"end\"\n    else:\n        ax = grid_left + col_j * cell_w + cell_w / 2\n        anchor = \"middle\"\n    extra.append(\n        f'<text x=\"{ax:.0f}\" y=\"{ay:.0f}\" text-anchor=\"{anchor}\" fill=\"#DDCC77\" '\n        f'style=\"font-size:26px;font-weight:bold;font-family:sans-serif;font-style:italic\">▼ {label}</text>'\n    )\n\nsvg = svg.replace(\"</svg>\", \"\\n\".join(extra) + \"\\n</svg>\")\n\n# Save\n_cairosvg.svg2png(bytestring=svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=2400, output_height=2400)\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as fout:\n    pygal_svg = chart.render(is_unicode=True)\n    fout.write(\n        f'<!DOCTYPE html>\\n<html>\\n<head>\\n<meta charset=\"utf-8\">\\n'\n        f\"<title>heatmap-basic - python - pygal - anyplot.ai</title>\\n\"\n        f\"<style>body{{margin:0;display:flex;justify-content:center;align-items:center;\"\n        f\"min-height:100vh;background:{PAGE_BG};}}.chart{{max-width:100%;height:auto;}}</style>\\n\"\n        f'</head>\\n<body>\\n<figure class=\"chart\">\\n{pygal_svg}\\n</figure>\\n</body>\\n</html>\\n'\n    )\n"}