1// ââ 3D isometric contribution graph ââââââââââââââââââââââââââââââââââââââââââ 2let _weeks = [], 3 _graphReady = false; 4 5function _colors() { 6 const dark = document.documentElement.getAttribute("data-theme") === "dark"; 7 return dark 8 ? ["#1e1c18", "#4a3520", "#8a6530", "#c4a060", "#f0d090"] 9 : ["#e8e3da", "#c4a070", "#a07840", "#7a5520", "#4a3010"]; 10} 11 12function _shade(hex, f) { 13 // hex is "#rrggbb"; each channel is read straight off the string. 14 const channel = (at) => 15 Math.min(255, Math.trunc(Number.parseInt(hex.slice(at, at + 2), 16) * f)); 16 return `rgb(${channel(1)},${channel(3)},${channel(5)})`; 17} 18 19function _renderCanvas(animate) { 20 if (!_weeks.length) { 21 return; 22 } 23 const pal = _colors(); 24 const CW = 9, 25 GAP = 2, 26 PX = CW + GAP, 27 PY = CW + GAP; 28 const MAX_H = 18, 29 MIN_H = 1, 30 DX = 3, 31 DY = 3; 32 const nC = _weeks.length, 33 nR = 7; 34 const maxN = Math.max( 35 1, 36 ..._weeks 37 .flat() 38 .filter(Boolean) 39 .map((d) => d.count) 40 ); 41 42 const OX = 4, 43 OY = MAX_H + DY + 4; 44 const W = OX + nC * PX + DX + 4; 45 const H = OY + nR * PY + 4; 46 47 const dpr = window.devicePixelRatio || 1; 48 contribCanvas.width = Math.ceil(W * dpr); 49 contribCanvas.height = Math.ceil(H * dpr); 50 contribCanvas.style.width = `${W}px`; 51 contribCanvas.style.height = `${H}px`; 52 53 // precompute target heights 54 const targets = []; 55 for (let row = 0; row < nR; row += 1) { 56 targets[row] = []; 57 for (let col = 0; col < nC; col += 1) { 58 const day = _weeks[col]?.[row]; 59 const count = day?.count ?? 0; 60 targets[row][col] = 61 count > 0 ? MIN_H + (count / maxN) * (MAX_H - MIN_H) : MIN_H; 62 } 63 } 64 65 function drawFrame(progress) { 66 const ctx = contribCanvas.getContext("2d"); 67 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 68 ctx.clearRect(0, 0, W, H); 69 70 const hits = []; 71 for (let row = 0; row < nR; row += 1) { 72 for (let col = 0; col < nC; col += 1) { 73 const day = _weeks[col]?.[row]; 74 const level = day?.level ?? 0; 75 const targetH = targets[row][col]; 76 77 // stagger: each column starts slightly later 78 const delay = (col / nC) * 0.5; 79 const local = Math.max(0, Math.min(1, (progress - delay) / (1 - 0.5))); 80 // ease out cubic 81 const ease = 1 - (1 - local) ** 3; 82 const h = MIN_H + (targetH - MIN_H) * ease; 83 84 const sx = OX + col * PX; 85 const by = OY + row * PY + CW; 86 const ty = by - h; 87 const c = pal[level]; 88 89 // front face 90 ctx.fillStyle = c; 91 ctx.fillRect(sx, ty, CW, h); 92 // top face 93 ctx.beginPath(); 94 ctx.moveTo(sx, ty); 95 ctx.lineTo(sx + CW, ty); 96 ctx.lineTo(sx + CW + DX, ty - DY); 97 ctx.lineTo(sx + DX, ty - DY); 98 ctx.closePath(); 99 ctx.fillStyle = _shade(c, 1.18); 100 ctx.fill(); 101 // right face 102 ctx.beginPath(); 103 ctx.moveTo(sx + CW, ty); 104 ctx.lineTo(sx + CW + DX, ty - DY); 105 ctx.lineTo(sx + CW + DX, by - DY); 106 ctx.lineTo(sx + CW, by); 107 ctx.closePath(); 108 ctx.fillStyle = _shade(c, 0.65); 109 ctx.fill(); 110 111 hits.push({ day, h, sx, ty }); 112 } 113 } 114 contribCanvas._hits = hits; 115 contribCanvas._CW = CW; 116 } 117 118 if (!animate) { 119 drawFrame(1); 120 return; 121 } 122 123 const duration = 800; 124 const start = performance.now(); 125 function tick(now) { 126 const progress = Math.min(1, (now - start) / duration); 127 drawFrame(progress); 128 if (progress < 1) { 129 requestAnimationFrame(tick); 130 } 131 } 132 requestAnimationFrame(tick); 133} 134 135contribCanvas.addEventListener("mousemove", (e) => { 136 const hits = contribCanvas._hits; 137 if (!hits) { 138 return; 139 } 140 const CW = contribCanvas._CW; 141 const rect = contribCanvas.getBoundingClientRect(); 142 const sc = 143 (Number.parseFloat(contribCanvas.style.width) || rect.width) / rect.width; 144 const mx = (e.clientX - rect.left) * sc; 145 const my = (e.clientY - rect.top) * sc; 146 147 let hit = null; 148 for (let i = hits.length - 1; i >= 0; i -= 1) { 149 const { sx, ty, h } = hits[i]; 150 if (mx >= sx && mx < sx + CW && my >= ty && my < ty + h) { 151 hit = hits[i]; 152 break; 153 } 154 } 155 156 if (hit?.day) { 157 const { count, date } = hit.day; 158 const label = new Date(`${date}T12:00:00`).toLocaleDateString("en-US", { 159 day: "numeric", 160 month: "short", 161 weekday: "short", 162 }); 163 contribTip.textContent = `${count || "no"} contribution${count === 1 ? "" : "s"} · ${label}`; 164 contribTip.style.display = "block"; 165 contribTip.style.left = `${e.clientX + 14}px`; 166 contribTip.style.top = `${e.clientY - 38}px`; 167 } else { 168 contribTip.style.display = "none"; 169 } 170}); 171contribCanvas.addEventListener("mouseleave", () => { 172 contribTip.style.display = "none"; 173}); 174 175async function _loadContribs() { 176 const res = await window.fetch( 177 "https://github-contributions-api.jogruber.de/v4/aymuos15?y=last" 178 ); 179 const days = (await res.json()).contributions || []; 180 const weeks = []; 181 let week = new Array(7).fill(null); 182 for (const day of days) { 183 const dow = new Date(`${day.date}T12:00:00`).getDay(); 184 if (dow === 0 && week.some(Boolean)) { 185 weeks.push([...week]); 186 week = new Array(7).fill(null); 187 } 188 week[dow] = day; 189 } 190 if (week.some(Boolean)) { 191 weeks.push(week); 192 } 193 return weeks; 194} 195 196// Prefetch contrib data at load time so it's ready when the PRs tab is opened 197_loadContribs() 198 .then((w) => { 199 _weeks = w; 200 _graphReady = true; 201 }) 202 .catch((err) => console.warn("Contrib graph prefetch:", err)); 203 204function showContribGraph() { 205 contribGraph.classList.add("visible"); 206 if (_graphReady) { 207 _renderCanvas(true); 208 return; 209 } 210 _loadContribs() 211 .then((w) => { 212 _weeks = w; 213 _graphReady = true; 214 _renderCanvas(true); 215 }) 216 .catch((err) => console.warn("Contrib graph:", err)); 217} 218function hideContribGraph() { 219 contribGraph.classList.remove("visible"); 220 contribTip.style.display = "none"; 221}
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.