Spaces:
Sleeping
Sleeping
| 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 |