rafmacalaba's picture
Initial deploy of PRWP Dataset Extraction Dashboard
5567216 verified
Raw
History Blame Contribute Delete
51.9 kB
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRWP Dataset Extraction Dashboard</title>
<!-- Inline theme initializer to prevent layout flashing -->
<script>
(function() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#f0f7fd',
100: '#e0effb',
200: '#bce0f7',
300: '#82c4f2',
400: '#40a5eb',
500: '#0071bc', // WBG Primary Blue
600: '#005e9e', // WBG Medium Blue
700: '#004a7d',
800: '#003e69',
900: '#002244', // WBG Deep Navy
}
}
}
}
}
</script>
<!-- Google Fonts: Fira Sans & Fira Code -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Vis.js CDN (for Graph Database visualization) -->
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
body {
font-family: 'Fira Sans', sans-serif;
}
.font-technical {
font-family: 'Fira Code', monospace;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
html.dark ::-webkit-scrollbar-track {
background: #070e1e;
}
html:not(.dark) ::-webkit-scrollbar-track {
background: #f8fafc;
}
html.dark ::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 3px;
}
html:not(.dark) ::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
html.dark ::-webkit-scrollbar-thumb:hover {
background: #334155;
}
html:not(.dark) ::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 dark:bg-[#070e1e] dark:text-slate-200 min-h-full flex flex-col antialiased transition-colors duration-200">
<!-- HUD Header -->
<header class="bg-white/80 dark:bg-slate-950/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-900 sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="h-9 w-9 bg-gradient-to-tr from-brand-600 to-blue-500 rounded-lg flex items-center justify-center shadow-lg shadow-brand-500/10">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 002 2h2a2 2 0 002-2z" />
</svg>
</div>
<div>
<h1 class="text-md font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-600 dark:from-white dark:to-slate-400 font-technical">PRWP Data Use Extraction</h1>
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Theme Toggle Button -->
<button id="theme-toggle" onclick="toggleTheme()" class="p-1.5 rounded-lg border border-slate-200 dark:border-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-900 transition flex items-center justify-center cursor-pointer shadow-sm" aria-label="Toggle theme">
<!-- Sun Icon (shown in dark mode) -->
<svg id="theme-sun" class="w-4 h-4 hidden" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
<!-- Moon Icon (shown in light mode) -->
<svg id="theme-moon" class="w-4 h-4 hidden" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</button>
<a href="https://huggingface.co/ai4data/datause-extraction" target="_blank" rel="noopener noreferrer" class="inline-flex items-center px-2.5 py-0.5 rounded border border-brand-500/30 bg-brand-500/5 text-[10px] font-medium text-brand-600 dark:text-brand-400 font-technical hover:bg-brand-500/10 hover:border-brand-500/50 transition">
MODEL: ai4data/datause-extraction
</a>
</div>
</div>
</header>
<!-- Main Container -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 flex-1 w-full space-y-8">
<!-- HUD Tab Controls -->
<div class="border-b border-slate-200 dark:border-slate-900">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button id="tab-dashboard-btn" onclick="switchTab('dashboard')" class="border-brand-500 text-brand-600 dark:text-brand-400 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical">
<span>[ Overview Dashboard ]</span>
</button>
<button id="tab-graph-btn" onclick="switchTab('graph')" class="border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:border-slate-300 dark:hover:border-slate-800 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical">
<span>[ Knowledge Graph Explorer ]</span>
</button>
</nav>
</div>
<!-- TAB 1: OVERVIEW DASHBOARD -->
<div id="tab-dashboard" class="space-y-8">
<!-- KPI Metrics Row -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- KPI 1 -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-5 shadow-sm relative overflow-hidden group hover:border-brand-500/40 hover:shadow-[0_0_15px_rgba(0,113,188,0.08)] transition-all duration-300 cursor-pointer">
<div class="absolute right-0 top-0 translate-x-2 -translate-y-2 w-16 h-16 bg-brand-600/5 rounded-full blur-xl group-hover:bg-brand-600/10 transition duration-300"></div>
<p class="text-[10px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">Total Papers</p>
<p id="kpi-total-papers" class="text-3xl font-bold mt-2 text-slate-900 dark:text-white font-technical">0</p>
</div>
<!-- KPI 2 -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-5 shadow-sm relative overflow-hidden group hover:border-brand-500/40 hover:shadow-[0_0_15px_rgba(0,113,188,0.08)] transition-all duration-300 cursor-pointer">
<div class="absolute right-0 top-0 translate-x-2 -translate-y-2 w-16 h-16 bg-emerald-500/5 rounded-full blur-xl group-hover:bg-emerald-500/10 transition duration-300"></div>
<p class="text-[10px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">Papers with Data</p>
<p id="kpi-papers-with-data" class="text-3xl font-bold mt-2 text-slate-900 dark:text-white font-technical">0</p>
<p class="text-[10px] text-slate-400 dark:text-slate-600 mt-2 font-technical"><span id="kpi-adoption-rate" class="text-emerald-600 dark:text-emerald-400 font-semibold">0%</span> Adoption</p>
</div>
<!-- KPI 3 -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-5 shadow-sm relative overflow-hidden group hover:border-brand-500/40 hover:shadow-[0_0_15px_rgba(0,113,188,0.08)] transition-all duration-300 cursor-pointer">
<div class="absolute right-0 top-0 translate-x-2 -translate-y-2 w-16 h-16 bg-violet-500/5 rounded-full blur-xl group-hover:bg-violet-500/10 transition duration-300"></div>
<p class="text-[10px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">Unique Datasets</p>
<p id="kpi-unique-datasets" class="text-3xl font-bold mt-2 text-slate-900 dark:text-white font-technical">0</p>
<p class="text-[10px] text-slate-400 dark:text-slate-600 mt-2 font-technical">Deduplicated</p>
</div>
<!-- KPI 4 -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-5 shadow-sm relative overflow-hidden group hover:border-brand-500/40 hover:shadow-[0_0_15px_rgba(0,113,188,0.08)] transition-all duration-300 cursor-pointer">
<div class="absolute right-0 top-0 translate-x-2 -translate-y-2 w-16 h-16 bg-cyan-500/5 rounded-full blur-xl group-hover:bg-cyan-500/10 transition duration-300"></div>
<p class="text-[10px] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">Avg Mentions</p>
<p id="kpi-avg-mentions" class="text-3xl font-bold mt-2 text-slate-900 dark:text-white font-technical">0.0</p>
<p class="text-[10px] text-slate-400 dark:text-slate-600 mt-2 font-technical">Per Data Paper</p>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Chart 1: Line Chart (Data Adoption Rate) -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-6 shadow-sm flex flex-col hover:border-slate-300 dark:hover:border-slate-800 transition duration-300">
<div class="flex items-center justify-between mb-4">
<h3 id="trends-chart-title" class="text-xs font-semibold text-slate-500 dark:text-slate-400 tracking-wider font-technical uppercase">// Data Adoption Rate (%)</h3>
<select id="trends-dataset-select" onchange="updateTrendsChart()" class="max-w-[180px] bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-850 text-slate-700 dark:text-slate-350 rounded px-2 py-1 text-[10px] font-technical focus:outline-none focus:border-brand-500 cursor-pointer">
<option value="all">All Datasets</option>
</select>
</div>
<div class="relative flex-1 min-h-[220px]">
<canvas id="chart-trends"></canvas>
</div>
</div>
<!-- Chart 2: Donut Chart (Typologies) -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-6 shadow-sm flex flex-col hover:border-slate-300 dark:hover:border-slate-800 transition duration-300">
<div class="flex items-center justify-between mb-4">
<h3 id="distribution-chart-title" class="text-xs font-semibold text-slate-500 dark:text-slate-400 tracking-wider font-technical uppercase">// Typology Distribution</h3>
<select id="distribution-select" onchange="updateDistributionChart()" class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-850 text-slate-700 dark:text-slate-350 rounded px-2 py-1 text-[10px] font-technical focus:outline-none focus:border-brand-500 cursor-pointer">
<option value="typology">Typology</option>
<option value="specificity">Specificity</option>
<option value="usage_context">Usage Context</option>
<option value="is_used">Usage Status</option>
</select>
</div>
<div class="relative flex-1 min-h-[220px] flex items-center justify-center">
<canvas id="chart-typologies"></canvas>
</div>
</div>
<!-- Chart 3: Horizontal Bar Chart (Top Datasets) -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-6 shadow-sm flex flex-col hover:border-slate-300 dark:hover:border-slate-800 transition duration-300">
<h3 class="text-xs font-semibold text-slate-500 dark:text-slate-400 tracking-wider mb-4 font-technical uppercase">// Top 10 Datasets (Doc Frequency)</h3>
<div class="relative flex-1 min-h-[220px]">
<canvas id="chart-top-datasets"></canvas>
</div>
</div>
</div>
<!-- Dataset Explorer Table Section -->
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl shadow-sm overflow-hidden hover:border-slate-300 dark:hover:border-slate-800 transition duration-300">
<div class="px-6 py-5 border-b border-slate-200 dark:border-slate-900 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 bg-slate-100/50 dark:bg-slate-950/20">
<div>
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200 font-technical uppercase">// Dataset Entity Explorer</h3>
<p class="text-[10px] text-slate-500 mt-0.5">Explore deduplicated dataset names and click to see referencing research papers</p>
</div>
<div class="flex items-center gap-3">
<input type="text" id="search-input" oninput="handleSearch()" placeholder="Search datasets or acronyms..." class="w-64 bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-900 rounded-lg px-3.5 py-1.5 text-xs text-slate-800 dark:text-slate-300 focus:outline-none focus:border-brand-500 transition font-technical">
<select id="typology-filter" onchange="handleFilter()" class="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-900 rounded-lg px-3.5 py-1.5 text-xs text-slate-800 dark:text-slate-300 focus:outline-none focus:border-brand-500 transition font-technical">
<option value="All">All Typologies</option>
<option value="Survey">Survey</option>
<option value="Administrative Data">Administrative Data</option>
<option value="Census">Census</option>
<option value="Geospatial/Satellite">Geospatial/Satellite</option>
<option value="Macro/Aggregate Indicators">Macro/Aggregate Indicators</option>
<option value="Unknown">Unknown</option>
</select>
</div>
</div>
<!-- Table Element -->
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-100/80 dark:bg-slate-950/50 border-b border-slate-200 dark:border-slate-900 text-[10px] font-semibold text-slate-500 uppercase tracking-wider font-technical">
<th class="py-3 px-6">Dataset Name</th>
<th class="py-3 px-6">Acronym</th>
<th class="py-3 px-6">Typology</th>
<th class="py-3 px-6">Producer</th>
<th class="py-3 px-6">Geography</th>
<th class="py-3 px-6 text-right">Doc Freq (DF)</th>
<th class="py-3 px-6 text-right">Total Mentions</th>
</tr>
</thead>
<tbody id="table-body" class="divide-y divide-slate-100 dark:divide-slate-900 text-xs text-slate-600 dark:text-slate-300">
<!-- JS Injected Rows -->
</tbody>
</table>
</div>
<!-- Table Pagination -->
<div class="bg-slate-100/40 dark:bg-slate-950/40 px-6 py-4 border-t border-slate-200 dark:border-slate-900 flex items-center justify-between font-technical">
<span class="text-[10px] text-slate-500" id="pagination-info">Showing 1 to 10 of 0 datasets</span>
<div class="flex items-center space-x-2">
<button id="btn-prev" onclick="prevPage()" class="px-3 py-1.5 rounded bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-900 text-[10px] text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-900 disabled:opacity-50 disabled:hover:bg-white" disabled>Previous</button>
<button id="btn-next" onclick="nextPage()" class="px-3 py-1.5 rounded bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-900 text-[10px] text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-900 disabled:opacity-50 disabled:hover:bg-white">Next</button>
</div>
</div>
</div>
</div>
<!-- TAB 2: KNOWLEDGE GRAPH EXPLORER -->
<div id="tab-graph" class="hidden space-y-6">
<div class="bg-white dark:bg-slate-900/30 border border-slate-200 dark:border-slate-900 rounded-xl p-6 shadow-sm space-y-4 hover:border-slate-300 dark:hover:border-slate-800 transition duration-300">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h3 class="text-sm font-semibold text-slate-850 dark:text-slate-200 font-technical uppercase">// Interactive Knowledge Graph Explorer</h3>
<p class="text-xs text-slate-500 mt-0.5">Visualize the linkages between research papers (amber diamonds) and canonical datasets (indigo dots)</p>
</div>
<div class="flex items-center space-x-2 font-technical text-xs">
<span class="text-slate-500 dark:text-slate-400">SHOW TOP:</span>
<select id="graph-limit-select" onchange="updateGraphLimit()" class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-850 text-slate-800 dark:text-slate-200 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-brand-500 focus:outline-none">
<option value="10">Top 10 Datasets</option>
<option value="20">Top 20 Datasets</option>
<option value="30" selected>Top 30 Datasets</option>
<option value="50">Top 50 Datasets</option>
<option value="100">Top 100 Datasets</option>
</select>
</div>
</div>
<!-- Network Container -->
<div class="relative bg-slate-50/50 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-900 rounded-xl overflow-hidden h-[600px] flex">
<div id="graph-network" class="flex-1 h-full"></div>
<!-- Graph Legend -->
<div class="absolute bottom-4 left-4 bg-white/95 dark:bg-slate-900/90 backdrop-blur border border-slate-200 dark:border-slate-800 rounded-lg p-3.5 space-y-2 text-[9px] text-slate-650 dark:text-slate-400 z-10 font-technical">
<p class="font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-500 mb-1">// Graph Legend</p>
<div class="flex items-center space-x-2">
<div class="w-3.5 h-3.5 rounded-full bg-brand-500 border border-brand-400"></div>
<span>Dataset Node</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-3.5 h-3.5 rounded bg-amber-500 border border-amber-400 transform rotate-45"></div>
<span>Paper Node</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-4 h-0.5 bg-slate-300 dark:bg-slate-700"></div>
<span>MENTIONS Edge</span>
</div>
</div>
<!-- Network Panel Info -->
<div class="absolute top-4 right-4 bg-white/95 dark:bg-slate-900/95 backdrop-blur border border-slate-200 dark:border-slate-800 rounded-lg p-4 w-72 text-xs text-slate-750 dark:text-slate-300 z-10 shadow-xl hidden font-technical" id="graph-node-details">
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-2 mb-2">
<h4 id="graph-node-title" class="font-bold text-slate-900 dark:text-white break-words">Node Info</h4>
<button onclick="document.getElementById('graph-node-details').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-400"></button>
</div>
<div class="space-y-2 text-[11px]" id="graph-node-body">
<!-- Dynamically Populated -->
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Interactive Slide Drawer for Dataset Citations -->
<div id="drawer" class="fixed inset-y-0 right-0 max-w-full flex pl-10 z-50 transform translate-x-full transition duration-300 ease-in-out">
<div class="w-screen max-w-md">
<div class="h-full flex flex-col bg-white dark:bg-slate-900 border-l border-slate-200 dark:border-slate-800 shadow-2xl">
<!-- Header -->
<div class="px-6 py-5 bg-slate-50 dark:bg-slate-950 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between font-technical">
<div>
<h2 id="drawer-title" class="text-sm font-bold text-slate-900 dark:text-white leading-6 break-words">Dataset Details</h2>
<p id="drawer-acronym" class="text-xs text-brand-600 dark:text-brand-400 font-medium uppercase mt-0.5"></p>
</div>
<button onclick="closeDrawer()" class="text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-400 p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto px-6 py-5 space-y-6">
<!-- Metadata Attributes -->
<div class="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-950 p-4 border border-slate-200 dark:border-slate-800 rounded-xl text-[11px] font-technical">
<div>
<p class="text-slate-500 dark:text-slate-400">Typology</p>
<p id="drawer-typology" class="font-semibold text-slate-850 dark:text-slate-200 mt-1">Unknown</p>
</div>
<div>
<p class="text-slate-500 dark:text-slate-400">Producer</p>
<p id="drawer-producer" class="font-semibold text-slate-850 dark:text-slate-200 mt-1 break-words">Unknown</p>
</div>
<div class="mt-2">
<p class="text-slate-500 dark:text-slate-400">Geography</p>
<p id="drawer-geography" class="font-semibold text-slate-850 dark:text-slate-200 mt-1 break-words">Global</p>
</div>
<div class="mt-2">
<p class="text-slate-500 dark:text-slate-400">Doc Frequency</p>
<p id="drawer-df" class="font-semibold text-slate-900 dark:text-white mt-1">0 papers</p>
</div>
</div>
<!-- Consolidated Variants -->
<div class="space-y-3">
<h3 class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">// Consolidated Variants (<span id="drawer-variant-count">0</span>)</h3>
<div id="drawer-variants-list" class="flex flex-wrap gap-2 max-h-40 overflow-y-auto p-3 bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl">
<!-- JS Injected Variants -->
</div>
</div>
<!-- Citing Papers Header -->
<div class="space-y-3">
<h3 class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider font-technical">// Referencing Papers (<span id="drawer-paper-count">0</span>)</h3>
<div id="drawer-papers-list" class="space-y-4">
<!-- JS Injected Citing Papers -->
</div>
</div>
</div>
</div>
</div>
</div>
<div id="drawer-backdrop" onclick="closeDrawer()" class="fixed inset-0 bg-slate-950/60 backdrop-blur-sm z-40 hidden"></div>
<!-- Import variables from dashboard_data.js -->
<script src="data/dashboard_data.js"></script>
<script>
// Tab switching state
let activeTab = 'dashboard';
// Pagination & Search States
let currentPage = 1;
const itemsPerPage = 10;
let filteredData = [...DASHBOARD_DATA.datasets];
// Chart instances
let trendsChart = null;
let typologiesChart = null;
let topDatasetsChart = null;
// Load summary KPIs
document.getElementById('kpi-total-papers').textContent = DASHBOARD_DATA.summary.total_papers.toLocaleString();
document.getElementById('kpi-papers-with-data').textContent = DASHBOARD_DATA.summary.papers_with_data.toLocaleString();
document.getElementById('kpi-adoption-rate').textContent = DASHBOARD_DATA.summary.data_adoption_rate + '%';
const namedMentions = DASHBOARD_DATA.summary.named_mentions ?? DASHBOARD_DATA.summary.total_mentions ?? 0;
document.getElementById('kpi-unique-datasets').textContent = DASHBOARD_DATA.summary.unique_canonical_datasets.toLocaleString();
const papersWithData = DASHBOARD_DATA.summary.papers_with_data;
const avgMentions = papersWithData > 0
? (namedMentions / papersWithData).toFixed(1)
: '0.0';
document.getElementById('kpi-avg-mentions').textContent = avgMentions;
// Populate trends dataset select dropdown
const trendsSelect = document.getElementById('trends-dataset-select');
const topDatasetsForSelect = DASHBOARD_DATA.datasets.slice(0, 15);
topDatasetsForSelect.forEach(ds => {
const opt = document.createElement('option');
opt.value = ds.canonical_name;
opt.textContent = ds.canonical_name.length > 25 ? ds.canonical_name.slice(0, 25) + '...' : ds.canonical_name;
trendsSelect.appendChild(opt);
});
// --- Chart Initializations ---
const isDarkTheme = document.documentElement.classList.contains('dark');
const chartGridColor = isDarkTheme ? '#1e293b' : '#e2e8f0';
const chartTextColor = isDarkTheme ? '#94a3b8' : '#475569';
const chartBorderColor = isDarkTheme ? '#070e1e' : '#ffffff';
const trendsCtx = document.getElementById('chart-trends').getContext('2d');
const trendsYears = DASHBOARD_DATA.annual_trends.map(t => t.year);
const trendsValues = DASHBOARD_DATA.annual_trends.map(t => t.percentage);
trendsChart = new Chart(trendsCtx, {
type: 'line',
data: {
labels: trendsYears,
datasets: [{
label: 'Adoption Rate (%)',
data: trendsValues,
borderColor: '#0071bc',
borderWidth: 2,
pointBackgroundColor: '#0d9488',
tension: 0.35,
fill: true,
backgroundColor: function(context) {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) return null;
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, 'rgba(0, 113, 188, 0.2)');
gradient.addColorStop(1, 'rgba(0, 113, 188, 0)');
return gradient;
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: chartGridColor }, ticks: { color: chartTextColor, font: { size: 9, family: 'Fira Code' } } },
y: { grid: { color: chartGridColor }, ticks: { color: chartTextColor, font: { size: 9, family: 'Fira Code' } }, min: 0 }
}
}
});
const typologiesCtx = document.getElementById('chart-typologies').getContext('2d');
const typLabels = DASHBOARD_DATA.typology_distribution.map(d => d.name);
const typValues = DASHBOARD_DATA.typology_distribution.map(d => d.value);
typologiesChart = new Chart(typologiesCtx, {
type: 'doughnut',
data: {
labels: typLabels,
datasets: [{
data: typValues,
backgroundColor: ['#0071bc', '#0d9488', '#8b5cf6', '#3b82f6', '#f59e0b', '#10b981', '#64748b', '#ec4899'],
borderWidth: 1.5,
borderColor: chartBorderColor
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: chartTextColor, boxWidth: 10, font: { size: 8, family: 'Fira Sans' } }
}
},
cutout: '70%'
}
});
const topCtx = document.getElementById('chart-top-datasets').getContext('2d');
const topDatasets = DASHBOARD_DATA.datasets.slice(0, 10);
const topLabels = topDatasets.map(d => d.canonical_name.length > 22 ? d.canonical_name.slice(0, 22) + '...' : d.canonical_name);
const topValues = topDatasets.map(d => d.document_frequency);
topDatasetsChart = new Chart(topCtx, {
type: 'bar',
data: {
labels: topLabels,
datasets: [{
data: topValues,
backgroundColor: 'rgba(0, 113, 188, 0.75)',
hoverBackgroundColor: '#0071bc',
borderRadius: 4
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
left: 15
}
},
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: chartGridColor }, ticks: { color: chartTextColor, font: { size: 9, family: 'Fira Code' } } },
y: {
grid: { display: false },
ticks: {
color: chartTextColor,
font: { size: 9, family: 'Fira Sans' },
padding: 8
}
}
}
}
});
// --- Table Filtering & Rendering ---
function renderTable() {
const tbody = document.getElementById('table-body');
tbody.innerHTML = '';
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length);
const pageData = filteredData.slice(startIndex, endIndex);
document.getElementById('pagination-info').textContent = filteredData.length > 0
? `Showing ${startIndex + 1} to ${endIndex} of ${filteredData.length} datasets`
: 'Showing 0 of 0 datasets';
document.getElementById('btn-prev').disabled = currentPage === 1;
document.getElementById('btn-next').disabled = endIndex >= filteredData.length;
if (pageData.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" class="py-8 text-center text-slate-500 font-technical">No matching datasets found.</td></tr>`;
return;
}
pageData.forEach(ds => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-slate-100/80 dark:hover:bg-slate-900/60 cursor-pointer border-b border-slate-100 dark:border-slate-900/40 transition duration-150';
tr.onclick = () => openDrawer(ds);
tr.innerHTML = `
<td class="py-3.5 px-6 font-semibold text-slate-800 dark:text-slate-200">${ds.canonical_name}</td>
<td class="py-3.5 px-6 text-brand-600 dark:text-brand-400 font-technical font-medium">${ds.acronym || '-'}</td>
<td class="py-3.5 px-6"><span class="px-2 py-0.5 rounded text-[10px] bg-slate-100 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 font-technical">${ds.typology}</span></td>
<td class="py-3.5 px-6 text-slate-600 dark:text-slate-400">${ds.producer}</td>
<td class="py-3.5 px-6 text-slate-600 dark:text-slate-400">${ds.geography}</td>
<td class="py-3.5 px-6 text-right font-bold text-slate-900 dark:text-white font-technical">${ds.document_frequency}</td>
<td class="py-3.5 px-6 text-right font-medium text-slate-450 dark:text-slate-500 font-technical">${ds.mention_frequency}</td>
`;
tbody.appendChild(tr);
});
}
function handleSearch() {
const q = document.getElementById('search-input').value.toLowerCase().trim();
const typ = document.getElementById('typology-filter').value;
filteredData = DASHBOARD_DATA.datasets.filter(ds => {
const matchesQuery = ds.canonical_name.toLowerCase().includes(q) || (ds.acronym && ds.acronym.toLowerCase().includes(q));
const matchesTypology = typ === 'All' || ds.typology === typ;
return matchesQuery && matchesTypology;
});
currentPage = 1;
renderTable();
}
function handleFilter() {
handleSearch();
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable();
}
}
function nextPage() {
if ((currentPage * itemsPerPage) < filteredData.length) {
currentPage++;
renderTable();
}
}
// --- Drawer Operations ---
function openDrawer(dataset) {
document.getElementById('drawer-title').textContent = dataset.canonical_name;
document.getElementById('drawer-acronym').textContent = dataset.acronym ? `(${dataset.acronym})` : '';
document.getElementById('drawer-typology').textContent = dataset.typology;
document.getElementById('drawer-producer').textContent = dataset.producer;
document.getElementById('drawer-geography').textContent = dataset.geography;
document.getElementById('drawer-df').textContent = dataset.document_frequency + ' papers';
document.getElementById('drawer-paper-count').textContent = dataset.papers.length;
const papersContainer = document.getElementById('drawer-papers-list');
papersContainer.innerHTML = '';
const variantsContainer = document.getElementById('drawer-variants-list');
variantsContainer.innerHTML = '';
document.getElementById('drawer-variant-count').textContent = dataset.variants ? dataset.variants.length : 0;
if (dataset.variants && dataset.variants.length > 0) {
dataset.variants.forEach(v => {
const chip = document.createElement('span');
chip.className = 'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-medium bg-slate-100 dark:bg-slate-900 text-slate-800 dark:text-slate-350 border border-slate-200 dark:border-slate-800';
chip.innerHTML = `${v.name} <span class="bg-slate-200 dark:bg-slate-800 px-1.5 py-0.5 rounded-full text-[9px] text-slate-500 dark:text-slate-400 font-technical">${v.count}</span>`;
variantsContainer.appendChild(chip);
});
} else {
variantsContainer.innerHTML = '<span class="text-xs text-slate-400">No variants consolidated</span>';
}
dataset.papers.forEach(p => {
const div = document.createElement('div');
div.className = 'bg-slate-50 dark:bg-slate-950 p-4 border border-slate-200 dark:border-slate-900 rounded-xl space-y-2 hover:border-slate-300 dark:hover:border-slate-800 transition duration-150';
const pagesBadges = p.pages.map(pg => `<span class="px-1.5 py-0.5 rounded text-[9px] bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 border border-brand-200 dark:border-brand-500/20 font-technical">p.${pg}</span>`).join(' ');
const titleHtml = p.pdf_url
? `<a href="${p.pdf_url}" target="_blank" class="hover:underline text-brand-600 dark:text-brand-450 block"><h4 class="text-xs font-bold text-slate-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-450 transition cursor-pointer leading-relaxed">${p.title}</h4></a>`
: `<h4 class="text-xs font-bold text-slate-900 dark:text-white leading-relaxed">${p.title}</h4>`;
div.innerHTML = `
<div class="flex items-start justify-between gap-2 font-technical">
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] bg-slate-200/50 dark:bg-slate-900 border border-slate-300/50 dark:border-slate-800 text-slate-600 dark:text-slate-500">ID: ${p.id}</span>
<span class="text-[10px] font-semibold text-slate-500">${p.year}</span>
</div>
${titleHtml}
<p class="text-[10px] text-slate-500 dark:text-slate-400 font-technical">AUTHORS: ${p.authors}</p>
<div class="flex items-center space-x-1.5 pt-1.5">
<span class="text-[9px] text-slate-650 dark:text-slate-600 font-semibold mr-1 font-technical">PAGES:</span>
${pagesBadges}
</div>
`;
papersContainer.appendChild(div);
});
document.getElementById('drawer').classList.remove('translate-x-full');
document.getElementById('drawer-backdrop').classList.remove('hidden');
}
function closeDrawer() {
document.getElementById('drawer').classList.add('translate-x-full');
document.getElementById('drawer-backdrop').classList.add('hidden');
}
// --- Tab Switcher ---
let network = null;
function switchTab(tab) {
activeTab = tab;
const dashboardTab = document.getElementById('tab-dashboard');
const graphTab = document.getElementById('tab-graph');
const dashboardBtn = document.getElementById('tab-dashboard-btn');
const graphBtn = document.getElementById('tab-graph-btn');
if (tab === 'dashboard') {
dashboardTab.classList.remove('hidden');
graphTab.classList.add('hidden');
dashboardBtn.className = 'border-brand-500 text-brand-600 dark:text-brand-400 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical';
graphBtn.className = 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:border-slate-300 dark:hover:border-slate-800 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical';
} else {
dashboardTab.classList.add('hidden');
graphTab.classList.remove('hidden');
dashboardBtn.className = 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:border-slate-300 dark:hover:border-slate-800 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical';
graphBtn.className = 'border-brand-500 text-brand-600 dark:text-brand-400 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 font-technical';
if (!network) {
setTimeout(initGraph, 100);
}
}
}
let graphLimit = 30;
function updateGraphLimit() {
const select = document.getElementById('graph-limit-select');
graphLimit = parseInt(select.value, 10);
initGraph();
}
function updateDistributionChart() {
const select = document.getElementById('distribution-select');
const val = select.value;
let labels = [];
let values = [];
let titleText = '';
if (val === 'typology') {
labels = DASHBOARD_DATA.typology_distribution.map(d => d.name);
values = DASHBOARD_DATA.typology_distribution.map(d => d.value);
titleText = '// Typology Distribution';
} else if (val === 'specificity') {
labels = DASHBOARD_DATA.specificity_distribution.map(d => d.name);
values = DASHBOARD_DATA.specificity_distribution.map(d => d.value);
titleText = '// Specificity Distribution';
} else if (val === 'usage_context') {
labels = DASHBOARD_DATA.usage_context_distribution.map(d => d.name);
values = DASHBOARD_DATA.usage_context_distribution.map(d => d.value);
titleText = '// Usage Context Distribution';
} else if (val === 'is_used') {
labels = DASHBOARD_DATA.is_used_distribution.map(d => d.name);
values = DASHBOARD_DATA.is_used_distribution.map(d => d.value);
titleText = '// Usage Status Distribution';
}
document.getElementById('distribution-chart-title').textContent = titleText;
if (typologiesChart) {
typologiesChart.data.labels = labels;
typologiesChart.data.datasets[0].data = values;
typologiesChart.update();
}
}
function updateTrendsChart() {
const select = document.getElementById('trends-dataset-select');
const val = select.value;
let titleText = '';
let yValues = [];
let labelName = '';
if (val === 'all') {
yValues = DASHBOARD_DATA.annual_trends.map(t => t.percentage);
titleText = '// Data Adoption Rate (%)';
labelName = 'Adoption Rate (%)';
} else {
const dataset = DASHBOARD_DATA.datasets.find(d => d.canonical_name === val);
if (dataset) {
const paperYears = dataset.papers.map(p => p.year);
const yearCounts = {};
paperYears.forEach(yr => {
yearCounts[yr] = (yearCounts[yr] || 0) + 1;
});
yValues = DASHBOARD_DATA.annual_trends.map(t => {
const count = yearCounts[t.year] || 0;
const pct = t.total_papers > 0 ? (count / t.total_papers * 100) : 0;
return parseFloat(pct.toFixed(2));
});
titleText = `// ${dataset.acronym || dataset.canonical_name.slice(0, 15)} Adoption Rate (%)`;
labelName = `${dataset.acronym || 'Dataset'} Share (%)`;
}
}
document.getElementById('trends-chart-title').textContent = titleText;
if (trendsChart) {
trendsChart.data.datasets[0].data = yValues;
trendsChart.data.datasets[0].label = labelName;
trendsChart.update();
}
}
function makeTooltip(html) {
const el = document.createElement('div');
el.className = 'font-technical text-[10px] p-2 leading-relaxed bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200 border border-slate-250 dark:border-slate-700 rounded shadow-md max-w-xs';
el.innerHTML = html;
return el;
}
// --- Vis.js Graph Network Explorer ---
function initGraph() {
const container = document.getElementById('graph-network');
// Destroy previous network to prevent double-initialization and memory leak
if (network) {
network.destroy();
network = null;
}
const topDatasets = DASHBOARD_DATA.datasets.slice(0, graphLimit);
const nodesArray = [];
const edgesArray = [];
const datasetIds = new Set();
const paperIds = new Set();
const isDark = document.documentElement.classList.contains('dark');
const nodeFontColor = isDark ? '#cbd5e1' : '#0f172a';
const edgeColor = isDark ? '#1e293b' : '#cbd5e1';
const edgeHighlight = isDark ? '#475569' : '#94a3b8';
topDatasets.forEach(ds => {
const dsId = 'dataset_' + ds.canonical_name.replace(/[^\w]/g, '_');
datasetIds.add(dsId);
nodesArray.push({
id: dsId,
label: ds.acronym || ds.canonical_name.split(' ').slice(0, 3).join(' ') + '...',
title: makeTooltip(`<b>Dataset</b>: ${ds.canonical_name}<br><b>Typology</b>: ${ds.typology}<br><b>DF</b>: ${ds.document_frequency}`),
shape: 'dot',
size: Math.max(15, Math.min(30, ds.document_frequency * 2.5)),
color: {
background: '#0071bc',
border: '#40a5eb',
highlight: { background: '#005e9e', border: '#0071bc' }
},
font: { color: nodeFontColor, size: 10, face: 'Fira Code' },
borderWidth: 2,
type: 'dataset',
meta: ds
});
ds.papers.slice(0, 3).forEach(p => {
const pId = 'paper_' + p.id;
if (!paperIds.has(pId)) {
paperIds.add(pId);
nodesArray.push({
id: pId,
label: 'Paper: ' + p.id,
title: makeTooltip(`<b>Paper Title</b>: ${p.title}<br><b>Year</b>: ${p.year}<br><b>Authors</b>: ${p.authors}`),
shape: 'diamond',
size: 10,
color: {
background: '#f59e0b',
border: '#fbbf24',
highlight: { background: '#d97706', border: '#f59e0b' }
},
font: { color: isDark ? '#94a3b8' : '#475569', size: 8, face: 'Fira Code' },
borderWidth: 1.5,
type: 'paper',
meta: p
});
}
edgesArray.push({
from: pId,
to: dsId,
color: { color: edgeColor, highlight: edgeHighlight },
width: 1,
length: 120
});
});
});
const nodes = new vis.DataSet(nodesArray);
const edges = new vis.DataSet(edgesArray);
const data = { nodes: nodes, edges: edges };
const options = {
interaction: {
hover: true,
tooltipDelay: 200,
hideEdgesOnDrag: true
},
physics: {
stabilization: {
enabled: true,
iterations: 150
},
barnesHut: {
gravitationalConstant: -2000,
centralGravity: 0.35,
springLength: 95,
springConstant: 0.04
}
}
};
network = new vis.Network(container, data, options);
network.on("selectNode", function (params) {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0];
const node = nodes.get(nodeId);
const detailPanel = document.getElementById('graph-node-details');
const detailTitle = document.getElementById('graph-node-title');
const detailBody = document.getElementById('graph-node-body');
detailPanel.classList.remove('hidden');
if (node.type === 'dataset') {
const ds = node.meta;
detailTitle.textContent = ds.acronym ? `${ds.canonical_name} (${ds.acronym})` : ds.canonical_name;
const variantsHtml = (ds.variants && ds.variants.length > 0)
? ds.variants.map(v => `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 text-[9px] text-slate-700 dark:text-slate-350 font-medium">${v.name} <span class="text-[8px] bg-slate-200 dark:bg-slate-850 text-slate-500 px-1 rounded">${v.count}</span></span>`).join(' ')
: '<span class="text-xs text-slate-450 dark:text-slate-500">None</span>';
detailBody.innerHTML = `
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Node Class</span><span class="font-bold text-brand-600 dark:text-brand-400">Dataset Entity</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Typology</span><span class="text-slate-700 dark:text-slate-200">${ds.typology}</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Producer</span><span class="text-slate-700 dark:text-slate-200 block break-words">${ds.producer}</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Geography</span><span class="text-slate-700 dark:text-slate-200 block break-words">${ds.geography}</span></div>
<div class="pt-2 border-t border-slate-200 dark:border-slate-900"><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Document Frequency</span><span class="text-slate-900 dark:text-white text-sm font-bold">${ds.document_frequency} unique papers</span></div>
<div class="pt-2 border-t border-slate-200 dark:border-slate-900">
<span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px] mb-1">Consolidated Variants</span>
<div class="flex flex-wrap gap-1 max-h-24 overflow-y-auto">${variantsHtml}</div>
</div>
`;
} else if (node.type === 'paper') {
const p = node.meta;
detailTitle.textContent = `Paper ID: ${p.id}`;
detailBody.innerHTML = `
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Node Class</span><span class="font-bold text-amber-600 dark:text-amber-500">Paper Reference</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Title</span><span class="text-slate-900 dark:text-white font-medium block leading-relaxed">${p.pdf_url ? `<a href="${p.pdf_url}" target="_blank" class="text-brand-600 dark:text-brand-400 hover:underline transition">${p.title}</a>` : p.title}</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Year</span><span class="text-slate-700 dark:text-slate-200">${p.year}</span></div>
<div><span class="text-slate-500 dark:text-slate-400 block uppercase tracking-wider text-[9px]">Authors</span><span class="text-slate-600 dark:text-slate-350 block break-words">${p.authors}</span></div>
`;
}
}
});
network.on("deselectNode", function () {
document.getElementById('graph-node-details').classList.add('hidden');
});
}
// --- Theme Controller Functions ---
function updateThemeUI(isDark) {
const sunIcon = document.getElementById('theme-sun');
const moonIcon = document.getElementById('theme-moon');
if (isDark) {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
} else {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
}
}
function updateChartsTheme(isDark) {
const gridColor = isDark ? '#1e293b' : '#e2e8f0';
const textColor = isDark ? '#94a3b8' : '#475569';
const chartBg = isDark ? '#070e1e' : '#ffffff';
if (trendsChart) {
trendsChart.options.scales.x.grid.color = gridColor;
trendsChart.options.scales.x.ticks.color = textColor;
trendsChart.options.scales.y.grid.color = gridColor;
trendsChart.options.scales.y.ticks.color = textColor;
trendsChart.update();
}
if (typologiesChart) {
typologiesChart.options.plugins.legend.labels.color = textColor;
typologiesChart.data.datasets[0].borderColor = chartBg;
typologiesChart.update();
}
if (topDatasetsChart) {
topDatasetsChart.options.scales.x.grid.color = gridColor;
topDatasetsChart.options.scales.x.ticks.color = textColor;
topDatasetsChart.options.scales.y.ticks.color = textColor;
topDatasetsChart.update();
}
}
function toggleTheme() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
updateThemeUI(isDark);
updateChartsTheme(isDark);
if (network) {
initGraph();
}
}
// Initialize UI theme states
const isInitialDark = document.documentElement.classList.contains('dark');
updateThemeUI(isInitialDark);
updateChartsTheme(isInitialDark);
renderTable();
</script>
</body>
</html>