--- title: Github Contribution Heatmap emoji: 😻 colorFrom: green colorTo: pink sdk: gradio sdk_version: 6.5.1 app_file: contribution_heatmap.py pinned: false license: mit short_description: 'GitHub-style heatmap with gradio''s new html component ' --- # 🟩 ContributionHeatmap A GitHub-style contribution heatmap component for [Gradio 6](https://www.gradio.app), built entirely with `gr.HTML`. No Svelte, no CLI tooling, no npm β€” just Python, HTML templates, CSS, and a sprinkle of JS. --- ## Features - **GitHub-style grid** β€” 365 cells laid out Sunβ†’Sat Γ— 53 weeks, with month labels and day-of-week markers. - **6 built-in color themes** β€” green, blue, purple, orange, pink, red. Themes switch dynamically without losing data. - **Click-to-edit** β€” click any cell to cycle its count (0 β†’ 1 β†’ 2 β†’ … β†’ 12 β†’ 0). The `change` event fires on every edit. - **Auto-computed stats** β€” longest streak, active days, best day, average per active day, and total contributions are all calculated in the template. - **Fully reactive** β€” update `value`, `year`, or any color prop via `gr.HTML(...)` and the entire component re-renders. - **API / MCP ready** β€” includes `api_info()` for Gradio's built-in API and MCP support. --- ## Requirements ``` gradio>=6.0 ``` No other dependencies. The component is a single Python file. --- ## Quickstart ### Minimal example ```python import gradio as gr from contribution_heatmap import ContributionHeatmap with gr.Blocks() as demo: heatmap = ContributionHeatmap() demo.launch() ``` This renders an empty heatmap for 2025 in the default green theme. Users can click cells to add contributions interactively. ### With initial data ```python data = { "2025-01-15": 4, "2025-01-16": 7, "2025-01-17": 12, "2025-03-01": 2, } with gr.Blocks() as demo: heatmap = ContributionHeatmap(value=data, year=2025, theme="purple") demo.launch() ``` --- ## Constructor ```python ContributionHeatmap( value: dict | None = None, year: int = 2025, theme: str = "green", c0: str | None = None, c1: str | None = None, c2: str | None = None, c3: str | None = None, c4: str | None = None, **kwargs, ) ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `value` | `dict \| None` | `{}` | Contribution data. Keys are date strings in `YYYY-MM-DD` format, values are integers (contribution count for that day). | | `year` | `int` | `2025` | The calendar year to render. | | `theme` | `str` | `"green"` | One of `"green"`, `"blue"`, `"purple"`, `"orange"`, `"pink"`, `"red"`. Sets the 5-level color palette. | | `c0`–`c4` | `str \| None` | `None` | Override individual color levels with hex values (e.g. `c4="#ff0000"`). When `None`, colors are derived from `theme`. | | `**kwargs` | | | Passed through to `gr.HTML` (e.g. `visible`, `elem_id`, `elem_classes`, `container`, `min_height`). | ### Data format The `value` dict maps date strings to integer counts: ```python { "2025-01-01": 3, # level 1 (1–2) "2025-01-02": 5, # level 2 (3–5) "2025-01-03": 8, # level 3 (6–9) "2025-01-04": 12, # level 4 (10+) } ``` Intensity levels are determined by these thresholds: | Count | Level | Visual | |-------|-------|--------| | 0 | 0 | Darkest (empty) | | 1–2 | 1 | Light | | 3–5 | 2 | Medium | | 6–9 | 3 | Bright | | 10+ | 4 | Brightest | --- ## Updating props This is the most important pattern to get right. When updating a `ContributionHeatmap` from an event handler, **return `gr.HTML(...)` β€” not a new `ContributionHeatmap(...)`**. This tells Gradio to update the existing component's props rather than replacing it entirely. ### βœ… Correct: update via `gr.HTML(...)` ```python # Change only the theme colors (data and year are preserved) def switch_theme(theme): colors = COLOR_SCHEMES[theme] return gr.HTML(c0=colors[0], c1=colors[1], c2=colors[2], c3=colors[3], c4=colors[4]) theme_dropdown.change(fn=switch_theme, inputs=[theme_dropdown], outputs=heatmap) ``` ```python # Update everything: data + year + colors def regenerate(year, theme): data = generate_my_data(year) colors = COLOR_SCHEMES[theme] return gr.HTML( value=data, year=year, c0=colors[0], c1=colors[1], c2=colors[2], c3=colors[3], c4=colors[4], ) btn.click(fn=regenerate, inputs=[year_dd, theme_dd], outputs=heatmap) ``` ### ❌ Wrong: returning a new instance ```python # DON'T do this β€” creates a new component instead of updating props def switch_theme(theme, data): return ContributionHeatmap(value=data, theme=theme) ``` ### Helper functions The module includes two convenience functions for building updates: ```python from contribution_heatmap import _theme_update, _full_update, COLOR_SCHEMES # Update colors only theme_dd.change(fn=_theme_update, inputs=[theme_dd], outputs=heatmap) # Update data + year + colors btn.click( fn=lambda y, t: _full_update(my_data, y, t), inputs=[year_dd, theme_dd], outputs=heatmap, ) ``` --- ## Events Since `ContributionHeatmap` extends `gr.HTML`, it supports all standard Gradio HTML events. The most useful one is `change`, which fires when a user clicks a cell: ```python def on_edit(data): """Called whenever a user clicks a cell.""" total = sum(data.values()) active = len([v for v in data.values() if v > 0]) return f"{active} active days, {total} total contributions" heatmap.change(fn=on_edit, inputs=heatmap, outputs=status_textbox) ``` The `data` received in the handler is the full `value` dict with the updated cell. --- ## Color themes Six built-in themes are available via the `COLOR_SCHEMES` dict: ```python from contribution_heatmap import COLOR_SCHEMES # Each theme is a list of 5 hex colors: [level0, level1, level2, level3, level4] print(COLOR_SCHEMES["green"]) # ['#161b22', '#0e4429', '#006d32', '#26a641', '#39d353'] ``` | Theme | Level 0 | Level 1 | Level 2 | Level 3 | Level 4 | |-------|---------|---------|---------|---------|---------| | `green` | `#161b22` | `#0e4429` | `#006d32` | `#26a641` | `#39d353` | | `blue` | `#161b22` | `#0a3069` | `#0550ae` | `#0969da` | `#54aeff` | | `purple` | `#161b22` | `#3b1f72` | `#6639a6` | `#8957e5` | `#bc8cff` | | `orange` | `#161b22` | `#6e3a07` | `#9a5b13` | `#d4821f` | `#f0b040` | | `pink` | `#161b22` | `#5c1a3a` | `#8b2252` | `#d63384` | `#f472b6` | | `red` | `#161b22` | `#6e1007` | `#9a2013` | `#d4401f` | `#f06040` | ### Custom colors You can pass any hex colors directly via `c0`–`c4`: ```python heatmap = ContributionHeatmap( value=data, c0="#1a1a2e", c1="#16213e", c2="#0f3460", c3="#533483", c4="#e94560", ) ``` Or add your own theme to `COLOR_SCHEMES`: ```python COLOR_SCHEMES["cyberpunk"] = ["#0a0a0a", "#1a0533", "#3d0066", "#7700cc", "#cc00ff"] heatmap = ContributionHeatmap(value=data, theme="cyberpunk") ``` --- ## Full example app Below is a complete working app with theme switching, pattern generation, and interactive editing: ```python import gradio as gr from contribution_heatmap import ( ContributionHeatmap, COLOR_SCHEMES, _theme_update, _full_update, ) import random from datetime import datetime, timedelta def generate_data(year, intensity=0.6): """Generate random contribution data.""" data = {} start = datetime(year, 1, 1) for i in range(365): d = start + timedelta(days=i) if d > datetime.now(): break if random.random() < intensity: data[d.strftime("%Y-%m-%d")] = random.randint(1, 15) return data with gr.Blocks() as demo: gr.Markdown("# My Contribution Tracker") heatmap = ContributionHeatmap( value=generate_data(2025), year=2025, theme="green" ) with gr.Row(): theme = gr.Dropdown( choices=list(COLOR_SCHEMES.keys()), value="green", label="Theme" ) year = gr.Dropdown(choices=[2023, 2024, 2025], value=2025, label="Year") regenerate = gr.Button("Regenerate") status = gr.Textbox(label="Info", interactive=False) # Theme changes β€” only update colors, preserve data theme.change(fn=_theme_update, inputs=[theme], outputs=heatmap) # Regenerate β€” new data + year + colors def on_regen(y, t): data = generate_data(int(y)) return _full_update(data, y, t), f"{len(data)} active days" regenerate.click(fn=on_regen, inputs=[year, theme], outputs=[heatmap, status]) # Track edits heatmap.change( fn=lambda d: f"Edited: {sum((d or {}).values())} total contributions", inputs=heatmap, outputs=status, ) demo.launch() ``` --- ## Use cases - **AI training logs** β€” visualize daily model training runs, fine-tuning sessions, or evaluation scores. - **Habit tracking** β€” meditation streaks, exercise days, reading logs. - **Coding activity** β€” render actual GitHub contribution data fetched via their API. - **Team dashboards** β€” show multiple heatmaps side-by-side for different team members or projects. - **Time-series overview** β€” any data that maps dates to counts. ### Multiple heatmaps ```python with gr.Blocks() as demo: gr.Markdown("# Team Activity") with gr.Row(): alice = ContributionHeatmap(value=alice_data, theme="blue", elem_id="alice") bob = ContributionHeatmap(value=bob_data, theme="purple", elem_id="bob") demo.launch() ``` --- ## How it works This component demonstrates key Gradio 6 `gr.HTML` capabilities: 1. **`html_template`** β€” JS template strings (`${...}`) render the grid, stats, and legend dynamically from `value`, `year`, and color props. 2. **`css_template`** β€” CSS is also templated with `${c0}`–`${c4}`, so colors re-render when props change without touching the HTML. 3. **`js_on_load`** β€” a click handler is attached once using event delegation on the parent element. It updates `props.value` and calls `trigger('change')` to notify Gradio. 4. **Component subclass** β€” `ContributionHeatmap` extends `gr.HTML`, setting default templates and accepting `theme`/`year`/color props. The `api_info()` method enables API and MCP usage. --- ## API info When used with Gradio's API or MCP integration, the component exposes: ```json { "type": "object", "description": "Dict mapping YYYY-MM-DD to int counts" } ``` Example API call: ```python from gradio_client import Client client = Client("http://localhost:7860") result = client.predict( {"2025-01-01": 5, "2025-01-02": 10}, api_name="/predict" ) ``` --- ## License MIT