Files
WebGIS_Project/03/Choropleth.html
T
2026-06-10 18:07:01 +07:00

568 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Choropleth — Kota Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--accent: #58a6ff;
--text: #e6edf3;
--muted: #8b949e;
--c1: #fff7bc;
--c2: #fec44f;
--c3: #fb9a29;
--c4: #ec7014;
--c5: #cc4c02;
--c6: #8c2d04;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Space Mono', monospace;
background: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── HEADER ─────────────────────────────────────── */
header {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 24px;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 1000;
flex-shrink: 0;
}
.logo-dot {
width: 10px; height: 10px;
background: var(--accent);
border-radius: 50%;
box-shadow: 0 0 12px var(--accent);
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
0%,100% { opacity:1; transform:scale(1); }
50% { opacity:.5; transform:scale(1.4); }
}
header h1 {
font-family: 'Syne', sans-serif;
font-size: 16px;
font-weight: 800;
letter-spacing: .05em;
color: var(--text);
}
header span.sub {
font-size: 11px;
color: var(--muted);
margin-left: 4px;
font-weight: 400;
}
.mode-btns {
margin-left: auto;
display: flex;
gap: 6px;
}
.mode-btn {
padding: 5px 14px;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
color: var(--muted);
font-family: 'Space Mono', monospace;
font-size: 11px;
cursor: pointer;
transition: all .2s;
}
.mode-btn.active,
.mode-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(88,166,255,.08);
}
/* ── MAIN ────────────────────────────────────────── */
.main {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── SIDEBAR ─────────────────────────────────────── */
.sidebar {
width: 280px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-section {
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.sidebar-section h3 {
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 12px;
}
/* Legend */
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 7px;
font-size: 11px;
}
.legend-swatch {
width: 24px; height: 14px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label { color: var(--text); }
.legend-sub { color: var(--muted); font-size: 10px; }
/* Stat cards */
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stat-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
cursor: pointer;
transition: border-color .2s;
}
.stat-card:hover, .stat-card.active {
border-color: var(--accent);
}
.stat-name {
font-size: 10px;
color: var(--muted);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-value {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 800;
color: var(--text);
}
.stat-unit { font-size: 10px; color: var(--muted); font-weight: 400; }
/* Detail panel */
#detail-panel {
display: none;
}
#detail-panel.visible {
display: block;
}
.detail-name {
font-family: 'Syne', sans-serif;
font-size: 18px;
font-weight: 800;
color: var(--text);
margin-bottom: 14px;
line-height: 1.2;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 7px 0;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.detail-row:last-child { border-bottom: none; }
.detail-key { color: var(--muted); }
.detail-val { color: var(--text); font-weight: 700; }
.density-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
margin-top: 10px;
overflow: hidden;
}
.density-fill {
height: 100%;
border-radius: 2px;
background: linear-gradient(90deg, var(--c2), var(--c5));
transition: width .6s ease;
}
#placeholder {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
#placeholder em {
display: block;
font-size: 24px;
margin-bottom: 8px;
opacity: .4;
}
/* ── MAP ─────────────────────────────────────────── */
#map {
flex: 1;
background: #0a0f16;
}
/* Override Leaflet UI for dark mode */
.leaflet-control-zoom a {
background: var(--surface) !important;
color: var(--text) !important;
border-color: var(--border) !important;
}
.leaflet-control-zoom a:hover {
background: var(--border) !important;
}
.leaflet-bar { border: 1px solid var(--border) !important; }
/* ── INFO TOOLTIP ────────────────────────────────── */
.info-box {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 6px;
padding: 10px 14px;
font-family: 'Space Mono', monospace;
font-size: 12px;
color: var(--text) !important;
box-shadow: 0 4px 20px rgba(0,0,0,.4) !important;
min-width: 160px;
}
.info-box strong {
display: block;
font-family: 'Syne', sans-serif;
font-size: 14px;
font-weight: 800;
margin-bottom: 6px;
color: var(--accent);
}
.info-box span {
display: block;
color: var(--muted);
font-size: 11px;
}
/* ── TOOLTIP POPUPS ─────────────────────────────── */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
box-shadow: 0 8px 32px rgba(0,0,0,.6) !important;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content {
color: var(--text) !important;
font-family: 'Space Mono', monospace !important;
font-size: 12px !important;
margin: 12px 16px !important;
}
/* scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
</style>
</head>
<body>
<header>
<div class="logo-dot"></div>
<div>
<h1>WebGIS Pontianak <span class="sub">// Choropleth</span></h1>
</div>
<div class="mode-btns">
<button class="mode-btn active" onclick="setMode('density',this)">Kepadatan</button>
<button class="mode-btn" onclick="setMode('penduduk',this)">Penduduk</button>
</div>
</header>
<div class="main">
<!-- SIDEBAR -->
<div class="sidebar">
<!-- Legend -->
<div class="sidebar-section" id="legend-wrap">
<h3>Legenda</h3>
<div id="legend-items"></div>
</div>
<!-- Kecamatan Cards -->
<div class="sidebar-section">
<h3>Kecamatan</h3>
<div class="stat-grid" id="stat-grid"></div>
</div>
<!-- Detail -->
<div class="sidebar-section">
<h3>Detail</h3>
<div id="detail-panel">
<div class="detail-name" id="d-name"></div>
<div class="detail-row"><span class="detail-key">Penduduk</span><span class="detail-val" id="d-pop"></span></div>
<div class="detail-row"><span class="detail-key">Luas Wilayah</span><span class="detail-val" id="d-area"></span></div>
<div class="detail-row"><span class="detail-key">Kepadatan</span><span class="detail-val" id="d-den"></span></div>
<div class="density-bar"><div class="density-fill" id="d-bar"></div></div>
</div>
<div id="placeholder">
<em></em>
Klik area di peta untuk melihat data kecamatan secara detail.
</div>
</div>
</div>
<!-- MAP -->
<div id="map"></div>
</div>
<script src="pontianak.js"></script>
<script>
// ── CONFIG ────────────────────────────────────────────────────────────────────
const MODES = {
density: {
key: 'density',
label: 'Kepadatan Penduduk',
unit: 'jiwa/km²',
breaks: [2000, 5000, 7000, 10000, 12000],
colors: ['#fff7bc','#fec44f','#fb9a29','#ec7014','#cc4c02','#8c2d04'],
fmt: v => v.toLocaleString('id-ID') + ' jiwa/km²',
},
penduduk: {
key: 'penduduk',
label: 'Jumlah Penduduk',
unit: 'jiwa',
breaks: [75000, 85000, 95000, 105000, 115000],
colors: ['#c6dbef','#9ecae1','#6baed6','#3182bd','#2171b5','#084594'],
fmt: v => v.toLocaleString('id-ID') + ' jiwa',
}
};
let currentMode = 'density';
let geojsonLayer, lastSelected;
// ── MAP INIT ──────────────────────────────────────────────────────────────────
const map = L.map('map', { zoomControl: true }).setView([-0.02, 109.34], 12);
// Dark OSM tiles
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap contributors © CARTO',
maxZoom: 19
}).addTo(map);
// ── HELPERS ───────────────────────────────────────────────────────────────────
function getColor(value, mode) {
const m = MODES[mode];
for (let i = 0; i < m.breaks.length; i++) {
if (value <= m.breaks[i]) return m.colors[i];
}
return m.colors[m.colors.length - 1];
}
function style(feature) {
const val = feature.properties[MODES[currentMode].key];
return {
fillColor: getColor(val, currentMode),
fillOpacity: 0.72,
color: '#0d1117',
weight: 1.5,
opacity: 1,
};
}
function highlightStyle(feature) {
return {
fillOpacity: 0.9,
color: '#58a6ff',
weight: 2.5,
};
}
// ── SIDEBAR UPDATERS ──────────────────────────────────────────────────────────
function buildLegend() {
const m = MODES[currentMode];
const container = document.getElementById('legend-items');
container.innerHTML = '';
const labels = [
`< ${m.breaks[0].toLocaleString('id-ID')}`,
...m.breaks.slice(0,-1).map((v,i) => `${v.toLocaleString('id-ID')} ${m.breaks[i+1].toLocaleString('id-ID')}`),
`> ${m.breaks[m.breaks.length-1].toLocaleString('id-ID')}`,
];
m.colors.forEach((c,i) => {
container.innerHTML += `
<div class="legend-item">
<div class="legend-swatch" style="background:${c}"></div>
<span class="legend-label">${labels[i]} <span class="legend-sub">${m.unit}</span></span>
</div>`;
});
}
function buildCards() {
const grid = document.getElementById('stat-grid');
grid.innerHTML = '';
const m = MODES[currentMode];
pontianakData.features.forEach(f => {
const p = f.properties;
const val = p[m.key];
grid.innerHTML += `
<div class="stat-card" id="card-${slugify(p.Ket)}" onclick="selectByName('${p.Ket}')">
<div class="stat-name">${p.Ket.replace('Pontianak ','')}</div>
<div class="stat-value">${formatShort(val)} <span class="stat-unit">${m.unit.split('/')[0]}</span></div>
</div>`;
});
}
function slugify(s) { return s.replace(/\s+/g,'_'); }
function formatShort(v) {
if (v >= 1000) return (v/1000).toFixed(1) + 'k';
return v.toLocaleString('id-ID');
}
function showDetail(props) {
document.getElementById('detail-panel').classList.add('visible');
document.getElementById('placeholder').style.display = 'none';
document.getElementById('d-name').textContent = props.Ket;
document.getElementById('d-pop').textContent = props.penduduk.toLocaleString('id-ID') + ' jiwa';
document.getElementById('d-area').textContent = props.luas_km2.toFixed(2) + ' km²';
document.getElementById('d-den').textContent = props.density.toLocaleString('id-ID') + ' jiwa/km²';
// Bar fill based on density max (~12000)
const pct = Math.min(100, (props.density / 13000) * 100);
document.getElementById('d-bar').style.width = pct + '%';
// Highlight card
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
const card = document.getElementById('card-' + slugify(props.Ket));
if (card) card.classList.add('active');
}
// ── INTERACTION ───────────────────────────────────────────────────────────────
function onEachFeature(feature, layer) {
layer.on({
mouseover(e) {
const l = e.target;
l.setStyle(highlightStyle(feature));
l.bringToFront();
infoBox.update(feature.properties);
},
mouseout(e) {
if (lastSelected !== e.target) {
geojsonLayer.resetStyle(e.target);
}
infoBox.update(null);
},
click(e) {
map.fitBounds(e.target.getBounds(), { padding: [40, 40] });
if (lastSelected && lastSelected !== e.target) {
geojsonLayer.resetStyle(lastSelected);
}
e.target.setStyle(highlightStyle(feature));
e.target.bringToFront();
lastSelected = e.target;
showDetail(feature.properties);
}
});
}
function selectByName(name) {
geojsonLayer.eachLayer(l => {
if (l.feature.properties.Ket === name) {
map.fitBounds(l.getBounds(), { padding: [40, 40] });
if (lastSelected && lastSelected !== l) geojsonLayer.resetStyle(lastSelected);
l.setStyle(highlightStyle(l.feature));
l.bringToFront();
lastSelected = l;
showDetail(l.feature.properties);
}
});
}
// ── INFO BOX ──────────────────────────────────────────────────────────────────
const infoBox = L.control({ position: 'topright' });
infoBox.onAdd = function() {
this._div = L.DomUtil.create('div', 'info-box');
this.update(null);
return this._div;
};
infoBox.update = function(props) {
if (!props) {
this._div.innerHTML = '<strong>Peta Pontianak</strong><span>Arahkan kursor ke area</span>';
return;
}
const m = MODES[currentMode];
const val = props[m.key];
this._div.innerHTML = `
<strong>${props.Ket}</strong>
<span>${m.label}</span>
<span style="color:#e6edf3;font-size:14px;font-weight:700;margin-top:2px;display:block">
${val.toLocaleString('id-ID')} <small style="color:#8b949e;font-size:10px">${m.unit}</small>
</span>`;
};
infoBox.addTo(map);
// ── RENDER ─────────────────────────────────────────────────────────────────────
function renderMap() {
if (geojsonLayer) map.removeLayer(geojsonLayer);
lastSelected = null;
geojsonLayer = L.geoJson(pontianakData, {
style,
onEachFeature,
}).addTo(map);
buildLegend();
buildCards();
// Reset detail
document.getElementById('detail-panel').classList.remove('visible');
document.getElementById('placeholder').style.display = '';
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
}
function setMode(mode, btn) {
currentMode = mode;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderMap();
if (lastSelected) infoBox.update(null);
}
// Init
renderMap();
</script>
</body>
</html>