| | --- |
| | 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 |