ksei / index.html
alterzick's picture
Add 3 files
c080690 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Stock Ownership Analytics Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.chart-container {
position: relative;
height: 350px;
width: 100%;
}
.loading-spinner {
border-top-color: #3498db;
animation: spinner 1.2s linear infinite;
}
@keyframes spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.database-item {
transition: all 0.2s;
}
.database-item:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen">
<!-- Navbar -->
<nav class="bg-white shadow-lg sticky top-0 z-50 border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<div class="text-2xl font-bold text-indigo-600 flex items-center gap-2">
<i class="fas fa-chart-line"></i>
<span>StockOwnership Insights</span>
</div>
</div>
<div class="hidden md:flex space-x-8">
<a href="#dashboard" class="text-gray-700 hover:text-indigo-600 font-medium">Dashboard</a>
<a href="#uploader" class="text-gray-700 hover:text-indigo-600 font-medium">Upload Data</a>
<a href="#database" class="text-gray-700 hover:text-indigo-600 font-medium">Database</a>
<a href="#download" class="text-gray-700 hover:text-indigo-600 font-medium">Export</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="py-16 px-6 text-center bg-gradient-to-r from-indigo-600 to-purple-600 text-white">
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl md:text-5xl font-bold mb-6 leading-tight">
Analisis Kepemilikan Saham
</h1>
<p class="text-lg md:text-xl opacity-90 mb-10">
Pantau pertumbuhan kepemilikan saham oleh investor lokal, asing, dan institusi secara real-time.
Ekstrak data dari KSEI dan analisis dalam dashboard interaktif.
</p>
<button onclick="scrollToSection('uploader')" class="bg-white text-indigo-700 font-semibold px-8 py-3 rounded-full shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center gap-2 mx-auto">
<i class="fas fa-file-import"></i> Mulai Analisis
</button>
</div>
</section>
<!-- Upload Section -->
<section id="uploader" class="py-16 px-6 bg-white">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-3xl font-bold text-gray-800 mb-4">Unggah Data KSEI</h2>
<p class="text-gray-600">
Unggah file CSV yang diunduh dari <a href="https://web.ksei.co.id/Download/" target="_blank" class="text-indigo-600 hover:underline">KSEI</a>.
Format yang didukung: CSV dengan pembatas <strong>|</strong> dan header <strong>Ticker|Nama Saham|Lokal|Asing|Institusi|Tanggal</strong>.
</p>
</div>
<div id="upload-box" class="border-2 border-dashed border-gray-300 rounded-xl p-10 text-center hover:border-indigo-400 transition-colors duration-200 cursor-pointer relative overflow-hidden group">
<input type="file" id="csv-upload" accept=".csv" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" onchange="handleFileSelect(event)" />
<div class="flex flex-col items-center">
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 group-hover:text-indigo-500 transition-colors duration-200"></i>
<p class="mt-4 text-gray-600 font-medium">Seret dan lepas file CSV, atau klik untuk memilih</p>
<p class="text-sm text-gray-500 mt-2">Format: CSV dengan delimiter |, maks 10MB</p>
</div>
</div>
<div id="upload-meta" class="mt-6 bg-gray-50 p-6 rounded-xl hidden">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Informasi Data</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Bulan</label>
<select id="upload-month" class="w-full border border-gray-300 rounded-lg px-3 py-2">
<option value="1">Januari</option>
<option value="2">Februari</option>
<option value="3">Maret</option>
<option value="4">April</option>
<option value="5">Mei</option>
<option value="6">Juni</option>
<option value="7">Juli</option>
<option value="8">Agustus</option>
<option value="9">September</option>
<option value="10">Oktober</option>
<option value="11">November</option>
<option value="12">Desember</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tahun</label>
<input type="number" id="upload-year" class="w-full border border-gray-300 rounded-lg px-3 py-2"
value="2023" min="2010" max="2030" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor Laporan</label>
<input type="number" id="upload-number" class="w-full border border-gray-300 rounded-lg px-3 py-2"
value="1" min="1" />
</div>
</div>
<div class="mt-4 flex justify-end">
<button onclick="saveToDatabase()" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
Simpan ke Database
</button>
</div>
</div>
<div id="loading" class="flex flex-col items-center justify-center mt-8 hidden">
<div class="w-16 h-16 border-4 border-gray-200 border-t-indigo-500 rounded-full loading-spinner"></div>
<p class="mt-4 text-gray-600">Memproses data...</p>
</div>
<div id="preview" class="mt-8 hidden">
<h3 class="text-xl font-semibold text-gray-800 mb-4">Pratinjau Data</h3>
<div class="overflow-x-auto bg-gray-50 rounded-lg border">
<table id="data-preview-table" class="min-w-full text-sm"></table>
</div>
<div class="mt-4 flex justify-end">
<button onclick="processData()" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
Proses Data
</button>
</div>
</div>
</div>
</section>
<!-- Database Section -->
<section id="database" class="py-16 px-6 bg-gray-50">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-3xl font-bold text-gray-800 mb-4">Database Laporan Kepemilikan</h2>
<p class="text-gray-600">Kelola dan akses laporan bulanan kepemilikan saham</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-10" id="database-container">
<!-- Database items will be inserted here -->
<div class="text-center py-10 text-gray-500">
<i class="fas fa-database text-5xl mb-4 opacity-30"></i>
<p>Belum ada data dalam database</p>
</div>
</div>
</div>
</section>
<!-- Dashboard Section -->
<section id="dashboard" class="py-16 px-6 bg-gray-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-3xl font-bold text-gray-800 mb-4">Dashboard Analitik</h2>
<p class="text-gray-600">Analisa perubahan kepemilikan saham secara mendalam</p>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-10">
<div class="bg-white p-6 rounded-xl shadow-md border">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<i class="fas fa-user-friends"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total Saham</p>
<p id="total-stocks" class="text-2xl font-bold text-gray-800">-</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-md border">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-arrow-up"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Saham Asing Naik</p>
<p id="foreign-up" class="text-2xl font-bold text-gray-800">-</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-md border">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<i class="fas fa-building"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Saham Institusi</p>
<p id="institutional-total" class="text-2xl font-bold text-gray-800">-</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-md border">
<div class="flex items-center">
<div class="p-3 rounded-full bg-indigo-100 text-indigo-600">
<i class="fas fa-trending-up"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Rata-Rata Kenaikan Asing</p>
<p id="foreign-growth" class="text-2xl font-bold text-gray-800">-</p>
</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10">
<!-- Ownership Distribution -->
<div class="bg-white p-6 rounded-xl shadow-md border">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Distribusi Kepemilikan (Rata-rata)</h3>
<div class="chart-container">
<canvas id="ownershipChart"></canvas>
</div>
</div>
<!-- Growth Trend -->
<div class="bg-white p-6 rounded-xl shadow-md border">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Trend Pertumbuhan Asing</h3>
<div class="chart-container">
<canvas id="growthChart"></canvas>
</div>
</div>
</div>
<!-- Top Movers -->
<div class="bg-white p-6 rounded-xl shadow-md border mb-10">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Saham dengan Kenaikan Kepemilikan Asing Tertinggi</h3>
<div class="overflow-x-auto">
<table id="top-movers-table" class="min-w-full text-sm">
<thead>
<tr class="border-b">
<th class="py-2 text-left">Ticker</th>
<th class="py-2 text-left">Nama</th>
<th class="py-2 text-right">Kenaikan (%)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Export Section -->
<section id="download" class="py-16 px-6 bg-white">
<div class="max-w-4xl mx-auto text-center">
<h2 class="text-3xl font-bold text-gray-800 mb-4">Ekspor Hasil Analisis</h2>
<p class="text-gray-600 mb-8">
Ekspor data dan hasil analisis ke format CSV untuk disimpan atau digunakan di aplikasi lain.
</p>
<button onclick="exportAnalysis()" class="bg-green-600 hover:bg-green-700 text-white font-semibold px-8 py-3 rounded-full shadow-lg flex items-center gap-2 mx-auto transition">
<i class="fas fa-file-export"></i> Ekspor ke CSV
</button>
</div>
</section>
<!-- Footer -->
<footer class="py-8 bg-gray-800 text-white text-center">
<div class="max-w-4xl mx-auto px-6">
<p>StockOwnership Insights Tool &copy; 2023 | Data bersumber dari <a href="https://web.ksei.co.id" class="text-indigo-400 hover:underline">KSEI</a></p>
<p class="text-sm text-gray-400 mt-2">Alat analisis ini tidak berafiliasi dengan KSEI. Gunakan dengan tanggung jawab Anda sendiri.</p>
</div>
</footer>
<script>
// Global Variables
let rawData = [];
let processedData = [];
let ownershipChart = null;
let growthChart = null;
let database = JSON.parse(localStorage.getItem('ownershipDatabase')) || [];
// Load database on startup
document.addEventListener('DOMContentLoaded', function() {
renderDatabase();
});
// Scroll to section
function scrollToSection(id) {
document.getElementById(id).scrollIntoView({ behavior: 'smooth' });
}
// Handle file upload
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
document.getElementById('loading').classList.remove('hidden');
document.getElementById('preview').classList.add('hidden');
document.getElementById('upload-meta').classList.add('hidden');
const reader = new FileReader();
reader.onload = function(e) {
const csv = e.target.result;
Papa.parse(csv, {
header: true,
skipEmptyLines: true,
delimiter: "|", // Specify pipe as delimiter
complete: function(results) {
rawData = results.data;
previewData(rawData.slice(0, 10));
document.getElementById('loading').classList.add('hidden');
document.getElementById('preview').classList.remove('hidden');
document.getElementById('upload-meta').classList.remove('hidden');
},
error: function(error) {
alert("Error parsing CSV: " + error);
document.getElementById('loading').classList.add('hidden');
}
});
};
reader.readAsText(file);
}
// Preview data
function previewData(data) {
const table = document.getElementById('data-preview-table');
table.innerHTML = '';
// Create header
const thead = document.createElement('thead');
thead.innerHTML = '<tr class="border-b"><th class="py-2 text-left">Ticker</th><th class="py-2 text-left">Nama</th><th class="py-2 text-right">Lokal</th><th class="py-2 text-right">Asing</th><th class="py-2 text-right">Institusi</th><th class="py-2 text-right">Tanggal</th></tr>';
table.appendChild(thead);
// Create body
const tbody = document.createElement('tbody');
data.forEach(row => {
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-gray-50';
tr.innerHTML = `
<td class="py-2 font-mono">${row.Ticker || '-'}</td>
<td class="py-2">${row['Nama Saham'] || '-'}</td>
<td class="py-2 text-right">${formatNumber(row.Lokal)}</td>
<td class="py-2 text-right">${formatNumber(row.Asing)}</td>
<td class="py-2 text-right">${formatNumber(row.Institusi)}</td>
<td class="py-2 text-right">${row.Tanggal || '-'}</td>
`;
tbody.appendChild(tr);
});
table.appendChild(tbody);
}
// Process data
function processData() {
// Convert string numbers to floats
processedData = rawData.map(row => ({
...row,
Lokal: parseFloat(row.Lokal) || 0,
Asing: parseFloat(row.Asing) || 0,
Institusi: parseFloat(row.Institusi) || 0
})).filter(row => row.Ticker && !isNaN(row.Asing));
// Calculate metrics
updateMetrics();
// Render charts
renderCharts();
// Fill top movers
renderTopMovers();
// Show dashboard
scrollToSection('dashboard');
}
// Format numbers with commas
function formatNumber(num) {
if (typeof num !== 'number') num = parseFloat(num);
if (isNaN(num)) return '-';
return num.toLocaleString('id-ID', { maximumFractionDigits: 2 });
}
// Update summary metrics
function updateMetrics() {
const totalStocks = processedData.length;
const foreignUp = processedData.filter(row => row.Asing > 0).length;
const institutionalTotal = processedData.reduce((sum, row) => sum + row.Institusi, 0);
const avgForeignGrowth = processedData.length > 0
? processedData.reduce((sum, row) => sum + row.Asing, 0) / processedData.length
: 0;
document.getElementById('total-stocks').textContent = totalStocks;
document.getElementById('foreign-up').textContent = foreignUp;
document.getElementById('institutional-total').textContent = formatNumber(institutionalTotal);
document.getElementById('foreign-growth').textContent = formatNumber(avgForeignGrowth) + '%';
}
// Render charts
function renderCharts() {
// Ownership distribution chart
const avgLocal = processedData.reduce((sum, row) => sum + row.Lokal, 0) / processedData.length;
const avgForeign = processedData.reduce((sum, row) => sum + row.Asing, 0) / processedData.length;
const avgInstitutional = processedData.reduce((sum, row) => sum + row.Institusi, 0) / processedData.length;
const ctx1 = document.getElementById('ownershipChart').getContext('2d');
if (ownershipChart) ownershipChart.destroy();
ownershipChart = new Chart(ctx1, {
type: 'pie',
data: {
labels: ['Investor Lokal', 'Investor Asing', 'Institusi'],
datasets: [{
data: [avgLocal, avgForeign, avgInstitutional],
backgroundColor: ['#6366F1', '#10B981', '#8B5CF6'],
borderWidth: 2,
borderColor: '#FFFFFF',
hoverOffset: 10
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// Growth trend chart
// Group by date and calculate average foreign ownership
const groupedByDate = {};
processedData.forEach(row => {
const date = row.Tanggal;
if (!groupedByDate[date]) {
groupedByDate[date] = { sum: 0, count: 0 };
}
groupedByDate[date].sum += parseFloat(row.Asing) || 0;
groupedByDate[date].count++;
});
const dates = Object.keys(groupedByDate).sort();
const avgGrowth = dates.map(date => groupedByDate[date].sum / groupedByDate[date].count);
const ctx2 = document.getElementById('growthChart').getContext('2d');
if (growthChart) growthChart.destroy();
growthChart = new Chart(ctx2, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Rata-rata Kepemilikan Asing (%)',
data: avgGrowth,
borderColor: '#EC4899',
backgroundColor: 'rgba(236, 72, 153, 0.1)',
fill: true,
tension: 0.4,
pointBackgroundColor: '#EC4899'
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Pertumbuhan (%)'
}
},
x: {
title: {
display: true,
text: 'Tanggal'
}
}
}
}
});
}
// Render top movers
function renderTopMovers() {
const sorted = [...processedData]
.sort((a, b) => (parseFloat(b.Asing) || 0) - (parseFloat(a.Asing) || 0))
.slice(0, 10);
const table = document.getElementById('top-movers-table').getElementsByTagName('tbody')[0];
table.innerHTML = '';
sorted.forEach(row => {
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-gray-50';
tr.innerHTML = `
<td class="py-2 font-mono font-semibold">${row.Ticker}</td>
<td class="py-2">${row['Nama Saham'] || '-'}</td>
<td class="py-2 text-right text-green-600 font-semibold">${formatNumber(row.Asing)}%</td>
`;
table.appendChild(tr);
});
}
// Save to database
function saveToDatabase() {
if (rawData.length === 0) {
alert('Belum ada data untuk disimpan.');
return;
}
const month = document.getElementById('upload-month').value;
const year = document.getElementById('upload-year').value;
const number = document.getElementById('upload-number').value;
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"];
const entry = {
id: Date.now(),
month: parseInt(month),
year: parseInt(year),
number: parseInt(number),
monthName: monthNames[month - 1],
date: new Date().toISOString(),
recordCount: rawData.length
};
database.push(entry);
database.sort((a, b) => new Date(b.year, b.month) - new Date(a.year, a.month));
localStorage.setItem('ownershipDatabase', JSON.stringify(database));
renderDatabase();
alert(`Data berhasil disimpan ke database!`);
}
// Render database
function renderDatabase() {
const container = document.getElementById('database-container');
container.innerHTML = '';
if (database.length === 0) {
container.innerHTML = `
<div class="text-center py-10 text-gray-500 col-span-2">
<i class="fas fa-database text-5xl mb-4 opacity-30"></i>
<p>Belum ada data dalam database</p>
</div>
`;
return;
}
database.forEach(item => {
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"];
const itemDiv = document.createElement('div');
itemDiv.className = 'bg-white p-6 rounded-xl shadow-md border database-item';
itemDiv.innerHTML = `
<div class="flex justify-between items-start">
<div>
<span class="inline-block bg-indigo-100 text-indigo-800 text-xs px-3 py-1 rounded-full font-medium">
No. ${item.number}
</span>
<h3 class="text-xl font-bold text-gray-800 mt-2">${item.monthName} ${item.year}</h3>
<p class="text-gray-600">Diunggah: ${new Date(item.date).toLocaleDateString('id-ID')}</p>
</div>
<div class="text-right">
<span class="inline-block bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded-full">
${item.recordCount} saham
</span>
<div class="mt-2 space-x-2">
<button onclick="loadFromDatabase(${item.id})" class="text-blue-600 hover:text-blue-800 text-sm">
<i class="fas fa-chart-bar"></i> Lihat
</button>
<button onclick="deleteFromDatabase(${item.id})" class="text-red-600 hover:text-red-800 text-sm">
<i class="fas fa-trash"></i> Hapus
</button>
</div>
</div>
</div>
`;
container.appendChild(itemDiv);
});
}
// Load from database
function loadFromDatabase(id) {
const item = database.find(d => d.id === id);
if (!item) return;
alert(`Fitur lengkap akan diintegrasikan. Saat ini, fungsi ini akan memuat data laporan ${item.monthName} ${item.year}`);
// This would load the actual CSV data from IndexedDB or localStorage
// For demo, we're just showing the alert
}
// Delete from database
function deleteFromDatabase(id) {
if (confirm('Apakah Anda yakin ingin menghapus laporan ini dari database?')) {
database = database.filter(d => d.id !== id);
localStorage.setItem('ownershipDatabase', JSON.stringify(database));
renderDatabase();
}
}
// Export analysis
function exportAnalysis() {
if (processedData.length === 0) {
alert('Tidak ada data untuk diekspor. Silakan unggah dan proses data terlebih dahulu.');
return;
}
// Prepare export data
const exportData = processedData.map(row => ({
'Ticker': row.Ticker,
'Nama Saham': row['Nama Saham'],
'Kepemilikan Lokal (%)': formatNumber(row.Lokal),
'Kepemilikan Asing (%)': formatNumber(row.Asing),
'Kepemilikan Institusi (%)': formatNumber(row.Institusi),
'Tanggal': row.Tanggal,
'Kategori': row.Asing > 0 ? 'Net Buy' : 'Net Sell'
}));
// Convert to CSV
const csv = Papa.unparse(exportData);
// Create download link
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'analisis-kepemilikan-saham.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-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/ksei" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>