Programming
map
import json
import plotly.express as px
import pandas as pd
from django.shortcuts import render
import os
from general.functions import read_recent_file
from general.formatters import format_number
from django.contrib.auth.decorators import login_required
@login_required
def mapper(request):
npl_or_portfel = request.GET.get('choice')
if npl_or_portfel == 'npl':
colorising = 'percent'
color = 'Reds'
else:
colorising = 'total'
color = 'Greens'
# Sample DataFrame with fictional population data
all_regions = {
'region_code': ['UZAN', 'UZBU', 'UZJI', 'UZQA', 'UZNG', 'UZNW', 'UZSA', 'UZSU', 'UZSI', 'UZXO', 'UZFA', 'UZQR', 'UZTK'], # Combine UZTO and UZTK into UZTK
'region_name_rus': [
'Андижанская область',
'Бухарская область',
'Джизакская область',
'Кашкадарьинская область',
'Наманганская область',
'Навоийская область',
'Самаркандская область',
'Сурхандарьинская область',
'Сырдарьинская область',
'Хорезмская область',
'Ферганская область',
'Республика Каракалпакстан',
'Ташкент (город и область)' # Combined name
],
'region_name_show': [
'Андижан',
'Бухара',
'Джизак',
'Кашкадаря',
'Наманган',
'Навоий',
'Самарканд',
'Сурхандаря',
'Сырдаря',
'Хорезм',
'Фергана',
'Каракалпакстан',
'Ташкент' # Combined name
],
'population': [
3100000, 1900000, 1400000, 3200000, 2700000, 1000000, 3800000, 2600000, 850000, 1600000, 3700000, 1900000,
2900000 + 2900000 # Sum population of UZTO and UZTK
]
}
region_mapping = {
'Андижанская область': 'Андижанская область',
'Бухарская область': 'Бухарская область',
'Кашкадарьинская область': 'Кашкадарьинская область',
'Ферганская область': 'Ферганская область',
'г. Ташкент с учетом Ташкентской области': 'Ташкент (город и область)', # Map to combined region
'Наманганская область': 'Наманганская область',
'Республика Каракалпакстан': 'Республика Каракалпакстан',
'Самаркандская область': 'Самаркандская область',
'Сурхандарьинская область': 'Сурхандарьинская область',
'Хорезмская область': 'Хорезмская область'
}
region_mapping_1 = {
'Андижанская область': 'Андижан',
'Бухарская область': 'Бухара',
'Кашкадарьинская область': 'Кашкадаря',
'Ферганская область': 'Фергана',
'г. Ташкент с учетом Ташкентской области': 'Ташкент', # Map to combined region
'Наманганская область': 'Наманган',
'Республика Каракалпакстан': 'Каракалпакстан',
'Самаркандская область': 'Самарканд',
'Сурхандарьинская область': 'Сурхандаря',
'Хорезмская область': 'Хорезм'
}
df = pd.DataFrame(all_regions)
# Load and process loan portfolio data
lp, lp_name = read_recent_file()
print("This is", lp.columns)
lp_reg = pd.pivot_table(lp, index='region', columns='is_npl', values='Ос. кр. на балансе(экв.брутто)', aggfunc='sum').reset_index()
lp_reg[['yes', 'no']] = lp_reg[['yes', 'no']].fillna(0)
lp_reg['yes'] = lp_reg['yes'].fillna(0)
lp_reg['total'] = lp_reg['yes'] + lp_reg['no']
lp_reg['total'] = lp_reg['total'].fillna(0)
lp_reg['percent'] = lp_reg['yes'] / lp_reg['total'].replace(0, 1)*100 # Avoid division by zero
lp_reg['geojson_region'] = lp_reg['region'].map(region_mapping_1)
# Aggregate Tashkent City and Region data
lp_reg = lp_reg.groupby('geojson_region').agg({
'yes': 'sum',
'no': 'sum',
'total': 'sum',
'percent': 'mean' # Average percent for combined region
}).reset_index()
# Merge with all regions to ensure all are included
df = pd.merge(df, lp_reg, left_on='region_name_show', right_on='geojson_region', how='left')
df['total'] = df['total'].fillna(0)
df['yes'] = df['yes'].fillna(0)
df['percent'] = df['percent'].fillna(0)
df['percent']= df['percent'].round(1)
df = df.sort_values(colorising, ascending = False)
# Load GeoJSON
geojson_path = 'service/uz.json'
with open(geojson_path, 'r') as f:
geojson_data = json.load(f)
# Modify GeoJSON to combine UZTO and UZTK
for feature in geojson_data['features']:
if feature['properties']['id'] == 'UZTO':
feature['properties']['id'] = 'UZTK' # Map UZTO to UZTK
# Create Plotly Choroplethmapbox
df['NPL'] = df['yes'].apply(format_number).fillna(0)
df['КП'] = df['total'].apply(format_number).fillna(0)
fig = px.choropleth_mapbox(
df,
geojson=geojson_data,
locations='region_code',
featureidkey='properties.id',
color=colorising,
color_continuous_scale=color,
range_color=[0, df[colorising].max() or 1],
mapbox_style='carto-positron',
zoom=5,
center={'lat': 41.3775, 'lon': 64.5853},
opacity=0.7,
labels={'total': 'Объем кредитов (у.е.)', 'region_name_show': 'Регион', 'NPL': 'NPL90+', 'percent': 'Доля NPL (%)'},
hover_data=['region_name_rus', 'КП', 'percent', 'NPL'],
width=1100,
height=800
)
# Update layout for a polished look
fig.update_layout(
modebar={
'orientation': 'v', # Vertical orientation for left-side modebar
'remove': ['pan', 'lasso', 'select', 'select2d', 'box'], # Remove pan, lasso, and box select buttons
'bgcolor': 'rgba(255, 255, 255, 0.5)', # Semi-transparent background
'color': 'black',
},
margin={'r': 0, 't': 0, 'l': 0, 'b': 0},
mapbox=dict(
accesstoken='your_mapbox_token_here' # Replace with your Mapbox token
),
paper_bgcolor='rgba(245,245,245,1)',
plot_bgcolor='rgba(245,245,245,1)',
showlegend=True,
transition=dict(duration=500, easing='cubic-in-out')
)
# Convert Plotly figure to JSON for client-side updates
return render(request, 'service/map.html', {'lp_name':lp_name,
'plot_div': fig.to_html(full_html=False),
'regions': df[['region_name_show', 'total', 'percent', 'NPL']].to_dict('records'),
'data_json': df[['region_code', 'total', 'percent', 'NPL']].to_json(orient='records')
}){% extends 'base.html' %}
{% load currency_filters %}
{% block extra_css %}
<style>
/* Defining modern color palette and typography */
:root {
--primary: #1e3a8a; /* Deep blue for elegance */
--secondary: #3b82f6; /* Vibrant blue for accents */
--success: #10b981; /* Green for positive metrics */
--warning: #f59e0b; /* Amber for caution */
--danger: #ef4444; /* Red for critical metrics */
--former: #61a7d62a; /* Red for critical metrics */
--light: #f8fafc; /* Light background for contrast */
--dark: #111827; /* Dark text for readability */
--accent: #8b5cf6; /* Purple for highlights */
--shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
}
/* Form Styling */
.form-container {
display: inline-flex;
justify-content: center;
padding: 0.5rem; /* Reduced padding for less vertical space */
background: var(--former);
box-shadow: var(--shadow-sm);
border-radius: 12px;
margin: 0.5rem auto; /* Reduced margin */
max-width: 350px; /* Slightly narrower container */
}
.form-label {
font-size: 0.9rem; /* Smaller label font */
font-weight: 600;
color: var(--primary);
margin-right: 0.75rem; /* Slightly smaller gap */
align-self: center;
}
select, button {
padding: 0.5rem; /* Reduced padding for smaller elements */
border-radius: 8px;
border: 1px solid var(--secondary);
font-size: 0.9rem; /* Smaller font size */
font-family: var(--font-sans);
transition: all 0.3s ease;
}
select {
background: #ffffff;
margin-right: 0.75rem; /* Slightly smaller gap */
cursor: pointer;
}
select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
}
button {
background: var(--secondary);
color: #ffffff;
border: none;
cursor: pointer;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
button:hover {
background: var(--accent);
transform: translateY(-2px);
}
/* Region Grid */
.region-grid {
display: grid;
grid-template-columns: repeat(2, 1fr); /* Two columns for larger screens */
gap: 1rem;
padding: 1rem;
}
.region-card {
border: none;
border-radius: 12px;
padding: 0.02rem;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: var(--shadow-sm);
background: linear-gradient(145deg, #ffffff, #f1f5f9);
position: relative;
overflow: hidden;
}
.region-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
transition: height 0.4s ease;
}
.region-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-md);
}
.region-card:hover::before {
height: 8px;
}
.region-card.low::before {
background: var(--success);
}
.region-card.medium::before {
background: var(--warning);
}
.region-card.high::before {
background: var(--danger);
}
.region-name {
font-size: 1.1rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 0.2rem;
letter-spacing: -0.02em;
padding-left: 0.4rem;
}
.credit-value, .npl-percent, .npl-value {
font-size: 0.9rem;
margin-bottom: 0.2rem;
font-weight: 500;
padding-left: 0.4rem;
}
.credit-value {
color: var(--success);
}
.npl-value {
color: var(--secondary);
}
/* Map Container */
.map-container {
background: #ffffff;
border-radius: 20px;
box-shadow: var(--shadow-sm);
height: 90vh;
position: relative;
overflow: hidden;
transition: all 0.4s ease;
}
.map-container:hover {
box-shadow: var(--shadow-md);
transform: translateY(-5px);
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 8px solid #e5e7eb;
border-top: 8px solid var(--accent);
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 0.7s linear infinite;
z-index: 10;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); }
70% { box-shadow: 0 0 0 12px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
.region-grid, .map-container {
animation: fadeIn 0.8s ease-out forwards;
}
.header-title {
position: relative;
display: inline-block;
padding-bottom: 10px;
margin-bottom: 1rem;
font-size: 2.5rem;
font-weight: 800;
color: var(--primary);
text-transform: uppercase;
letter-spacing: -0.03em;
}
.header-title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: linear-gradient(90deg, var(--secondary), var(--accent));
border-radius: 4px;
transition: width 0.3s ease;
}
.header-title:hover::after {
width: 100px;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.region-grid {
grid-template-columns: 1fr;
}
.region-name {
font-size: 1.1rem;
}
.credit-value, .npl-percent, .npl-value {
font-size: 0.9rem;
}
.map-container {
height: 70vh;
}
.header-title {
font-size: 2rem;
}
.form-container {
flex-direction: row; /* Keep horizontal on mobile */
gap: 0.5rem;
padding: 0.5rem;
}
select, button {
padding: 0.4rem; /* Even smaller padding for mobile */
font-size: 0.85rem; /* Slightly smaller font */
}
.form-label {
font-size: 0.85rem;
}
}
</style>
{% endblock extra_css %}
{% block extra_js %}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loader = document.getElementById('loader');
const map = document.getElementById('map');
// Smooth loader fade-out when map is ready
setTimeout(() => {
loader.style.opacity = '0';
loader.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
loader.style.display = 'none';
map.style.opacity = '1';
map.style.transition = 'opacity 0.5s ease';
}, 500);
}, 800);
// Enhanced pulse effect on cards
const cards = document.querySelectorAll('.region-card');
cards.forEach((card, index) => {
setTimeout(() => {
card.style.animation = 'pulse 1.8s ease-in-out';
setTimeout(() => {
card.style.animation = '';
}, 1800);
}, index * 150);
});
// Add hover effect to form elements
const select = document.querySelector('select');
const button = document.querySelector('button');
select.addEventListener('focus', () => {
select.style.transform = 'scale(1.02)';
});
select.addEventListener('blur', () => {
select.style.transform = 'scale(1)';
});
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.05)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
});
});
</script>
{% endblock extra_js %}
{% block content %}
<div class="min-vh-100 d-flex flex-column">
<div class="container-fluid py-4">
<hr style="border: none; height: 2px; background: linear-gradient(90deg, transparent, var(--accent), transparent);">
<h1 class="page-header" style="text-align: center;">
Кредитный Портфель и NPL по Регионам {{lp_name}}
</h1>
<div class="form-container">
<form action="" method="GET" id="choice">
<label class="form-label" for="choice-select">Сортировать по:</label>
<select name="choice" id="choice-select">
<option value="portfel" {% if request.GET.choice == 'portfel' %}selected{% endif %}>Портфель</option>
<option value="npl" {% if request.GET.choice == 'npl' %}selected{% endif %}>NPL</option>
</select>
<button type="submit">Выбрать</button>
</form>
</div>
</div>
<!-- Main Content -->
<div class="container-fluid flex-grow-1 py-4">
<div class="row g-4">
<!-- Sidebar -->
<div class="col-lg-4">
<div class="region-grid">
{% for region in regions %}
<div class="region-card {% if region.percent < 5 %}low{% elif region.percent < 10 %}medium{% else %}high{% endif %}">
<p class="region-name">{{ region.region_name_show }}</p>
<p class="credit-value">Кредиты: {{ region.total|format_currency }}</p>
<p class="npl-percent" style="{% if region.percent < 3 %}color: var(--success);{% elif region.percent < 7 %}color: var(--secondary);{% else %}color: var(--danger);{% endif %}">
Доля NPL: {{ region.percent|floatformat:1 }}%
</p>
<p class="npl-value">NPL90+: {{ region.NPL|format_currency }}</p>
</div>
{% endfor %}
</div>
</div>
<!-- Map -->
<div class="col-lg-8">
<div class="map-container">
<div id="loader" class="loader"></div>
<div id="map" class="h-100" style="opacity: 0;">{{ plot_div | safe }}</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
PO
powerty
Muallif
· Staff
Okt. 16, 2026
Okt. 20, 2026
24
Ko'rishlar
0
Yoqishlar
8m
O'qildi