1(function () { 2 const CELL = 11; 3 const GAP = 2; 4 const ROWS = 7; 5 const LEVEL_OPACITY = [0.05, 0.25, 0.5, 0.75, 1.0]; 6 const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 7 const DAYS = ['', 'Mon', '', 'Wed', '', 'Fri', '']; 8 9 // ç¼åé ç½® 10 const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3å°æ¶ 11 const CACHE_KEY_PREFIX = 'github-heatmap-'; 12 13 function formatDate(dateStr) { 14 const d = new Date(dateStr); 15 return `${d.getFullYear()}å¹´${d.getMonth() + 1}æ${d.getDate()}æ¥`; 16 } 17 18 function buildHeatmapInnerHTML(username) { 19 return ` 20 <div class="github-heatmap-glass-container"> 21 <div class="github-heatmap-glass-card"> 22 <div class="github-heatmap-header"> 23 <span class="github-heatmap-title">GitHub</span> 24 <span class="github-heatmap-stats"> 25 ${username}è¿å»ä¸å¹´ <strong class="github-heatmap-count">--</strong> æ¬¡è´¡ç® 26 </span> 27 <div class="github-heatmap-legend"> 28 <span class="legend-label">å°</span> 29 <div class="legend-cell" style="--opacity: 0.05;"></div> 30 <div class="legend-cell" style="--opacity: 0.25;"></div> 31 <div class="legend-cell" style="--opacity: 0.5;"></div> 32 <div class="legend-cell" style="--opacity: 0.75;"></div> 33 <div class="legend-cell" style="--opacity: 1;"></div> 34 <span class="legend-label">å¤</span> 35 </div> 36 </div> 37 <div class="github-heatmap-content"> 38 <svg class="github-heatmap-svg" preserveAspectRatio="xMidYMid meet"></svg> 39 </div> 40 </div> 41 <div class="github-heatmap-tooltip"> 42 <div class="github-heatmap-tooltip__arrow"></div> 43 <div class="github-heatmap-tooltip__content"></div> 44 </div> 45 </div> 46 `; 47 } 48 49 // ä»ç¼åè·åæ°æ® 50 function getCachedData(username) { 51 try { 52 const cacheKey = CACHE_KEY_PREFIX + username; 53 const cached = localStorage.getItem(cacheKey); 54 const cacheTime = localStorage.getItem(cacheKey + '-time'); 55 56 if (cached && cacheTime) { 57 const age = Date.now() - parseInt(cacheTime, 10); 58 if (age < CACHE_DURATION) { 59 return JSON.parse(cached); 60 } 61 } 62 } catch (e) { 63 // localStorage ä¸å¯ç¨ï¼éç§æ¨¡å¼çï¼ï¼éé»å¤±è´¥ 64 } 65 return null; 66 } 67 68 // ä¿åæ°æ®å°ç¼å 69 function setCachedData(username, data) { 70 try { 71 const cacheKey = CACHE_KEY_PREFIX + username; 72 localStorage.setItem(cacheKey, JSON.stringify(data)); 73 localStorage.setItem(cacheKey + '-time', Date.now().toString()); 74 } catch (e) { 75 // localStorage ä¸å¯ç¨æå·²æ»¡ï¼éé»å¤±è´¥ 76 } 77 } 78 79 // æ¸ é¤ç¼åæ°æ® 80 function clearCachedData(username) { 81 try { 82 const cacheKey = CACHE_KEY_PREFIX + username; 83 localStorage.removeItem(cacheKey); 84 localStorage.removeItem(cacheKey + '-time'); 85 } catch (e) { 86 // localStorage ä¸å¯ç¨ï¼éé»å¤±è´¥ 87 } 88 } 89 90 // éªè¯ API ååºæ ¼å¼ 91 function isValidHeatmapData(data) { 92 return data && 93 typeof data === 'object' && 94 data.total && typeof data.total.lastYear === 'number' && 95 Array.isArray(data.contributions) && 96 data.contributions.every(day => 97 day && typeof day.date === 'string' && 98 typeof day.count === 'number' && 99 typeof day.level === 'number' 100 ); 101 } 102 103 // ä» API è·åæ°æ® 104 async function fetchHeatmapData(username) { 105 const response = await fetch(`https://github-contributions-api.jogruber.de/v4/${username}?y=last`); 106 if (!response.ok) { 107 throw new Error('GitHub contributions API error: ' + response.status); 108 } 109 const data = await response.json(); 110 111 // éªè¯ååºæ ¼å¼ 112 if (!isValidHeatmapData(data)) { 113 throw new Error('Invalid API response format'); 114 } 115 116 return data; 117 } 118 119 async function loadHeatmap(container) { 120 // é¿å éå¤åå§å 121 if (container.dataset.githubHeatmapInitialized === 'true') return; 122 container.dataset.githubHeatmapInitialized = 'true'; 123 124 const username = container.getAttribute('data-username') || 'Wcowin'; 125 container.innerHTML = buildHeatmapInnerHTML(username); 126 127 const svg = container.querySelector('.github-heatmap-svg'); 128 const statsEl = container.querySelector('.github-heatmap-count'); 129 const tooltipEl = container.querySelector('.github-heatmap-tooltip'); 130 131 if (!svg || !statsEl || !tooltipEl) return; 132 133 // å°è¯è·åç¼åæ°æ® 134 const cachedData = getCachedData(username); 135 136 if (cachedData && isValidHeatmapData(cachedData)) { 137 // æç¼åï¼å æ¾ç¤ºç¼åæ°æ® 138 renderHeatmap(container, cachedData, username); 139 140 // åå°éé»å·æ°ï¼ä¸é»å¡ï¼å¤±è´¥æ¶æ¸ çç¼åï¼ 141 fetchHeatmapData(username) 142 .then(data => { 143 setCachedData(username, data); 144 renderHeatmap(container, data, username); 145 }) 146 .catch(() => { 147 // è·åå¤±è´¥ï¼æ¸ é¤è¿æç¼åï¼ä¸æ¬¡ä¼éæ°å°è¯ 148 clearCachedData(username); 149 }); 150 } else { 151 // æ ç¼åæç¼åæ æï¼ä» API è·å
152 try { 153 const data = await fetchHeatmapData(username); 154 setCachedData(username, data); 155 renderHeatmap(container, data, username); 156 } catch (error) { 157 console.error('Error loading GitHub contributions:', error); 158 statsEl.textContent = 'å 载失败'; 159 // æ¸ é¤å¯è½åå¨çæ æç¼å 160 clearCachedData(username); 161 } 162 } 163 } 164 165 function renderHeatmap(container, data, username) { 166 const svg = container.querySelector('.github-heatmap-svg'); 167 const statsEl = container.querySelector('.github-heatmap-count'); 168 const tooltipEl = container.querySelector('.github-heatmap-tooltip'); 169 170 if (!svg || !statsEl || !tooltipEl) return; 171 172 // å®å ¨è·åè´¡ç®æ° 173 const totalContributions = data.total && typeof data.total.lastYear === 'number' 174 ? data.total.lastYear 175 : 0; 176 statsEl.textContent = totalContributions.toString(); 177 178 // å®å ¨è·åè´¡ç®æ°æ® 179 const contributions = Array.isArray(data.contributions) ? data.contributions : []; 180 181 const weeks = Math.ceil(contributions.length / ROWS); 182 const labelOffset = 28; 183 const headerOffset = 16; 184 const svgWidth = labelOffset + weeks * (CELL + GAP); 185 const svgHeight = headerOffset + ROWS * (CELL + GAP); 186 187 svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`); 188 svg.innerHTML = ''; 189 190 const monthLabels = []; 191 let lastMonth = -1; 192 for (let w = 0; w < weeks; w++) { 193 const idx = w * ROWS; 194 if (idx < contributions.length) { 195 const date = new Date(contributions[idx].date); 196 if (!isNaN(date.getTime())) {
197 const month = date.getMonth(); 198 if (month !== lastMonth) { 199 monthLabels.push({ label: MONTHS[month], x: labelOffset + w * (CELL + GAP) }); 200 lastMonth = month; 201 } 202 } 203 } 204 } 205 206 const ns = 'http://www.w3.org/2000/svg'; 207 208 monthLabels.forEach((m) => { 209 const text = document.createElementNS(ns, 'text'); 210 text.setAttribute('x', m.x); 211 text.setAttribute('y', 11); 212 text.setAttribute('class', 'heatmap-label'); 213 text.setAttribute('font-size', 10); 214 text.textContent = m.label; 215 svg.appendChild(text); 216 }); 217 218 DAYS.forEach((d, i) => { 219 if (d) { 220 const text = document.createElementNS(ns, 'text'); 221 text.setAttribute('x', 0); 222 text.setAttribute('y', headerOffset + i * (CELL + GAP) + CELL - 1); 223 text.setAttribute('class', 'heatmap-label'); 224 text.setAttribute('font-size', 9); 225 text.textContent = d; 226 svg.appendChild(text); 227 } 228 }); 229 230 contributions.forEach((day, idx) => { 231 // éªè¯æ¥ææ°æ® 232 if (!day || typeof day.date !== 'string') return; 233 234 const col = Math.floor(idx / ROWS); 235 const row = idx % ROWS; 236 const rect = document.createElementNS(ns, 'rect'); 237 238 rect.setAttribute('x', labelOffset + col * (CELL + GAP)); 239 rect.setAttribute('y', headerOffset + row * (CELL + GAP)); 240 rect.setAttribute('width', CELL); 241 rect.setAttribute('height', CELL); 242 rect.setAttribute('rx', 2); 243 rect.setAttribute('ry', 2); 244 rect.setAttribute('class', 'heatmap-cell'); 245 rect.setAttribute('data-date', day.date); 246 rect.setAttribute('data-count', day.count || 0); 247 248 // å®å ¨ç level å¼ 249 const level = typeof day.level === 'number' ? day.level : 0; 250 const opacity = LEVEL_OPACITY[Math.min(level, 4)]; 251 rect.setAttribute('fill', `rgba(35, 154, 59, ${opacity})`); 252 253 rect.addEventListener('mouseenter', (e) => { 254 const date = rect.getAttribute('data-date'); 255 const count = parseInt(rect.getAttribute('data-count') || '0', 10); 256 tooltip(date, count, e); 257 }); 258 259 rect.addEventListener('mousemove', (e) => { 260 const date = rect.getAttribute('data-date'); 261 const count = parseInt(rect.getAttribute('data-count') || '0', 10); 262 tooltip(date, count, e); 263 }); 264 265 rect.addEventListener('mouseleave', () => { 266 tooltipEl.classList.remove('visible'); 267 }); 268 269 rect.addEventListener('click', () => { 270 const date = day.date; 271 const url = `https://github.com/${username}?tab=overview&from=${date}&to=${date}`; 272 window.open(url, '_blank', 'noopener,noreferrer'); 273 }); 274 275 svg.appendChild(rect); 276 }); 277 278 if (!document.querySelector('style[data-heatmap-style]')) { 279 const style = document.createElement('style'); 280 style.setAttribute('data-heatmap-style', ''); 281 style.textContent = ` 282 .heatmap-label { 283 fill: rgba(0, 0, 0, 0.4); 284 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 285 font-size: 10px; 286 } 287 [data-md-color-scheme="slate"] .heatmap-label { 288 fill: rgba(255, 255, 255, 0.7); 289 } 290 .heatmap-cell { 291 transition: opacity 150ms, filter 150ms; 292 cursor: default; 293 filter: drop-shadow(0 0 0 rgba(35, 154, 59, 0)); 294 } 295 .heatmap-cell:hover { 296 opacity: 1 !important; 297 filter: drop-shadow(0 2px 6px rgba(35, 154, 59, 0.4)); 298 } 299 `; 300 document.head.appendChild(style); 301 } 302 303 function tooltip(date, count, event) { 304 const dateObj = new Date(date); 305 const dateText = !isNaN(dateObj.getTime()) 306 ? formatDate(date) 307 : date; 308 const text = count > 0 ? `${dateText}: ${count} 次贡ç®` : `${dateText}: æ è´¡ç®`; 309 const contentEl = tooltipEl.querySelector('.github-heatmap-tooltip__content'); 310 if (contentEl) { 311 contentEl.textContent = text; 312 } 313 tooltipEl.classList.add('visible'); 314 315 const target = event.currentTarget; 316 const margin = 8; 317 const gap = 8; 318 319 if (target && target.getBoundingClientRect) { 320 const cellRect = target.getBoundingClientRect(); 321 322 // å æ¾ç¤º tooltip æè½è·åæ£ç¡®å°ºå¯¸ 323 tooltipEl.style.visibility = 'hidden'; 324 tooltipEl.style.display = 'block'; 325 const tooltipRect = tooltipEl.getBoundingClientRect(); 326 tooltipEl.style.visibility = ''; 327 tooltipEl.style.display = ''; 328 329 let x = cellRect.left + cellRect.width / 2 - tooltipRect.width / 2; 330 let y = cellRect.top - tooltipRect.height - gap; 331 let arrowPosition = cellRect.left + cellRect.width / 2; 332 let isAbove = true; 333 334 const maxX = window.innerWidth - tooltipRect.width - margin; 335 x = Math.max(margin, Math.min(x, maxX)); 336 337 if (y < margin) { 338 y = cellRect.bottom + gap; 339 isAbove = false;
340 } 341 342 tooltipEl.style.left = x + 'px'; 343 tooltipEl.style.top = y + 'px'; 344 345 tooltipEl.classList.remove('arrow-top', 'arrow-bottom'); 346 tooltipEl.classList.add(isAbove ? 'arrow-bottom' : 'arrow-top'); 347 348 const arrowEl = tooltipEl.querySelector('.github-heatmap-tooltip__arrow'); 349 if (arrowEl) { 350 const arrowOffset = arrowPosition - x; 351 arrowEl.style.left = arrowOffset + 'px'; 352 } 353 } 354 } 355 } 356 357 async function initHeatmaps() { 358 const containers = document.querySelectorAll('.github-heatmap'); 359 if (!containers.length) return; 360 await Promise.all(Array.from(containers).map((c) => loadHeatmap(c))); 361 } 362 363 if (document.readyState === 'loading') { 364 document.addEventListener('DOMContentLoaded', initHeatmaps); 365 } else { 366 initHeatmaps(); 367 } 368})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.