map / app /templates /index.html
atsuga's picture
Upload 40 files
6f10462 verified
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>East Java Legal Cases Mapping</title>
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Font Awesome untuk icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Chart.js untuk diagram interaktif -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="container">
<h1 class="logo">
<i class="fas fa-balance-scale"></i>
East Java Legal Cases Mapping
</h1>
<nav class="nav">
<ul>
<li><a href="#home" class="nav-link">Home</a></li>
<li><a href="#statistics" class="nav-link">Statistics</a></li>
<li><a href="#about" class="nav-link">About</a></li>
</ul>
</nav>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Search and Map Section -->
<section id="home" class="map-section">
<div class="container-fluid" style="max-width: 100%; padding: 20px 40px;">
<!-- Summary Widgets -->
<!-- Pastikan Font Awesome sudah di-include -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<div class="summary-container" style="
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
">
<!-- Total Putusan -->
<div class="summary-card" style="
flex: 1 1 200px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div class="summary-icon" style="font-size: 24px; color: #2E86AB; margin-bottom: 8px;">
<i class="fas fa-gavel"></i>
</div>
<div class="summary-title" style="font-size: 14px; color: #555;">Total Criminal Verdicts</div>
<div class="summary-value" id="totalPutusan" style="font-size: 24px; font-weight: bold;">0</div>
</div>
<!-- Total Pengadilan Negeri -->
<div class="summary-card" style="
flex: 1 1 200px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
">
<div class="summary-icon" style="font-size: 24px; color: #FF7F0E; margin-bottom: 8px;">
<i class="fas fa-landmark"></i>
</div>
<div class="summary-title" style="font-size: 14px; color: #555;">Total District Courts</div>
<div class="summary-value" id="totalPN" style="font-size: 24px; font-weight: bold;">0</div>
</div>
</div>
<!-- Filter Controls -->
<div class="filter-panel" style="background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: flex;
gap: 15px;
align-items: flex-end;
flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #333;">Categories:</label>
<select id="categoryFilter" style="width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;">
<option value="all">All Categories</option>
<option value="pidana">Pidana Umum</option>
</select>
</div>
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #333;">Years:</label>
<select id="yearFilter" style="width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;">
<option value="all">All Years</option>
<!-- Will be populated dynamically from API -->
</select>
</div>
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #333;">Crime Types:</label>
<select id="crimeFilter" style="width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;">
<option value="all">All Types</option>
<!-- Will be populated dynamically from API -->
</select>
</div>
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #333;">Cities/Regencies:</label>
<select id="cityFilter" style="width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;">
<option value="all">All Regencies</option>
<!-- Will be populated dynamically from API -->
</select>
</div>
</div>
<!-- Interactive Heatmap with Click-to-Zoom -->
<div style="background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="background: linear-gradient(135deg, #2C5F8D 0%, #1e3c72 100%);
padding: 15px;
color: white;">
<h3 style="margin: 0; font-size: 18px;">
East Java Legal Cases Interactive Map
</h3>
<p style="margin: 5px 0 0 0; font-size: 13px; opacity: 0.9;">
Hover for more information • Click regency for detailed cases
</p>
</div>
<iframe id="mapIframe" src="{{ url_for('map_view') }}"
style="width: 100%;
height: 650px;
border: none;">
</iframe>
</div>
<!-- Charts Section - Diagram Interaktif dan Dinamis -->
<div class="charts-row" style="
margin-top: 60px;
display: flex;
gap: 20px;
flex-wrap: wrap;
">
<!-- Chart 1 -->
<div style="
flex: 1;
min-width: 400px;
max-width: 50%;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h3 id="yearHeading" style="margin:0 0 15px 0; font-size:18px;">
Case Trends Per Year
</h3>
<canvas id="casesYearChart" height="120"></canvas>
</div>
<!-- Chart 2 -->
<div style="
flex: 1;
min-width: 400px;
max-width: 50%;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h3 id="seasonalityHeading" style="margin:0 0 15px 0; font-size:18px;">
Case Pattern Per Month
</h3>
<canvas id="seasonalityChart"></canvas>
</div>
<!-- Chart 3 -->
<div style="
width: 100%;
margin-top: 30px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h3 id="forecast" style="margin:0 0 15px 0; font-size:18px;">
Case Forecast for Next Year
</h3>
<canvas id="forecastChart" height="120"></canvas>
</div>
<!-- Chart 4: Frekuensi 10 Jenis Tindak Pidana -->
<div style="
width: 100%;
margin-top: 30px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h3 style="margin:0 0 15px 0; font-size:18px;">
10 Highest Types of Crimes
</h3>
<canvas id="crimeTypeChart"></canvas>
</div>
<div style="
width: 100%;
margin-top: 20px;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
">
<h3 id="stackedHeading" style="margin-bottom: 15px; font-size: 18px;">
Sentencing Patterns for the 10 Highest Crimes
</h3>
<canvas id="stackedPNChart"></canvas>
</div>
<table id="kasusTable" class="table table-striped">
<thead>
<tr id="kasusHead"></tr>
</thead>
<tbody id="kasusBody"></tbody>
</table>
<div class="pagination-container" style="
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-start;
margin-top: 15px;
flex-wrap: wrap;
">
<button id="prevKasus" class="btn-pagination">Prev</button>
<span id="kasusPageInfo" style="font-weight: 500; min-width: 80px; text-align: center;"></span>
<button id="nextKasus" class="btn-pagination">Next</button>
</div>
<style>
.btn-pagination {
background-color: #2E86AB;
color: white;
border: none;
padding: 6px 14px;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
}
.btn-pagination:hover {
background-color: #1B4F72;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.btn-pagination:disabled {
background-color: #cccccc;
cursor: not-allowed;
box-shadow: none;
}
</style>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="footer">
<div class="container">
</div>
</footer>
<!-- JavaScript for Filter Functionality -->
<script>
// Filter change events
// ==================== FILTER FUNCTIONALITY ====================
let currentFilters = {
year: 'all',
crime: 'all',
city: 'all'
};
let allCharts = {}; // Store chart instances
let filtersInitialized = false; // Track if event listeners are attached
function animateNumber(element, target) {
let current = 0;
const increment = target / 50;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
element.textContent = target.toLocaleString();
clearInterval(timer);
} else {
element.textContent = Math.floor(current).toLocaleString();
}
}, 20);
}
function applyFilters() {
const year = document.getElementById('yearFilter').value;
const crime = document.getElementById('crimeFilter').value;
const city = document.getElementById('cityFilter').value;
currentFilters.year = year;
currentFilters.crime = crime;
currentFilters.city = city;
// Build query string
const params = new URLSearchParams();
if (year !== 'all') params.append('year', year);
if (crime !== 'all') params.append('crime', crime);
if (city !== 'all') params.append('city', city);
const queryString = params.toString();
const statsUrl = '/api/statistics' + (queryString ? '?' + queryString : '');
const mapUrl = '/map' + (queryString ? '?' + queryString : '');
// Log for debugging
console.log('Applying filters - Year:', year, 'Crime:', crime, 'City:', city);
console.log('Stats URL:', statsUrl);
console.log('Map URL:', mapUrl);
// Reload map with filters
const mapIframe = document.getElementById('mapIframe');
if (mapIframe) {
mapIframe.src = mapUrl;
console.log('Map iframe reloaded');
}
// Reload data with filters
loadStatistics(statsUrl);
}
// ==================== CHART.JS - DIAGRAM INTERAKTIF ====================
function loadStatistics(url) {
fetch(url)
.then(response => response.json())
.then(apiData => {
console.log('Data dari API:', apiData);
// Update statistics section
// updateStatistics(apiData);
if (apiData) {
document.getElementById("totalPutusan").innerText = apiData.total_cases || 0;
document.getElementById("totalPN").innerText = apiData.total_pn || 0;
}
// Populate year filter dropdown if year_options available
if (apiData.year_options) {
const yearFilter = document.getElementById('yearFilter');
const prev = currentFilters.year;
yearFilter.innerHTML = '<option value="all">All Years</option>';
apiData.year_options.forEach(year => {
const option = document.createElement('option');
option.value = year.value;
option.textContent = `${year.label} (${year.count.toLocaleString()})`;
yearFilter.appendChild(option);
});
yearFilter.value = prev;
}
// Populate crime filter dropdown if crime_types available
if (apiData.crime_types) {
const crimeFilter = document.getElementById('crimeFilter');
const prev = currentFilters.crime;
crimeFilter.innerHTML = '<option value="all">All Types</option>';
apiData.crime_types.slice(0, 20).forEach(crime => {
const option = document.createElement('option');
option.value = crime.value;
option.textContent = `${crime.label} (${crime.count.toLocaleString()})`;
crimeFilter.appendChild(option);
});
crimeFilter.value = prev;
}
// Populate city filter dropdown if city_options available
if (apiData.city_options && !document.getElementById('cityFilter').dataset.populated) {
const cityFilter = document.getElementById('cityFilter');
apiData.city_options.forEach(city => {
const option = document.createElement('option');
option.value = city.value;
option.textContent = city.label;
cityFilter.appendChild(option);
});
cityFilter.dataset.populated = 'true';
}
// Initialize event listeners after dropdowns are populated
if (!filtersInitialized) {
document.getElementById('yearFilter').addEventListener('change', applyFilters);
document.getElementById('crimeFilter').addEventListener('change', applyFilters);
document.getElementById('cityFilter').addEventListener('change', applyFilters);
// document.getElementById('resetFilter').addEventListener('click', resetFilters);
filtersInitialized = true;
console.log('Filter event listeners initialized');
}
// Clean existing chart instances
Object.values(allCharts).forEach(ch => ch.destroy());
// === TREN KASUS TAHUNAN - LINE CHART INTERAKTIF ===
if (apiData.year_options) {
const ctx = document.getElementById('casesYearChart').getContext('2d');
// Urutkan ascending khusus untuk chart
const sortedYears = apiData.year_options.slice().reverse();
const years = sortedYears.map(x => Number(x.value));
const counts = sortedYears.map(x => Number(x.count));
const avgCases = counts.reduce((a, b) => a + b, 0) / counts.length;
// Hapus chart lama
if (allCharts.casesYearChart) allCharts.casesYearChart.destroy();
// Hitung perubahan signifikan (>15%)
const annotations = [];
apiData.year_options.forEach((item, i) => {
if (i === 0) return;
const prev = apiData.year_options[i - 1].count;
const change = ((item.count - prev) / prev) * 100;
if (Math.abs(change) > 15) {
annotations.push({
type: 'label',
xValue: Number(item.value),
yValue: Number(item.count),
backgroundColor: 'white',
borderColor: '#444',
borderWidth: 1,
padding: 6,
content: `${change > 0 ? '+' : ''}${change.toFixed(0)}%`,
color: change > 0 ? 'green' : 'red',
font: { weight: 'bold' },
yAdjust: -20
});
}
});
allCharts.casesYearChart = new Chart(ctx, {
type: 'line',
data: {
labels: years,
datasets: [{
label: `Yearly Case Trends (Average ${avgCases.toFixed(0)} Case)`,
data: counts,
borderWidth: 3,
tension: 0.25,
borderColor: '#2E86AB',
pointBackgroundColor: 'white',
pointBorderColor: '#2E86AB',
pointBorderWidth: 2,
pointRadius: 6,
pointHoverRadius: 7
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function (ctx) {
const index = ctx.dataIndex;
const current = ctx.raw;
if (index === 0) {
return `${current.toLocaleString()} case (first year)`;
}
const prev = ctx.chart.data.datasets[0].data[index - 1];
const change = ((current - prev) / prev) * 100;
const sign = change >= 0 ? '+' : '';
return `${current.toLocaleString()} case (${sign}${change.toFixed(1)}%)`;
}
}
},
annotation: {
annotations: annotations
}
},
scales: {
x: {
ticks: { autoSkip: false }
},
y: {
beginAtZero: false,
grid: { color: 'rgba(0,0,0,0.1)' }
}
}
}
});
document.getElementById('yearHeading').textContent =
allCharts.casesYearChart.data.datasets[0].label;
}
if (apiData.seasonal_data) {
const ctx = document.getElementById('seasonalityChart').getContext('2d');
// Nama bulan
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
// Pastikan full 12 bulan tetap ada
const fullData = Array.from({ length: 12 }, (_, i) => {
const entry = apiData.seasonal_data.find(x => Number(x.month) === i + 1);
return entry ? Number(entry.count) : 0;
});
const months = Array.from({ length: 12 }, (_, i) => i + 1);
// Highest & lowest bulan dengan kasus > 0
const filtered = fullData.filter(x => x > 0);
const maxVal = Math.max(...filtered);
const minVal = Math.min(...filtered);
const maxMonth = fullData.indexOf(maxVal) + 1;
const minMonth = fullData.indexOf(minVal) + 1;
// Persentase perubahan bulan-ke-bulan
const percentChanges = fullData.map((count, i) => {
if (i === 0) return null;
const prev = fullData[i - 1];
return prev > 0 ? ((count - prev) / prev) * 100 : null;
});
// Rata-rata kasus per bulan
const avgCases = fullData.reduce((a, b) => a + b, 0) / fullData.length;
// Hapus chart lama
if (allCharts.seasonalityChart) allCharts.seasonalityChart.destroy();
allCharts.seasonalityChart = new Chart(ctx, {
type: 'bar',
data: {
labels: monthNames,
datasets: [{
label: `Monthly Cases (Average ${avgCases.toFixed(0)} Case)`,
data: fullData,
borderWidth: 2,
backgroundColor: fullData.map((v, i) =>
(i + 1 === maxMonth) ? '#E57373' :
(i + 1 === minMonth) ? '#81C784' :
'#B0C4DE'
),
borderColor: fullData.map((v, i) =>
(i + 1 === maxMonth) ? '#B71C1C' :
(i + 1 === minMonth) ? '#1B5E20' :
'#1E3A5F'
)
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => {
const monthIdx = ctx.dataIndex;
const raw = ctx.raw.toLocaleString();
const pct = percentChanges[monthIdx];
if (pct === null) return ` ${raw} kasus`;
const sign = pct > 0 ? '+' : '';
return ` ${raw} case (${sign}${pct.toFixed(1)}%)`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.1)' }
}
}
}
});
document.getElementById('seasonalityHeading').textContent =
allCharts.seasonalityChart.data.datasets[0].label;
}
// === CHART FORECAST: TREND HISTORIS + PREDIKSI ===
if (apiData.forecast_result) {
const history = apiData.forecast_result.history || [];
const future = apiData.forecast_result.forecast || [];
// Jika tidak ada data, hentikan
if (history.length === 0 && future.length === 0) {
console.warn("Forecast data empty");
return;
}
const allLabels = [
...history.map(r => r.date),
...future.map(r => r.date)
];
const historyValues = history.map(r => r.count);
// Forecast dimulai setelah historis → sisipkan null di awal
const forecastValues = [
...Array(history.length).fill(null),
...future.map(r => r.forecast)
];
const ctxForecast = document.getElementById('forecastChart').getContext('2d');
if (allCharts.forecastChart) allCharts.forecastChart.destroy();
allCharts.forecastChart = new Chart(ctxForecast, {
type: 'line',
data: {
labels: allLabels,
datasets: [
{
label: "Historical Data",
data: historyValues,
borderWidth: 2,
tension: 0.3
},
{
label: "Forecast",
data: forecastValues,
borderWidth: 2,
borderDash: [6, 4],
tension: 0.3
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
callbacks: {
label: ctx => {
const val = ctx.raw;
if (val === null) return null;
return `${ctx.dataset.label}: ${val.toLocaleString()}`;
}
}
}
},
scales: {
x: {
ticks: {
autoSkip: true,
maxTicksLimit: 12
}
},
y: {
beginAtZero: true
}
}
}
});
// Untuk debugging
document.getElementById('forecastChart').textContent =
allCharts.forecastChart.data.datasets[0].label;
}
// === CHART 3: FREKUENSI 10 JENIS TINDAK PIDANA TERTINGGI ===
if (apiData.crime_types) {
// Ambil urutan berdasarkan count
const sortedCrimeTypes = apiData.crime_types
.sort((a, b) => b.count - a.count)
.slice(0, 10); // ambil 10 teratas
const labels = sortedCrimeTypes.map(x => x.label);
const values = sortedCrimeTypes.map(x => x.count);
const ctx3 = document.getElementById('crimeTypeChart').getContext('2d');
if (allCharts.crimeTypeChart) allCharts.crimeTypeChart.destroy();
allCharts.crimeTypeChart = new Chart(ctx3, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Total Cases',
data: values,
borderWidth: 1
}]
},
options: {
indexAxis: 'y', // horizontal bar
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => ctx.raw.toLocaleString()
}
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
callback: value => value.toLocaleString()
}
}
}
}
});
document.getElementById('crimeTypeChart').textContent =
allCharts.crimeTypeChart.data.datasets[0].label;
}
if (apiData.kasus_percentage) {
const ctx = document.getElementById('stackedPNChart').getContext('2d');
// Ambil 10 teratas berdasarkan kontribusi_kasus
const top10 = apiData.kasus_percentage
.slice()
.sort((a, b) => Number(b.kontribusi_kasus) - Number(a.kontribusi_kasus))
.slice(0, 10);
const crimeLabels = top10.map(r => r.tindak_pidana);
// Kolom persentase yang tersedia
const percentageKeys = [
"penjara",
"penjara_seumur_hidup",
"denda",
"bebas_bersyarat",
"bebas_dakwaan",
"hukuman_mati",
].filter(k => top10[0].hasOwnProperty(k));
const colors = [
'#E57373', // soft red (selaras dengan max background)
'#81C784', // soft green (selaras dengan min background)
'#64B5F6', // soft light blue
'#FFB74D', // soft orange
'#BA68C8', // soft purple
'#4DB6AC', // soft teal
'#B0C4DE', // soft steel-blue (warna default kamu)
];
const kasusColumnLabels = {
"bebas_bersyarat": "Conditional Release",
"bebas_dakwaan": "Acquittal",
"denda": "Fine",
"hukuman_mati": "Death Penalty",
"penjara": "Imprisonment",
"penjara_seumur_hidup": "Life Imprisonment",
};
const datasets = percentageKeys.map((key, idx) => ({
label: kasusColumnLabels[key] || key.replace(/_/g, ' ').toUpperCase(),
data: top10.map(r => Number(r[key]) || 0),
backgroundColor: colors[idx],
borderWidth: 1
}));
if (allCharts.stackedPNChart) allCharts.stackedPNChart.destroy();
allCharts.stackedPNChart = new Chart(ctx, {
type: 'bar',
data: {
labels: crimeLabels,
datasets: datasets
},
options: {
indexAxis: 'y',
responsive: true,
scales: {
x: {
stacked: true,
max: 100,
ticks: {
callback: v => v + "%"
}
},
y: {
stacked: true
}
},
plugins: {
legend: { position: 'right' },
tooltip: {
callbacks: {
label: ctx => {
const raw = ctx.raw ?? 0;
if (raw <= 0) return null;
return `${ctx.dataset.label}: ${raw.toFixed(3)}%`;
}
}
}
}
}
});
}
kasusDataFull = apiData.kasus_percentage.slice();
kasusPage = 0;
renderKasusTablePage();
// Animasi saat scroll ke diagram
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, { threshold: 0.1 });
document.querySelectorAll('canvas').forEach(canvas => {
canvas.parentElement.style.opacity = '0';
canvas.parentElement.style.transform = 'translateY(30px)';
canvas.parentElement.style.transition = 'all 0.6s ease-out';
observer.observe(canvas.parentElement);
});
})
.catch(error => {
console.error('Error loading statistics:', error);
alert('Failed to load statistic data. Please refresh the page.');
});
}
function renderKasusTablePage() {
const head = document.getElementById("kasusHead");
const body = document.getElementById("kasusBody");
const pageInfo = document.getElementById("kasusPageInfo");
body.innerHTML = "";
const sorted = kasusDataFull
.slice()
.sort((a, b) => Number(b.kontribusi_kasus) - Number(a.kontribusi_kasus));
const start = kasusPage * kasusPerPage;
const end = start + kasusPerPage;
const pageData = sorted.slice(start, end);
if (pageData.length === 0) return;
// Mapping kolom → label tampilan
const kasusColumnLabels = {
"bebas_bersyarat": "Conditional Release (%)",
"bebas_dakwaan": "Acquittal (%)",
"denda": "Fine (%)",
"hukuman_mati": "Death Penalty (%)",
"kategori_pidana": "Category",
"kontribusi_kasus": "Case Contribution (%)",
"penjara": "Imprisonment (%)",
"penjara_seumur_hidup": "Life Imprisonment (%)",
"rata-rata_denda": "Average Fine (Rupiah)",
"rata-rata_penjara": "Average Imprisonment (Months)",
"penjara_seumur_hidup": "Life Imprisonment (%)",
"tindak_pidana": "Crime Action"
};
// === HEADER ===
head.innerHTML = "";
Object.keys(pageData[0]).forEach(col => {
const label = kasusColumnLabels[col] || col;
head.innerHTML += `<th>${label}</th>`;
});
// === BODY ===
pageData.forEach(row => {
let rowHTML = "<tr>";
Object.keys(row).forEach(col => {
let val = row[col];
if (typeof val === "number" && !Number.isInteger(val)) {
val = val.toFixed(3);
}
if (val === null || val === undefined || val === "") {
val = "-";
}
rowHTML += `<td>${val}</td>`;
});
rowHTML += "</tr>";
body.innerHTML += rowHTML;
});
const totalPages = Math.ceil(kasusDataFull.length / kasusPerPage);
pageInfo.innerText = `Page ${kasusPage + 1} / ${totalPages}`;
}
// Buttons
document.getElementById("prevKasus").onclick = function () {
if (kasusPage > 0) {
kasusPage--;
renderKasusTablePage();
}
};
document.getElementById("nextKasus").onclick = function () {
const totalPages = Math.ceil(kasusDataFull.length / kasusPerPage);
if (kasusPage < totalPages - 1) {
kasusPage++;
renderKasusTablePage();
}
};
// ================= GLOBAL PAGINATION STATE =================
let kasusPage = 0;
const kasusPerPage = 10;
let kasusDataFull = [];
// Load statistics on page load
loadStatistics('/api/statistics');
</script>
</body>
</html>