| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Soccer Team Performance Predictor</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/brain/0.6.3/brain.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .neuron { |
| transition: all 0.3s ease; |
| } |
| .neuron:hover { |
| transform: scale(1.1); |
| } |
| .network-container { |
| perspective: 1000px; |
| } |
| .layer { |
| transform-style: preserve-3d; |
| } |
| .connection { |
| stroke-dasharray: 1000; |
| stroke-dashoffset: 1000; |
| animation: draw 1.5s forwards; |
| } |
| @keyframes draw { |
| to { |
| stroke-dashoffset: 0; |
| } |
| } |
| .progress-ring__circle { |
| transition: stroke-dashoffset 0.35s; |
| transform: rotate(-90deg); |
| transform-origin: 50% 50%; |
| } |
| .team-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
| } |
| .team-card { |
| transition: all 0.3s ease; |
| } |
| .soccer-pitch { |
| background: linear-gradient(to bottom, #4ade80, #22c55e); |
| position: relative; |
| overflow: hidden; |
| } |
| .soccer-pitch::before { |
| content: ""; |
| position: absolute; |
| top: 0; |
| left: 50%; |
| width: 2px; |
| height: 100%; |
| background: white; |
| transform: translateX(-50%); |
| } |
| .soccer-pitch::after { |
| content: ""; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| width: 100px; |
| height: 100px; |
| border: 2px solid white; |
| border-radius: 50%; |
| transform: translate(-50%, -50%); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl font-bold text-gray-800 mb-2">⚽ Soccer Team Performance Predictor</h1> |
| <p class="text-gray-600 max-w-2xl mx-auto">A neural network that predicts team loss probability based on key soccer performance metrics</p> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Network Configuration</h2> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Hidden Layers</label> |
| <input type="range" id="hiddenLayersInput" min="1" max="5" value="3" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <div class="flex justify-between text-xs text-gray-500"> |
| <span>1</span> |
| <span>2</span> |
| <span>3</span> |
| <span>4</span> |
| <span>5</span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Neurons per Layer</label> |
| <input type="range" id="neuronsPerLayerInput" min="3" max="10" value="6" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <div class="flex justify-between text-xs text-gray-500"> |
| <span>3</span> |
| <span>4</span> |
| <span>5</span> |
| <span>6</span> |
| <span>7</span> |
| <span>8</span> |
| <span>9</span> |
| <span>10</span> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Learning Rate</label> |
| <input type="range" id="learningRateInput" min="0.01" max="0.5" step="0.01" value="0.2" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| <div class="flex justify-between text-xs text-gray-500"> |
| <span>0.01</span> |
| <span>0.25</span> |
| <span>0.5</span> |
| </div> |
| </div> |
| |
| <div class="flex space-x-3 pt-2"> |
| <button id="trainBtn" class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg flex items-center justify-center"> |
| <i class="fas fa-brain mr-2"></i> Train Network |
| </button> |
| <button id="resetBtn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded-lg flex items-center justify-center"> |
| <i class="fas fa-redo mr-2"></i> Reset |
| </button> |
| </div> |
| </div> |
| |
| <div class="mt-6"> |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Training Status</h3> |
| <div id="trainingStatus" class="text-sm text-gray-600 bg-gray-100 p-3 rounded-lg"> |
| Network not trained yet |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Network Architecture</h2> |
| <div id="networkVisualization" class="network-container h-64 flex justify-center items-center"> |
| <svg id="networkSvg" width="100%" height="100%" viewBox="0 0 500 300"></svg> |
| </div> |
| |
| <div class="mt-6"> |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Error Over Time</h3> |
| <div class="bg-gray-100 p-2 rounded-lg"> |
| <canvas id="errorChart" height="150"></canvas> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Make a Prediction</h2> |
| <div class="space-y-3"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Goals Conceded per Game</label> |
| <input id="goalsConcededInput" type="number" step="0.1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" placeholder="1.2"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Shots on Target per Game</label> |
| <input id="shotsOnTargetInput" type="number" step="0.1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" placeholder="5.5"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Pass Accuracy %</label> |
| <input id="passAccuracyInput" type="number" step="0.1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" placeholder="85.0"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Tackles per Game</label> |
| <input id="tacklesInput" type="number" step="0.1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" placeholder="15.0"> |
| </div> |
| |
| <button id="predictBtn" class="w-full bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg mt-4 flex items-center justify-center"> |
| <i class="fas fa-calculator mr-2"></i> Predict Loss Probability |
| </button> |
| |
| <div id="predictionResult" class="mt-4 p-4 rounded-lg bg-green-50 hidden"> |
| <h4 class="font-medium text-green-800 mb-1">Prediction Result</h4> |
| <p id="predictionText" class="text-green-600"></p> |
| <div class="mt-3 flex items-center"> |
| <div class="w-12 h-12 mr-3"> |
| <svg class="progress-ring" width="48" height="48"> |
| <circle class="progress-ring__circle" stroke="#E5E7EB" stroke-width="4" fill="transparent" r="20" cx="24" cy="24"/> |
| <circle class="progress-ring__circle" stroke="#10B981" stroke-width="4" fill="transparent" r="20" cx="24" cy="24"/> |
| </svg> |
| </div> |
| <div class="text-2xl font-bold text-green-600" id="predictionPercentage">0%</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mt-12"> |
| <h2 class="text-2xl font-bold text-gray-800 mb-6">Top European Clubs</h2> |
| <div id="teamPredictions" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"></div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const config = { |
| hiddenLayers: 3, |
| neuronsPerLayer: 6, |
| learningRate: 0.2, |
| iterations: 20000, |
| errorThresh: 0.005 |
| }; |
| |
| |
| let soccerTeams = [ |
| { name: "Manchester City", logo: "🔵", goalsConceded: 0.8, shotsOnTarget: 6.7, passAccuracy: 89.2, tackles: 12.3 }, |
| { name: "Real Madrid", logo: "⚪", goalsConceded: 1.1, shotsOnTarget: 5.9, passAccuracy: 87.5, tackles: 14.2 }, |
| { name: "Bayern Munich", logo: "🔴", goalsConceded: 0.9, shotsOnTarget: 6.3, passAccuracy: 88.1, tackles: 13.7 }, |
| { name: "Liverpool", logo: "🔴", goalsConceded: 1.2, shotsOnTarget: 6.1, passAccuracy: 85.7, tackles: 15.8 }, |
| { name: "Paris Saint-Germain", logo: "🔵", goalsConceded: 1.0, shotsOnTarget: 5.8, passAccuracy: 86.9, tackles: 11.9 }, |
| { name: "Barcelona", logo: "🔵🔴", goalsConceded: 1.3, shotsOnTarget: 5.5, passAccuracy: 87.8, tackles: 12.5 }, |
| { name: "Chelsea", logo: "🔵", goalsConceded: 1.4, shotsOnTarget: 4.9, passAccuracy: 84.3, tackles: 16.2 }, |
| { name: "AC Milan", logo: "⚫🔴", goalsConceded: 1.2, shotsOnTarget: 4.7, passAccuracy: 83.5, tackles: 14.9 } |
| ]; |
| |
| |
| let net = new brain.NeuralNetwork(); |
| let errorChart; |
| let trainingData = generateTrainingData(); |
| |
| |
| const hiddenLayersInput = document.getElementById('hiddenLayersInput'); |
| const neuronsPerLayerInput = document.getElementById('neuronsPerLayerInput'); |
| const learningRateInput = document.getElementById('learningRateInput'); |
| const trainBtn = document.getElementById('trainBtn'); |
| const resetBtn = document.getElementById('resetBtn'); |
| const goalsConcededInput = document.getElementById('goalsConcededInput'); |
| const shotsOnTargetInput = document.getElementById('shotsOnTargetInput'); |
| const passAccuracyInput = document.getElementById('passAccuracyInput'); |
| const tacklesInput = document.getElementById('tacklesInput'); |
| const predictBtn = document.getElementById('predictBtn'); |
| const predictionResult = document.getElementById('predictionResult'); |
| const predictionText = document.getElementById('predictionText'); |
| const predictionPercentage = document.getElementById('predictionPercentage'); |
| const teamPredictions = document.getElementById('teamPredictions'); |
| const trainingStatus = document.getElementById('trainingStatus'); |
| |
| |
| initUI(); |
| drawNetwork(); |
| initChart(); |
| renderTeamCards(); |
| |
| |
| hiddenLayersInput.addEventListener('input', updateConfig); |
| neuronsPerLayerInput.addEventListener('input', updateConfig); |
| learningRateInput.addEventListener('input', updateConfig); |
| trainBtn.addEventListener('click', trainNetwork); |
| resetBtn.addEventListener('click', resetNetwork); |
| predictBtn.addEventListener('click', predictLossProbability); |
| |
| |
| function generateTrainingData() { |
| |
| const data = []; |
| |
| for (let i = 0; i < 100; i++) { |
| |
| const goalsConceded = 0.5 + Math.random() * 2.5; |
| const shotsOnTarget = 2.0 + Math.random() * 6.0; |
| const passAccuracy = 70.0 + Math.random() * 25.0; |
| const tackles = 8.0 + Math.random() * 12.0; |
| |
| |
| |
| |
| let lossProbability = |
| (goalsConceded / 3.0) * 0.4 + |
| ((8.0 - shotsOnTarget) / 6.0) * 0.3 + |
| ((95.0 - passAccuracy) / 25.0) * 0.2 + |
| ((20.0 - tackles) / 12.0) * 0.1; |
| |
| |
| lossProbability += (Math.random() - 0.5) * 0.1; |
| |
| |
| lossProbability = Math.max(0, Math.min(1, lossProbability)); |
| |
| data.push({ |
| input: { |
| goalsConceded: normalize(goalsConceded, 0.5, 3.0), |
| shotsOnTarget: normalize(shotsOnTarget, 2.0, 8.0), |
| passAccuracy: normalize(passAccuracy, 70.0, 95.0), |
| tackles: normalize(tackles, 8.0, 20.0) |
| }, |
| output: { |
| loss: lossProbability |
| } |
| }); |
| } |
| |
| return data; |
| } |
| |
| function normalize(value, min, max) { |
| return (value - min) / (max - min); |
| } |
| |
| function denormalize(value, min, max) { |
| return value * (max - min) + min; |
| } |
| |
| function initUI() { |
| |
| hiddenLayersInput.value = config.hiddenLayers; |
| neuronsPerLayerInput.value = config.neuronsPerLayer; |
| learningRateInput.value = config.learningRate; |
| |
| |
| const circle = document.querySelector('.progress-ring__circle:last-child'); |
| const radius = circle.r.baseVal.value; |
| const circumference = radius * 2 * Math.PI; |
| |
| circle.style.strokeDasharray = circumference; |
| circle.style.strokeDashoffset = circumference; |
| } |
| |
| function updateConfig() { |
| config.hiddenLayers = parseInt(hiddenLayersInput.value); |
| config.neuronsPerLayer = parseInt(neuronsPerLayerInput.value); |
| config.learningRate = parseFloat(learningRateInput.value); |
| |
| drawNetwork(); |
| } |
| |
| function resetNetwork() { |
| net = new brain.NeuralNetwork(); |
| trainingStatus.textContent = "Network reset - not trained"; |
| trainingStatus.className = "text-sm text-gray-600 bg-gray-100 p-3 rounded-lg"; |
| |
| |
| if (errorChart) { |
| errorChart.data.labels = []; |
| errorChart.data.datasets[0].data = []; |
| errorChart.update(); |
| } |
| |
| |
| predictionResult.classList.add('hidden'); |
| } |
| |
| function trainNetwork() { |
| trainBtn.disabled = true; |
| trainBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Training...'; |
| trainingStatus.textContent = "Training in progress..."; |
| trainingStatus.className = "text-sm text-green-600 bg-green-100 p-3 rounded-lg"; |
| |
| |
| net = new brain.NeuralNetwork({ |
| hiddenLayers: [Array(config.hiddenLayers).fill(config.neuronsPerLayer)].flat(), |
| learningRate: config.learningRate, |
| iterations: config.iterations, |
| errorThresh: config.errorThresh, |
| log: true, |
| logPeriod: 1000, |
| callback: function(info) { |
| updateTrainingStatus(info.iterations, info.error); |
| updateChart(info.iterations, info.error); |
| }, |
| callbackPeriod: 1000 |
| }); |
| |
| |
| setTimeout(() => { |
| net.train(trainingData, (err, info) => { |
| trainBtn.disabled = false; |
| trainBtn.innerHTML = '<i class="fas fa-brain mr-2"></i> Train Network'; |
| |
| if (err) { |
| trainingStatus.textContent = "Training failed: " + err; |
| trainingStatus.className = "text-sm text-red-600 bg-red-100 p-3 rounded-lg"; |
| } else { |
| trainingStatus.textContent = `Training complete! Final error: ${info.error.toFixed(6)} after ${info.iterations} iterations`; |
| trainingStatus.className = "text-sm text-green-600 bg-green-100 p-3 rounded-lg"; |
| } |
| |
| |
| animateNetwork(); |
| }); |
| }, 100); |
| } |
| |
| function updateTrainingStatus(iterations, error) { |
| trainingStatus.textContent = `Training... Iteration: ${iterations}, Error: ${error.toFixed(6)}`; |
| } |
| |
| function initChart() { |
| const ctx = document.getElementById('errorChart').getContext('2d'); |
| errorChart = new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: [], |
| datasets: [{ |
| label: 'Training Error', |
| data: [], |
| borderColor: 'rgb(16, 185, 129)', |
| backgroundColor: 'rgba(16, 185, 129, 0.1)', |
| borderWidth: 2, |
| tension: 0.4, |
| fill: true |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: 'Error' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: 'Iterations' |
| } |
| } |
| }, |
| plugins: { |
| legend: { |
| display: false |
| }, |
| tooltip: { |
| callbacks: { |
| label: function(context) { |
| return `Error: ${context.parsed.y.toFixed(6)}`; |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| function updateChart(iteration, error) { |
| errorChart.data.labels.push(iteration); |
| errorChart.data.datasets[0].data.push(error); |
| errorChart.update(); |
| } |
| |
| function drawNetwork() { |
| const svg = document.getElementById('networkSvg'); |
| svg.innerHTML = ''; |
| |
| const width = 500; |
| const height = 300; |
| const layerCount = config.hiddenLayers + 2; |
| const neuronRadius = 15; |
| |
| |
| for (let layer = 0; layer < layerCount; layer++) { |
| const isInput = layer === 0; |
| const isOutput = layer === layerCount - 1; |
| |
| let neuronCount; |
| if (isInput) neuronCount = 4; |
| else if (isOutput) neuronCount = 1; |
| else neuronCount = config.neuronsPerLayer; |
| |
| const layerX = 50 + (width - 100) * (layer / (layerCount - 1)); |
| |
| |
| for (let n = 0; n < neuronCount; n++) { |
| const neuronY = height / 2 + (n - (neuronCount - 1) / 2) * 40; |
| |
| |
| const neuron = document.createElementNS("http://www.w3.org/2000/svg", "circle"); |
| neuron.setAttribute("cx", layerX); |
| neuron.setAttribute("cy", neuronY); |
| neuron.setAttribute("r", neuronRadius); |
| neuron.setAttribute("class", "neuron"); |
| |
| if (isInput) { |
| neuron.setAttribute("fill", "#4ADE80"); |
| } else if (isOutput) { |
| neuron.setAttribute("fill", "#3B82F6"); |
| } else { |
| neuron.setAttribute("fill", "#F59E0B"); |
| } |
| |
| svg.appendChild(neuron); |
| |
| |
| if (isInput) { |
| const labels = ["Goals Conceded", "Shots on Target", "Pass %", "Tackles"]; |
| const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); |
| text.setAttribute("x", layerX - 50); |
| text.setAttribute("y", neuronY + 5); |
| text.setAttribute("text-anchor", "end"); |
| text.setAttribute("class", "text-xs font-medium fill-gray-700"); |
| text.textContent = labels[n]; |
| svg.appendChild(text); |
| } else if (isOutput) { |
| const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); |
| text.setAttribute("x", layerX + 50); |
| text.setAttribute("y", neuronY + 5); |
| text.setAttribute("text-anchor", "start"); |
| text.setAttribute("class", "text-xs font-medium fill-gray-700"); |
| text.textContent = "Loss %"; |
| svg.appendChild(text); |
| } |
| } |
| } |
| } |
| |
| function animateNetwork() { |
| const svg = document.getElementById('networkSvg'); |
| const connections = []; |
| |
| const width = 500; |
| const height = 300; |
| const layerCount = config.hiddenLayers + 2; |
| const neuronRadius = 15; |
| |
| |
| for (let layer = 0; layer < layerCount - 1; layer++) { |
| const isFirstLayer = layer === 0; |
| const isLastLayer = layer === layerCount - 2; |
| |
| let fromNeuronCount; |
| if (isFirstLayer) fromNeuronCount = 4; |
| else fromNeuronCount = config.neuronsPerLayer; |
| |
| let toNeuronCount; |
| if (isLastLayer) toNeuronCount = 1; |
| else toNeuronCount = config.neuronsPerLayer; |
| |
| const fromLayerX = 50 + (width - 100) * (layer / (layerCount - 1)); |
| const toLayerX = 50 + (width - 100) * ((layer + 1) / (layerCount - 1)); |
| |
| for (let from = 0; from < fromNeuronCount; from++) { |
| const fromY = height / 2 + (from - (fromNeuronCount - 1) / 2) * 40; |
| |
| for (let to = 0; to < toNeuronCount; to++) { |
| const toY = height / 2 + (to - (toNeuronCount - 1) / 2) * 40; |
| |
| const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); |
| line.setAttribute("x1", fromLayerX + neuronRadius); |
| line.setAttribute("y1", fromY); |
| line.setAttribute("x2", toLayerX - neuronRadius); |
| line.setAttribute("y2", toY); |
| line.setAttribute("stroke", "#9CA3AF"); |
| line.setAttribute("stroke-width", "1"); |
| line.setAttribute("class", "connection"); |
| svg.appendChild(line); |
| |
| connections.push(line); |
| } |
| } |
| } |
| |
| |
| connections.forEach((conn, i) => { |
| setTimeout(() => { |
| conn.style.strokeDashoffset = "0"; |
| }, i * 20); |
| }); |
| } |
| |
| function predictLossProbability() { |
| if (!net.trainOpts) { |
| predictionText.textContent = "Please train the network first"; |
| predictionResult.classList.remove('hidden'); |
| predictionResult.className = "mt-4 p-4 rounded-lg bg-yellow-50"; |
| return; |
| } |
| |
| |
| const goalsConceded = parseFloat(goalsConcededInput.value) || 1.2; |
| const shotsOnTarget = parseFloat(shotsOnTargetInput.value) || 5.5; |
| const passAccuracy = parseFloat(passAccuracyInput.value) || 85.0; |
| const tackles = parseFloat(tacklesInput.value) || 15.0; |
| |
| |
| const normalizedInput = { |
| goalsConceded: normalize(goalsConceded, 0.5, 3.0), |
| shotsOnTarget: normalize(shotsOnTarget, 2.0, 8.0), |
| passAccuracy: normalize(passAccuracy, 70.0, 95.0), |
| tackles: normalize(tackles, 8.0, 20.0) |
| }; |
| |
| |
| const output = net.run(normalizedInput); |
| const lossProbability = output.loss; |
| const percentage = Math.round(lossProbability * 100); |
| |
| |
| predictionText.textContent = `Based on the team's stats (Goals Conceded: ${goalsConceded.toFixed(1)}, Shots on Target: ${shotsOnTarget.toFixed(1)}, Pass Accuracy: ${passAccuracy.toFixed(1)}%, Tackles: ${tackles.toFixed(1)}), the predicted loss probability is:`; |
| predictionPercentage.textContent = `${percentage}%`; |
| |
| |
| const circle = document.querySelector('.progress-ring__circle:last-child'); |
| const radius = circle.r.baseVal.value; |
| const circumference = radius * 2 * Math.PI; |
| const offset = circumference - (percentage / 100) * circumference; |
| |
| circle.style.strokeDashoffset = offset; |
| |
| |
| if (percentage > 70) { |
| circle.style.stroke = "#EF4444"; |
| predictionPercentage.className = "text-2xl font-bold text-red-600"; |
| } else if (percentage > 50) { |
| circle.style.stroke = "#F59E0B"; |
| predictionPercentage.className = "text-2xl font-bold text-yellow-600"; |
| } else { |
| circle.style.stroke = "#10B981"; |
| predictionPercentage.className = "text-2xl font-bold text-green-600"; |
| } |
| |
| predictionResult.classList.remove('hidden'); |
| predictionResult.className = "mt-4 p-4 rounded-lg bg-green-50"; |
| } |
| |
| function renderTeamCards() { |
| teamPredictions.innerHTML = ''; |
| |
| soccerTeams.forEach(team => { |
| const card = document.createElement('div'); |
| card.className = 'bg-white rounded-lg shadow-md p-4 team-card hover:shadow-lg transition-all'; |
| card.innerHTML = ` |
| <div class="flex items-center mb-3"> |
| <div class="text-2xl mr-3">${team.logo}</div> |
| <h3 class="font-semibold text-gray-800">${team.name}</h3> |
| </div> |
| <div class="grid grid-cols-2 gap-2 text-sm mb-3"> |
| <div><span class="text-gray-500">Goals Conceded:</span> ${team.goalsConceded.toFixed(1)}</div> |
| <div><span class="text-gray-500">Shots on Target:</span> ${team.shotsOnTarget.toFixed(1)}</div> |
| <div><span class="text-gray-500">Pass %:</span> ${team.passAccuracy.toFixed(1)}%</div> |
| <div><span class="text-gray-500">Tackles:</span> ${team.tackles.toFixed(1)}</div> |
| </div> |
| <div class="flex items-center"> |
| <button class="w-full bg-green-100 hover:bg-green-200 text-green-800 py-2 px-3 rounded flex items-center justify-center predict-btn" data-team='${JSON.stringify(team)}'> |
| <i class="fas fa-calculator mr-2 text-xs"></i> Predict Performance |
| </button> |
| </div> |
| `; |
| teamPredictions.appendChild(card); |
| }); |
| |
| |
| document.querySelectorAll('.predict-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| const team = JSON.parse(this.getAttribute('data-team')); |
| goalsConcededInput.value = team.goalsConceded; |
| shotsOnTargetInput.value = team.shotsOnTarget; |
| passAccuracyInput.value = team.passAccuracy; |
| tacklesInput.value = team.tackles; |
| |
| if (net.trainOpts) { |
| predictLossProbability(); |
| } else { |
| predictionText.textContent = "Please train the network first"; |
| predictionResult.classList.remove('hidden'); |
| predictionResult.className = "mt-4 p-4 rounded-lg bg-yellow-50"; |
| } |
| }); |
| }); |
| } |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MarcRyan/stp" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |