mas / index.html
Volodya2000's picture
Add 2 files
8f269d7 verified
Raw History Blame Contribute Delete
54.8 kB
<!DOCTYPE html>
<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>