Created
April 26, 2026 12:04
-
-
Save duckida/2f876b475934a99a34912fa9701eb435 to your computer and use it in GitHub Desktop.
S&P 500 Stock Heatmap (programmed by duckida/nanoloop)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Stock Heatmap</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background-color: #1a1a1a; | |
| color: #fff; | |
| padding: 20px; | |
| min-height: 100vh; | |
| transition: background-color 0.3s, color 0.3s; | |
| } | |
| body.light-theme { | |
| background-color: #f5f5f5; | |
| color: #1a1a1a; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| font-weight: 300; | |
| margin-bottom: 10px; | |
| color: #fff; | |
| } | |
| body.light-theme h1 { | |
| color: #1a1a1a; | |
| } | |
| .subtitle { | |
| color: #aaa; | |
| font-size: 1rem; | |
| margin-bottom: 20px; | |
| } | |
| body.light-theme .subtitle { | |
| color: #666; | |
| } | |
| .theme-toggle { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border: none; | |
| padding: 10px 15px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 1.2rem; | |
| transition: background-color 0.3s; | |
| } | |
| body.light-theme .theme-toggle { | |
| background: rgba(0, 0, 0, 0.1); | |
| } | |
| .theme-toggle:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| body.light-theme .theme-toggle:hover { | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| .status-bar { | |
| display: flex; | |
| justify-content: center; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .status-item { | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| transition: transform 0.15s ease, background-color 0.3s; | |
| } | |
| body.light-theme .status-item { | |
| background: rgba(0, 0, 0, 0.1); | |
| } | |
| .status-item.up { | |
| color: #4caf50; | |
| } | |
| .status-item.down { | |
| color: #f44336; | |
| } | |
| .status-item.total { | |
| color: #2196f3; | |
| } | |
| .grid-container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| overflow-x: auto; | |
| } | |
| #grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); | |
| gap: 4px; | |
| } | |
| .stock-cell { | |
| aspect-ratio: 1; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| border-radius: 4px; | |
| font-size: 0.7rem; | |
| font-weight: 500; | |
| transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s, opacity 0.3s; | |
| cursor: pointer; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stock-cell:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| z-index: 10; | |
| } | |
| .stock-cell.up { | |
| background-color: #2e7d32; | |
| color: #fff; | |
| } | |
| .stock-cell.down { | |
| background-color: #c62828; | |
| color: #fff; | |
| } | |
| .stock-cell.flat { | |
| background-color: #555; | |
| color: #fff; | |
| } | |
| body.light-theme .stock-cell.up { | |
| background-color: #4caf50; | |
| } | |
| body.light-theme .stock-cell.down { | |
| background-color: #f44336; | |
| } | |
| body.light-theme .stock-cell.flat { | |
| background-color: #9e9e9e; | |
| } | |
| .ticker { | |
| font-weight: 700; | |
| font-size: 0.8rem; | |
| margin-bottom: 2px; | |
| } | |
| .change { | |
| font-size: 0.7rem; | |
| opacity: 0.9; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| font-size: 1.2rem; | |
| color: #aaa; | |
| } | |
| body.light-theme .loading { | |
| color: #666; | |
| } | |
| .error { | |
| text-align: center; | |
| padding: 20px; | |
| background: rgba(244, 67, 54, 0.2); | |
| border-radius: 8px; | |
| margin: 20px auto; | |
| max-width: 600px; | |
| } | |
| body.light-theme .error { | |
| background: rgba(244, 67, 54, 0.1); | |
| color: #c62828; | |
| } | |
| .last-updated { | |
| text-align: center; | |
| margin-top: 30px; | |
| color: #777; | |
| font-size: 0.9rem; | |
| } | |
| body.light-theme .last-updated { | |
| color: #999; | |
| } | |
| /* Modal styles */ | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.7); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| body.light-theme .modal-overlay { | |
| background: rgba(0, 0, 0, 0.5); | |
| } | |
| .modal-overlay.active { | |
| display: flex; | |
| } | |
| .modal { | |
| background: #2a2a2a; | |
| border-radius: 12px; | |
| padding: 30px; | |
| min-width: 300px; | |
| max-width: 400px; | |
| text-align: center; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); | |
| position: relative; | |
| } | |
| body.light-theme .modal { | |
| background: #fff; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); | |
| } | |
| .modal-close { | |
| position: absolute; | |
| top: 10px; | |
| right: 15px; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| color: #aaa; | |
| background: none; | |
| border: none; | |
| } | |
| .modal-close:hover { | |
| color: #fff; | |
| } | |
| body.light-theme .modal-close:hover { | |
| color: #1a1a1a; | |
| } | |
| .modal-ticker { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| } | |
| .modal-company { | |
| font-size: 1rem; | |
| color: #aaa; | |
| margin-bottom: 15px; | |
| } | |
| body.light-theme .modal-company { | |
| color: #666; | |
| } | |
| .modal-price { | |
| font-size: 2.5rem; | |
| font-weight: 300; | |
| margin-bottom: 15px; | |
| } | |
| .modal-change { | |
| font-size: 1.2rem; | |
| margin-bottom: 20px; | |
| } | |
| .modal-change.up { | |
| color: #4caf50; | |
| } | |
| .modal-change.down { | |
| color: #f44336; | |
| } | |
| .modal-details { | |
| font-size: 0.9rem; | |
| color: #aaa; | |
| border-top: 1px solid #444; | |
| padding-top: 15px; | |
| margin-top: 15px; | |
| } | |
| body.light-theme .modal-details { | |
| color: #666; | |
| border-top-color: #ddd; | |
| } | |
| @media (max-width: 768px) { | |
| body { | |
| padding: 10px; | |
| } | |
| h1 { | |
| font-size: 1.8rem; | |
| } | |
| #grid { | |
| grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); | |
| gap: 3px; | |
| } | |
| .stock-cell { | |
| font-size: 0.6rem; | |
| } | |
| .ticker { | |
| font-size: 0.7rem; | |
| } | |
| .change { | |
| font-size: 0.6rem; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| #grid { | |
| grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); | |
| gap: 2px; | |
| } | |
| .stock-cell { | |
| font-size: 0.55rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <button class="theme-toggle" id="theme-toggle">🌙</button> | |
| <header> | |
| <h1>Stock Market Heatmap</h1> | |
| <div class="subtitle">Real-time performance of S&P 500 stocks</div> | |
| <div class="status-bar"> | |
| <div class="status-item up" id="up-count">Up: 0</div> | |
| <div class="status-item down" id="down-count">Down: 0</div> | |
| <div class="status-item total" id="total-count">Total: 0</div> | |
| </div> | |
| </header> | |
| <!-- Modal for stock details --> | |
| <div class="modal-overlay" id="modal-overlay"> | |
| <div class="modal"> | |
| <button class="modal-close" id="modal-close">×</button> | |
| <div class="modal-ticker" id="modal-ticker">AAPL</div> | |
| <div class="modal-company" id="modal-company">Apple Inc.</div> | |
| <div class="modal-price" id="modal-price">$0.00</div> | |
| <div class="modal-change" id="modal-change">+0.00%</div> | |
| <div class="modal-details"> | |
| <div>Previous Close: <span id="modal-prev-close">$0.00</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid-container"> | |
| <div id="grid"></div> | |
| <div class="loading" id="loading">Loading stock data...</div> | |
| <div class="error" id="error" style="display: none;"></div> | |
| </div> | |
| <div class="last-updated" id="last-updated"></div> | |
| <script> | |
| // Theme toggle | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const body = document.body; | |
| // Load saved theme preference | |
| const savedTheme = localStorage.getItem('theme'); | |
| if (savedTheme === 'light') { | |
| body.classList.add('light-theme'); | |
| themeToggle.textContent = '☀️'; | |
| } | |
| themeToggle.addEventListener('click', () => { | |
| body.classList.toggle('light-theme'); | |
| const isLight = body.classList.contains('light-theme'); | |
| themeToggle.textContent = isLight ? '☀️' : '🌙'; | |
| localStorage.setItem('theme', isLight ? 'light' : 'dark'); | |
| }); | |
| // Modal functionality | |
| const modalOverlay = document.getElementById('modal-overlay'); | |
| const modalClose = document.getElementById('modal-close'); | |
| const modalTicker = document.getElementById('modal-ticker'); | |
| const modalCompany = document.getElementById('modal-company'); | |
| const modalPrice = document.getElementById('modal-price'); | |
| const modalChange = document.getElementById('modal-change'); | |
| const modalPrevClose = document.getElementById('modal-prev-close'); | |
| // Store stock data for modal | |
| let stockData = {}; | |
| function showModal(ticker) { | |
| const stock = stockData[ticker]; | |
| if (!stock) return; | |
| const pct = ((stock.current / stock.prevClose - 1) * 100).toFixed(2); | |
| const sign = pct >= 0 ? '+' : ''; | |
| const changeClass = stock.current > stock.prevClose ? 'up' : | |
| stock.current < stock.prevClose ? 'down' : ''; | |
| modalTicker.textContent = stock.ticker; | |
| modalPrice.textContent = `$${stock.current.toFixed(2)}`; | |
| modalCompany.textContent = stock.name || stock.ticker; | |
| modalChange.textContent = `${sign}${pct}%`; | |
| modalChange.className = `modal-change ${changeClass}`; | |
| modalPrevClose.textContent = `$${stock.prevClose.toFixed(2)}`; | |
| modalOverlay.classList.add('active'); | |
| } | |
| function hideModal() { | |
| modalOverlay.classList.remove('active'); | |
| } | |
| modalClose.addEventListener('click', hideModal); | |
| modalOverlay.addEventListener('click', (e) => { | |
| if (e.target === modalOverlay) { | |
| hideModal(); | |
| } | |
| }); | |
| // Close modal on Escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| hideModal(); | |
| } | |
| }); | |
| // Tickers list (deduplicated) | |
| const tickers = [ | |
| "AAPL","MSFT","NVDA","AMZN","GOOGL","GOOG","META","BRK-B","TSLA","AVGO", | |
| "LLY","WMT","JPM","V","XOM","UNH","MA","ORCL","COST","HD", | |
| "PG","NFLX","JNJ","ABBV","CRM","BAC","KO","CVX","MRK","ADBE", | |
| "AMD","PEP","TMO","LIN","WFC","CSCO","ACN","MCD","ABT","DIS", | |
| "GE","PM","INTU","TXN","DHR","CAT","AXP","VZ","AMAT","PLTR", | |
| "QCOM","PFE","UBER","IBM","UNP","AMGN","ISRG","NOW","LOW","SPGI", | |
| "RTX","HON","COP","NEE","GS","DELL","BKNG","T","ELV","SYK", | |
| "C","TJX","PGR","LRCX","VRTX","MS","LMT","ETN","BLK","BSX", | |
| "BA","REGN","CI","ADP","MMC","CB","PLD","PANW","MDLZ","ADI", | |
| "AMT","SBUX","GILD","MU","DE","ZTS","CIEN","SYY","MELI", | |
| "MO","AEE","AEP","AIG","AMP","AME","APH","AON","APA","APO", | |
| "MMM","AOS","AES","AFL","A","APD","ABNB","AKAM","ALB","ARE", | |
| "ALGN","ALLE","LNT","ALL","AMCR","AMG","ANSS","AVY","BALL","BBY", | |
| "BDX","BEN","BF-B","BG","BIIB","BIO","BK","BKR","BLDR","BMY", | |
| "BR","BRO","BWA","BX","BXP","CAG","CAH","CARR","CBRE","CCI", | |
| "CCL","CDNS","CDW","CE","CEG","CF","CFG","CHD","CHRW","CHTR", | |
| "CINF","CL","CLX","CMA","CMCSA","CME","CMG","CMI","CMS","CNC", | |
| "CNP","COF","CPAY","CPB","CPRT","CPT","CRL","CTAS","CTRA","CTSH", | |
| "CTVA","CVS","D","DAL","DAY","DD","DECK","DFS","DG","DGX", | |
| "DHI","DISH","DLTR","DOC","DOV","DOW","DPZ","DRI","DTE","DUK", | |
| "DVA","DVN","DXCM","EA","EBAY","ECL","ED","EFX","EG","EIX", | |
| "EL","EMN","EMR","ENPH","EOG","EPAM","EQIX","EQR","EQT","ERIE", | |
| "ES","ESS","ETR","ETSY","EVRG","EW","EXC","EXPD","EXPE","EXR", | |
| "F","FANG","FAST","FCX","FDS","FDX","FE","FFIV","FI","FICO", | |
| "FIS","FITB","FMC","FOXA","FOX","FRT","FSLR","FTNT","FTV","GD", | |
| "GDDY","GEHC","GEN","GEV","GH","GL","GLW","GM","GNRC","GPC", | |
| "GPK","GRMN","GWW","HAL","HAS","HBAN","HCA","HES","HIG","HII", | |
| "HLT","HOLX","HRL","HSIC","HST","HSY","HUM","HWM","ICE","IDXX", | |
| "IEX","IFF","ILMN","INCY","INTC","IP","IPG","IQV","IR","IRM", | |
| "IT","ITW","IVZ","J","JBHT","JBL","JKHY","K","KDP","KEY", | |
| "KEYS","KHC","KIM","KLAC","KMB","KMI","KMX","KR","L","LDOS", | |
| "LEN","LH","LHX","LKQ","LULU","LUV","LVS","LW","LYB","LYV", | |
| "M","MAA","MAR","MAS","PAYC","PAYX","PCAR","PCG","PEAK","PEG", | |
| "PENN","PFG","PH","PHM","PKG","PKI","PNR","PNW","POOL","PPG", | |
| "PPL","PRU","PSA","PSX","PTC","PWR","PYPL","QRVO","RCL","RE", | |
| "REG","RF","RHI","RJF","RL","RMD","ROK","ROL","ROP","ROST", | |
| "RSG","RVTY","SBAC","SCHW","SHW","SJM","SLB","SNA","SNPS","SO", | |
| "SPG","SRE","STE","STT","STX","STZ","SWK","SWKS","SYF","TAP", | |
| "TDG","TDY","TECH","TEL","TER","TFC","TFX","TGT","TMUS","TPR", | |
| "TRGP","TRMB","TROW","TRV","TSCO","TSN","TT","TTWO","TXT","TYL", | |
| "UAL","UDR","UHS","ULTA","UNM","UPS","URI","USB","VFC","VICI", | |
| "VLO","VMC","VNO","VRSK","VRSN","VTR","VTRS","WAB","WAT","WBA", | |
| "WBD","WDC","WEC","WELL","WES","WFRD","WHR","WM","WMB","WST", | |
| "WTW","WY","WYNN","XEL","XLY","XRAY","XYL","YUM","ZBH","ZBRA" | |
| ]; | |
| // Deduplicate | |
| const uniqueTickers = [...new Set(tickers)]; | |
| const grid = document.getElementById('grid'); | |
| const loading = document.getElementById('loading'); | |
| const error = document.getElementById('error'); | |
| const upCountEl = document.getElementById('up-count'); | |
| const downCountEl = document.getElementById('down-count'); | |
| const totalCountEl = document.getElementById('total-count'); | |
| const lastUpdatedEl = document.getElementById('last-updated'); | |
| async function fetchData(isInitialLoad = false) { | |
| try { | |
| if (isInitialLoad) { | |
| loading.style.display = 'block'; | |
| } | |
| error.style.display = 'none'; | |
| // Don't clear the grid - we'll update it smoothly | |
| // Split tickers into chunks to avoid URL length limits | |
| const chunkSize = 50; | |
| let allResults = []; | |
| for (let i = 0; i < uniqueTickers.length; i += chunkSize) { | |
| const chunk = uniqueTickers.slice(i, i + chunkSize); | |
| const url = `https://stock-prices.on99.app/quotes?symbols=${chunk.join(',')}`; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const text = await response.text(); | |
| const lines = text.split('\n'); | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| // Skip header line | |
| if (line.startsWith('quotes[')) continue; | |
| const parts = parseCSVLine(line); | |
| if (parts.length < 11) continue; | |
| try { | |
| const ticker = parts[0]; | |
| const name = parts[1]; | |
| const current = parseFloat(parts[3]); | |
| const prevClose = parseFloat(parts[10]); | |
| if (!isNaN(current) && !isNaN(prevClose) && prevClose > 0) { | |
| allResults.push({ ticker, name, current, prevClose }); | |
| } | |
| } catch (e) {} | |
| } | |
| } catch (e) { | |
| console.error('Fetch error for chunk:', e); | |
| } | |
| } | |
| // Sort into up and down | |
| const up = []; | |
| const down = []; | |
| for (const stock of allResults) { | |
| if (stock.current > stock.prevClose) { | |
| up.push(stock); | |
| } else if (stock.current < stock.prevClose) { | |
| down.push(stock); | |
| } | |
| } | |
| // Sort by percentage change | |
| const pctSort = (a, b) => (b.current / b.prevClose) - (a.current / a.prevClose); | |
| up.sort(pctSort); | |
| down.sort(pctSort); | |
| // Update counts with animation | |
| if (upCountEl.textContent !== `Up: ${up.length}`) { | |
| upCountEl.style.transform = 'scale(1.1)'; | |
| upCountEl.textContent = `Up: ${up.length}`; | |
| setTimeout(() => { upCountEl.style.transform = 'scale(1)'; }, 150); | |
| } | |
| if (downCountEl.textContent !== `Down: ${down.length}`) { | |
| downCountEl.style.transform = 'scale(1.1)'; | |
| downCountEl.textContent = `Down: ${down.length}`; | |
| setTimeout(() => { downCountEl.style.transform = 'scale(1)'; }, 150); | |
| } | |
| if (totalCountEl.textContent !== `Total: ${up.length + down.length}`) { | |
| totalCountEl.style.transform = 'scale(1.1)'; | |
| totalCountEl.textContent = `Total: ${up.length + down.length}`; | |
| setTimeout(() => { totalCountEl.style.transform = 'scale(1)'; }, 150); | |
| } | |
| // Store stock data for modal | |
| stockData = {}; | |
| for (const stock of [...up, ...down]) { | |
| stockData[stock.ticker] = stock; | |
| } | |
| // Update grid smoothly | |
| updateGridSmooth([...up, ...down]); | |
| loading.style.display = 'none'; | |
| // Update timestamp | |
| const now = new Date(); | |
| lastUpdatedEl.textContent = `Last updated: ${now.toLocaleTimeString()}`; | |
| } catch (e) { | |
| loading.style.display = 'none'; | |
| error.style.display = 'block'; | |
| let errorMsg = `Error fetching data: ${e.message}. Retrying in 10 seconds...`; | |
| error.textContent = errorMsg; | |
| console.error('Fetch error:', e); | |
| } | |
| } | |
| function updateGridSmooth(newStocks) { | |
| // Get existing cells | |
| const existingCells = {}; | |
| document.querySelectorAll('.stock-cell').forEach(cell => { | |
| existingCells[cell.getAttribute('data-ticker')] = cell; | |
| }); | |
| // Create map of new stocks | |
| const newStockMap = {}; | |
| for (const stock of newStocks) { | |
| newStockMap[stock.ticker] = stock; | |
| } | |
| // Update existing cells | |
| for (const ticker in existingCells) { | |
| if (newStockMap[ticker]) { | |
| const stock = newStockMap[ticker]; | |
| const pct = ((stock.current / stock.prevClose - 1) * 100).toFixed(2); | |
| const sign = pct >= 0 ? '+' : ''; | |
| const newCls = stock.current > stock.prevClose ? 'up' : | |
| stock.current < stock.prevClose ? 'down' : 'flat'; | |
| const cell = existingCells[ticker]; | |
| const changeEl = cell.querySelector('.change'); | |
| const oldPct = changeEl.textContent; | |
| const newPct = `${sign}${pct}%`; | |
| // Update class if changed | |
| cell.classList.remove('up', 'down', 'flat'); | |
| cell.classList.add(newCls); | |
| // Animate if value changed | |
| if (oldPct !== newPct) { | |
| changeEl.style.transform = 'scale(1.2)'; | |
| changeEl.style.transition = 'transform 0.2s ease'; | |
| changeEl.textContent = newPct; | |
| setTimeout(() => { | |
| changeEl.style.transform = 'scale(1)'; | |
| }, 200); | |
| } | |
| // Update title | |
| cell.title = `${stock.ticker}: ${sign}${pct}%`; | |
| } else { | |
| // Remove cell with fade out | |
| existingCells[ticker].style.opacity = '0'; | |
| existingCells[ticker].style.transform = 'scale(0.8)'; | |
| setTimeout(() => { | |
| existingCells[ticker].remove(); | |
| }, 300); | |
| } | |
| } | |
| // Add new cells | |
| const fragment = document.createDocumentFragment(); | |
| for (const stock of newStocks) { | |
| if (!existingCells[stock.ticker]) { | |
| const pct = ((stock.current / stock.prevClose - 1) * 100).toFixed(2); | |
| const sign = pct >= 0 ? '+' : ''; | |
| const cls = stock.current > stock.prevClose ? 'up' : | |
| stock.current < stock.prevClose ? 'down' : 'flat'; | |
| const cell = document.createElement('div'); | |
| cell.className = `stock-cell ${cls}`; | |
| cell.setAttribute('data-ticker', stock.ticker); | |
| cell.title = `${stock.ticker}: ${sign}${pct}%`; | |
| cell.innerHTML = ` | |
| <span class="ticker">${stock.ticker}</span> | |
| <span class="change">${sign}${pct}%</span> | |
| `; | |
| // Add fade in animation | |
| cell.style.opacity = '0'; | |
| cell.style.transform = 'scale(0.8)'; | |
| cell.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; | |
| // Add click handler | |
| cell.addEventListener('click', () => { | |
| showModal(stock.ticker); | |
| }); | |
| fragment.appendChild(cell); | |
| } | |
| } | |
| // Append new cells | |
| if (fragment.children.length > 0) { | |
| grid.appendChild(fragment); | |
| // Trigger animation | |
| requestAnimationFrame(() => { | |
| grid.querySelectorAll('.stock-cell[style*="opacity: 0"]').forEach(cell => { | |
| cell.style.opacity = '1'; | |
| cell.style.transform = 'scale(1)'; | |
| }); | |
| }); | |
| } | |
| // Reorder cells to match the sorted order | |
| const orderedStocks = newStocks.map(s => s.ticker); | |
| const cellMap = {}; | |
| grid.querySelectorAll('.stock-cell').forEach(cell => { | |
| cellMap[cell.getAttribute('data-ticker')] = cell; | |
| }); | |
| orderedStocks.forEach((ticker, index) => { | |
| const cell = cellMap[ticker]; | |
| if (cell) { | |
| grid.appendChild(cell); | |
| } | |
| }); | |
| } | |
| function parseCSVLine(line) { | |
| const result = []; | |
| let current = ''; | |
| let inQuotes = false; | |
| for (let i = 0; i < line.length; i++) { | |
| const char = line[i]; | |
| if (char === '"') { | |
| inQuotes = !inQuotes; | |
| } else if (char === ',' && !inQuotes) { | |
| result.push(current.trim()); | |
| current = ''; | |
| } else { | |
| current += char; | |
| } | |
| } | |
| result.push(current.trim()); | |
| return result; | |
| } | |
| // Fetch on load and refresh every 10 seconds | |
| fetchData(true); | |
| setInterval(() => fetchData(false), 10000); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment