Spaces:
Running
Running
Download index.html from Volodya2000/mas: direct link, hf CLI and curl.
- Browser
- Download file 54.8 kB
-
https://huggingface.co/spaces/Volodya2000/mas/resolve/main/index.html
- Command line
-
hf download hf://spaces/Volodya2000/mas/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Volodya2000/mas/resolve/main/index.html
54.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mobile User Acquisition Dashboard</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed #3b82f6; | |
| border-radius: 0.5rem; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #10b981; | |
| background-color: #f0fdf4; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 10px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: #cbd5e1; | |
| border-radius: 10px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: #94a3b8; | |
| } | |
| .sparkline { | |
| height: 40px; | |
| width: 100px; | |
| } | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltiptext { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: #333; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -100px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltiptext { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255,255,255,.3); | |
| border-radius: 50%; | |
| border-top-color: #fff; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="min-h-screen"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm"> | |
| <div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex justify-between items-center"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0"> | |
| <i class="fas fa-mobile-alt text-blue-500 text-2xl"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <h1 class="text-xl font-bold text-gray-900">Mobile User Acquisition Dashboard</h1> | |
| <p class="text-sm text-gray-500">Analyze Facebook Ads performance by day and country</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="relative"> | |
| <i class="fas fa-bell text-gray-500 hover:text-blue-500 cursor-pointer"></i> | |
| <span class="absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500"></span> | |
| </div> | |
| <div class="flex items-center"> | |
| <img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""> | |
| <span class="ml-2 text-sm font-medium text-gray-700">John Doe</span> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8"> | |
| <!-- Upload Section --> | |
| <div class="bg-white shadow rounded-lg p-6 mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-medium text-gray-900">Upload Facebook Ads Data</h2> | |
| <button id="sampleDataBtn" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"> | |
| <i class="fas fa-download mr-2"></i>Download Sample Data | |
| </button> | |
| </div> | |
| <div id="dropzone" class="dropzone p-10 text-center cursor-pointer"> | |
| <div class="space-y-4"> | |
| <i class="fas fa-cloud-upload-alt text-blue-500 text-4xl"></i> | |
| <p class="text-lg font-medium text-gray-700">Drag & drop your CSV file here</p> | |
| <p class="text-sm text-gray-500">or click to browse files</p> | |
| <input type="file" id="fileInput" class="hidden" accept=".csv"> | |
| <button id="browseBtn" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"> | |
| <i class="fas fa-folder-open mr-2"></i>Browse Files | |
| </button> | |
| </div> | |
| </div> | |
| <div id="fileInfo" class="mt-4 hidden"> | |
| <div class="flex items-center justify-between bg-gray-50 p-3 rounded-md"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-file-csv text-blue-500 text-xl mr-3"></i> | |
| <div> | |
| <p class="text-sm font-medium text-gray-700" id="fileName">filename.csv</p> | |
| <p class="text-xs text-gray-500" id="fileSize">1.2 MB</p> | |
| </div> | |
| </div> | |
| <button id="analyzeBtn" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"> | |
| <i class="fas fa-chart-line mr-2"></i>Analyze Data | |
| </button> | |
| </div> | |
| </div> | |
| <div id="loadingIndicator" class="mt-4 hidden"> | |
| <div class="flex items-center justify-center p-4 bg-blue-50 rounded-md"> | |
| <div class="loading-spinner mr-3"></div> | |
| <span class="text-blue-700 font-medium">Analyzing your data...</span> | |
| </div> | |
| </div> | |
| <div id="errorMessage" class="mt-4 hidden"> | |
| <div class="flex items-center justify-between bg-red-50 p-3 rounded-md"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-exclamation-circle text-red-500 text-xl mr-3"></i> | |
| <div> | |
| <p class="text-sm font-medium text-red-700" id="errorText">Error message here</p> | |
| </div> | |
| </div> | |
| <button id="dismissErrorBtn" class="px-2 py-1 text-red-500 hover:text-red-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dashboard Content (Initially Hidden) --> | |
| <div id="dashboardContent" class="hidden"> | |
| <!-- Summary Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6"> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Spend</p> | |
| <p class="text-2xl font-bold text-gray-900" id="totalSpend">$12,345</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-500"> | |
| <i class="fas fa-dollar-sign"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex items-center justify-between text-sm"> | |
| <span class="text-green-500 font-medium" id="spendChange">+12%</span> | |
| <span class="text-gray-500">vs last period</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Installs</p> | |
| <p class="text-2xl font-bold text-gray-900" id="totalInstalls">8,642</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-green-100 text-green-500"> | |
| <i class="fas fa-download"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex items-center justify-between text-sm"> | |
| <span class="text-green-500 font-medium" id="installsChange">+8%</span> | |
| <span class="text-gray-500">vs last period</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">CPI</p> | |
| <p class="text-2xl font-bold text-gray-900" id="avgCPI">$1.42</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-purple-100 text-purple-500"> | |
| <i class="fas fa-percentage"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex items-center justify-between text-sm"> | |
| <span class="text-red-500 font-medium" id="cpiChange">-5%</span> | |
| <span class="text-gray-500">vs last period</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">ROAS</p> | |
| <p class="text-2xl font-bold text-gray-900" id="avgROAS">2.8</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-500"> | |
| <i class="fas fa-chart-pie"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex items-center justify-between text-sm"> | |
| <span class="text-green-500 font-medium" id="roasChange">+15%</span> | |
| <span class="text-gray-500">vs last period</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Charts --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <!-- Performance Over Time --> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium text-gray-900">Performance Over Time</h3> | |
| <div class="flex space-x-2"> | |
| <button class="px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600" onclick="updateTimeRange('7D')">7D</button> | |
| <button class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50" onclick="updateTimeRange('30D')">30D</button> | |
| <button class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50" onclick="updateTimeRange('90D')">90D</button> | |
| </div> | |
| </div> | |
| <div class="h-80"> | |
| <canvas id="performanceChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Country Breakdown --> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium text-gray-900">Country Performance</h3> | |
| <div class="relative"> | |
| <select id="countryMetricSelect" class="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" onchange="updateCountryChart()"> | |
| <option value="spend">By Spend</option> | |
| <option value="installs">By Installs</option> | |
| <option value="cpi">By CPI</option> | |
| <option value="roas">By ROAS</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="h-80"> | |
| <canvas id="countryChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Insights Section --> | |
| <div class="bg-white shadow rounded-lg p-6 mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium text-gray-900">Key Insights</h3> | |
| <button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600" onclick="generateInsights()"> | |
| <i class="fas fa-sync-alt mr-2"></i>Refresh Insights | |
| </button> | |
| </div> | |
| <div id="insightsContainer" class="space-y-4"> | |
| <!-- Insights will be dynamically inserted here --> | |
| </div> | |
| </div> | |
| <!-- Data Table --> | |
| <div class="bg-white shadow rounded-lg overflow-hidden mb-6"> | |
| <div class="px-6 py-4 border-b border-gray-200"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-medium text-gray-900">Detailed Performance Data</h3> | |
| <div class="flex space-x-3"> | |
| <div class="relative"> | |
| <select id="countryFilter" class="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" onchange="filterTable()"> | |
| <option value="all">All Countries</option> | |
| <!-- Countries will be dynamically inserted here --> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| </div> | |
| <button class="px-4 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" onclick="exportData()"> | |
| <i class="fas fa-download mr-2"></i>Export | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto custom-scrollbar"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Campaign</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Spend</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Installs</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CPI</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ROAS</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Trend</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="dataTableBody" class="bg-white divide-y divide-gray-200"> | |
| <!-- Table rows will be dynamically inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between"> | |
| <div class="flex-1 flex justify-between sm:hidden"> | |
| <button class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" onclick="prevPage()"> | |
| Previous | |
| </button> | |
| <button class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" onclick="nextPage()"> | |
| Next | |
| </button> | |
| </div> | |
| <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> | |
| <div> | |
| <p class="text-sm text-gray-700"> | |
| Showing <span class="font-medium" id="currentPageStart">1</span> to <span class="font-medium" id="currentPageEnd">4</span> of <span class="font-medium" id="totalRecords">24</span> results | |
| </p> | |
| </div> | |
| <div> | |
| <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination"> | |
| <button class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" onclick="prevPage()"> | |
| <span class="sr-only">Previous</span> | |
| <i class="fas fa-chevron-left"></i> | |
| </button> | |
| <button aria-current="page" class="z-10 bg-blue-50 border-blue-500 text-blue-600 relative inline-flex items-center px-4 py-2 border text-sm font-medium" id="page1"> | |
| 1 | |
| </button> | |
| <button class="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium" id="page2" onclick="goToPage(2)"> | |
| 2 | |
| </button> | |
| <button class="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium" id="page3" onclick="goToPage(3)"> | |
| 3 | |
| </button> | |
| <span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700"> | |
| ... | |
| </span> | |
| <button class="bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center px-4 py-2 border text-sm font-medium" id="page8" onclick="goToPage(8)"> | |
| 8 | |
| </button> | |
| <button class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" onclick="nextPage()"> | |
| <span class="sr-only">Next</span> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| </nav> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| // Global variables | |
| let parsedData = []; | |
| let performanceChart = null; | |
| let countryChart = null; | |
| let currentPage = 1; | |
| const rowsPerPage = 10; | |
| let filteredData = []; | |
| let currentTimeRange = '7D'; | |
| // File Upload Handling | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const browseBtn = document.getElementById('browseBtn'); | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const dashboardContent = document.getElementById('dashboardContent'); | |
| const sampleDataBtn = document.getElementById('sampleDataBtn'); | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const errorText = document.getElementById('errorText'); | |
| const dismissErrorBtn = document.getElementById('dismissErrorBtn'); | |
| // Handle drag and drop events | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropzone.classList.add('active'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('active'); | |
| } | |
| // Handle file drop | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Handle file selection via button | |
| browseBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| handleFiles(fileInput.files); | |
| }); | |
| // Dismiss error message | |
| dismissErrorBtn.addEventListener('click', () => { | |
| errorMessage.classList.add('hidden'); | |
| }); | |
| // Process uploaded files | |
| function handleFiles(files) { | |
| if (files.length) { | |
| const file = files[0]; | |
| if (file.type === 'text/csv' || file.name.endsWith('.csv')) { | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| fileInfo.classList.remove('hidden'); | |
| // Store the file for analysis | |
| analyzeBtn.onclick = () => { | |
| analyzeData(file); | |
| }; | |
| } else { | |
| showError('Please upload a CSV file.'); | |
| } | |
| } | |
| } | |
| // Format file size | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| // Show error message | |
| function showError(message) { | |
| errorText.textContent = message; | |
| errorMessage.classList.remove('hidden'); | |
| } | |
| // Analyze the uploaded CSV file | |
| function analyzeData(file) { | |
| loadingIndicator.classList.remove('hidden'); | |
| fileInfo.classList.add('hidden'); | |
| Papa.parse(file, { | |
| header: true, | |
| complete: function(results) { | |
| loadingIndicator.classList.add('hidden'); | |
| // Validate the data structure | |
| if (!validateDataStructure(results.data)) { | |
| showError('The CSV file does not contain the required columns. Please check the sample data format.'); | |
| return; | |
| } | |
| // Process the data | |
| parsedData = processData(results.data); | |
| filteredData = [...parsedData]; | |
| // Show the dashboard | |
| dashboardContent.classList.remove('hidden'); | |
| // Initialize the dashboard | |
| initializeDashboard(); | |
| }, | |
| error: function(error) { | |
| loadingIndicator.classList.add('hidden'); | |
| showError('Error parsing the CSV file: ' + error.message); | |
| } | |
| }); | |
| } | |
| // Validate the CSV structure | |
| function validateDataStructure(data) { | |
| if (data.length === 0) return false; | |
| const firstRow = data[0]; | |
| return ( | |
| 'date' in firstRow && | |
| 'country' in firstRow && | |
| 'campaign' in firstRow && | |
| 'spend' in firstRow && | |
| 'installs' in firstRow && | |
| 'revenue' in firstRow | |
| ); | |
| } | |
| // Process the raw data | |
| function processData(rawData) { | |
| return rawData.map(row => { | |
| // Clean and convert data types | |
| const spend = parseFloat(row.spend) || 0; | |
| const installs = parseInt(row.installs) || 0; | |
| const revenue = parseFloat(row.revenue) || 0; | |
| // Calculate derived metrics | |
| const cpi = installs > 0 ? spend / installs : 0; | |
| const roas = spend > 0 ? revenue / spend : 0; | |
| return { | |
| date: row.date, | |
| country: row.country, | |
| campaign: row.campaign, | |
| spend: spend, | |
| installs: installs, | |
| revenue: revenue, | |
| cpi: cpi, | |
| roas: roas | |
| }; | |
| }).filter(row => row.date); // Filter out rows with no date | |
| } | |
| // Initialize the dashboard with data | |
| function initializeDashboard() { | |
| // Update summary cards | |
| updateSummaryCards(); | |
| // Initialize charts | |
| initializeCharts(); | |
| // Populate the data table | |
| populateDataTable(); | |
| // Generate insights | |
| generateInsights(); | |
| // Populate country filter dropdown | |
| populateCountryFilter(); | |
| } | |
| // Update summary cards | |
| function updateSummaryCards() { | |
| if (parsedData.length === 0) return; | |
| // Calculate totals | |
| const totalSpend = parsedData.reduce((sum, row) => sum + row.spend, 0); | |
| const totalInstalls = parsedData.reduce((sum, row) => sum + row.installs, 0); | |
| const totalRevenue = parsedData.reduce((sum, row) => sum + row.revenue, 0); | |
| // Calculate averages | |
| const avgCPI = totalInstalls > 0 ? totalSpend / totalInstalls : 0; | |
| const avgROAS = totalSpend > 0 ? totalRevenue / totalSpend : 0; | |
| // Format values | |
| document.getElementById('totalSpend').textContent = formatCurrency(totalSpend); | |
| document.getElementById('totalInstalls').textContent = formatNumber(totalInstalls); | |
| document.getElementById('avgCPI').textContent = formatCurrency(avgCPI); | |
| document.getElementById('avgROAS').textContent = avgROAS.toFixed(2); | |
| // For demo purposes, we'll use random changes | |
| document.getElementById('spendChange').textContent = randomChange(); | |
| document.getElementById('installsChange').textContent = randomChange(); | |
| document.getElementById('cpiChange').textContent = randomChange(); | |
| document.getElementById('roasChange').textContent = randomChange(); | |
| } | |
| // Format currency | |
| function formatCurrency(value) { | |
| return '$' + value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}); | |
| } | |
| // Format number | |
| function formatNumber(value) { | |
| return value.toLocaleString(); | |
| } | |
| // Generate random percentage change for demo | |
| function randomChange() { | |
| const changes = ['+12%', '+8%', '+15%', '-5%', '+3%', '-2%', '+20%']; | |
| return changes[Math.floor(Math.random() * changes.length)]; | |
| } | |
| // Initialize charts | |
| function initializeCharts() { | |
| // Performance Over Time Chart | |
| const performanceCtx = document.getElementById('performanceChart').getContext('2d'); | |
| performanceChart = new Chart(performanceCtx, { | |
| type: 'line', | |
| data: getPerformanceChartData('7D'), | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true | |
| } | |
| } | |
| } | |
| }); | |
| // Country Breakdown Chart | |
| const countryCtx = document.getElementById('countryChart').getContext('2d'); | |
| countryChart = new Chart(countryCtx, { | |
| type: 'bar', | |
| data: getCountryChartData('spend'), | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| stacked: false, | |
| }, | |
| y: { | |
| stacked: false, | |
| beginAtZero: true | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Get performance chart data based on time range | |
| function getPerformanceChartData(range) { | |
| // Group data by date | |
| const dateGroups = {}; | |
| parsedData.forEach(row => { | |
| if (!dateGroups[row.date]) { | |
| dateGroups[row.date] = { | |
| spend: 0, | |
| installs: 0 | |
| }; | |
| } | |
| dateGroups[row.date].spend += row.spend; | |
| dateGroups[row.date].installs += row.installs; | |
| }); | |
| // Sort dates | |
| const sortedDates = Object.keys(dateGroups).sort(); | |
| // Apply time range filter | |
| let filteredDates = sortedDates; | |
| if (range === '7D' && sortedDates.length > 7) { | |
| filteredDates = sortedDates.slice(-7); | |
| } else if (range === '30D' && sortedDates.length > 30) { | |
| filteredDates = sortedDates.slice(-30); | |
| } else if (range === '90D' && sortedDates.length > 90) { | |
| filteredDates = sortedDates.slice(-90); | |
| } | |
| // Prepare chart data | |
| return { | |
| labels: filteredDates, | |
| datasets: [ | |
| { | |
| label: 'Spend', | |
| data: filteredDates.map(date => dateGroups[date].spend), | |
| borderColor: '#3b82f6', | |
| backgroundColor: 'rgba(59, 130, 246, 0.05)', | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4 | |
| }, | |
| { | |
| label: 'Installs', | |
| data: filteredDates.map(date => dateGroups[date].installs), | |
| borderColor: '#10b981', | |
| backgroundColor: 'rgba(16, 185, 129, 0.05)', | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4 | |
| } | |
| ] | |
| }; | |
| } | |
| // Get country chart data based on selected metric | |
| function getCountryChartData(metric) { | |
| // Group data by country | |
| const countryGroups = {}; | |
| parsedData.forEach(row => { | |
| if (!countryGroups[row.country]) { | |
| countryGroups[row.country] = { | |
| spend: 0, | |
| installs: 0, | |
| revenue: 0, | |
| count: 0 | |
| }; | |
| } | |
| countryGroups[row.country].spend += row.spend; | |
| countryGroups[row.country].installs += row.installs; | |
| countryGroups[row.country].revenue += row.revenue; | |
| countryGroups[row.country].count += 1; | |
| }); | |
| // Calculate metrics per country | |
| const countries = Object.keys(countryGroups); | |
| const metricData = countries.map(country => { | |
| const group = countryGroups[country]; | |
| switch (metric) { | |
| case 'spend': | |
| return group.spend; | |
| case 'installs': | |
| return group.installs; | |
| case 'cpi': | |
| return group.installs > 0 ? group.spend / group.installs : 0; | |
| case 'roas': | |
| return group.spend > 0 ? group.revenue / group.spend : 0; | |
| default: | |
| return group.spend; | |
| } | |
| }); | |
| // Sort countries by metric value (descending) | |
| const sortedIndices = [...Array(countries.length).keys()] | |
| .sort((a, b) => metricData[b] - metricData[a]); | |
| const sortedCountries = sortedIndices.map(i => countries[i]); | |
| const sortedData = sortedIndices.map(i => metricData[i]); | |
| // Limit to top 10 countries | |
| const displayCountries = sortedCountries.slice(0, 10); | |
| const displayData = sortedData.slice(0, 10); | |
| // Determine label and color based on metric | |
| let label, backgroundColor; | |
| switch (metric) { | |
| case 'spend': | |
| label = 'Spend'; | |
| backgroundColor = '#3b82f6'; | |
| break; | |
| case 'installs': | |
| label = 'Installs'; | |
| backgroundColor = '#10b981'; | |
| break; | |
| case 'cpi': | |
| label = 'CPI'; | |
| backgroundColor = '#8b5cf6'; | |
| break; | |
| case 'roas': | |
| label = 'ROAS'; | |
| backgroundColor = '#f59e0b'; | |
| break; | |
| default: | |
| label = 'Spend'; | |
| backgroundColor = '#3b82f6'; | |
| } | |
| return { | |
| labels: displayCountries, | |
| datasets: [{ | |
| label: label, | |
| data: displayData, | |
| backgroundColor: backgroundColor, | |
| }] | |
| }; | |
| } | |
| // Update time range for performance chart | |
| function updateTimeRange(range) { | |
| currentTimeRange = range; | |
| performanceChart.data = getPerformanceChartData(range); | |
| performanceChart.update(); | |
| // Update button states | |
| document.querySelectorAll('#performanceChart + div button').forEach(btn => { | |
| btn.classList.remove('bg-blue-500', 'text-white'); | |
| btn.classList.add('bg-white', 'border', 'border-gray-300', 'text-gray-700'); | |
| }); | |
| const activeBtn = document.querySelector(`#performanceChart + div button:nth-child(${ | |
| range === '7D' ? 1 : range === '30D' ? 2 : 3 | |
| })`); | |
| activeBtn.classList.remove('bg-white', 'border', 'border-gray-300', 'text-gray-700'); | |
| activeBtn.classList.add('bg-blue-500', 'text-white'); | |
| } | |
| // Update country chart based on selected metric | |
| function updateCountryChart() { | |
| const metric = document.getElementById('countryMetricSelect').value; | |
| countryChart.data = getCountryChartData(metric); | |
| countryChart.update(); | |
| } | |
| // Populate data table | |
| function populateDataTable() { | |
| const tableBody = document.getElementById('dataTableBody'); | |
| tableBody.innerHTML = ''; | |
| // Calculate pagination | |
| const startIndex = (currentPage - 1) * rowsPerPage; | |
| const endIndex = Math.min(startIndex + rowsPerPage, filteredData.length); | |
| document.getElementById('currentPageStart').textContent = startIndex + 1; | |
| document.getElementById('currentPageEnd').textContent = endIndex; | |
| document.getElementById('totalRecords').textContent = filteredData.length; | |
| // Add rows | |
| for (let i = startIndex; i < endIndex; i++) { | |
| const row = filteredData[i]; | |
| const tr = document.createElement('tr'); | |
| // Random sparkline data | |
| const sparklineData = Array(7).fill(0).map(() => Math.floor(Math.random() * 20) + 5); | |
| tr.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${row.date}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.country}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.campaign}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatCurrency(row.spend)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatNumber(row.installs)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatCurrency(row.cpi)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.roas.toFixed(2)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="sparkline"> | |
| <canvas id="sparkline${i}"></canvas> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| <button class="text-blue-500 hover:text-blue-700 mr-3"> | |
| <i class="fas fa-chart-bar"></i> | |
| </button> | |
| <button class="text-green-500 hover:text-green-700"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| </td> | |
| `; | |
| tableBody.appendChild(tr); | |
| // Initialize sparkline | |
| setTimeout(() => { | |
| const ctx = document.getElementById(`sparkline${i}`).getContext('2d'); | |
| new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: Array(7).fill(''), | |
| datasets: [{ | |
| data: sparklineData, | |
| borderColor: getRandomColor(), | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| tension: 0.4 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| x: { display: false }, | |
| y: { display: false } | |
| } | |
| } | |
| }); | |
| }, 0); | |
| } | |
| // Update pagination buttons | |
| updatePaginationButtons(); | |
| } | |
| // Get random color for sparklines | |
| function getRandomColor() { | |
| const colors = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#ef4444']; | |
| return colors[Math.floor(Math.random() * colors.length)]; | |
| } | |
| // Update pagination buttons | |
| function updatePaginationButtons() { | |
| const totalPages = Math.ceil(filteredData.length / rowsPerPage); | |
| // For demo purposes, we'll just handle up to 8 pages | |
| if (totalPages <= 1) { | |
| document.querySelector('.pagination-nav').classList.add('hidden'); | |
| } else { | |
| document.querySelector('.pagination-nav').classList.remove('hidden'); | |
| } | |
| // Update active page button | |
| document.querySelectorAll('.pagination-nav button[id^="page"]').forEach(btn => { | |
| btn.classList.remove('bg-blue-50', 'border-blue-500', 'text-blue-600'); | |
| btn.classList.add('bg-white', 'border-gray-300', 'text-gray-500'); | |
| }); | |
| const activePageBtn = document.getElementById(`page${Math.min(currentPage, 8)}`); | |
| if (activePageBtn) { | |
| activePageBtn.classList.remove('bg-white', 'border-gray-300', 'text-gray-500'); | |
| activePageBtn.classList.add('bg-blue-50', 'border-blue-500', 'text-blue-600'); | |
| } | |
| } | |
| // Filter table by country | |
| function filterTable() { | |
| const country = document.getElementById('countryFilter').value; | |
| if (country === 'all') { | |
| filteredData = [...parsedData]; | |
| } else { | |
| filteredData = parsedData.filter(row => row.country === country); | |
| } | |
| currentPage = 1; | |
| populateDataTable(); | |
| } | |
| // Populate country filter dropdown | |
| function populateCountryFilter() { | |
| const countryFilter = document.getElementById('countryFilter'); | |
| // Get unique countries | |
| const countries = [...new Set(parsedData.map(row => row.country))].sort(); | |
| // Add options | |
| countries.forEach(country => { | |
| const option = document.createElement('option'); | |
| option.value = country; | |
| option.textContent = country; | |
| countryFilter.appendChild(option); | |
| }); | |
| } | |
| // Pagination functions | |
| function prevPage() { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| populateDataTable(); | |
| } | |
| } | |
| function nextPage() { | |
| if (currentPage * rowsPerPage < filteredData.length) { | |
| currentPage++; | |
| populateDataTable(); | |
| } | |
| } | |
| function goToPage(page) { | |
| currentPage = page; | |
| populateDataTable(); | |
| } | |
| // Export data | |
| function exportData() { | |
| // In a real app, this would export the data | |
| alert('Export functionality would be implemented here in a real application.'); | |
| } | |
| // Generate insights | |
| function generateInsights() { | |
| const insightsContainer = document.getElementById('insightsContainer'); | |
| insightsContainer.innerHTML = ''; | |
| if (parsedData.length === 0) return; | |
| // Sample insights (in a real app, these would be generated from the data) | |
| const insights = [ | |
| { | |
| type: 'positive', | |
| icon: 'bolt', | |
| title: 'Spike Detected', | |
| content: `A significant spike in installs was detected on <span class="font-medium">${getRandomDate()}</span> in <span class="font-medium">${getRandomCountry()}</span>. This correlates with a campaign optimization change made that day.`, | |
| stats: [ | |
| {label: 'increase', value: `${Math.floor(Math.random() * 50) + 20}%`, positive: true}, | |
| {label: 'correlation', value: (Math.random() * 0.5 + 0.5).toFixed(2), neutral: true} | |
| ] | |
| }, | |
| { | |
| type: 'negative', | |
| icon: 'exclamation-triangle', | |
| title: 'High CPI Market', | |
| content: `<span class="font-medium">${getRandomCountry()}</span> has a CPI of <span class="font-medium">${formatCurrency(Math.random() * 3 + 2)}</span>, which is ${Math.floor(Math.random() * 100) + 50}% higher than the average. Consider adjusting bids or creative strategy.`, | |
| stats: [ | |
| {label: 'above avg', value: `${Math.floor(Math.random() * 100) + 50}%`, negative: true} | |
| ] | |
| }, | |
| { | |
| type: 'positive', | |
| icon: 'chart-line', | |
| title: 'Strong ROAS Correlation', | |
| content: `Video creatives in <span class="font-medium">${getRandomCountry()}</span> show a strong positive correlation (${(Math.random() * 0.3 + 0.7).toFixed(2)}) with ROAS. Consider scaling video ad production for this market.`, | |
| stats: [ | |
| {label: 'correlation', value: (Math.random() * 0.3 + 0.7).toFixed(2), positive: true} | |
| ] | |
| } | |
| ]; | |
| // Add insights to the container | |
| insights.forEach(insight => { | |
| const insightDiv = document.createElement('div'); | |
| let borderColor, bgColor, textColor; | |
| if (insight.type === 'positive') { | |
| borderColor = 'border-blue-100'; | |
| bgColor = 'bg-blue-50'; | |
| textColor = 'text-blue-500'; | |
| } else if (insight.type === 'negative') { | |
| borderColor = 'border-red-100'; | |
| bgColor = 'bg-red-50'; | |
| textColor = 'text-red-500'; | |
| } else { | |
| borderColor = 'border-green-100'; | |
| bgColor = 'bg-green-50'; | |
| textColor = 'text-green-500'; | |
| } | |
| insightDiv.className = `p-4 border ${borderColor} rounded-lg ${bgColor}`; | |
| insightDiv.innerHTML = ` | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 mt-1"> | |
| <i class="fas fa-${insight.icon} ${textColor}"></i> | |
| </div> | |
| <div class="ml-3"> | |
| <h4 class="text-sm font-medium text-gray-900">${insight.title}</h4> | |
| <p class="text-sm text-gray-600 mt-1">${insight.content}</p> | |
| <div class="mt-2 flex space-x-4"> | |
| ${insight.stats.map(stat => ` | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ | |
| stat.positive ? 'bg-green-100 text-green-800' : | |
| stat.negative ? 'bg-red-100 text-red-800' : | |
| 'bg-blue-100 text-blue-800' | |
| }"> | |
| <i class="fas fa-${stat.positive ? 'arrow-up' : stat.negative ? 'arrow-up' : 'link'} mr-1"></i> | |
| ${stat.label}: ${stat.value} | |
| </span> | |
| `).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| insightsContainer.appendChild(insightDiv); | |
| }); | |
| } | |
| // Helper function to get a random country from the data | |
| function getRandomCountry() { | |
| const countries = [...new Set(parsedData.map(row => row.country))]; | |
| return countries[Math.floor(Math.random() * countries.length)]; | |
| } | |
| // Helper function to get a random date from the data | |
| function getRandomDate() { | |
| const dates = [...new Set(parsedData.map(row => row.date))]; | |
| return dates[Math.floor(Math.random() * dates.length)]; | |
| } | |
| // Sample data download | |
| sampleDataBtn.addEventListener('click', () => { | |
| // Create sample CSV data | |
| const sampleData = [ | |
| ['date', 'country', 'campaign', 'spend', 'installs', 'revenue'], | |
| ['2023-06-15', 'United States', 'Summer Sale', '1245.67', '876', '3456.78'], | |
| ['2023-06-15', 'Germany', 'Premium Offer', '987.32', '543', '1234.56'], | |
| ['2023-06-14', 'Japan', 'New Feature', '2345.89', '721', '4567.89'], | |
| ['2023-06-14', 'Brazil', 'Video Campaign', '876.54', '1023', '2345.67'], | |
| ['2023-06-13', 'France', 'Mobile Promo', '765.43', '432', '1234.56'], | |
| ['2023-06-13', 'United Kingdom', 'App Install', '654.32', '321', '987.65'] | |
| ]; | |
| // Convert to CSV string | |
| const csvContent = sampleData.map(row => row.join(',')).join('\n'); | |
| // Create download link | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.setAttribute('href', url); | |
| link.setAttribute('download', 'sample_fb_ads_data.csv'); | |
| link.style.visibility = 'hidden'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| }); | |
| </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=Volodya2000/mas" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |