ysharma's picture
ysharma HF Staff
Update README.md
700f8df verified
---
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