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 %}
Foydali bo'ldimi? Yoqmadi 0 Baholash uchun kiring