ysharma HF Staff commited on
Commit
26dd397
Β·
verified Β·
1 Parent(s): 1a2334e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +425 -0
app.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ πŸ”₯ GitHub-Style Contribution Heatmap β€” Custom Gradio 6 Component
3
+ Built entirely with gr.HTML: dynamic templates, CSS, JS interactivity, and custom props.
4
+ """
5
+
6
+ import gradio as gr
7
+ import random
8
+ import math
9
+ from datetime import datetime, timedelta
10
+
11
+ # ── Color Schemes ────────────────────────────────────────────────────────────
12
+ COLOR_SCHEMES = {
13
+ "green": ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
14
+ "blue": ["#161b22", "#0a3069", "#0550ae", "#0969da", "#54aeff"],
15
+ "purple": ["#161b22", "#3b1f72", "#6639a6", "#8957e5", "#bc8cff"],
16
+ "orange": ["#161b22", "#6e3a07", "#9a5b13", "#d4821f", "#f0b040"],
17
+ "pink": ["#161b22", "#5c1a3a", "#8b2252", "#d63384", "#f472b6"],
18
+ "red": ["#161b22", "#6e1007", "#9a2013", "#d4401f", "#f06040"],
19
+ }
20
+
21
+ # ── HTML / CSS / JS Templates (shared across all instances) ──────────────────
22
+
23
+ HEATMAP_HTML = """
24
+ <div class="heatmap-container">
25
+ <div class="heatmap-header">
26
+ <h2>${(() => {
27
+ const total = Object.values(value || {}).reduce((a, b) => a + b, 0);
28
+ return 'πŸ“Š ' + total.toLocaleString() + ' contributions in ' + year;
29
+ })()}</h2>
30
+ <div class="legend">
31
+ <span>Less</span>
32
+ <div class="legend-box" style="background:${c0}"></div>
33
+ <div class="legend-box" style="background:${c1}"></div>
34
+ <div class="legend-box" style="background:${c2}"></div>
35
+ <div class="legend-box" style="background:${c3}"></div>
36
+ <div class="legend-box" style="background:${c4}"></div>
37
+ <span>More</span>
38
+ </div>
39
+ </div>
40
+ <div class="month-labels">
41
+ ${(() => {
42
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
43
+ return months.map((m, i) =>
44
+ '<span style="grid-column:' + (Math.floor(i * 4.33) + 2) + '">' + m + '</span>'
45
+ ).join('');
46
+ })()}
47
+ </div>
48
+ <div class="heatmap-grid">
49
+ <div class="day-labels">
50
+ <span></span><span>Mon</span><span></span><span>Wed</span><span></span><span>Fri</span><span></span>
51
+ </div>
52
+ <div class="cells">
53
+ ${(() => {
54
+ const v = value || {};
55
+ const sd = new Date(year, 0, 1);
56
+ const pad = sd.getDay();
57
+ const cells = [];
58
+ for (let i = 0; i < pad; i++) cells.push('<div class="cell empty"></div>');
59
+ const totalDays = Math.floor((new Date(year, 11, 31) - sd) / 86400000) + 1;
60
+ for (let d = 0; d < totalDays; d++) {
61
+ const dt = new Date(year, 0, 1 + d);
62
+ const key = dt.getFullYear() + '-' + String(dt.getMonth()+1).padStart(2,'0') + '-' + String(dt.getDate()).padStart(2,'0');
63
+ const count = v[key] || 0;
64
+ let lv = 0;
65
+ if (count > 0) lv = 1;
66
+ if (count >= 3) lv = 2;
67
+ if (count >= 6) lv = 3;
68
+ if (count >= 10) lv = 4;
69
+ const mn = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
70
+ const tip = count + ' contributions on ' + mn[dt.getMonth()] + ' ' + dt.getDate() + ', ' + year;
71
+ cells.push('<div class="cell level-' + lv + '" data-date="' + key + '" data-count="' + count + '" title="' + tip + '"></div>');
72
+ }
73
+ return cells.join('');
74
+ })()}
75
+ </div>
76
+ </div>
77
+ <div class="stats-bar">
78
+ ${(() => {
79
+ const v = value || {};
80
+ const totalDays = Math.floor((new Date(year, 11, 31) - new Date(year, 0, 1)) / 86400000) + 1;
81
+ let streak = 0, maxStreak = 0, total = 0, active = 0, best = 0;
82
+ const vals = [];
83
+ for (let d = 0; d < totalDays; d++) {
84
+ const dt = new Date(year, 0, 1 + d);
85
+ const key = dt.getFullYear() + '-' + String(dt.getMonth()+1).padStart(2,'0') + '-' + String(dt.getDate()).padStart(2,'0');
86
+ const c = v[key] || 0;
87
+ total += c;
88
+ if (c > 0) { streak++; maxStreak = Math.max(maxStreak, streak); active++; best = Math.max(best, c); vals.push(c); }
89
+ else { streak = 0; }
90
+ }
91
+ const avg = vals.length ? (total / vals.length).toFixed(1) : '0';
92
+ const stats = [
93
+ ['πŸ”₯', maxStreak, 'Longest Streak'],
94
+ ['πŸ“…', active, 'Active Days'],
95
+ ['⚑', best, 'Best Day'],
96
+ ['πŸ“ˆ', avg, 'Avg / Active Day'],
97
+ ['πŸ†', total.toLocaleString(), 'Total'],
98
+ ];
99
+ return stats.map(s =>
100
+ '<div class="stat"><span class="stat-value">' + s[1] + '</span><span class="stat-label">' + s[0] + ' ' + s[2] + '</span></div>'
101
+ ).join('');
102
+ })()}
103
+ </div>
104
+ </div>
105
+ """
106
+
107
+ HEATMAP_CSS = """
108
+ .heatmap-container {
109
+ background: #0d1117;
110
+ border-radius: 12px;
111
+ padding: 24px;
112
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
113
+ color: #c9d1d9;
114
+ overflow-x: auto;
115
+ }
116
+ .heatmap-header {
117
+ display: flex; justify-content: space-between; align-items: center;
118
+ margin-bottom: 12px; flex-wrap: wrap; gap: 10px;
119
+ }
120
+ .heatmap-header h2 { margin: 0; font-size: 16px; font-weight: 500; color: #f0f6fc; }
121
+ .legend { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #8b949e; }
122
+ .legend-box { width: 12px; height: 12px; border-radius: 2px; }
123
+ .month-labels {
124
+ display: grid; grid-template-columns: 30px repeat(53, 1fr);
125
+ font-size: 11px; color: #8b949e; margin-bottom: 4px; padding-left: 2px;
126
+ }
127
+ .heatmap-grid { display: flex; gap: 4px; }
128
+ .day-labels {
129
+ display: grid; grid-template-rows: repeat(7, 1fr);
130
+ font-size: 11px; color: #8b949e; width: 30px; gap: 2px;
131
+ }
132
+ .day-labels span { height: 13px; display: flex; align-items: center; }
133
+ .cells {
134
+ display: grid; grid-template-rows: repeat(7, 1fr);
135
+ grid-auto-flow: column; gap: 2px; flex: 1;
136
+ }
137
+ .cell {
138
+ width: 13px; height: 13px; border-radius: 2px; cursor: pointer;
139
+ transition: all 0.15s ease; outline: 1px solid rgba(27,31,35,0.06);
140
+ }
141
+ .cell:hover {
142
+ outline: 2px solid #58a6ff; outline-offset: -1px;
143
+ transform: scale(1.3); z-index: 1;
144
+ }
145
+ .cell.empty { visibility: hidden; }
146
+ .level-0 { background: ${c0}; }
147
+ .level-1 { background: ${c1}; }
148
+ .level-2 { background: ${c2}; }
149
+ .level-3 { background: ${c3}; }
150
+ .level-4 { background: ${c4}; }
151
+ .stats-bar {
152
+ display: flex; justify-content: space-around; margin-top: 20px;
153
+ padding-top: 16px; border-top: 1px solid #21262d;
154
+ flex-wrap: wrap; gap: 12px;
155
+ }
156
+ .stat { display: flex; flex-direction: column; align-items: center; gap: 4px; }
157
+ .stat-value { font-size: 22px; font-weight: 700; color: ${c4}; }
158
+ .stat-label { font-size: 12px; color: #8b949e; }
159
+ """
160
+
161
+ HEATMAP_JS = """
162
+ element.addEventListener('click', (e) => {
163
+ if (e.target && e.target.classList.contains('cell') && !e.target.classList.contains('empty')) {
164
+ const date = e.target.dataset.date;
165
+ const cur = parseInt(e.target.dataset.count) || 0;
166
+ const next = cur >= 12 ? 0 : cur + 1;
167
+ const nv = {...(props.value || {})};
168
+ if (next === 0) delete nv[date]; else nv[date] = next;
169
+ props.value = nv;
170
+ trigger('change');
171
+ }
172
+ });
173
+ """
174
+
175
+
176
+ # ── Component Class ──────────────────────────────────────────────────────────
177
+
178
+ class ContributionHeatmap(gr.HTML):
179
+ """Reusable GitHub-style contribution heatmap built on gr.HTML."""
180
+
181
+ def __init__(self, value=None, year=2025, theme="green",
182
+ c0=None, c1=None, c2=None, c3=None, c4=None, **kwargs):
183
+ if value is None:
184
+ value = {}
185
+ # Use explicit c0-c4 if provided (from gr.HTML updates), else derive from theme
186
+ colors = COLOR_SCHEMES.get(theme, COLOR_SCHEMES["green"])
187
+ super().__init__(
188
+ value=value,
189
+ year=year,
190
+ c0=c0 or colors[0],
191
+ c1=c1 or colors[1],
192
+ c2=c2 or colors[2],
193
+ c3=c3 or colors[3],
194
+ c4=c4 or colors[4],
195
+ html_template=HEATMAP_HTML,
196
+ css_template=HEATMAP_CSS,
197
+ js_on_load=HEATMAP_JS,
198
+ **kwargs,
199
+ )
200
+
201
+ def api_info(self):
202
+ return {"type": "object", "description": "Dict mapping YYYY-MM-DD to int counts"}
203
+
204
+
205
+ # ── Helpers ──────────────────────────────────────────────────────────────────
206
+
207
+ def _theme_update(theme):
208
+ """Return gr.HTML update with only color props β€” preserves value/year."""
209
+ c = COLOR_SCHEMES.get(theme, COLOR_SCHEMES["green"])
210
+ return gr.HTML(c0=c[0], c1=c[1], c2=c[2], c3=c[3], c4=c[4])
211
+
212
+
213
+ def _full_update(data, year, theme):
214
+ """Return gr.HTML update with value + year + colors all set."""
215
+ c = COLOR_SCHEMES.get(theme, COLOR_SCHEMES["green"])
216
+ return gr.HTML(value=data, year=int(year), c0=c[0], c1=c[1], c2=c[2], c3=c[3], c4=c[4])
217
+
218
+
219
+ # ── Pattern Generators ───────────────────────────────────────────────────────
220
+
221
+ def _dates(year):
222
+ start = datetime(year, 1, 1)
223
+ end = datetime(year, 12, 31)
224
+ d = start
225
+ while d <= end:
226
+ yield d, d.strftime("%Y-%m-%d")
227
+ d += timedelta(days=1)
228
+
229
+
230
+ def pattern_random(intensity, year):
231
+ data = {}
232
+ for d, key in _dates(year):
233
+ if d > datetime.now(): break
234
+ if random.random() < intensity:
235
+ r = random.random()
236
+ if r < 0.4: data[key] = random.randint(1, 2)
237
+ elif r < 0.7: data[key] = random.randint(3, 5)
238
+ elif r < 0.9: data[key] = random.randint(6, 9)
239
+ else: data[key] = random.randint(10, 15)
240
+ return data
241
+
242
+
243
+ def pattern_streak(year):
244
+ data, in_streak = {}, False
245
+ for d, key in _dates(year):
246
+ if d > datetime.now(): break
247
+ if random.random() < 0.08: in_streak = not in_streak
248
+ if in_streak: data[key] = random.randint(2, 14)
249
+ return data
250
+
251
+
252
+ def pattern_weekday(year):
253
+ data = {}
254
+ for d, key in _dates(year):
255
+ if d > datetime.now(): break
256
+ if d.weekday() < 5 and random.random() < 0.75:
257
+ data[key] = random.randint(1, 10)
258
+ return data
259
+
260
+
261
+ def pattern_weekend(year):
262
+ data = {}
263
+ for d, key in _dates(year):
264
+ if d > datetime.now(): break
265
+ if d.weekday() >= 5 and random.random() < 0.85:
266
+ data[key] = random.randint(3, 15)
267
+ return data
268
+
269
+
270
+ def pattern_momentum(year):
271
+ data, total = {}, 365
272
+ for i, (d, key) in enumerate(_dates(year)):
273
+ if d > datetime.now(): break
274
+ prob = 0.1 + 0.8 * (i / total)
275
+ if random.random() < prob:
276
+ data[key] = random.randint(1, max(1, int(1 + 14 * (i / total))))
277
+ return data
278
+
279
+
280
+ def pattern_sine(year):
281
+ data = {}
282
+ for i, (d, key) in enumerate(_dates(year)):
283
+ if d > datetime.now(): break
284
+ w = (math.sin(2 * math.pi * i / 90) + 1) / 2
285
+ if random.random() < 0.3 + 0.6 * w:
286
+ data[key] = max(1, int(w * 14) + random.randint(0, 2))
287
+ return data
288
+
289
+
290
+ def pattern_seasonal(year):
291
+ data = {}
292
+ for d, key in _dates(year):
293
+ if d > datetime.now(): break
294
+ if d.month in (1, 2, 6, 7, 8, 12):
295
+ if random.random() < 0.8: data[key] = random.randint(3, 15)
296
+ elif random.random() < 0.2:
297
+ data[key] = random.randint(1, 4)
298
+ return data
299
+
300
+
301
+ def pattern_burst(year):
302
+ data, days = {}, list(_dates(year))
303
+ for _ in range(random.randint(8, 15)):
304
+ si = random.randint(0, len(days) - 20)
305
+ for j in range(random.randint(3, 14)):
306
+ if si + j < len(days):
307
+ d, key = days[si + j]
308
+ if d > datetime.now(): break
309
+ data[key] = random.randint(5, 15)
310
+ return data
311
+
312
+
313
+ PATTERNS = {
314
+ "🎲 Random": lambda i, y: pattern_random(i, y),
315
+ "πŸ”₯ Streaks": lambda i, y: pattern_streak(y),
316
+ "πŸ’Ό Weekday Warrior": lambda i, y: pattern_weekday(y),
317
+ "πŸŒ™ Weekend Hacker": lambda i, y: pattern_weekend(y),
318
+ "πŸ“ˆ Growing Momentum": lambda i, y: pattern_momentum(y),
319
+ "🌊 Sine Wave": lambda i, y: pattern_sine(y),
320
+ "β˜€οΈ Seasonal": lambda i, y: pattern_seasonal(y),
321
+ "πŸ’₯ Burst Mode": lambda i, y: pattern_burst(y),
322
+ }
323
+
324
+
325
+ # ── Gradio App ───────────────────────────────────────────────────────────────
326
+
327
+ with gr.Blocks(
328
+ title="πŸ”₯ Contribution Heatmap",
329
+ ) as demo:
330
+
331
+ gr.Markdown(
332
+ """
333
+ # 🟩 GitHub-Style Contribution Heatmap
334
+ *Built entirely with Gradio 6's `gr.HTML` component β€” custom templates, CSS, and JS interactivity.*
335
+
336
+ **Click any cell** to cycle its intensity (0 β†’ 12 β†’ 0). Use the controls below to generate patterns and switch themes.
337
+ """
338
+ )
339
+
340
+ heatmap = ContributionHeatmap(
341
+ value=pattern_random(0.6, 2025), year=2025, theme="green"
342
+ )
343
+
344
+ with gr.Row():
345
+ with gr.Column(scale=1):
346
+ theme_dd = gr.Dropdown(
347
+ choices=list(COLOR_SCHEMES.keys()),
348
+ value="green",
349
+ label="🎨 Color Theme",
350
+ )
351
+ with gr.Column(scale=1):
352
+ year_dd = gr.Dropdown(
353
+ choices=[2023, 2024, 2025],
354
+ value=2025,
355
+ label="πŸ“… Year",
356
+ )
357
+ with gr.Column(scale=1):
358
+ pattern_dd = gr.Dropdown(
359
+ choices=list(PATTERNS.keys()),
360
+ value="🎲 Random",
361
+ label="🧬 Pattern",
362
+ )
363
+ with gr.Column(scale=1):
364
+ intensity = gr.Slider(
365
+ 0.1, 1.0, value=0.6, step=0.1, label="πŸ“Š Intensity"
366
+ )
367
+
368
+ with gr.Row():
369
+ generate_btn = gr.Button("✨ Generate", variant="primary", size="lg")
370
+ clear_btn = gr.Button("πŸ—‘οΈ Clear All", variant="stop")
371
+
372
+ status = gr.Textbox(label="Status", interactive=False)
373
+
374
+ # ── Events ────────────────────────────────────────────────────────────
375
+ # FIX: return gr.HTML(prop=...) to update props on the EXISTING component
376
+ # instead of returning a new ContributionHeatmap(...) instance.
377
+
378
+ theme_dd.change(
379
+ fn=lambda theme: _theme_update(theme),
380
+ inputs=[theme_dd],
381
+ outputs=heatmap,
382
+ )
383
+
384
+ year_dd.change(
385
+ fn=lambda y, t, i, p: (
386
+ _full_update(PATTERNS.get(p, PATTERNS["🎲 Random"])(i, int(y)), y, t),
387
+ f"πŸ“… Showing {y} β€” {len((d := PATTERNS.get(p, PATTERNS['🎲 Random'])(i, int(y))))} active days, {sum(d.values()):,} contributions",
388
+ ),
389
+ inputs=[year_dd, theme_dd, intensity, pattern_dd],
390
+ outputs=[heatmap, status],
391
+ )
392
+
393
+ def on_generate(int_val, theme, year_val, pat):
394
+ gen = PATTERNS.get(pat, PATTERNS["🎲 Random"])
395
+ data = gen(int_val, int(year_val))
396
+ total = sum(data.values())
397
+ return (
398
+ _full_update(data, year_val, theme),
399
+ f"✨ {pat} β€” {len(data)} active days Β· {total:,} contributions",
400
+ )
401
+
402
+ generate_btn.click(
403
+ fn=on_generate,
404
+ inputs=[intensity, theme_dd, year_dd, pattern_dd],
405
+ outputs=[heatmap, status],
406
+ )
407
+
408
+ clear_btn.click(
409
+ fn=lambda t, y: (_full_update({}, y, t), "πŸ—‘οΈ Cleared all contributions"),
410
+ inputs=[theme_dd, year_dd],
411
+ outputs=[heatmap, status],
412
+ )
413
+
414
+ heatmap.change(
415
+ fn=lambda d: f"✏️ Edited: {len([v for v in (d or {}).values() if v > 0])} active days · {sum((d or {}).values()):,} contributions" if isinstance(d, dict) else "",
416
+ inputs=heatmap,
417
+ outputs=status,
418
+ )
419
+
420
+
421
+ if __name__ == "__main__":
422
+ demo.launch(
423
+ theme=gr.themes.Soft(primary_hue="green"),
424
+ css="footer { display: none !important; }",
425
+ )