<!DOCTYPE html><html> <head> <meta charset="utf-8" /> <title>Statistiche Live per Minuto</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet"> <style> body { font-family: 'Montserrat', sans-serif; margin: 0; } .match-header { display: flex; justify-content: space-between; align-items: center; flex-direction: row-reverse; font-size: 10px; background-color: #2E5339; padding: 10px; border-top-right-radius: 10px; border-top-left-radius: 10px; } .teams { font-weight: bold; font-size: 1.1em; } .score { font-size: 1.2em; margin: 0 10px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 1px; text-align: center; font-size: 8px; color: black; } th { background-color: #E8E4D8; } th p { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; padding: 0; margin: 0; font-size: 11px; } .team-logo { width: 20px; vertical-align: middle; margin-right: 4px; } .time-col { font-weight: bold; background-color: #f0f0f0; min-width: 35px; } .filter-container { border-bottom: 1px solid #ccc; padding: 10px; /* border-radius: 8px; */ background: #fafafa; } .filter-container h3 { margin: 0 0 10px 0; } .checkbox-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 5px; } label { font-size: 13px; cursor: pointer; } input[type="checkbox"] { vertical-align: middle; margin-right: 4px; cursor: pointer; } .controls { margin-top: 8px; text-align: center; } .controls button { margin-right: 6px; } #output { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; text-align: -webkit-center; padding: 20px 0; background: #A5BE00; } .match-card { color: white; width: 95%; border-radius: 10px; filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5)); } .table-wrapper { border-right: 1px solid #2E5339; border-left: 1px solid #2E5339; } /* ๐ต Colori per squadre */ .home-row { background-color: #f5faff; /* Azzurrino chiaro */ } .away-row { background-color: #fff7f5; /* Rosato chiaro */ } .separator { background-color: #2E5339; border: 0; } .analisi { background-color: #2E5339; padding: 10px; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; font-size: 0.8em; } @media (max-width: 1200px) { #output { grid-template-columns: repeat(2, 1fr); /* 2 schede per riga */ } } @media (max-width: 768px) { #output { grid-template-columns: 1fr; /* 1 scheda per riga su mobile */ } } </style></head> <body> <div class="filter-container"> <!-- <h3>Seleziona statistiche da visualizzare:</h3> --> <div id="checkboxContainer" class="checkbox-grid"></div> <div class="controls"> <!-- <button id="selectAll">Seleziona tutto</button> <button id="deselectAll">Deseleziona tutto</button> --> <label style="margin-left:12px;"> Aggiorna ogni <input id="refreshInterval" type="number" value="60" min="5" style="width:70px; margin-left:6px; margin-right:6px;"> s </label> <button id="applyInterval">Applica</button> </div> </div> <div id="output"></div> <script> const statMap = { ball_possession: "Ball Possession", blocked_shots: "Blocked Shots", corner_kicks: "Corner Kicks", expected_goals: "Expected Goals", fouls: "Fouls", goalkeeper_saves: "Goalkeeper Saves", goals_prevented: "Goals Prevented", offsides: "Offsides", "passes_%": "Passes %", passes_accurate: "Passes Accurate", red_cards: "Red Cards", shots_insidebox: "Shots Inside Box", shots_off_goal: "Shots Off Goal", shots_on_goal: "Shots On Goal", shots_outsidebox: "Shots Outside Box", total_passes: "Total Passes", total_shots: "Total Shots", yellow_cards: "Yellow Cards", gol: "Goals", }; const defaultStats = [ "gol", "ball_possession", "shots_insidebox", "shots_on_goal", "total_shots", "shots_off_goal", "shots_outsidebox", "goalkeeper_saves", "red_cards" ]; const columnOrder = [ "gol", "total_shots", "shots_on_goal", "shots_off_goal", "shots_insidebox", "shots_outsidebox", "ball_possession", "goalkeeper_saves", "red_cards" ]; let REFRESH_INTERVAL = 60000; let refreshTimer; // โ
Parsing sicuro dei numeri e percentuali const parseNum = v => { if (v === undefined || v === null) return 0; const clean = String(v).replace("%", "").trim(); const num = parseFloat(clean); return isNaN(num) ? 0 : num; }; // โ
Calcolo indice pressione aggiornato function calcolaIndicePressione(rows, side = "home") { if (!rows || rows.length < 2) return 0; const latest = rows.at(-1); const old = rows[rows.length - 10]; const dShots = parseNum(latest[`${side}_total_shots`]) - parseNum(old[`${side}_total_shots`]); const dOnGoal = parseNum(latest[`${side}_shots_on_goal`]) - parseNum(old[`${side}_shots_on_goal`]); const dInside = parseNum(latest[`${side}_shots_insidebox`]) - parseNum(old[`${side}_shots_insidebox`]); const dPoss = parseNum(latest[`${side}_ball_possession`]) - parseNum(old[`${side}_ball_possession`]); //console.log("team:", latest[`team_${side}_name`]) //console.log("dShots:",dShots) //console.log("dOnGoal:",dOnGoal) //console.log("dInside:",dInside) //console.log("dPoss:",dPoss) const indice = Math.max(0, dShots * 2 + dOnGoal * 3 + dInside * 2 + dPoss * 0.4); return Number(indice.toFixed(2)); } // โ
Analisi testuale migliorata function generaAnalisi(rows) { const ph = calcolaIndicePressione(rows, "home"); console.log("ph", ph) const pa = calcolaIndicePressione(rows, "away"); console.log("pa", pa) const soglia = 8; let testo = ""; if (ph >= soglia && pa >= soglia) { if (Math.abs(ph - pa) < 2) testo = "โ๏ธ Entrambe le squadre in pressione simile"; else if (ph > pa) testo = "๐ Pressione leggermente maggiore squadra di casa"; else testo = "โ ๏ธ Pressione leggermente maggiore squadra ospite"; } else if (ph >= soglia) { testo = "๐ฅ Pressione alta squadra di casa โ possibile gol HOME"; } else if (pa >= soglia) { testo = "๐จ Pressione alta squadra ospite โ possibile gol AWAY"; } else { testo = "Partita stabile, nessuna pressione marcata."; } return `<div class="analisi">${testo}</div>`; } // Impostazioni di aggiornamento const restartAutoRefresh = () => { clearInterval(refreshTimer); refreshTimer = setInterval(caricaCSV, REFRESH_INTERVAL); }; function creaCheckbox() { document.getElementById("applyInterval").onclick = () => { REFRESH_INTERVAL = Math.max(5000, +document.getElementById("refreshInterval").value * 1000 || 60000); restartAutoRefresh(); }; } async function caricaCSV() { const scrollY = window.scrollY; try { const text = await (await fetch("livestats/partite_live_stats.csv?_=" + Date.now(), { cache: "no-store" })).text(); if (text.startsWith("<!DOCTYPE html>")) return console.warn("โ ๏ธ HTML ricevuto invece del CSV"); const [header, ...rows] = text.trim().split("\n"); const headers = header.split(","); const data = rows.map(r => Object.fromEntries(headers.map((h, i) => [h.trim(), (r.split(",")[i] || "").trim()]))); const matches = data.reduce((acc, r) => { (acc[r.partita_id] ??= []).push(r); return acc; }, {}); const container = document.getElementById("output"); container.innerHTML = ""; // Ordina partite per pressione totale const matchList = Object.values(matches).map(rows => { const ph = calcolaIndicePressione(rows, "home"); const pa = calcolaIndicePressione(rows, "away"); const pressioneTotale = ph + pa; return { rows, pressioneTotale }; }).sort((a, b) => b.pressioneTotale - a.pressioneTotale); matchList.forEach(({ rows }) => { const tabellaDiv = document.createElement("div"); tabellaDiv.innerHTML = creaTabella(rows, Object.keys(rows[0])); container.appendChild(tabellaDiv); }); requestAnimationFrame(() => window.scrollTo(0, scrollY)); } catch (e) { console.error("Errore fetch CSV:", e); } } function creaTabella(rows, headers) { const last = rows.at(-1); const homeStats = Array.from(new Set(columnOrder .map(stat => stat === "gol_home" ? "gol_home" : "home_" + stat.replace(/^(home_|away_)/, "")) .filter(h => headers.includes(h)) )); const awayStats = Array.from(new Set(columnOrder .map(stat => stat === "gol_away" ? "gol_away" : "away_" + stat.replace(/^(home_|away_)/, "")) .filter(h => headers.includes(h)) )); function parseNum(v) { const n = parseInt(v, 10); return isNaN(n) ? 0 : n; } // Mantiene solo l'ultima riga per ogni combinazione tempo + extra-time const uniqueByTime = Object.values( rows.reduce((acc, r) => { const mt = parseNum(r.match_time); const et = parseNum(r.match_extra_time); const key = `${mt}-${et}`; acc[key] = r; return acc; }, {}) ); // Ordina in ordine decrescente per tempo const sorted = uniqueByTime.sort((a, b) => { const mtA = parseNum(a.match_time); const mtB = parseNum(b.match_time); const etA = parseNum(a.match_extra_time); const etB = parseNum(b.match_extra_time); if (mtA === mtB) return etB - etA; return mtB - mtA; }); // Colonne = minuti const timeCols = sorted.map(r => { const mt = parseNum(r.match_time); const et = parseNum(r.match_extra_time); return r.match_status === "FT" ? "FT" : mt ? `${mt}${et ? "+" + et : ""}` : ""; }); const visibleCols = timeCols.slice(0, 10); const visibleSorted = sorted.slice(0, 10); // Righe per squadra (senza nome squadra) // ๐ Invertito: righe = minuti, colonne = statistiche function mkTimeRows(side, stats, colorClass) { return visibleSorted.map((r, idx) => { const mt = parseNum(r.match_time); const et = parseNum(r.match_extra_time); const label = r.match_status === "FT" ? "FT" : mt ? `${mt}${et ? "+" + et : ""}` : ""; let rowHtml = `<tr class="${colorClass}"><td class="time">${label}</td>`; for (let s of stats) { let rawValue = r[s]; if (rawValue === undefined || rawValue === "") { rowHtml += "<td></td>"; continue; } const isPercent = s.includes("ball_possession") || String(rawValue).includes("%"); const current = parseFloat(String(rawValue).replace("%", "")) || 0; const prev = parseFloat(String(visibleSorted[idx + 1]?.[s] || "").replace("%", "")) || current; const diff = current - prev; const diffText = diff !== 0 ? `<span style="color:${diff > 0 ? 'green' : 'red'}; font-size:9px;"> (${diff > 0 ? '+' : ''}${diff.toFixed(isPercent ? 1 : 0)})</span>` : ""; let bg = ""; if (diff > 0) bg = "background-color: rgba(0, 200, 0, 0.15);"; else if (diff < 0) bg = "background-color: rgba(255, 0, 0, 0.1);"; const displayValue = isPercent ? current.toFixed(1) + "%" : current; rowHtml += `<td style="${bg}">${displayValue}${diffText}</td>`; } rowHtml += "</tr>"; return rowHtml; }).join(""); } const id = `tbl-${Math.random().toString(36).slice(2, 9)}`; return `<div class="match-card rotated"> <div class="match-header"> <div class="league">${last.league_name} | ${last.league_country}</div> <div class="teams"> <div class="result"> <img src="${last.team_home_logo}" class="team-logo"> ${last.team_home_name} ${last.home_gol} - ${last.away_gol} ${last.team_away_name} <img src="${last.team_away_logo}" class="team-logo"> </div> </div> </div> <div class="table-wrapper"> <table id="${id}" class="rotated-table"> <tr> <th></th> ${homeStats.map(s => `<th>${statMap[s.replace("home_", "")] || s}</th>`).join("")} </tr> ${mkTimeRows("home", homeStats, "home-row")} <tr><td colspan="${homeStats.length + 1}" class="separator"></td></tr> ${mkTimeRows("away", awayStats, "away-row")} </table> </div> ${generaAnalisi(rows)}</div>`; } function updateTableCols(table, times, rows) { const headerRow = table.querySelector("tr"); const statRows = [...table.querySelectorAll("tr:not(:first-child)")]; headerRow.innerHTML = `<th colspan="2"></th>${times.map(t => `<th>${t}</th>`).join("")}`; statRows.forEach(tr => { const statKey = tr.querySelector(".stat-name")?.textContent; if (!statKey) return; }); } // ๐น Gestione visibilitร colonne function aggiornaVisibilitaColonne() { const selected = getCheckedStats(); document.querySelectorAll("th[data-stat], td[data-stat]").forEach(c => { c.style.display = selected.includes(c.dataset.stat) ? "" : "none"; }); document.querySelectorAll("table").forEach(t => { const home = t.querySelectorAll('th[data-side="home"]:not([style*="display: none"])').length; const away = t.querySelectorAll('th[data-side="away"]:not([style*="display: none"])').length; const ths = t.querySelectorAll("tr:first-child th"); if (ths[1]) ths[1].colSpan = home || 1; if (ths[2]) ths[2].colSpan = away || 1; }); } creaCheckbox(); caricaCSV(); restartAutoRefresh(); </script> </body> </html>