import folium
from folium import plugins
import json
import os
import pandas as pd
from collections import Counter
import folium
from folium import plugins
import json
import os
import pandas as pd
from collections import Counter
def load_case_data_from_csv(filter_year='all', filter_crime='all', filter_city='all'):
"""Load case data from a single cleaned CSV file."""
cleaned_csv_path = "/app/cleaned_data.csv"
if not os.path.exists(cleaned_csv_path):
raise FileNotFoundError(f"{cleaned_csv_path} not found.")
# load 1 file saja
data = pd.read_csv(cleaned_csv_path, on_bad_lines='skip')
# normalisasi ke lowercase
data_lower = data.map(
lambda x: x.lower().strip() if isinstance(x, str) and x.strip() != "" else x
)
# normalisasi nama pengadilan → ambil kota-nya saja
if 'lembaga_peradilan' in data_lower.columns:
data_lower['kota'] = (
data_lower['lembaga_peradilan']
.str.replace(r'^pn\s+', '', regex=True)
.str.strip()
.str.title()
)
else:
data_lower['kota'] = None
# parsing tanggal
if 'tanggal_musyawarah' in data_lower.columns:
data_lower['tanggal'] = pd.to_datetime(
data_lower['tanggal_musyawarah'], errors='coerce'
)
data_lower['tahun_putusan'] = data_lower['tanggal'].dt.year.astype('Int64')
df = data_lower.copy()
# filter year
if filter_year != 'all':
try:
year_val = int(filter_year)
df = df[df['tahun_putusan'] == year_val]
except:
pass
# filter crime
if filter_crime != 'all' and 'kata_kunci' in df.columns:
df = df[df['kata_kunci'] == filter_crime.lower()]
# filter city
if filter_city != 'all':
df = df[df['kota'].str.lower() == filter_city.lower()]
if df.empty:
return {}
# agregasi
case_data = {}
# group by kota
grouped = df.groupby('kota')
for kota, group in grouped:
total_cases = len(group)
# ambil top 10 kejahatan
if 'kata_kunci' in group.columns:
crime_counts = group['kata_kunci'].value_counts().head(10)
else:
crime_counts = {}
cases = {crime.title(): int(count) for crime, count in crime_counts.items()}
case_data[kota] = {
'total': total_cases,
'cases': cases,
}
return case_data
def create_heatmap_interactive(filter_year='all', filter_crime='all', filter_city='all'):
"""Create an interactive Folium choropleth heatmap with click-to-zoom and case information.
Args:
filter_year: Filter by specific year or 'all' for all years
filter_crime: Filter by specific crime type or 'all' for all crimes
filter_city: Filter by specific city/kabupaten or 'all' for all cities
Returns:
str: HTML string for embedding the Folium map.
"""
# Load real case data from CSV with filters
real_case_data = load_case_data_from_csv(filter_year=filter_year, filter_crime=filter_crime, filter_city=filter_city)
# Load GeoJSON data with metric
try:
with open('app/static/geojson/jatim_kabkota_metric.geojson', encoding='utf-8') as f:
geojson_data = json.load(f)
except:
# Fallback to original if metric version doesn't exist
with open('data/geojson/jatim_kabkota.geojson', encoding='utf-8') as f:
geojson_data = json.load(f)
# Update features with real case data
for feature in geojson_data['features']:
kabupaten_name = feature['properties'].get('name', feature['properties'].get('NAMOBJ', ''))
if kabupaten_name in real_case_data:
# Use real data
data = real_case_data[kabupaten_name]
feature['properties']['metric'] = data['total']
feature['properties']['cases'] = data['cases']
feature['properties']['total_cases'] = data['total']
else:
# Fallback jika tidak ada data
feature['properties']['metric'] = 0
feature['properties']['cases'] = {}
feature['properties']['total_cases'] = 0
# Calculate bounds from all features for Jawa Timur only
all_bounds = []
for feature in geojson_data['features']:
geom = feature['geometry']
if geom['type'] == 'Polygon':
for coord in geom['coordinates'][0]:
all_bounds.append([coord[1], coord[0]])
elif geom['type'] == 'MultiPolygon':
for poly in geom['coordinates']:
for coord in poly[0]:
all_bounds.append([coord[1], coord[0]])
# Get min/max bounds for Jawa Timur
if all_bounds:
lats = [b[0] for b in all_bounds]
lons = [b[1] for b in all_bounds]
min_lat, max_lat = min(lats), max(lats)
min_lon, max_lon = min(lons), max(lons)
# Add small buffer (0.1 degrees)
buffer = 0.1
bounds = [
[min_lat - buffer, min_lon - buffer], # Southwest
[max_lat + buffer, max_lon + buffer] # Northeast
]
else:
# Fallback bounds for Jawa Timur
bounds = [[-8.8, 111.0], [-6.0, 114.5]]
# Create Folium map with restricted bounds
m = folium.Map(
location=[-7.5, 112.5],
zoom_start=9, # Start zoom level 9 - nyaman lihat seluruh Jatim
min_zoom=8, # Min zoom 8 - bisa lihat peta lebih luas sedikit
max_zoom=13, # Max zoom 13 - cukup untuk detail
tiles=None, # No tiles initially
zoom_control=True, # Tampilkan tombol zoom
scrollWheelZoom=False, # Matikan scroll wheel zoom - hanya pakai tombol
prefer_canvas=True,
max_bounds=True,
min_lat=bounds[0][0],
max_lat=bounds[1][0],
min_lon=bounds[0][1],
max_lon=bounds[1][1]
)
# Add tiles only for Jawa Timur area using TileLayer with bounds
folium.TileLayer(
tiles='CartoDB positron',
attr='CartoDB',
name='Base Map',
overlay=False,
control=False,
bounds=bounds
).add_to(m)
# Don't use fit_bounds here - it will override zoom_start
# Instead, we set zoom_start=9 above and let JavaScript handle bounds
# Create choropleth layer
choropleth = folium.Choropleth(
geo_data=geojson_data,
name='Legal Case Heatmap',
data={f['properties']['name']: f['properties']['metric'] for f in geojson_data['features']},
columns=['name', 'metric'],
key_on='feature.properties.name',
fill_color='OrRd',
fill_opacity=0.8,
line_opacity=0.5,
line_weight=1.5,
legend_name='Number of Cases',
highlight=True,
).add_to(m)
# Add interactive tooltips and popups with click-to-zoom
for feature in geojson_data['features']:
props = feature['properties']
name = props.get('name', 'Unknown')
total = props.get('total_cases', props.get('metric', 0))
cases = props.get('cases', {})
# Get centroid for marker
lat = props.get('centroid_lat')
lon = props.get('centroid_lon')
# Create detailed popup content
case_list = '
'.join([f'{k}: {v} case' for k, v in cases.items() if v > 0])
popup_html = f'''