Rafael Lopez

ubigeo

......@@ -14,46 +14,37 @@
let selectedIdx = $state(-1);
let debounceTimer;
// Mapeo de clasificadores landing → filtros modal
const classifierToFilter = {
entidad: 'entidad',
objeto: 'objeto',
rubro: 'rubro',
finfun: 'finfun',
organismo: 'organismo',
fuente: 'fuente'
};
function getInitialFilters() {
const mode = get(landingSearchMode);
const classifiers = get(landingSelectedClassifiers);
if (mode === 'clasificadores' && classifiers.length > 0) {
const filters = { entidad: false, objeto_gasto: false, rubro: false, finfun: false, organismo: false, fuente: false };
classifiers.forEach(c => {
const key = classifierToFilter[c];
if (key === 'objeto') filters.objeto_gasto = true;
else if (key && filters[key] !== undefined) filters[key] = true;
});
return filters;
let searchMode = $state(get(landingSearchMode));
let selectedClassifiers = $state([...get(landingSelectedClassifiers)]);
const CLASIFICADORES = [
{ id: 'entidad', label: '¿Quién gasta?', tecnico: 'Institucional' },
{ id: 'objeto', label: '¿En qué se gasta?', tecnico: 'Objeto de gasto' },
{ id: 'finfun', label: '¿Para qué se gasta?', tecnico: 'Finalidad y función' },
{ id: 'geografico', label: '¿Dónde se gasta?', tecnico: 'Geográfico' },
{ id: 'sectores', label: '¿En qué sectores?', tecnico: 'Sectores económicos' },
{ id: 'rubro', label: '¿Con qué recursos?', tecnico: 'Rubros' },
{ id: 'organismo', label: '¿Quién financia?', tecnico: 'Organismos' },
{ id: 'fuente', label: 'Origen del ingreso', tecnico: 'Fuentes' },
];
function toggleClassifier(id) {
if (selectedClassifiers.includes(id)) {
selectedClassifiers = selectedClassifiers.filter(c => c !== id);
} else {
selectedClassifiers = [...selectedClassifiers, id];
}
return { entidad: true, objeto_gasto: true, rubro: true, finfun: true, organismo: true, fuente: true };
}
let searchFilters = $state(getInitialFilters());
function toggleFilter(tipo) {
searchFilters[tipo] = !searchFilters[tipo];
// Re-buscar con filtros actualizados
// Sync to landing store
landingSelectedClassifiers.set(selectedClassifiers);
if (searchVal.length >= 2) doSearch(searchVal);
}
// Filtros activos → class_ param para Typesense
// Clasificadores seleccionados → class_ param para Typesense
const CLASS_API_MAP = { geografico: 'ubigeo', sectores: 'acteco' };
function getActiveClasses() {
const map = { entidad: 'entidad', objeto_gasto: 'objeto', finfun: 'finfun' };
return Object.entries(searchFilters)
.filter(([_, v]) => v)
.map(([k]) => map[k] || k)
.filter(Boolean);
if (selectedClassifiers.length === 0) return [];
return selectedClassifiers.map(c => CLASS_API_MAP[c] || c);
}
function parseMetadatos(meta) {
......@@ -66,33 +57,39 @@
searchLoading = true;
try {
const classes = getActiveClasses();
const isClassMode = searchMode === 'clasificadores' && classes.length > 0;
const isProgramMode = searchMode === 'programas';
const params = new URLSearchParams({
q: query,
is_class: 'true',
per_page: '30'
per_page: '50'
});
if (classes.length > 0 && classes.length < 6) {
if (isClassMode) {
params.set('is_class', 'true');
params.set('class_', classes.join(','));
} else if (isProgramMode) {
params.set('is_class', 'false');
}
// Si ninguno: busca en todo
const res = await fetch(`/api/search?${params}`);
if (!res.ok) throw new Error();
const data = await res.json();
searchResults = (data.hits || []).map(hit => {
const meta = parseMetadatos(hit.document.metadatos);
const cls = hit.document.class_;
let tipo = cls;
const isClass = hit.document.is_class === true;
let tipo = cls || 'programa';
let codigo = '';
let nombre = hit.document.texto;
let highlight = hit.highlights?.[0]?.snippet || nombre;
if (cls === 'entidad') {
if (isClass && cls === 'entidad') {
const esDA = !!meta.da;
codigo = esDA ? `${meta.entidad}.${meta.da}` : String(meta.entidad);
tipo = 'entidad';
} else if (cls === 'objeto') {
} else if (isClass && cls === 'objeto') {
codigo = meta.objeto_subpartida || meta.objeto_partida || meta.objeto_subgrupo || meta.objeto_grupo || '';
tipo = 'objeto_gasto';
} else if (cls === 'finfun') {
} else if (isClass && cls === 'finfun') {
const fin = String(meta.finfun_finalidad || '');
if (meta.finfun_funcion !== undefined && meta.finfun_grpfuncion !== undefined) {
codigo = `${fin}.${meta.finfun_grpfuncion}.${meta.finfun_funcion}`;
......@@ -102,9 +99,16 @@
codigo = fin;
}
tipo = 'finfun';
} else if (isClass && cls === 'ubigeo') {
codigo = String(meta.municipio_ubigeo || '');
tipo = 'ubigeo';
} else if (!isClass) {
// Programa/proyecto
tipo = 'programa';
codigo = `${meta.entidad || ''}-${meta.programa || ''}-${meta.proyecto || ''}`;
}
return { tipo, codigo, nombre, highlight };
return { tipo, codigo, nombre, highlight, devengado: hit.document.devengado, gestion: hit.document.gestion };
});
selectedIdx = -1;
} catch {
......@@ -115,10 +119,31 @@
let filteredResults = $derived(searchResults);
// Focus input and sync filters when modal opens
// Focus input and sync state when modal opens
$effect(() => {
if (open) {
searchFilters = getInitialFilters();
const storeMode = get(landingSearchMode);
const storeClassifiers = get(landingSelectedClassifiers);
searchMode = storeMode;
selectedClassifiers = [...storeClassifiers];
// Inferir del contexto de ruta si no hay clasificadores
if (storeMode !== 'clasificadores' || storeClassifiers.length === 0) {
if (typeof window !== 'undefined') {
const path = window.location.pathname;
if (path.startsWith('/entidad/')) {
searchMode = 'clasificadores';
selectedClassifiers = ['entidad'];
} else if (path.startsWith('/objeto/')) {
searchMode = 'clasificadores';
selectedClassifiers = ['objeto'];
} else if (path.startsWith('/finfun/')) {
searchMode = 'clasificadores';
selectedClassifiers = ['finfun'];
}
}
}
tick().then(() => {
searchInput?.focus();
});
......@@ -160,19 +185,18 @@
function goToResult(item) {
closeModal();
if (item.tipo === 'entidad') {
goto(`/entidad/${item.codigo}`);
} else if (item.tipo === 'objeto_gasto') {
goto(`/objeto/${item.codigo}`);
} else if (item.tipo === 'finfun') {
goto(`/finfun/${item.codigo}`);
} else if (item.tipo === 'rubro') {
goto(`/rubro/${item.codigo}`);
} else if (item.tipo === 'organismo') {
goto(`/organismo/${item.codigo}`);
} else if (item.tipo === 'fuente') {
goto(`/fuente/${item.codigo}`);
}
const routes = {
entidad: '/entidad/',
objeto_gasto: '/objeto/',
finfun: '/finfun/',
rubro: '/rubro/',
organismo: '/organismo/',
fuente: '/fuente/',
ubigeo: '/ubicacion/',
programa: '/proyecto/'
};
const base = routes[item.tipo];
if (base) goto(`${base}${item.codigo}`);
}
function handleBackdropClick(e) {
......@@ -212,6 +236,16 @@
label: 'Fuente',
color: '#7BC9A1',
description: 'Origen del financiamiento: TGN, créditos...'
},
ubigeo: {
label: 'Ubicación',
color: '#6B9FD4',
description: 'Municipios y ubicaciones geográficas'
},
programa: {
label: 'Programa',
color: '#8A8578',
description: 'Programas y proyectos de inversión'
}
};
</script>
......@@ -229,7 +263,7 @@
bind:this={searchInput}
type="text"
class="search-modal-input"
placeholder="Buscar instituciones, gastos, ingresos..."
placeholder={searchMode === 'programas' ? 'Buscar programas y proyectos...' : selectedClassifiers.length > 0 ? 'Buscar en clasificadores seleccionados...' : 'Selecciona al menos un clasificador...'}
value={searchVal}
oninput={handleInput}
/>
......@@ -238,85 +272,30 @@
</button>
</div>
<!-- Filters section with explicit descriptions -->
<!-- Mode toggle + classifier pills -->
<div class="search-modal-filters">
<div class="filters-header">
<span class="filters-label">Buscar en:</span>
</div>
<div class="filters-grid">
<button
class="search-filter-card"
class:filter-active={searchFilters.entidad}
onclick={() => toggleFilter('entidad')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.entidad.color}"></span>
<span class="filter-title">Instituciones</span>
<span class="filter-check">{searchFilters.entidad ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.entidad.description}</span>
<div class="mode-toggle">
<button class="mode-btn" class:active={searchMode === 'programas'} onclick={() => { searchMode = 'programas'; selectedClassifiers = []; landingSearchMode.set('programas'); landingSelectedClassifiers.set([]); if (searchVal.length >= 2) doSearch(searchVal); }}>
Programas
</button>
<button
class="search-filter-card"
class:filter-active={searchFilters.objeto_gasto}
onclick={() => toggleFilter('objeto_gasto')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.objeto_gasto.color}"></span>
<span class="filter-title">Gastos</span>
<span class="filter-check">{searchFilters.objeto_gasto ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.objeto_gasto.description}</span>
</button>
<button
class="search-filter-card"
class:filter-active={searchFilters.rubro}
onclick={() => toggleFilter('rubro')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.rubro.color}"></span>
<span class="filter-title">Ingresos</span>
<span class="filter-check">{searchFilters.rubro ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.rubro.description}</span>
</button>
<button
class="search-filter-card"
class:filter-active={searchFilters.finfun}
onclick={() => toggleFilter('finfun')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.finfun.color}"></span>
<span class="filter-title">Finalidad</span>
<span class="filter-check">{searchFilters.finfun ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.finfun.description}</span>
</button>
<button
class="search-filter-card"
class:filter-active={searchFilters.organismo}
onclick={() => toggleFilter('organismo')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.organismo.color}"></span>
<span class="filter-title">Organismos</span>
<span class="filter-check">{searchFilters.organismo ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.organismo.description}</span>
</button>
<button
class="search-filter-card"
class:filter-active={searchFilters.fuente}
onclick={() => toggleFilter('fuente')}
>
<div class="filter-card-header">
<span class="filter-dot" style="background:{typeConfig.fuente.color}"></span>
<span class="filter-title">Fuentes</span>
<span class="filter-check">{searchFilters.fuente ? '✓' : ''}</span>
</div>
<span class="filter-desc">{typeConfig.fuente.description}</span>
<button class="mode-btn" class:active={searchMode === 'clasificadores'} onclick={() => { searchMode = 'clasificadores'; landingSearchMode.set('clasificadores'); if (searchVal.length >= 2) doSearch(searchVal); }}>
Clasificadores
</button>
</div>
{#if searchMode === 'clasificadores'}
<div class="classifier-pills">
{#each CLASIFICADORES as cls}
<button
class="classifier-pill"
class:active={selectedClassifiers.includes(cls.id)}
onclick={() => toggleClassifier(cls.id)}
>
<span class="pill-label">{cls.label}</span>
<span class="pill-tecnico">{cls.tecnico}</span>
</button>
{/each}
</div>
{/if}
</div>
<!-- Results -->
......@@ -485,11 +464,14 @@
background: rgba(0, 0, 0, 0.1);
}
/* Filters */
/* Filters: mode toggle + classifier pills */
.search-modal-filters {
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
gap: 10px;
}
:global(html:not(.dark)) .search-modal-filters {
......@@ -497,90 +479,98 @@
background: rgba(0, 0, 0, 0.02);
}
.filters-header {
margin-bottom: 8px;
.mode-toggle {
display: flex;
gap: 4px;
background: rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 3px;
}
.filters-label {
font-size: 0.6875rem;
:global(html:not(.dark)) .mode-toggle {
background: rgba(0, 0, 0, 0.05);
}
.mode-btn {
flex: 1;
padding: 7px 14px;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: var(--font-sans);
border: none;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.5;
opacity: 0.7;
transition: all 0.2s;
}
.filters-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
.mode-btn:hover {
opacity: 0.9;
}
.search-filter-card {
display: flex;
flex-direction: column;
gap: 3px;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
opacity: 0.5;
.mode-btn.active {
opacity: 1;
background: rgba(255, 255, 255, 0.12);
color: var(--theme-titulo);
}
:global(html:not(.dark)) .search-filter-card {
background: rgba(0, 0, 0, 0.03);
border-color: rgba(0, 0, 0, 0.1);
:global(html:not(.dark)) .mode-btn.active {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.search-filter-card:hover {
opacity: 0.8;
background: rgba(255, 255, 255, 0.08);
.classifier-pills {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
:global(html:not(.dark)) .search-filter-card:hover {
background: rgba(0, 0, 0, 0.06);
.classifier-pill {
display: flex;
align-items: baseline;
gap: 4px;
padding: 5px 10px;
font-size: 0.8125rem;
font-weight: 500;
font-family: var(--font-sans);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.65;
transition: all 0.2s;
}
.search-filter-card.filter-active {
opacity: 1;
border-color: rgba(201, 167, 81, 0.5);
background: rgba(201, 167, 81, 0.1);
.pill-label {
color: var(--theme-titulo);
}
.filter-card-header {
display: flex;
align-items: center;
gap: 6px;
.pill-tecnico {
font-size: 0.6875rem;
opacity: 0.7;
}
.filter-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
:global(html:not(.dark)) .classifier-pill {
border-color: rgba(0, 0, 0, 0.12);
}
.filter-title {
font-size: 0.8125rem;
font-weight: 600;
color: var(--theme-titulo);
flex: 1;
.classifier-pill:hover {
opacity: 0.8;
background: rgba(255, 255, 255, 0.06);
}
.filter-check {
font-size: 0.6875rem;
color: #C9A751;
font-weight: 600;
:global(html:not(.dark)) .classifier-pill:hover {
background: rgba(0, 0, 0, 0.04);
}
.filter-desc {
font-size: 0.625rem;
color: var(--theme-texto);
opacity: 0.6;
line-height: 1.3;
.classifier-pill.active {
opacity: 1;
border-color: rgba(201, 167, 81, 0.5);
background: rgba(201, 167, 81, 0.12);
color: var(--theme-titulo);
}
/* Results */
......
import { writable } from 'svelte/store';
// Persiste el estado de búsqueda del landing entre navegaciones
export const landingSearchMode = writable('programas'); // 'programas' | 'clasificadores'
export const landingSearchMode = writable('todo'); // 'todo' | 'programas' | 'clasificadores'
export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...]
export const landingSearchQuery = writable('');
......
import { writable } from 'svelte/store';
// Cache global del mapa - se carga una sola vez en toda la sesión
export const mapaCache = writable(null);
......@@ -8,6 +8,7 @@
let searchFocused = false;
let searchVal = $landingSearchQuery || '';
let searchInput;
let resultsAreaEl;
let isLoading = false;
let results = null;
let error = null;
......@@ -59,28 +60,49 @@
}
}
$: sortedHits = allHits.length > 0 ? [...allHits].sort((a, b) => {
$: sortedHits = filteredHits.length > 0 ? [...filteredHits].sort((a, b) => {
const dir = sortOrder === 'desc' ? 1 : -1;
if (sortBy === 'alpha') return dir * (a.document.texto || '').localeCompare(b.document.texto || '');
return dir * ((b.document.devengado || 0) - (a.document.devengado || 0));
}) : [];
// Detectar tipos de clasificadores presentes en los resultados
// Mapeo inverso: API class_ → id interno del clasificador
const CLASS_REVERSE_MAP = { ubigeo: 'geografico', acteco: 'sectores' };
// Filtrar hits según clasificadores seleccionados (la API puede devolver tipos extra)
$: filteredHits = (() => {
if (searchMode !== 'clasificadores' || selectedClassifiers.length === 0) return allHits;
const allowedClasses = new Set(selectedClassifiers.map(c => {
const map = { geografico: 'ubigeo', sectores: 'acteco' };
return map[c] || c;
}));
return allHits.filter(h => allowedClasses.has(h.document.class_));
})();
$: activeClassTypes = (() => {
const types = new Set();
allHits.forEach(h => { if (h.document.class_) types.add(h.document.class_); });
filteredHits.forEach(h => { if (h.document.class_) types.add(h.document.class_); });
return types;
})();
$: isMixedClassSearch = activeClassTypes.size > 1;
$: isEntidadSearchActive = !isMixedClassSearch && activeClassTypes.has('entidad');
$: isObjetoSearchActive = !isMixedClassSearch && activeClassTypes.has('objeto');
$: isFinfunSearchActive = !isMixedClassSearch && activeClassTypes.has('finfun');
$: isUbigeoSearchActive = !isMixedClassSearch && activeClassTypes.has('ubigeo');
// Labels para tipos de clasificador
const CLASS_LABELS = {
entidad: 'Entidades',
objeto: 'Objetos de Gasto',
finfun: 'Finalidad y Función',
rubro: 'Rubros de Ingreso',
organismo: 'Organismos',
fuente: 'Fuentes de Financiamiento',
ubigeo: 'Ubicación geográfica',
geografico: 'Ubicación geográfica',
acteco: 'Sectores Económicos',
sectores: 'Sectores Económicos',
};
// Extraer código de finfun desde metadatos (formato con puntos: 1, 1.1, 1.1.3)
......@@ -108,12 +130,12 @@
// Agrupar resultados según tipo
$: groupedHits = (() => {
if (allHits.length === 0) return null;
if (filteredHits.length === 0) return null;
// Búsqueda mixta: agrupar por tipo de clasificador
if (isMixedClassSearch) {
// Filtrar duplicados de finfun
const filtered = allHits.filter(hit => {
const filtered = filteredHits.filter(hit => {
if (hit.document.class_ !== 'finfun') return true;
const m = parseMetadatos(hit.document.metadatos);
return !(m.finfun_funcion === 0 && m.finfun_grpfuncion !== undefined);
......@@ -149,7 +171,7 @@
if (isEntidadSearchActive) {
const groups = {};
allHits.forEach(hit => {
filteredHits.forEach(hit => {
const m = parseMetadatos(hit.document.metadatos);
const subarea = extractAfterHyphen(m.entidad_desc_subarea) || 'Otros';
if (!groups[subarea]) {
......@@ -180,7 +202,7 @@
if (isObjetoSearchActive) {
const groups = {};
allHits.forEach(hit => {
filteredHits.forEach(hit => {
const m = parseMetadatos(hit.document.metadatos);
const groupName = extractAfterHyphen(m.objeto_desc_grupo) || 'Otros';
if (!groups[groupName]) {
......@@ -209,7 +231,7 @@
if (isFinfunSearchActive) {
// Filtrar duplicados: función 0 es idéntica a su grupo función padre
const filtered = allHits.filter(hit => {
const filtered = filteredHits.filter(hit => {
const m = parseMetadatos(hit.document.metadatos);
return !(m.finfun_funcion === 0 && m.finfun_grpfuncion !== undefined);
});
......@@ -242,6 +264,11 @@
return sortedGroups;
}
if (isUbigeoSearchActive) {
// Sin agrupación, se usa la vista plana
return null;
}
return null;
})();
......@@ -258,16 +285,52 @@
// Clasificadores unificados (transversales)
const CLASIFICADORES = [
{ id: 'entidad', label: '¿Quién gasta?', icon: '🏛️' },
{ id: 'objeto', label: '¿En qué se gasta?', icon: '📦' },
{ id: 'finfun', label: '¿Para qué se gasta?', icon: '🎯' },
{ id: 'geografico', label: '¿Dónde se gasta?', icon: '📍' },
{ id: 'sectores', label: '¿En qué sectores se gasta?', icon: '🏢' },
{ id: 'rubro', label: '¿Con qué recursos?', icon: '💰' },
{ id: 'organismo', label: '¿Quién financia?', icon: '🏦' },
{ id: 'fuente', label: 'Origen del ingreso', icon: '💵' },
{ id: 'entidad', label: '¿Quién gasta?', icon: '🏛️', tecnico: 'Clasificador institucional', desc: 'Ministerios, municipios, universidades, empresas públicas y todas las entidades del sector público que ejecutan presupuesto.' },
{ id: 'objeto', label: '¿En qué se gasta?', icon: '📦', tecnico: 'Objeto de gasto', desc: 'Categorías estandarizadas del destino del gasto: sueldos, viáticos, materiales, servicios básicos, inversiones, deuda, etc.' },
{ id: 'finfun', label: '¿Para qué se gasta?', icon: '🎯', tecnico: 'Finalidad y función', desc: 'El propósito del gasto según su función social: salud, educación, defensa, justicia, infraestructura, etc.' },
{ id: 'geografico', label: '¿Dónde se gasta?', icon: '📍', tecnico: 'Geográfico', desc: 'Distribución territorial del gasto por departamento y municipio.' },
{ id: 'sectores', label: '¿En qué sectores se gasta?', icon: '🏢', tecnico: 'Sectores económicos', desc: 'Sectores de actividad económica donde se dirige el gasto: agricultura, minería, manufactura, transporte, etc.' },
{ id: 'rubro', label: '¿Con qué recursos?', icon: '💰', tecnico: 'Rubros de ingreso', desc: 'Tipos de recursos que financian el gasto: impuestos, tasas, regalías, transferencias, donaciones, créditos, etc.' },
{ id: 'organismo', label: '¿Quién financia?', icon: '🏦', tecnico: 'Organismos financiadores', desc: 'Origen institucional del financiamiento: Tesoro General, gobiernos subnacionales, organismos internacionales, etc.' },
{ id: 'fuente', label: 'Origen del ingreso', icon: '💵', tecnico: 'Fuentes de financiamiento', desc: 'Clasificación por tipo de fuente: recursos propios, transferencias, crédito interno/externo, donaciones.' },
];
// Modal explicativo contextual
let showExplainModal = false;
let explainClassifier = null; // id del clasificador a explicar
function openExplain() {
if (selectedClassifiers.length === 1) {
explainClassifier = selectedClassifiers[0];
} else {
explainClassifier = null; // modal genérico
}
showExplainModal = true;
}
const EXPLAIN_CONTENT = {
entidad: {
titulo: 'Instituciones públicas',
subtitulo: 'Clasificador institucional',
intro: 'El presupuesto público boliviano se ejecuta a través de aproximadamente 2000 instituciones. Cada una recibe recursos, los gasta y reporta su ejecución.',
bloques: [
{
titulo: 'Entidades',
texto: 'Son las instituciones principales: ministerios, gobiernos municipales, universidades, empresas públicas, fondos, etc. Hay aproximadamente 500 y cada una tiene presupuesto propio consolidado.'
},
{
titulo: 'Unidades internas',
texto: 'Dentro de cada entidad pueden existir unidades con ejecución propia: hospitales municipales, concejos, direcciones ejecutivas, centros de salud, etc. Hay aproximadamente 1500 y siempre pertenecen a una entidad madre.'
},
{
titulo: 'Jerarquía',
texto: 'Las entidades se organizan en: Sector, Subsector, Área, Subárea y Entidad. Por ejemplo: Sector Público → No Financiero → Administración Territorial → Gobiernos Municipales → Gobierno Municipal de Cobija.'
}
],
link: { href: '/clasificadores/institucional', texto: 'Ver la lista completa de instituciones' }
}
};
function toggleClassifier(id) {
if (selectedClassifiers.includes(id)) {
selectedClassifiers = selectedClassifiers.filter(c => c !== id);
......@@ -282,9 +345,15 @@
}
function setMode(mode) {
searchMode = mode;
if (mode === 'programas') {
if (searchMode === mode) {
// Click en la misma card → deseleccionar, volver a "todo"
searchMode = 'todo';
selectedClassifiers = [];
} else {
searchMode = mode;
if (mode === 'programas') {
selectedClassifiers = [];
}
}
}
......@@ -464,6 +533,19 @@
return () => { sectionObserver?.disconnect(); cleanupHero?.(); };
});
// Medir posición del panel para max-height dinámico
function updateResultsHeight() {
if (resultsAreaEl) {
const rect = resultsAreaEl.getBoundingClientRect();
resultsAreaEl.style.setProperty('--results-top', `${rect.top}px`);
}
}
// Actualizar al cambiar resultados
$: if (allHits.length > 0 && resultsAreaEl) {
updateResultsHeight();
}
async function handleInput(e) {
searchVal = e.target.value;
clearTimeout(debounceTimer);
......@@ -481,13 +563,6 @@
return;
}
const needsClassifiers = searchMode === 'clasificadores';
if (needsClassifiers && selectedClassifiers.length === 0) {
results = null;
error = null;
return;
}
debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS);
}
......@@ -505,20 +580,25 @@
currentPage = page;
try {
const isClassSearch = searchMode === 'clasificadores';
const isClassSearch = searchMode === 'clasificadores' && selectedClassifiers.length > 0;
const isProgramSearch = searchMode === 'programas';
const params = new URLSearchParams({
q: query,
is_class: isClassSearch ? 'true' : 'false',
mode: 'gastos',
page: page.toString()
});
// Para clasificadores, pedir más resultados para tener conteos precisos
// Mapeo de ids internos a class_ de la API
const CLASS_API_MAP = { geografico: 'ubigeo', sectores: 'acteco' };
if (isClassSearch) {
params.set('is_class', 'true');
params.set('per_page', '250');
params.set('class_', selectedClassifiers.map(c => CLASS_API_MAP[c] || c).join(','));
} else if (isProgramSearch) {
params.set('is_class', 'false');
}
if (isClassSearch && selectedClassifiers.length > 0) {
params.set('class_', selectedClassifiers.join(','));
}
// Si ninguno: no envía is_class → busca en todo
console.log('[Search]', `${API_BASE}?${params}`);
const res = await fetch(`${API_BASE}?${params}`);
......@@ -550,7 +630,6 @@
function handleGlobalKeydown(e) {
if (e.key === 'Escape' && (searchVal || allHits.length > 0)) {
clearSearch();
searchInput?.blur();
selectedResultIndex = -1;
}
}
......@@ -558,7 +637,6 @@
function handleKeydown(e) {
if (e.key === 'Escape') {
clearSearch();
searchInput?.blur();
selectedResultIndex = -1;
}
......@@ -596,9 +674,13 @@
let url;
const isFinfunClass = hit.document.class_ === 'finfun';
const isUbigeoClass = hit.document.class_ === 'ubigeo';
if (isEntidadClass) {
const entCodigo = meta.da ? `${meta.entidad}.${meta.da}` : meta.entidad;
url = `/entidad/${entCodigo}`;
} else if (isUbigeoClass) {
url = `/ubicacion/${meta.municipio_ubigeo}`;
} else if (isObjetoClass) {
url = `/objeto/${getObjetoCodigo(meta)}`;
} else if (isFinfunClass) {
......@@ -623,6 +705,7 @@
clearTimeout(debounceTimer);
clearTimeout(hintTimer);
searchInput?.focus();
searchInput?.focus();
}
// Helpers
......@@ -719,27 +802,38 @@
return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`;
}
$: stats = allHits.length > 0 ? getStats(
(isFinfunSearchActive || isMixedClassSearch) ? flatHitsForNav : allHits,
(isFinfunSearchActive || isMixedClassSearch) ? flatHitsForNav.length : (results?.found || allHits.length)
$: stats = filteredHits.length > 0 ? getStats(
(isFinfunSearchActive || isMixedClassSearch) ? flatHitsForNav : filteredHits,
(isFinfunSearchActive || isMixedClassSearch) ? flatHitsForNav.length : filteredHits.length
) : null;
// Texto contextual para resultados
const CONTEXT_LABELS = {
entidad: 'Buscando entre 2000 instituciones públicas',
objeto: 'Buscando entre 2500 categorías de gasto',
finfun: 'Buscando entre 200 finalidades y funciones',
geografico: 'Buscando entre 467 geografías del país',
sectores: 'Buscando entre sectores económicos',
rubro: 'Buscando entre rubros de ingreso',
organismo: 'Buscando entre organismos financiadores',
fuente: 'Buscando entre fuentes de financiamiento',
};
$: searchContextText = (() => {
if (searchMode === 'programas') {
return 'Buscando en programas y proyectos';
} else {
if (selectedClassifiers.length === 0) return '';
}
if (searchMode === 'clasificadores' && selectedClassifiers.length > 0) {
if (selectedClassifiers.length === 1) {
return CONTEXT_LABELS[selectedClassifiers[0]] || '';
}
const labels = selectedClassifiers.map(id => {
const clf = CLASIFICADORES.find(c => c.id === id);
return clf ? clf.label.replace('¿', '').replace('?', '').toLowerCase() : id;
return clf ? clf.tecnico.toLowerCase() : id;
});
if (labels.length === 1) {
return `Buscando por ${labels[0]}`;
} else {
return `Buscando por ${labels.slice(0, -1).join(', ')} y ${labels[labels.length - 1]}`;
}
return `Buscando en ${labels.slice(0, -1).join(', ')} y ${labels[labels.length - 1]}`;
}
return 'Buscando en todo';
})();
$: {
......@@ -747,14 +841,7 @@
const _classifiers = selectedClassifiers;
if (searchVal.length >= 3 && mounted) {
clearTimeout(debounceTimer);
const needsClassifiers = _mode === 'clasificadores';
if (!needsClassifiers || _classifiers.length > 0) {
debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS);
} else {
// No hay clasificadores seleccionados - limpiar resultados
results = null;
allHits = [];
}
debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS);
}
}
......@@ -807,15 +894,15 @@
<div class="ambient-glow"></div>
<!-- Título -->
<h1 class="hero-title" class:hero-collapsed={allHits.length > 0}>
<h1 class="hero-title">
Presupuesto <span class="gold">Abierto</span>
</h1>
<p class="hero-sub" class:hero-collapsed={allHits.length > 0}>
<p class="hero-sub">
Información histórica y diaria de todo el Estado Boliviano.
</p>
<!-- Buscador -->
<div class="search-wrap" class:search-wrap-with-results={allHits.length > 0}>
<div class="search-wrap">
<div class="search-bar" class:search-focused={searchVal.length > 0} class:has-results={isSearching}>
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke={searchVal.length > 0 ? '#FFFFFF' : '#F0C14D'} stroke-width="2" stroke-linecap="round">
......@@ -846,6 +933,7 @@
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<kbd class="search-esc" on:click={clearSearch}>Esc</kbd>
{/if}
{#if isLoading}
<div class="search-loading"><span class="loading-dot"></span></div>
......@@ -854,11 +942,11 @@
</div>
<!-- ─── ÁREA DE RESULTADOS ─── -->
<div class="results-area">
<div class="results-area" bind:this={resultsAreaEl}>
<!-- ─── CARDS DE MODO (se tapan con resultados) ─── -->
<div class="section-group" class:section-group-hidden={allHits.length > 0}>
<span class="section-label">Configura tu búsqueda</span>
<div class="section-group">
<span class="section-label">Refinar búsqueda</span>
<div class="mode-cards">
<!-- Card: Programas y Proyectos -->
......@@ -986,36 +1074,31 @@
</a>
</div>
<!-- Estados de búsqueda (mensajes) -->
{#if showMinLengthHint && searchVal.length > 0 && searchVal.length < 3}
<div class="results-message">
<div class="results-hint">Escribe al menos 3 letras para buscar</div>
</div>
{:else if error && !results}
<div class="results-message">
<div class="results-error">{error}</div>
</div>
{:else if isLoading && !results}
<div class="results-message">
<div class="results-loading">Buscando...</div>
</div>
{:else if results && results.hits?.length === 0}
<div class="results-message">
<div class="results-empty">Sin resultados para "{searchVal}"</div>
</div>
{/if}
<!-- Título contextual de búsqueda -->
{#if allHits.length > 0 && searchContextText}
<div class="search-context-title">{searchContextText}</div>
{/if}
<!-- Contenedor de resultados (siempre montado para evitar flicker) -->
<!-- Contenedor de resultados (overlay absoluto) -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="results-content results-content-filled"
class:results-content-visible={allHits.length > 0}
class:results-content-visible={allHits.length > 0 || (results && results.hits?.length === 0)}
on:mousedown|preventDefault
>
{#if stats}
{#if results && results.hits?.length === 0 && !isLoading}
<div class="results-message-inline">
<span class="results-empty">Sin resultados para "{searchVal}"</span>
</div>
{/if}
<!-- Título contextual -->
{#if allHits.length > 0 && searchContextText}
<div class="search-context-row">
<span class="search-context-title">{searchContextText}</span>
{#if searchMode === 'clasificadores' && selectedClassifiers.length > 0}
<button class="search-explain-link" on:click|stopPropagation={openExplain}>
¿Qué busco?
</button>
{/if}
</div>
{/if}
{#if stats && !isUbigeoSearchActive}
<div class="results-summary">
<div class="summary-row">
<div class="summary-stats">
......@@ -1150,6 +1233,24 @@
</div>
</div>
</a>
{:else if hitClass === 'ubigeo'}
<a href="/ubicacion/{meta.municipio_ubigeo}" class="result-card result-card-grouped"
class:result-card-selected={selectedResultIndex === globalIdx}
data-result-index={globalIdx}>
<div class="result-content">
<div class="result-text">
{#if hit.highlights?.[0]?.snippet}
{@html hit.highlights[0].snippet}
{:else}
{hit.document.texto}
{/if}
</div>
<div class="result-meta">
<span class="result-subarea">{meta.desc_provincia}</span>
<span class="result-monto">{formatMonto(hit.document.devengado)}</span>
</div>
</div>
</a>
{/if}
{/each}
</div>
......@@ -1165,21 +1266,28 @@
{@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null}
{@const isObjetoClass = hit.document.class_ === 'objeto'}
{@const isFinfunClass = hit.document.class_ === 'finfun'}
{@const isUbigeoClass = hit.document.class_ === 'ubigeo'}
{@const isClassResult = hit.document.is_class === true}
{@const hitType = hit.document.class_}
<a
href={isEntidadClass
? `/entidad/${meta.da ? `${meta.entidad}.${meta.da}` : meta.entidad}`
: isObjetoClass
? `/objeto/${getObjetoCodigo(meta)}`
: isFinfunClass
? `/finfun/${getFinfunCodigo(meta)}`
: (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)}
: isUbigeoClass
? `/ubicacion/${meta.municipio_ubigeo}`
: isObjetoClass
? `/objeto/${getObjetoCodigo(meta)}`
: isFinfunClass
? `/finfun/${getFinfunCodigo(meta)}`
: (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)}
class="result-card"
class:result-card-selected={selectedResultIndex === i}
data-result-index={i}
>
<div class="result-content">
<div class="result-text">
{#if isMixedClassSearch && hitType}
<span class="result-type-badge">{CLASS_LABELS[hitType] || hitType}</span>
{/if}
{#if hit.highlights?.[0]?.snippet}
{@html hit.highlights[0].snippet}
{:else}
......@@ -1190,8 +1298,12 @@
{#if isEntidadClass}
{#if isDA}<span class="result-parent">Dependiente de {parentEntity}</span>{/if}
{#if subarea}<span class="result-subarea">{subarea}</span>{/if}
{:else if isUbigeoClass}
<span class="result-subarea">Departamento de {meta.desc_departamento}</span>
<span class="result-sep">·</span>
<span class="result-subarea">Provincia {meta.desc_provincia}</span>
{:else if isClassResult}
<span class="result-class-type">{hit.document.class_}</span>
<span class="result-class-type">{CLASS_LABELS[hitType] || hitType}</span>
{:else}
<span class="result-year">{formatYears(meta.gestion)}</span>
<span class="result-sep">·</span>
......@@ -1616,6 +1728,87 @@
</footer>
</div>
<!-- Modal explicativo contextual -->
{#if showExplainModal}
{@const activeContents = selectedClassifiers.map(id => EXPLAIN_CONTENT[id]).filter(Boolean)}
{@const isSingle = activeContents.length === 1}
<div class="explain-backdrop" on:click={() => { showExplainModal = false; }} role="dialog" aria-modal="true">
<div class="explain-modal" on:click|stopPropagation>
<div class="explain-header">
<h2 class="explain-title">{isSingle ? activeContents[0].titulo : '¿Qué busco?'}</h2>
<button class="explain-close" on:click={() => { showExplainModal = false; }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{#if isSingle}
{@const content = activeContents[0]}
<span class="explain-subtitle">{content.subtitulo}</span>
<p class="explain-intro">{content.intro}</p>
<div class="explain-list">
{#each content.bloques as bloque}
<div class="explain-item">
<span class="explain-item-label">{bloque.titulo}</span>
<p class="explain-item-desc">{bloque.texto}</p>
</div>
{/each}
</div>
{#if content.link}
<a href={content.link.href} class="explain-cta" on:click={() => { showExplainModal = false; }}>
{content.link.texto}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
{/if}
{:else if activeContents.length > 1}
{#each activeContents as content, idx}
<div class="explain-section" class:explain-section-border={idx > 0}>
<div class="explain-section-header">
<h3 class="explain-section-title">{content.titulo}</h3>
<span class="explain-subtitle">{content.subtitulo}</span>
</div>
<p class="explain-intro">{content.intro}</p>
<div class="explain-list">
{#each content.bloques as bloque}
<div class="explain-item">
<span class="explain-item-label">{bloque.titulo}</span>
<p class="explain-item-desc">{bloque.texto}</p>
</div>
{/each}
</div>
{#if content.link}
<a href={content.link.href} class="explain-cta" on:click={() => { showExplainModal = false; }}>
{content.link.texto}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
{/if}
</div>
{/each}
{:else}
<p class="explain-intro">
Los clasificadores son las categorías estandarizadas que usa el Estado boliviano para organizar su presupuesto. Cada uno responde a una pregunta diferente sobre el gasto o ingreso público.
</p>
<div class="explain-list">
{#each CLASIFICADORES as clf}
<div class="explain-item">
<div class="explain-item-header">
<span class="explain-item-label">{clf.label}</span>
<span class="explain-item-tecnico">{clf.tecnico}</span>
</div>
<p class="explain-item-desc">{clf.desc}</p>
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap');
:global(*){margin:0;padding:0;box-sizing:border-box}
......@@ -1692,10 +1885,9 @@
/* Search */
.search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)}
.search-wrap-with-results{background:rgba(255,255,255,0.04);border-radius:20px;border:1px solid rgba(255,255,255,0.08);padding:6px}
.search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.05);border-radius:16px;padding:8px 14px 8px 14px;transition:all 0.3s cubic-bezier(0.16,1,0.3,1);box-shadow:0 12px 40px rgba(0,0,0,0.4),0 4px 12px rgba(0,0,0,0.2);position:relative}
.search-focused{background:rgba(255,255,255,0.12)}
.search-has-results{border-radius:16px 16px 0 0}
.search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.05);border-radius:16px;padding:8px 14px 8px 14px;transition:all 0.6s cubic-bezier(0.16,1,0.3,1);box-shadow:0 12px 40px rgba(0,0,0,0.4),0 4px 12px rgba(0,0,0,0.2);position:relative}
.search-focused{background:rgba(255,255,255,0.1);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px)}
.has-results{border-radius:16px 16px 0 0;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
.search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px}
.search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s}
.search-meta-item svg{display:block;flex-shrink:0}
......@@ -1760,6 +1952,10 @@
/* Search clear button */
.search-clear{background:none;border:none;cursor:pointer;color:#6B6860;padding:4px;display:flex;align-items:center;justify-content:center;transition:color 0.2s;flex-shrink:0}
.search-esc{font-family:inherit;font-size:0.625rem;padding:3px 6px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);border-radius:4px;color:#6B6860;cursor:pointer;flex-shrink:0;transition:all 0.15s}
.search-esc:hover{background:rgba(255,255,255,0.14);color:#B8B5AD}
:global(html:not(.dark)) .search-esc{background:rgba(0,0,0,0.05);border-color:rgba(0,0,0,0.1);color:#888}
:global(html:not(.dark)) .search-esc:hover{background:rgba(0,0,0,0.1);color:#555}
.search-clear:hover{color:#F5F0E8}
/* Search Results Dropdown */
......@@ -1785,8 +1981,8 @@
.pill:hover::before{opacity:1}
.pill:hover{transform:translateY(-1px)}
.scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:100;transition:opacity 0.3s;color:#B8B5AD}
.scroll-hint-hidden{opacity:0;pointer-events:none}
.scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:10;transition:opacity 0.3s;color:#B8B5AD}
.scroll-hint-hidden{opacity:0;pointer-events:none;visibility:hidden}
.scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7}
.scroll-icon-mouse{display:block}
.scroll-icon-touch{display:none}
......@@ -1852,7 +2048,7 @@
.radio-dot-active::after{opacity:1}
/* Results Area */
.results-area{width:100%;position:relative;min-height:60px;text-align:center}
.results-area{width:100%;position:relative;text-align:center;margin-top:0;padding-top:0}
/* Daily Card */
.daily-card{display:flex;align-items:center;gap:12px;padding:14px 18px;background:rgba(232,168,76,0.04);border:1px solid rgba(232,168,76,0.15);border-radius:12px;text-decoration:none;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)}
......@@ -1867,17 +2063,47 @@
.daily-card:hover .daily-card-arrow{opacity:1;transform:translateX(4px)}
/* Results Messages */
.results-message{text-align:center;padding:20px}
.results-message-inline{text-align:center;padding:20px}
.results-hint,.results-loading{font-family:var(--sans);font-size:14px;color:#6B6860}
.results-error{font-family:var(--sans);font-size:14px;color:#C44B3F}
.results-empty{font-family:var(--sans);font-size:14px;color:#8B8880}
/* Search Context Title */
.search-context-title{font-family:var(--sans);font-size:12px;font-weight:500;color:#9B9890;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;padding-left:4px;text-align:left}
.search-context-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding:0 4px}
.search-context-title{font-family:var(--sans);font-size:12px;font-weight:500;color:#9B9890;text-transform:uppercase;letter-spacing:0.05em}
.search-explain-link{background:none;border:none;border-bottom:1px dotted #9B9890;color:#9B9890;font-size:11px;font-family:var(--sans);cursor:pointer;padding:0;transition:color 0.2s,border-color 0.2s}
.search-explain-link:hover{color:#C9A751;border-bottom-color:#C9A751}
/* Modal explicativo */
.explain-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;padding:2rem;animation:fadeIn 0.15s ease-out}
.explain-modal{width:100%;max-width:560px;max-height:80vh;overflow-y:auto;background:rgba(40,40,38,0.85);backdrop-filter:blur(40px) saturate(200%);-webkit-backdrop-filter:blur(40px) saturate(200%);border:1px solid rgba(255,255,255,0.12);border-radius:16px;padding:1.5rem;box-shadow:0 25px 50px rgba(0,0,0,0.5)}
:global(html:not(.dark)) .explain-modal{background:rgba(255,255,255,0.85);border-color:rgba(0,0,0,0.08);box-shadow:0 25px 50px rgba(0,0,0,0.15)}
.explain-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}
.explain-title{font-size:1.1rem;font-weight:700;color:var(--theme-titulo);margin:0}
.explain-subtitle{font-size:0.75rem;color:var(--theme-texto);opacity:0.6;margin-top:2px;display:block}
.explain-close{background:none;border:none;cursor:pointer;color:var(--theme-texto);opacity:0.6;padding:4px;transition:opacity 0.15s}
.explain-close:hover{opacity:1}
.explain-intro{font-size:0.8125rem;color:var(--theme-texto);line-height:1.5;margin-bottom:1.25rem;opacity:0.8}
.explain-list{display:flex;flex-direction:column;gap:0.75rem}
.explain-item{padding:0.75rem;background:rgba(255,255,255,0.05);border-radius:10px;border:1px solid rgba(255,255,255,0.06)}
:global(html:not(.dark)) .explain-item{background:rgba(0,0,0,0.03);border-color:rgba(0,0,0,0.06)}
.explain-item-header{display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.25rem}
.explain-item-label{font-size:0.875rem;font-weight:600;color:var(--theme-titulo)}
.explain-item-tecnico{font-size:0.75rem;color:var(--theme-texto);opacity:0.6}
.explain-item-desc{font-size:0.75rem;color:var(--theme-texto);opacity:0.75;line-height:1.4;margin:0.25rem 0 0}
.explain-section{margin-top:0}
.explain-section-border{margin-top:1.25rem;padding-top:1.25rem;border-top:1px solid rgba(255,255,255,0.08)}
:global(html:not(.dark)) .explain-section-border{border-top-color:rgba(0,0,0,0.08)}
.explain-section-header{margin-bottom:0.5rem}
.explain-section-title{font-size:1rem;font-weight:700;color:var(--theme-titulo);margin:0}
.explain-cta{display:inline-flex;align-items:center;gap:0.4rem;margin-top:1.25rem;padding:0.6rem 1rem;font-size:0.8125rem;font-weight:600;color:var(--theme-titulo);background:rgba(201,167,81,0.12);border:1px solid rgba(201,167,81,0.25);border-radius:10px;text-decoration:none;transition:all 0.2s}
.explain-cta:hover{background:rgba(201,167,81,0.2);border-color:rgba(201,167,81,0.4)}
/* Results Content */
.results-content{max-height:0;overflow:hidden;opacity:0;transition:all 0.4s cubic-bezier(0.16,1,0.3,1)}
.results-content-visible{max-height:60vh;overflow-y:auto;opacity:1;background:#1C1C1A;border-radius:16px;border:none;padding:14px 10px;text-align:left;position:relative;z-index:10;overscroll-behavior:contain}
.results-content{max-height:0;overflow:hidden;opacity:0;transition:max-height 0.5s cubic-bezier(0.16,1,0.3,1),opacity 0.3s ease;position:absolute;left:0;right:0;z-index:9999;top:-2px}
.results-content-visible{max-height:calc(100dvh - var(--results-top, 200px) - 24px);overflow-y:auto;opacity:1;background:rgba(28,28,26,0.6);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);border-radius:12px 12px 16px 16px;border:none;padding:14px 10px;text-align:left;overscroll-behavior:contain;box-shadow:none;margin-top:0;scrollbar-width:none;-ms-overflow-style:none}
.results-content-visible::-webkit-scrollbar{display:none}
:global(html:not(.dark)) .results-content-visible{background:rgba(255,255,255,0.85)}
/* Results Summary */
.results-summary{background:transparent;border-radius:0;padding:0 0 12px 0;margin-bottom:12px;border-bottom:1px solid rgba(255,255,255,0.08)}
......@@ -1918,7 +2144,8 @@
.result-card-grouped:last-child{border-bottom:none}
.result-card-grouped:hover{background:rgba(255,255,255,0.04)}
.result-content{flex:1;min-width:0}
.result-text{font-family:var(--sans);font-size:14px;color:#F5F0E8;line-height:1.4}
.result-text{font-family:var(--sans);font-size:14px;color:#F5F0E8;line-height:1.4;display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}
.result-type-badge{font-size:0.6rem;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;padding:2px 6px;border-radius:4px;background:rgba(201,167,81,0.12);color:#C9A751;flex-shrink:0;white-space:nowrap}
.result-text :global(mark){background:rgba(255,213,79,0.9);color:#1C1C1A;border-radius:3px;padding:1px 5px;font-weight:600}
.result-meta{display:flex;align-items:center;gap:10px;margin-top:6px;flex-wrap:wrap}
.result-parent,.result-subarea,.result-class-type,.result-objeto-nivel{font-family:var(--mono);font-size:11px;color:#8B8880}
......@@ -2191,9 +2418,9 @@
.pills-list{gap:5px}
.mode-card-radio{top:14px;right:14px}
.radio-dot{width:16px;height:16px}
.results-area{margin-top:4px;min-height:40px}
.results-content-visible{padding:2px 4px}
.search-wrap-with-results{padding:2px}
.results-area{min-height:40px}
.results-content-visible{max-height:calc(100vh - 160px);padding:6px 4px}
.search-wrap-with-results{padding:0}
.daily-card{padding:16px 18px;gap:12px;border-radius:14px;margin-bottom:24px}
.daily-card-title{font-size:14px}
.daily-card-text{font-size:12px}
......@@ -2322,9 +2549,9 @@
.mode-card-radio{top:12px;right:12px}
.radio-dot{width:14px;height:14px}
.radio-dot::after{width:6px;height:6px}
.results-area{margin-top:2px;min-height:28px}
.results-content-visible{padding:0 2px}
.search-wrap-with-results{padding:2px}
.results-area{min-height:28px}
.results-content-visible{max-height:calc(100vh - 140px);padding:4px 2px}
.search-wrap-with-results{padding:0}
.section-group{margin-top:12px}
.section-label{font-size:9px;margin-bottom:8px}
.section-label-goto{margin-top:12px}
......
// Proxy para evitar CORS
const API_BASE = 'http://34.66.240.236/api/proyecto/search';
const API_BASE = 'http://34.171.18.2/api/proyecto/search';
export async function GET({ url }) {
const q = url.searchParams.get('q') || '';
const is_class = url.searchParams.get('is_class') || 'false';
const mode = url.searchParams.get('mode') || 'gastos'; // 'gastos' | 'ingresos'
const class_ = url.searchParams.get('class_') || ''; // comma-separated: entidad, objeto, finfun, acteco
const page = url.searchParams.get('page') || '1'; // Paginación
const per_page = url.searchParams.get('per_page') || ''; // Resultados por página
const is_class = url.searchParams.get('is_class'); // null si no viene
const mode = url.searchParams.get('mode') || '';
const class_ = url.searchParams.get('class_') || '';
const page = url.searchParams.get('page') || '1';
const per_page = url.searchParams.get('per_page') || '';
// Build URL with all parameters
const apiUrl = new URL(API_BASE);
apiUrl.searchParams.set('q', q);
apiUrl.searchParams.set('is_class', is_class);
apiUrl.searchParams.set('page', page);
// Solo enviar is_class si viene explícitamente
if (is_class !== null) {
apiUrl.searchParams.set('is_class', is_class);
}
if (per_page) apiUrl.searchParams.set('per_page', per_page);
if (mode) apiUrl.searchParams.set('mode', mode);
if (class_) apiUrl.searchParams.set('class_', class_);
......
......@@ -5,21 +5,29 @@
import { supabase } from '$lib/supabase';
let { data } = $props();
const entidad = data.entidad;
const isDA = data.isDA;
const nombreDA = data.nombreDA;
const nombreEntidadMadre = data.nombreEntidadMadre;
const codigoEntidadPadre = data.codigoEntidadPadre;
let entidad = $derived(data.entidad);
let isDA = $derived(data.isDA);
let nombreDA = $derived(data.nombreDA);
let nombreEntidadMadre = $derived(data.nombreEntidadMadre);
let codigoEntidadPadre = $derived(data.codigoEntidadPadre);
// Datos desde el loader (Supabase)
let codigoSeleccionado = $derived($page.params.codigo);
let gestionSeleccionada = $state(data.gestionInicial);
let resumenData = $state(data.resumenData);
let resumenData = $derived(data.resumenData);
let distribucionesData = $state(data.distribucionesData);
let cargando = $state(false);
let distCache = $state({ [data.gestionInicial]: data.distribucionesData });
let cargandoDist = $state(false);
// Resetear al cambiar de entidad
$effect(() => {
const g = data.gestionInicial;
gestionSeleccionada = g;
distribucionesData = data.distribucionesData;
distCache = { [g]: data.distribucionesData };
});
async function cargarDistribuciones(gestion) {
if (distCache[gestion]) {
distribucionesData = distCache[gestion];
......@@ -148,14 +156,21 @@
return years;
});
function parseRanking(rankingStr) {
if (!rankingStr) return null;
const parts = String(rankingStr).split('/');
if (parts.length !== 2) return null;
return { posicion: parseInt(parts[0]), total: parseInt(parts[1]) };
}
let rankingGastos = $derived(() => {
const row = historiaGastos.find(d => d.gestion === gestionSeleccionada);
return row ? { posicion: row.ranking, total: '~600' } : null;
return row ? parseRanking(row.ranking) : null;
});
let rankingIngresos = $derived(() => {
const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada);
return row ? { posicion: row.ranking, total: '~600' } : null;
return row ? parseRanking(row.ranking) : null;
});
let distFiltradas = $derived(distribucionesData);
......@@ -672,34 +687,42 @@
{#if rankingGastos()}
{@const r = rankingGastos()}
{@const total = parseInt(r.total) || 600}
{@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))}
{@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
<div class="ranking-card">
<span class="ranking-card-label">Ranking en gasto</span>
<div class="ranking-headline">
<span class="ranking-pos-big">#{r.posicion}</span>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> instituciones · {r.posicion <= total / 2 ? 'entre las que más gastan' : 'entre las que menos gastan'}</span>
</div>
<div class="ranking-bars">
{#each Array(80) as _, i}
<div class="ranking-bar-tick"></div>
{/each}
<div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
</div>
<div class="ranking-caption">
<span class="ranking-pos">#{r.posicion}</span> de {total} entidades
<div class="ranking-extremos">
<span class="ranking-extremo">Menos gasto</span>
<span class="ranking-extremo">Más gasto</span>
</div>
</div>
{/if}
{#if tieneIngresos && rankingIngresos()}
{@const r = rankingIngresos()}
{@const total = parseInt(r.total) || 600}
{@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))}
{@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))}
<div class="ranking-card">
<span class="ranking-card-label">Ranking en ingresos</span>
<div class="ranking-headline">
<span class="ranking-pos-big">#{r.posicion}</span>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> instituciones · {r.posicion <= total / 2 ? 'entre las que más ingresan' : 'entre las que menos ingresan'}</span>
</div>
<div class="ranking-bars">
{#each Array(80) as _, i}
<div class="ranking-bar-tick"></div>
{/each}
<div class="ranking-marker" style="left: {barIdx * 1.25}%"></div>
</div>
<div class="ranking-caption">
<span class="ranking-pos">#{r.posicion}</span> de {total} entidades
<div class="ranking-extremos">
<span class="ranking-extremo">Menos ingresos</span>
<span class="ranking-extremo">Más ingresos</span>
</div>
</div>
{/if}
......@@ -712,12 +735,19 @@
<div class="clasificadores-grid">
{#each clasificadoresGasto as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
......@@ -735,12 +765,19 @@
<div class="clasificadores-grid">
{#each clasificadoresIngreso as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
......@@ -1039,14 +1076,39 @@
background: #D4A574;
}
.ranking-caption {
margin-top: 0.35rem;
font-size: 0.7rem;
.ranking-extremos {
display: flex;
justify-content: space-between;
margin-top: 0.25rem;
}
.ranking-extremo {
font-size: 0.6rem;
color: var(--theme-texto);
opacity: 0.6;
opacity: 0.4;
}
.ranking-headline {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.ranking-pos-big {
font-size: 1.5rem;
font-weight: 700;
color: var(--theme-titulo);
line-height: 1;
}
.ranking-pos-context {
font-size: 0.8rem;
color: var(--theme-texto);
opacity: 0.7;
}
.ranking-pos {
.ranking-total-num {
font-weight: 700;
color: var(--theme-titulo);
opacity: 1;
......@@ -1150,7 +1212,7 @@
:global(html:not(.dark)) .gestion-option.active {
background: rgba(90, 157, 191, 0.1);
color: #5A9DBF;
color: #3D7A9C;
}
.seccion-sub {
......@@ -1230,7 +1292,7 @@
.historia-monto.ingresos,
.historia-monto.gastos {
color: #5A9DBF;
color: #3D7A9C;
}
:global(html.dark) .historia-monto.ingresos,
......@@ -1270,20 +1332,22 @@
}
.area-path {
fill: rgba(90, 157, 191, 0.12);
fill: rgba(61, 122, 156, 0.4);
}
.line-path {
stroke: #5A9DBF;
stroke: #3D7A9C;
opacity: 0.9;
}
.dot {
fill: #5A9DBF;
fill: #3D7A9C;
opacity: 0.9;
transition: r 0.15s;
}
.dot.active {
fill: #5A9DBF;
fill: #3D7A9C;
}
.hover-line {
......@@ -1339,12 +1403,25 @@
.clasificador-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
overflow: hidden;
}
.clasificador-nombres {
display: flex;
align-items: baseline;
gap: 0.4rem;
min-width: 0;
overflow: hidden;
}
.clasificador-valores {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.clasificador-monto {
font-size: 0.95rem;
font-weight: 600;
......@@ -1353,6 +1430,15 @@
flex-shrink: 0;
}
.clasificador-pct {
font-size: 0.8rem;
font-weight: 600;
color: var(--theme-texto);
opacity: 0.5;
white-space: nowrap;
flex-shrink: 0;
}
.clasificador-padre {
font-size: 0.8rem;
font-weight: 400;
......
<script>
import { onMount } from 'svelte';
import * as d3 from 'd3';
let mapaData = $state(null);
let boliviaPath = $state('');
let deptoPath = $state('');
let muniPath = $state('');
let error = $state('');
let debug = $state('');
onMount(async () => {
try {
const res = await fetch('/mapa.json');
mapaData = await res.json();
debug = `bolivia: ${mapaData.bolivia.features.length} features, `;
debug += `deptos: ${mapaData.departamentos.features.length}, `;
debug += `munis: ${mapaData.municipios.features.length}`;
// Quitar CRS y corregir winding order
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
// Corregir winding order: invertir todos los anillos
function fixWinding(geojson) {
const fix = (coords, isHole) => {
// Para geo coords: exterior debe ser counterclockwise (area < 0 en D3)
// Simplemente invertimos todos los anillos exteriores
coords.forEach((ring, i) => {
if (i === 0) ring.reverse(); // exterior
// holes quedan como están
});
};
for (const feat of geojson.features || [geojson]) {
const geom = (feat.geometry || feat);
if (geom.type === 'Polygon') {
fix(geom.coordinates);
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(poly => fix(poly));
}
}
}
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
const bounds = d3.geoBounds(mapaData.bolivia);
debug += ` | bounds: ${JSON.stringify(bounds)}`;
const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia);
const pathGen = d3.geoPath(projection);
boliviaPath = pathGen(mapaData.bolivia) || '';
debug += ` | boliviaPath length: ${boliviaPath.length}`;
deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' ');
debug += ` | deptoPath length: ${deptoPath.length}`;
const muni = mapaData.municipios.features[0];
muniPath = pathGen(muni) || '';
debug += ` | muni code: ${muni.properties.codigo}`;
} catch (e) {
error = e.message;
}
});
</script>
<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;">
<h1>Test Mapa</h1>
<p style="font-size: 12px; color: #888;">{debug}</p>
{#if error}
<p style="color: red;">{error}</p>
{/if}
<svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;">
{#if boliviaPath}
<path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" />
{/if}
{#if deptoPath}
<path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" />
{/if}
{#if muniPath}
<path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" />
{/if}
</svg>
</div>
import { supabase } from '$lib/supabase';
export async function load({ fetch }) {
// Cargar resumen y población en paralelo
const [resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('entidad_resumen')
.select('codigo, desc, gestion, devengado, ranking')
.eq('tipo', 'gastos')
.eq('tipo_codigo', 'municipio_ubigeo')
.eq('gestion', 2025)
.order('devengado', { ascending: false })
.limit(500),
fetch('/poblacion.csv').then(r => r.text())
]);
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
if (parseInt(gestion) === 2025) {
poblacionMap[cod] = parseInt(pob);
}
});
const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025];
return {
resumen: resumenRes.data || [],
poblacionMap,
gestiones,
gestionInicial: 2025
};
}
<script>
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import * as d3 from 'd3';
import { supabase } from '$lib/supabase';
import { mapaCache } from '$lib/stores/mapaCache';
let { data } = $props();
let resumen = $state(data.resumen);
let poblacionMap = $state(data.poblacionMap);
let gestionSeleccionada = $state(data.gestionInicial);
let modoPerCapita = $state(false);
let dimensionSeleccionada = $state('total'); // total, objeto, finfun, acteco
let gestionDropdownOpen = $state(false);
let hoveredMunicipio = $state(null);
let drawerOpen = $state(false);
let hoveredQuintil = $state(-1);
let activeQuintiles = $state(new Set([0, 1, 2, 3, 4]));
let sortDrawer = $state('monto');
let sortDrawerOrder = $state('desc');
let mostrarDepartamentos = $state(false);
let clasificadorDropdownOpen = $state(false);
let clasificadorSeleccionado = $state('total'); // total, objeto, finfun, acteco
let subPartidaSearch = $state('');
let subPartidaSeleccionada = $state(null);
const CLASIFICADORES_MAPA = [
{ id: 'total', label: 'Gasto total', disponible: true },
{ id: 'objeto', label: 'Objetos de gasto', disponible: false },
{ id: 'finfun', label: 'Finalidad y función', disponible: false },
{ id: 'acteco', label: 'Sectores económicos', disponible: false },
];
function titleCase(str) {
if (!str) return '';
return str.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
// Mapa de municipio a departamento (desde geojson)
let muniDepartamento = $derived.by(() => {
if (!mapaData) return {};
const map = {};
mapaData.municipios.features.forEach(f => {
map[String(f.properties.codigo)] = f.properties.DEPARTAMEN;
});
return map;
});
// Slider de rango
let rangoMin = $state(0);
let rangoMax = $state(100);
// Mapa
let mapaData = $state(null);
let projection = $state(null);
let pathGen = $state(null);
function fixWinding(geojson) {
for (const feat of geojson.features || [geojson]) {
const geom = feat.geometry || feat;
if (geom.type === 'Polygon') {
geom.coordinates.forEach((ring, i) => { if (i === 0) ring.reverse(); });
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(poly => poly.forEach((ring, i) => { if (i === 0) ring.reverse(); }));
}
}
}
// Crear mapa de datos por código (cacheado, no función)
let datosPorCodigo = $derived.by(() => {
console.time('datosPorCodigo');
const map = {};
resumen.forEach(d => {
const val = modoPerCapita && poblacionMap[d.codigo]
? d.devengado / poblacionMap[d.codigo]
: d.devengado;
map[d.codigo] = { ...d, valor: val };
});
console.timeEnd('datosPorCodigo');
return map;
});
// Detectar modo oscuro
let isDark = $state(false);
// 5 quintiles
const colorsLight = ['#f2ece6', '#e0c8b0', '#c4897d', '#a86858', '#8B4A3A'];
const colorsDark = ['#2E2B27', '#4A4035', '#6B5A48', '#9A8050', '#C9A751'];
let palette = $derived(isDark ? colorsDark : colorsLight);
let colorScale = $derived.by(() => {
const valores = Object.values(datosPorCodigo).map(d => d.valor).filter(v => v > 0).sort((a, b) => a - b);
if (valores.length === 0) return d3.scaleQuantile().domain([0, 1]).range(palette);
return d3.scaleQuantile().domain(valores).range(palette);
});
// Cortes de quintiles para la leyenda
let quintiles = $derived.by(() => {
if (!colorScale.quantiles) return [];
const cortes = colorScale.quantiles();
const items = [];
for (let i = 0; i < palette.length; i++) {
const desde = i === 0 ? valoresExtremos.min : cortes[i - 1];
const hasta = i < cortes.length ? cortes[i] : valoresExtremos.max;
items.push({ color: palette[i], desde, hasta });
}
return items;
});
// Valores extremos para el slider
let valoresExtremos = $derived.by(() => {
const vals = Object.values(datosPorCodigo).map(d => d.valor).filter(v => v > 0);
if (vals.length === 0) return { min: 0, max: 1 };
return { min: d3.min(vals), max: d3.max(vals) };
});
// Slider con escala logarítmica (más resolución en valores bajos)
function sliderToVal(pct) {
const { min, max } = valoresExtremos;
if (min <= 0 || max <= 0) return min + (max - min) * (pct / 100);
const logMin = Math.log(min);
const logMax = Math.log(max);
return Math.exp(logMin + (logMax - logMin) * (pct / 100));
}
let sliderValMin = $derived(sliderToVal(rangoMin));
let sliderValMax = $derived(sliderToVal(rangoMax));
// Quintil de un valor
function getQuintilIdx(valor) {
if (!colorScale.quantiles || quintiles.length === 0) return -1;
const cortes = colorScale.quantiles();
for (let i = 0; i < cortes.length; i++) {
if (valor < cortes[i]) return i;
}
return palette.length - 1;
}
// Municipio visible según rango y quintiles activos
function enRango(codigo) {
const datos = datosPorCodigo[codigo];
if (!datos || datos.valor <= 0) return false;
if (rangoMin > 0 && datos.valor < sliderValMin * 0.999) return false;
if (rangoMax < 100 && datos.valor > sliderValMax * 1.001) return false;
const qi = getQuintilIdx(datos.valor);
return activeQuintiles.has(qi);
}
// Opacidad del municipio según hover de quintil
function opacidadMuni(codigo) {
if (hoveredQuintil === -1) return 1;
const datos = datosPorCodigo[codigo];
if (!datos) return 0.15;
const qi = getQuintilIdx(datos.valor);
return qi === hoveredQuintil ? 1 : 0.15;
}
function toggleQuintil(idx) {
const next = new Set(activeQuintiles);
if (next.has(idx)) {
next.delete(idx);
} else {
next.add(idx);
}
activeQuintiles = next;
}
// Ranking fijo por monto (posición nunca cambia)
let rankingFijo = $derived.by(() => {
const sorted = Object.values(datosPorCodigo)
.filter(d => d.valor > 0)
.sort((a, b) => b.valor - a.valor);
const map = {};
sorted.forEach((d, i) => { map[d.codigo] = i + 1; });
return map;
});
// Barras (filtradas por rango y quintiles, ordenables)
let barrasRankeadas = $derived.by(() => {
const dir = sortDrawerOrder === 'desc' ? 1 : -1;
return Object.values(datosPorCodigo)
.filter(d => {
if (d.valor <= 0) return false;
if (rangoMin > 0 && d.valor < sliderValMin * 0.999) return false;
if (rangoMax < 100 && d.valor > sliderValMax * 1.001) return false;
return activeQuintiles.has(getQuintilIdx(d.valor));
})
.sort((a, b) => {
if (sortDrawer === 'alfa') return dir * (a.desc || '').localeCompare(b.desc || '');
if (sortDrawer === 'dept') return dir * (muniDepartamento[a.codigo] || '').localeCompare(muniDepartamento[b.codigo] || '');
return dir * (b.valor - a.valor);
});
});
function formatearMonto(valor) {
if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)} mil millones`;
if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)} millones`;
if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)} mil`;
return valor.toFixed(0);
}
function formatearPerCapita(valor) {
if (valor >= 1e6) return `${(valor / 1e6).toFixed(1)} millones`;
if (valor >= 1e4) return `${(valor / 1e3).toFixed(1)} mil`;
return Math.round(valor).toLocaleString('es-BO');
}
function fmt(valor) {
return modoPerCapita ? formatearPerCapita(valor) : formatearMonto(valor);
}
let unidad = $derived(modoPerCapita ? 'Bs por persona' : 'de Bolivianos');
// Cache de población por año (parsear CSV una sola vez)
let poblacionCache = $state({});
let pobCSV = $state(null);
async function getPoblacionAno(gestion) {
if (poblacionCache[gestion]) return poblacionCache[gestion];
if (!pobCSV) {
pobCSV = await fetch('/poblacion.csv').then(r => r.text());
}
const map = {};
pobCSV.split('\n').slice(1).forEach(line => {
const [cod, g, pob] = line.split(',');
if (parseInt(g) === gestion) {
map[cod] = parseInt(pob);
}
});
poblacionCache[gestion] = map;
return map;
}
// Cache de resumen por año
let resumenCache = $state({});
async function cargarGestion(gestion) {
console.time('cargarGestion');
if (!resumenCache[gestion]) {
console.time('supabase');
const { data: rows } = await supabase
.schema('ppto')
.from('entidad_resumen')
.select('codigo, desc, gestion, devengado, ranking')
.eq('tipo', 'gastos')
.eq('tipo_codigo', 'municipio_ubigeo')
.eq('gestion', gestion)
.order('devengado', { ascending: false })
.limit(500);
console.timeEnd('supabase');
resumenCache[gestion] = rows || [];
}
resumen = resumenCache[gestion];
console.time('poblacion');
poblacionMap = await getPoblacionAno(gestion);
console.timeEnd('poblacion');
console.timeEnd('cargarGestion');
}
onMount(async () => {
const cached = get(mapaCache);
if (cached) {
mapaData = cached;
} else {
const res = await fetch('/mapa.json');
mapaData = await res.json();
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
mapaCache.set(mapaData);
}
projection = d3.geoMercator().fitSize([500, 600], mapaData.bolivia);
pathGen = d3.geoPath(projection);
isDark = document.documentElement.classList.contains('dark');
const themeObserver = new MutationObserver(() => {
isDark = document.documentElement.classList.contains('dark');
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
function handleClickOutside(e) {
if (gestionDropdownOpen && !e.target.closest('.gestion-selector')) {
gestionDropdownOpen = false;
}
if (drawerOpen && !e.target.closest('.ranking-drawer') && !e.target.closest('.drawer-toggle')) {
drawerOpen = false;
}
if (clasificadorDropdownOpen && !e.target.closest('.clasificador-selector')) {
clasificadorDropdownOpen = false;
}
}
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
});
</script>
<svelte:head>
<title>Gasto por geografía | Presupuesto Público</title>
</svelte:head>
<div class="dashboard">
<div class="mapa-fullscreen">
<div class="mapa-top-controls">
<nav class="breadcrumb">
<button class="back-btn" onclick={() => { history.back(); }}>←</button>
<a href="/">Inicio</a>
<span class="sep">/</span>
<span>Ubicación geográfica</span>
</nav>
<!-- Fila 1: Dropdowns inline -->
<div class="titulo-dropdowns">
<div class="clasificador-selector">
<button class="clasificador-btn" onclick={() => { clasificadorDropdownOpen = !clasificadorDropdownOpen; }}>
<svg class="gestion-chevron" class:open={clasificadorDropdownOpen} width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
<span>{CLASIFICADORES_MAPA.find(c => c.id === clasificadorSeleccionado)?.label || 'Gasto total'}</span>
</button>
{#if clasificadorDropdownOpen}
<div class="clasificador-dropdown">
{#each CLASIFICADORES_MAPA as cls}
<button
class="clasificador-option"
class:active={clasificadorSeleccionado === cls.id}
class:disabled={!cls.disponible}
onclick={() => { if (cls.disponible) { clasificadorSeleccionado = cls.id; clasificadorDropdownOpen = false; } }}
>
<span class="option-label">{cls.label}</span>
{#if cls.sub}
<span class="option-sub">{cls.sub}</span>
{/if}
</button>
{/each}
</div>
{/if}
</div>
<span class="titulo-sep">por geografía</span>
<div class="gestion-selector">
<button class="gestion-btn" onclick={() => { gestionDropdownOpen = !gestionDropdownOpen; }}>
<span>{gestionSeleccionada}</span>
<svg class="gestion-chevron" class:open={gestionDropdownOpen} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
{#if gestionDropdownOpen}
<div class="gestion-dropdown">
{#each data.gestiones as g}
<button
class="gestion-option"
class:active={gestionSeleccionada === g}
onclick={() => { gestionSeleccionada = g; gestionDropdownOpen = false; cargarGestion(g); }}
>
{g}
</button>
{/each}
</div>
{/if}
</div>
</div>
<!-- Fila 2: Controles -->
<div class="controls-row">
<div class="percapita-toggle">
<button class="percapita-btn" class:active={!modoPerCapita} onclick={() => { modoPerCapita = false; }}>Total</button>
<button class="percapita-btn" class:active={modoPerCapita} onclick={() => { modoPerCapita = true; }}>Per cápita</button>
</div>
<button class="dept-toggle" class:active={mostrarDepartamentos} onclick={() => { mostrarDepartamentos = !mostrarDepartamentos; }}>
{mostrarDepartamentos ? '✓ ' : ''}Límites departamentales
</button>
<button class="drawer-toggle" onclick={() => { drawerOpen = !drawerOpen; }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M3 12h18M3 18h18"/>
</svg>
Ranking
</button>
</div>
</div>
<!-- Mapa -->
<div class="mapa-container">
<div class="mapa-card">
{#if mapaData && pathGen}
<svg viewBox="0 0 500 600" class="mapa-svg-full">
<path d={pathGen(mapaData.bolivia)} class="mapa-pais-fill" />
<path d={pathGen(mapaData.bolivia)} class="mapa-pais-outline" />
{#each mapaData.municipios.features as feat}
{@const cod = String(feat.properties.codigo)}
{@const datos = datosPorCodigo[cod]}
{@const visible = enRango(cod)}
{@const color = datos && visible ? colorScale(datos.valor) : 'transparent'}
<a href="/ubicacion/{cod}">
<path
d={pathGen(feat)}
fill={color}
stroke={hoveredMunicipio === cod ? (isDark ? '#F5F0E8' : '#1d1d1f') : (visible ? (isDark ? 'rgba(255,255,255,0.05)' : 'white') : 'transparent')}
stroke-width={hoveredMunicipio === cod ? 2 : 0.3}
opacity={visible ? opacidadMuni(cod) : 0}
style="cursor: pointer; transition: fill 0.2s, opacity 0.2s;"
onmouseenter={() => { if (visible) hoveredMunicipio = cod; }}
onmouseleave={() => { hoveredMunicipio = null; }}
/>
</a>
{/each}
{#if mostrarDepartamentos}
{#each mapaData.departamentos.features as feat}
<path d={pathGen(feat)} fill="none" stroke={isDark ? '#F5F0E8' : '#1d1d1f'} stroke-width="2.5" style="pointer-events:none;" />
{/each}
{/if}
</svg>
{/if}
<!-- Leyenda quintiles -->
{#if quintiles.length > 0}
<div class="leyenda-quintiles" onmouseleave={() => { hoveredQuintil = -1; }}>
<span class="leyenda-titulo">Filtrar por rango</span>
{#each quintiles as q, i}
<button
class="quintil-item"
class:dimmed={!activeQuintiles.has(i)}
onmouseenter={() => { hoveredQuintil = i; }}
onclick={() => { toggleQuintil(i); }}
>
<div class="quintil-color" style="background: {q.color}"></div>
<span class="quintil-label">{fmt(q.desde)} – {fmt(q.hasta)} {modoPerCapita ? 'Bs' : ''}</span>
</button>
{/each}
</div>
{/if}
<!-- Tooltip fijo dentro del card -->
<div class="mapa-tooltip-fijo">
{#if hoveredMunicipio && datosPorCodigo[hoveredMunicipio]}
{@const d = datosPorCodigo[hoveredMunicipio]}
{@const allBarras = barrasRankeadas}
{@const pos = allBarras.findIndex(b => b.codigo === hoveredMunicipio) + 1}
<span class="tooltip-nombre">{titleCase(d.desc)}</span>
<span class="tooltip-dept">{titleCase(muniDepartamento[hoveredMunicipio] || '')}</span>
<span class="tooltip-valor">{fmt(d.valor)} {unidad}</span>
<span class="tooltip-rank">#{pos}/{allBarras.length}</span>
{/if}
</div>
<span class="mapa-muni-count">{barrasRankeadas.length} municipios</span>
</div>
</div>
<!-- Controles abajo -->
<div class="mapa-bottom-controls">
<div class="rango-slider">
<div class="rango-track-wrap">
<span class="rango-val-float" style="left: {rangoMin}%">{fmt(sliderValMin)} Bs</span>
<span class="rango-val-float" style="left: {rangoMax}%">{fmt(sliderValMax)} Bs</span>
<div class="rango-gradiente"></div>
<div class="rango-mask-left" style="width: {rangoMin}%"></div>
<div class="rango-mask-right" style="width: {100 - rangoMax}%"></div>
<input type="range" min="0" max="100" bind:value={rangoMin} class="rango-input" />
<input type="range" min="0" max="100" bind:value={rangoMax} class="rango-input" />
</div>
</div>
</div>
</div>
<!-- Drawer lateral de ranking -->
<div class="ranking-drawer" class:open={drawerOpen}>
<div class="drawer-header">
<span class="drawer-titulo">{barrasRankeadas.length} municipios</span>
<button class="drawer-close" onclick={() => { drawerOpen = false; }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="drawer-table-wrap">
<table class="drawer-table">
<thead>
<tr>
<th class="th-rank th-sortable" onclick={() => { sortDrawer = 'monto'; sortDrawerOrder = 'desc'; }}>
#
</th>
<th class="th-sortable" onclick={() => { if (sortDrawer === 'alfa') sortDrawerOrder = sortDrawerOrder === 'asc' ? 'desc' : 'asc'; else { sortDrawer = 'alfa'; sortDrawerOrder = 'asc'; } }}>
Municipio {sortDrawer === 'alfa' ? (sortDrawerOrder === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="th-sortable" onclick={() => { if (sortDrawer === 'dept') sortDrawerOrder = sortDrawerOrder === 'asc' ? 'desc' : 'asc'; else { sortDrawer = 'dept'; sortDrawerOrder = 'asc'; } }}>
Depto. {sortDrawer === 'dept' ? (sortDrawerOrder === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="th-sortable th-right" onclick={() => { if (sortDrawer === 'monto') sortDrawerOrder = sortDrawerOrder === 'desc' ? 'asc' : 'desc'; else { sortDrawer = 'monto'; sortDrawerOrder = 'desc'; } }}>
{modoPerCapita ? 'Bs per cápita' : 'Bs'} {sortDrawer === 'monto' ? (sortDrawerOrder === 'desc' ? '↓' : '↑') : ''}
</th>
</tr>
</thead>
<tbody>
{#each barrasRankeadas as item, i}
{@const barColor = colorScale(item.valor)}
<tr
class="tabla-row"
class:hovered={hoveredMunicipio === item.codigo}
onmouseenter={() => { hoveredMunicipio = item.codigo; }}
onmouseleave={() => { hoveredMunicipio = null; }}
>
<td class="td-rank">{rankingFijo[item.codigo] || ''}</td>
<td class="td-nombre">
<span class="td-color-dot" style="background: {barColor}"></span>
<a href="/ubicacion/{item.codigo}">{titleCase(item.desc)}</a>
</td>
<td class="td-dept">{titleCase(muniDepartamento[item.codigo] || '')}</td>
<td class="td-monto">{fmt(item.valor)} Bs</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<style>
.dashboard {
min-height: 100vh;
background: var(--theme-body);
color: var(--theme-texto);
font-family: var(--font-sans);
position: relative;
}
:global(html:not(.dark)) .dashboard { background: #f5f5f7; }
/* Mapa fullscreen */
.mapa-fullscreen {
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Controles arriba */
.mapa-top-controls {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 3.5rem 1.5rem 0;
max-width: 700px;
margin: 0 auto;
width: 100%;
z-index: 10;
flex-shrink: 0;
}
.breadcrumb { font-size: 0.7rem; color: var(--theme-texto); opacity: 0.5; display: flex; align-items: center; gap: 0.3rem; }
.back-btn { background: none; border: none; cursor: pointer; color: var(--theme-texto); font-size: 0.85rem; padding: 0; opacity: 0.6; transition: opacity 0.15s; }
.back-btn:hover { opacity: 1; }
.breadcrumb a { color: var(--theme-texto); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; opacity: 1; }
.breadcrumb .sep { margin: 0 0.3rem; }
.titulo-dropdowns {
display: flex;
align-items: baseline;
gap: 0.4rem;
flex-wrap: wrap;
}
.titulo-sep {
font-size: 1.1rem;
font-weight: 700;
color: var(--theme-titulo);
}
.controls-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Selector de clasificador */
.clasificador-selector {
display: flex;
align-items: center;
gap: 0.4rem;
position: relative;
}
.clasificador-label {
font-size: 0.7rem;
color: var(--theme-texto);
opacity: 0.5;
}
.clasificador-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.125rem 0;
background: transparent;
border: none;
border-bottom: 1.5px dotted var(--theme-texto);
color: var(--theme-titulo);
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
font-family: var(--font-sans);
transition: border-color 0.2s;
}
.clasificador-btn:hover { border-bottom-color: var(--theme-accent); }
.clasificador-dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
min-width: 240px;
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 100;
overflow: hidden;
}
.clasificador-option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.6rem 0.75rem;
border: none;
background: transparent;
color: var(--theme-titulo);
font-size: 0.8rem;
font-weight: 500;
text-align: left;
cursor: pointer;
font-family: var(--font-sans);
transition: background 0.15s;
}
.clasificador-option:hover { background: var(--theme-surface-hover); }
.clasificador-option.active { background: rgba(107, 159, 212, 0.1); }
.clasificador-option.disabled { opacity: 0.4; cursor: default; }
.clasificador-option.disabled:hover { background: transparent; }
.option-label { flex: 1; }
.option-sub { font-size: 0.65rem; color: var(--theme-texto); opacity: 0.5; }
.option-badge {
font-size: 0.55rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 2px 5px;
border-radius: 3px;
background: var(--theme-borde);
color: var(--theme-texto);
opacity: 0.6;
}
.clasificador-search {
padding: 0.5rem;
border-top: 1px solid var(--theme-borde);
}
.clasificador-search input {
width: 100%;
padding: 0.4rem 0.6rem;
border: 1px solid var(--theme-borde);
border-radius: 6px;
background: var(--theme-body);
color: var(--theme-titulo);
font-size: 0.75rem;
font-family: var(--font-sans);
outline: none;
}
.clasificador-search input:focus { border-color: var(--theme-accent); }
/* Slider de rango */
.rango-slider {
flex: 1;
}
.rango-track-wrap {
position: relative;
height: 20px;
width: 100%;
margin-top: 1.25rem;
}
.rango-val-float {
position: absolute;
top: -1.1rem;
transform: translateX(-50%);
font-size: 0.6rem;
font-weight: 600;
color: var(--theme-titulo);
opacity: 0.7;
white-space: nowrap;
pointer-events: none;
z-index: 4;
}
.mapa-muni-count {
position: absolute;
bottom: 1rem;
left: 1.5rem;
font-size: 0.7rem;
font-weight: 600;
color: var(--theme-accent);
pointer-events: none;
}
.rango-gradiente {
position: absolute;
top: 50%;
left: 7px;
right: 7px;
height: 8px;
transform: translateY(-50%);
border-radius: 4px;
background: linear-gradient(to right, #f2ece6, #e0c8b0, #c4897d, #a86858, #8B4A3A);
}
:global(html.dark) .rango-gradiente {
background: linear-gradient(to right, #2E2B27, #4A4035, #6B5A48, #9A8050, #C9A751);
}
.rango-input {
position: absolute;
width: 100%;
top: 50%;
transform: translateY(-50%);
height: 20px;
z-index: 3;
margin: 0;
-webkit-appearance: none;
appearance: none;
background: transparent;
pointer-events: none;
}
.rango-input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #F5F0E8;
cursor: pointer;
pointer-events: all;
border: 3px solid #1C1C1A;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
margin-top: -7px;
}
:global(html:not(.dark)) .rango-input::-webkit-slider-thumb {
background: #1d1d1f;
border-color: #f5f5f7;
}
.rango-input::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #F5F0E8;
cursor: pointer;
pointer-events: all;
border: 3px solid #1C1C1A;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
:global(html:not(.dark)) .rango-input::-moz-range-thumb {
background: #1d1d1f;
border-color: #f5f5f7;
}
.rango-input::-webkit-slider-runnable-track { height: 0; background: transparent; }
.rango-input::-moz-range-track { height: 0; background: transparent; }
.rango-mask-left, .rango-mask-right {
position: absolute;
top: 50%;
height: 8px;
transform: translateY(-50%);
background: var(--theme-body);
opacity: 0.7;
pointer-events: none;
z-index: 2;
}
:global(html:not(.dark)) .rango-mask-left,
:global(html:not(.dark)) .rango-mask-right {
background: #f5f5f7;
}
.rango-mask-left {
left: 0;
border-radius: 4px 0 0 4px;
}
.rango-mask-right {
right: 0;
border-radius: 0 4px 4px 0;
}
.mapa-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--theme-titulo);
margin: 0;
}
/* Toggle per cápita */
.percapita-toggle {
display: flex;
gap: 2px;
background: rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 3px;
backdrop-filter: blur(12px);
}
:global(html:not(.dark)) .percapita-toggle { background: rgba(0, 0, 0, 0.05); }
.percapita-btn {
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-sans);
border: none;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.6;
transition: all 0.2s;
}
.percapita-btn:hover { opacity: 0.8; }
.percapita-btn.active { opacity: 1; background: rgba(255, 255, 255, 0.12); color: var(--theme-titulo); }
:global(html:not(.dark)) .percapita-btn.active { background: rgba(255, 255, 255, 0.8); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); }
/* Dropdown gestión */
.gestion-selector { position: relative; }
.gestion-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.125rem 0;
background: transparent;
border: none;
border-bottom: 1.5px dotted var(--theme-texto);
color: var(--theme-titulo);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
font-family: var(--font-sans);
}
.gestion-btn:hover { border-bottom-color: var(--theme-accent); }
.gestion-chevron { color: var(--theme-texto); opacity: 0.5; transition: transform 0.2s; }
.gestion-chevron.open { transform: rotate(180deg); }
.gestion-dropdown {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 100px;
max-height: 240px;
overflow-y: auto;
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 100;
}
.gestion-option {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: var(--theme-titulo);
font-size: 0.8125rem;
font-weight: 500;
text-align: left;
cursor: pointer;
font-family: var(--font-sans);
}
.gestion-option:hover { background: var(--theme-surface-hover); }
.gestion-option.active { background: rgba(107, 159, 212, 0.1); color: #6B9FD4; }
/* Drawer toggle */
.drawer-toggle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 6px 12px;
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-sans);
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--theme-texto);
cursor: pointer;
backdrop-filter: blur(12px);
transition: all 0.2s;
}
.drawer-toggle:hover { background: rgba(255, 255, 255, 0.14); color: var(--theme-titulo); }
:global(html:not(.dark)) .drawer-toggle { background: rgba(0, 0, 0, 0.05); border-color: rgba(0, 0, 0, 0.08); }
:global(html:not(.dark)) .drawer-toggle:hover { background: rgba(0, 0, 0, 0.1); }
/* Mapa container */
.mapa-container {
flex: 1;
display: flex;
align-items: stretch;
justify-content: center;
padding: 0.5rem 0;
min-height: 0;
overflow: hidden;
max-width: 700px;
margin: 0 auto;
width: 100%;
}
/* Controles abajo */
.mapa-bottom-controls {
display: flex;
align-items: flex-end;
gap: 1rem;
padding: 0.5rem 1.5rem 1.5rem;
max-width: 700px;
margin: 0 auto;
width: 100%;
flex-shrink: 0;
}
.mapa-card {
background: var(--theme-surface);
border-radius: 16px;
padding: 1.5rem;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.mapa-svg-full {
width: 100%;
height: 100%;
display: block;
}
/* Leyenda quintiles */
.leyenda-quintiles {
display: flex;
flex-direction: column;
gap: 3px;
position: absolute;
bottom: 1rem;
right: 1rem;
}
.leyenda-titulo {
font-size: 0.6rem;
font-weight: 600;
color: var(--theme-texto);
opacity: 0.4;
margin-bottom: 2px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.quintil-item {
display: flex;
align-items: center;
gap: 0.4rem;
background: none;
border: none;
cursor: pointer;
padding: 2px 0;
transition: opacity 0.2s;
font-family: var(--font-sans);
}
.quintil-item.dimmed {
opacity: 0.25;
}
.quintil-color {
width: 18px;
height: 12px;
border-radius: 2px;
flex-shrink: 0;
}
.quintil-label {
font-size: 0.65rem;
color: var(--theme-titulo);
opacity: 0.8;
white-space: nowrap;
}
.mapa-pais-fill { fill: #eeece8; }
:global(html.dark) .mapa-pais-fill { fill: #2A2A27; }
.mapa-pais-outline { fill: none; stroke: var(--theme-borde); stroke-width: 1; }
/* Tooltip fijo dentro del card */
.mapa-tooltip-fijo {
position: absolute;
top: 1.5rem;
right: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
pointer-events: none;
min-height: 3rem;
}
.tooltip-nombre { font-size: 0.9rem; font-weight: 700; color: var(--theme-titulo); }
.tooltip-dept { font-size: 0.7rem; color: var(--theme-texto); opacity: 0.5; }
.tooltip-valor { font-size: 0.8rem; color: var(--theme-texto); }
.tooltip-rank { font-size: 0.75rem; font-weight: 700; color: var(--theme-accent); }
/* Leyenda abajo */
.mapa-leyenda-bottom {
position: absolute;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.5rem;
}
.leyenda-label { font-size: 0.6rem; color: var(--theme-texto); opacity: 0.5; }
.leyenda-gradiente {
width: 120px;
height: 6px;
border-radius: 3px;
background: linear-gradient(to right, #e8e6e1, #3D7A9C);
}
:global(html.dark) .leyenda-gradiente { background: linear-gradient(to right, #2A2A27, #D4A574); }
/* Drawer lateral */
.ranking-drawer {
position: fixed;
top: 0;
right: 0;
width: 440px;
height: 100vh;
background: var(--theme-surface);
border-left: 1px solid var(--theme-borde);
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
z-index: 200;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.ranking-drawer.open {
transform: translateX(0);
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem;
border-bottom: 1px solid var(--theme-borde);
flex-shrink: 0;
}
.drawer-titulo {
font-size: 0.85rem;
font-weight: 600;
color: var(--theme-titulo);
}
.drawer-close {
background: none;
border: none;
cursor: pointer;
color: var(--theme-texto);
opacity: 0.6;
padding: 4px;
}
.drawer-close:hover { opacity: 1; }
.drawer-list {
flex: 1;
overflow-y: auto;
padding: 0.75rem;
scrollbar-width: none;
}
.drawer-list::-webkit-scrollbar { display: none; }
/* Tabla reactable */
.drawer-table-wrap {
flex: 1;
overflow-y: auto;
scrollbar-width: none;
}
.drawer-table-wrap::-webkit-scrollbar { display: none; }
.drawer-table {
width: 100%;
border-collapse: collapse;
font-size: 0.7rem;
}
.drawer-table thead {
position: sticky;
top: 0;
background: var(--theme-surface);
z-index: 1;
}
.drawer-table th {
padding: 0.5rem 0.4rem;
font-weight: 600;
color: var(--theme-texto);
opacity: 0.6;
text-align: left;
border-bottom: 1px solid var(--theme-borde);
white-space: nowrap;
font-size: 0.65rem;
}
.th-rank { width: 2rem; text-align: right; padding-right: 0.5rem; }
.th-sortable { cursor: pointer; transition: color 0.15s; user-select: none; }
.th-sortable:hover { color: var(--theme-titulo); opacity: 1; }
.th-right { text-align: right; }
.tabla-row { transition: background 0.1s; }
.tabla-row:hover, .tabla-row.hovered { background: var(--theme-surface-hover); }
.drawer-table td {
padding: 0.35rem 0.4rem;
border-bottom: 1px solid var(--theme-borde);
color: var(--theme-titulo);
}
.td-rank {
text-align: right;
padding-right: 0.5rem;
font-variant-numeric: tabular-nums;
color: var(--theme-texto);
opacity: 0.4;
}
.td-nombre {
display: flex;
align-items: center;
gap: 0.35rem;
}
.td-nombre a {
color: var(--theme-titulo);
text-decoration: none;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.td-nombre a:hover { text-decoration: underline; }
.td-color-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.td-dept {
color: var(--theme-texto);
opacity: 0.5;
white-space: nowrap;
}
.td-monto {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
white-space: nowrap;
}
/* Toggle departamentos */
.dept-toggle {
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-sans);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.6;
transition: all 0.2s;
}
:global(html:not(.dark)) .dept-toggle { border-color: rgba(0, 0, 0, 0.12); }
.dept-toggle:hover { opacity: 0.8; }
.dept-toggle.active { opacity: 1; color: var(--theme-titulo); background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.3); }
:global(html:not(.dark)) .dept-toggle.active { background: rgba(0, 0, 0, 0.05); border-color: rgba(0, 0, 0, 0.2); }
@media (max-width: 768px) {
.mapa-top-controls { padding: 4rem 1rem 0; }
.mapa-bottom-controls { padding: 0 1rem 1rem; }
.controls-row { flex-wrap: wrap; }
.ranking-drawer { width: 100%; }
.leyenda-quintiles {
position: static;
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
margin-top: 0.5rem;
}
.leyenda-titulo { display: none; }
.mapa-muni-count {
position: static;
margin-top: 0.25rem;
}
.mapa-card {
padding: 1rem;
}
}
</style>
import { supabase } from '$lib/supabase';
import { error } from '@sveltejs/kit';
export async function load({ params, fetch }) {
const codigo = params.ubigeo;
// Cargar resumen y población en paralelo
const [resumenRes, pobRes] = await Promise.all([
supabase
.schema('ppto')
.from('entidad_resumen')
.select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking')
.eq('codigo', codigo),
fetch('/poblacion.csv').then(r => r.text())
]);
if (resumenRes.error || !resumenRes.data?.length) {
throw error(404, 'Ubicación no encontrada');
}
// Parsear población para este código
const poblacionMap = {};
pobRes.split('\n').slice(1).forEach(line => {
const [cod, gestion, pob] = line.split(',');
if (cod === codigo) {
poblacionMap[parseInt(gestion)] = parseInt(pob);
}
});
// Determinar última gestión
const gestiones = [...new Set(resumenRes.data.map(d => d.gestion))].sort((a, b) => b - a);
const ultimaGestion = gestiones[0] || 2025;
// Cargar distribuciones de la última gestión
const distRes = await supabase
.schema('ppto')
.from('entidad_distribuciones')
.select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
.eq('codigo', codigo)
.eq('gestion', ultimaGestion);
const firstRow = resumenRes.data[0];
return {
nombre: firstRow.desc || `Ubicación ${codigo}`,
nombrePadre: firstRow.desc_padre || null,
codigo,
resumenData: resumenRes.data || [],
distribucionesData: distRes.data || [],
gestionInicial: ultimaGestion,
poblacionMap
};
}
<script>
import { onMount, tick } from 'svelte';
import { page } from '$app/stores';
import { get } from 'svelte/store';
import * as d3 from 'd3';
import { supabase } from '$lib/supabase';
import { mapaCache } from '$lib/stores/mapaCache';
let { data } = $props();
let nombre = $derived(data.nombre);
let nombrePadre = $derived(data.nombrePadre);
let poblacionMap = $derived(data.poblacionMap);
// Toggle total / per cápita (default per cápita en ubigeo)
let modoPerCapita = $state(true);
// Mapa geográfico
let mapaData = $state(null);
let mapaBoliviaPath = $state('');
let mapaDepartamentoPath = $state('');
let mapaMunicipioPath = $state('');
let mapaAllMunisPaths = $state([]);
let mapaHovered = $state(false);
let mapaPathGen = $state(null);
let mapaDepartamentoNombre = $state('');
function fixWinding(geojson) {
for (const feat of geojson.features || [geojson]) {
const geom = feat.geometry || feat;
if (geom.type === 'Polygon') {
geom.coordinates.forEach((ring, i) => { if (i === 0) ring.reverse(); });
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(poly => poly.forEach((ring, i) => { if (i === 0) ring.reverse(); }));
}
}
}
async function cargarMapa(codigo) {
const cached = get(mapaCache);
if (cached) {
mapaData = cached;
} else if (!mapaData) {
const res = await fetch('/mapa.json');
mapaData = await res.json();
delete mapaData.bolivia.crs;
delete mapaData.departamentos.crs;
delete mapaData.municipios.crs;
fixWinding(mapaData.bolivia);
fixWinding(mapaData.departamentos);
fixWinding(mapaData.municipios);
mapaCache.set(mapaData);
}
const codigoNum = parseInt(codigo);
const municipio = mapaData.municipios.features.find(
f => f.properties.codigo === codigoNum
);
if (!municipio) return;
const deptNombre = municipio.properties.DEPARTAMEN;
mapaDepartamentoNombre = deptNombre;
const departamento = mapaData.departamentos.features.find(
f => f.properties.DEPARTAMEN === deptNombre
);
const projection = d3.geoMercator().fitSize([200, 240], mapaData.bolivia);
const pathGen = d3.geoPath(projection);
mapaPathGen = pathGen;
mapaBoliviaPath = pathGen(mapaData.bolivia) || '';
mapaDepartamentoPath = departamento ? (pathGen(departamento) || '') : '';
mapaMunicipioPath = pathGen(municipio) || '';
// Pre-calcular paths de todos los municipios para hover
mapaAllMunisPaths = mapaData.municipios.features.map(f => ({
path: pathGen(f) || '',
codigo: f.properties.codigo,
isCurrent: f.properties.codigo === codigoNum
}));
}
// Datos desde el loader (Supabase)
let codigoSeleccionado = $derived($page.params.ubigeo);
let gestionSeleccionada = $state(data.gestionInicial);
let resumenData = $derived(data.resumenData);
let distribucionesData = $state(data.distribucionesData);
let cargando = $state(false);
let distCache = $state({ [data.gestionInicial]: data.distribucionesData });
let cargandoDist = $state(false);
// Resetear al cambiar de ubicación
let prevCodigo = codigoSeleccionado;
$effect(() => {
if (data.gestionInicial !== gestionSeleccionada || codigoSeleccionado !== prevCodigo) {
prevCodigo = codigoSeleccionado;
gestionSeleccionada = data.gestionInicial;
distribucionesData = data.distribucionesData;
distCache = { [data.gestionInicial]: data.distribucionesData };
}
});
async function cargarDistribuciones(gestion) {
if (distCache[gestion]) {
distribucionesData = distCache[gestion];
return;
}
cargandoDist = true;
const { data: rows } = await supabase
.schema('ppto')
.from('entidad_distribuciones')
.select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado')
.eq('codigo', codigoSeleccionado)
.eq('gestion', gestion);
const result = rows || [];
distCache[gestion] = result;
distribucionesData = result;
cargandoDist = false;
}
// Cargar distribuciones solo al cambiar gestión por el usuario
let prevGestion = gestionSeleccionada;
$effect(() => {
if (gestionSeleccionada !== prevGestion) {
prevGestion = gestionSeleccionada;
cargarDistribuciones(gestionSeleccionada);
}
});
// Hover en gráficos de historia
let hoveredIngresos = $state(null);
let hoveredGastos = $state(null);
// Dropdown de gestión
let gestionDropdownOpen = $state(false);
// Mini-buscador de entidades
let searchQuery = $state('');
let searchResults = $state([]);
let searchLoading = $state(false);
let searchOpen = $state(false);
let searchSelectedIdx = $state(-1);
let searchDebounce = null;
function parseMetadatos(meta) {
if (!meta) return {};
if (typeof meta === 'object') return meta;
try { return JSON.parse(meta); } catch { return {}; }
}
function onSearchInput() {
clearTimeout(searchDebounce);
if (searchQuery.length < 2) {
searchResults = [];
searchOpen = false;
return;
}
searchDebounce = setTimeout(async () => {
searchLoading = true;
try {
const params = new URLSearchParams({
q: searchQuery,
is_class: 'true',
class_: 'ubigeo',
per_page: '12'
});
const res = await fetch(`/api/search?${params}`);
if (!res.ok) throw new Error();
const json = await res.json();
searchResults = (json.hits || [])
.filter(hit => hit.document.class_ === 'ubigeo')
.map(hit => {
const meta = parseMetadatos(hit.document.metadatos);
return {
nombre: hit.document.texto,
codigo: String(meta.municipio_ubigeo || ''),
departamento: meta.desc_departamento,
provincia: meta.desc_provincia,
highlight: hit.highlights?.[0]?.snippet || hit.document.texto
};
});
searchOpen = searchResults.length > 0;
searchSelectedIdx = -1;
} catch {
searchResults = [];
}
searchLoading = false;
}, 250);
}
function navigateToEntity(item) {
window.location.href = `/ubicacion/${item.codigo}`;
}
function onSearchKeydown(e) {
if (!searchOpen) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
searchSelectedIdx = Math.min(searchSelectedIdx + 1, searchResults.length - 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
searchSelectedIdx = Math.max(searchSelectedIdx - 1, -1);
} else if (e.key === 'Enter' && searchSelectedIdx >= 0) {
e.preventDefault();
navigateToEntity(searchResults[searchSelectedIdx]);
} else if (e.key === 'Escape') {
searchOpen = false;
searchQuery = '';
}
}
function closeSearch() {
setTimeout(() => { searchOpen = false; }, 150);
}
// Filtrar solo años con población (2016+)
function filtrarConPoblacion(rows) {
return rows.filter(d => poblacionMap[d.gestion]);
}
// Aplicar per cápita si está activo
function aplicarPerCapita(rows) {
const filtered = filtrarConPoblacion(rows);
if (!modoPerCapita) return filtered;
return filtered.map(d => ({ ...d, devengado: d.devengado / poblacionMap[d.gestion] }));
}
// Datos derivados - siempre filtrados a años con población
let historiaGastos = $derived(
aplicarPerCapita(
resumenData
.filter(d => d.tipo === 'gastos')
.sort((a, b) => a.gestion - b.gestion)
)
);
let historiaIngresos = $derived(
aplicarPerCapita(
resumenData
.filter(d => d.tipo === 'ingresos')
.sort((a, b) => a.gestion - b.gestion)
)
);
let tieneIngresos = $derived(historiaIngresos.length > 0);
let gestiones = $derived(() => {
const years = [...new Set(resumenData.map(d => d.gestion))].filter(g => poblacionMap[g]).sort();
return years;
});
function parseRanking(rankingStr) {
if (!rankingStr) return null;
const parts = String(rankingStr).split('/');
if (parts.length !== 2) return null;
return { posicion: parseInt(parts[0]), total: parseInt(parts[1]) };
}
let rankingGastos = $derived(() => {
const row = historiaGastos.find(d => d.gestion === gestionSeleccionada);
return row ? parseRanking(row.ranking) : null;
});
let rankingIngresos = $derived(() => {
const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada);
return row ? parseRanking(row.ranking) : null;
});
let distFiltradas = $derived(distribucionesData);
function prepararSegmentos(data) {
const pob = modoPerCapita ? poblacionMap[gestionSeleccionada] : null;
return data
.map(d => ({
codigo: d.hijo,
nombre: d.desc_hijo,
monto: pob ? d.devengado / pob : d.devengado,
padre: d.desc_padre
}))
.filter(d => d.monto > 0)
.sort((a, b) => b.monto - a.monto);
}
let clasificadoresGasto = $derived.by(() => {
const gastos = distFiltradas.filter(d => d.tipo === 'gastos');
const dims = [
{ key: 'objeto', label: 'Objetos de gasto' },
{ key: 'finfun', label: 'Finalidad y función' },
{ key: 'acteco', label: 'Sectores económicos' }
];
return dims
.map(dim => ({
...dim,
data: prepararSegmentos(gastos.filter(d => d.dimension === dim.key))
}))
.filter(dim => dim.data.length > 0);
});
let clasificadoresIngreso = $derived.by(() => {
if (!tieneIngresos) return [];
const ingresos = distFiltradas.filter(d => d.tipo === 'ingresos');
const dims = [
{ key: 'rubro', label: 'Rubros de ingreso' },
{ key: 'organismo', label: 'Organismos financiadores' }
];
return dims
.map(dim => ({
...dim,
data: prepararSegmentos(ingresos.filter(d => d.dimension === dim.key))
}))
.filter(dim => dim.data.length > 0);
});
let barContainers = $state({});
let hoverData = $state({});
function getTotal(data) {
return data.reduce((sum, d) => sum + d.monto, 0);
}
function formatearMonto(valor) {
if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)} mil millones`;
if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)} millones`;
if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)} mil`;
return valor.toFixed(0);
}
function formatearPerCapita(valor) {
if (valor >= 1e6) return `${(valor / 1e6).toFixed(1)} millones`;
if (valor >= 1e4) return `${(valor / 1e3).toFixed(1)} mil`;
return Math.round(valor).toLocaleString('es-BO');
}
let unidadMonto = $derived(modoPerCapita ? 'Bs por persona al año' : 'de Bolivianos');
function fmt(valor) {
return modoPerCapita ? formatearPerCapita(valor) : formatearMonto(valor);
}
// Per cápita promedio anual del periodo
function perCapitaGlobal(tipo) {
const rawRows = resumenData.filter(d => d.tipo === tipo && poblacionMap[d.gestion]);
if (rawRows.length === 0) return 0;
const perCapitaAnuales = rawRows.map(d => d.devengado / poblacionMap[d.gestion]);
return perCapitaAnuales.reduce((s, v) => s + v, 0) / perCapitaAnuales.length;
}
function formatearMontoCorto(valor) {
if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`;
if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`;
if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)}K`;
return valor.toFixed(0);
}
function getChartColors() {
const isDark = document.documentElement.classList.contains('dark');
return {
barDefault: isDark ? 'rgba(107, 159, 212, 0.2)' : 'rgba(196, 137, 125, 0.2)',
barHighlight: isDark ? 'rgba(107, 159, 212, 0.85)' : '#c4897d',
barStroke: isDark ? 'rgba(107, 159, 212, 0.5)' : 'rgba(196, 137, 125, 0.5)',
barStrokeDefault: isDark ? 'transparent' : 'transparent'
};
}
function renderizarBarras(key, data) {
const container = barContainers[key];
if (!container || data.length === 0) return;
const total = getTotal(data);
const height = 48;
const radius = 4;
d3.select(container).selectAll('*').remove();
const containerRect = container.getBoundingClientRect();
const width = containerRect.width || 800;
const svg = d3.select(container)
.append('svg')
.attr('width', '100%')
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
.attr('preserveAspectRatio', 'none')
.style('display', 'block');
let cumulative = 0;
const segments = data.map(d => {
const segWidth = (d.monto / total) * width;
const segment = { ...d, x: cumulative, width: segWidth };
cumulative += segWidth;
return segment;
});
const chartColors = getChartColors();
const colorDefault = chartColors.barDefault;
const colorHover = chartColors.barHighlight;
const colorFirst = chartColors.barHighlight;
const bars = svg.selectAll('rect')
.data(segments)
.enter()
.append('rect')
.attr('x', d => d.x)
.attr('y', 2)
.attr('width', d => Math.max(d.width - 1, 1))
.attr('height', height - 4)
.attr('rx', (d, i) => {
if (i === 0) return radius;
if (i === segments.length - 1) return radius;
return 0;
})
.attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault)
.attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault)
.attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5)
.style('cursor', 'pointer')
.on('mouseenter', function(event, d) {
bars
.attr('fill', colorDefault)
.attr('stroke', chartColors.barStrokeDefault)
.attr('stroke-width', 0.5);
d3.select(this)
.attr('fill', colorHover)
.attr('stroke', chartColors.barStroke)
.attr('stroke-width', 1);
hoverData[key] = d;
hoverData = { ...hoverData };
})
.on('mouseleave', function() {
bars
.attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault)
.attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault)
.attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5);
hoverData[key] = null;
hoverData = { ...hoverData };
});
}
function getDisplayItem(key, data) {
const hover = hoverData[key];
if (hover) return hover;
return data[0] || null;
}
// Dimensiones del gráfico
const chartMargin = { top: 10, right: 10, bottom: 24, left: 48 };
const chartHeight = 200;
let chartWidth = $state(600);
let chartRefA = $state(null);
let chartRefB = $state(null);
let chartContainerEl = $derived(chartRefA || chartRefB);
let innerW = $derived(Math.max(chartWidth - chartMargin.left - chartMargin.right, 0));
let innerH = $derived(chartHeight - chartMargin.top - chartMargin.bottom);
// Escalas D3 para gastos
let xScaleGastos = $derived.by(() => {
if (historiaGastos.length === 0) return null;
return d3.scaleLinear()
.domain(d3.extent(historiaGastos, d => d.gestion))
.range([0, innerW]);
});
let yScaleGastos = $derived.by(() => {
if (historiaGastos.length === 0) return null;
return d3.scaleLinear()
.domain([0, d3.max(historiaGastos, d => d.devengado) * 1.1])
.range([innerH, 0])
.nice();
});
let lineGastos = $derived.by(() => {
if (!xScaleGastos || !yScaleGastos) return '';
const gen = d3.line()
.x(d => xScaleGastos(d.gestion))
.y(d => yScaleGastos(d.devengado))
.curve(d3.curveCatmullRom);
return gen(historiaGastos) || '';
});
let areaGastos = $derived.by(() => {
if (!xScaleGastos || !yScaleGastos) return '';
const gen = d3.area()
.x(d => xScaleGastos(d.gestion))
.y0(innerH)
.y1(d => yScaleGastos(d.devengado))
.curve(d3.curveCatmullRom);
return gen(historiaGastos) || '';
});
let yTicksGastos = $derived.by(() => {
if (!yScaleGastos) return [];
return yScaleGastos.ticks(4);
});
// Escalas D3 para ingresos
let xScaleIngresos = $derived.by(() => {
if (historiaIngresos.length === 0) return null;
return d3.scaleLinear()
.domain(d3.extent(historiaIngresos, d => d.gestion))
.range([0, innerW]);
});
let yScaleIngresos = $derived.by(() => {
if (historiaIngresos.length === 0) return null;
return d3.scaleLinear()
.domain([0, d3.max(historiaIngresos, d => d.devengado) * 1.1])
.range([innerH, 0])
.nice();
});
let lineIngresos = $derived.by(() => {
if (!xScaleIngresos || !yScaleIngresos) return '';
const gen = d3.line()
.x(d => xScaleIngresos(d.gestion))
.y(d => yScaleIngresos(d.devengado))
.curve(d3.curveCatmullRom);
return gen(historiaIngresos) || '';
});
let areaIngresos = $derived.by(() => {
if (!xScaleIngresos || !yScaleIngresos) return '';
const gen = d3.area()
.x(d => xScaleIngresos(d.gestion))
.y0(innerH)
.y1(d => yScaleIngresos(d.devengado))
.curve(d3.curveCatmullRom);
return gen(historiaIngresos) || '';
});
let yTicksIngresos = $derived.by(() => {
if (!yScaleIngresos) return [];
return yScaleIngresos.ticks(4);
});
function renderizarTodasLasBarras() {
setTimeout(() => {
clasificadoresGasto.forEach(({ key, data }) => {
if (barContainers[key]) renderizarBarras(key, data);
});
clasificadoresIngreso.forEach(({ key, data }) => {
if (barContainers[key]) renderizarBarras(key, data);
});
}, 100);
}
$effect(() => {
clasificadoresGasto;
clasificadoresIngreso;
tick().then(() => renderizarTodasLasBarras());
});
onMount(() => {
cargarMapa(codigoSeleccionado);
const observer = new MutationObserver(() => { renderizarTodasLasBarras(); });
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
function handleClickOutside(e) {
if (gestionDropdownOpen && !e.target.closest('.gestion-selector')) {
gestionDropdownOpen = false;
}
}
document.addEventListener('click', handleClickOutside);
return () => { observer.disconnect(); document.removeEventListener('click', handleClickOutside); };
});
</script>
<svelte:head>
<title>{nombre} | Presupuesto Público</title>
</svelte:head>
{#if cargando}
<div class="dashboard">
<p>Cargando datos...</p>
</div>
{:else}
<div class="dashboard">
<div class="nav-spacer"></div>
<!-- Header sticky -->
<header class="sticky-header">
<div class="sticky-row">
<div class="sticky-left">
<h1 class="entidad-nombre">{nombre}</h1>
{#if nombrePadre}
<span class="entidad-meta-text">{nombrePadre}</span>
{/if}
<div class="entidad-search" class:focused={searchOpen || searchQuery.length > 0}>
<svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
<input
type="text"
placeholder="Buscar otra ubicación..."
bind:value={searchQuery}
oninput={onSearchInput}
onkeydown={onSearchKeydown}
onblur={closeSearch}
onfocus={() => { if (searchResults.length > 0) searchOpen = true; }}
/>
{#if searchQuery}
<button class="search-clear" onclick={() => { searchQuery = ''; searchResults = []; searchOpen = false; }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
{/if}
{#if searchOpen || (searchQuery.length >= 2 && searchLoading)}
<div class="search-dropdown">
{#if searchLoading}
<div class="search-msg">Buscando...</div>
{:else if searchResults.length === 0}
<div class="search-msg">Sin resultados</div>
{:else}
{#each searchResults as item, i}
<button
class="search-item"
class:selected={searchSelectedIdx === i}
onmousedown={() => navigateToEntity(item)}
onmouseenter={() => { searchSelectedIdx = i; }}
>
<span class="item-name">{@html item.highlight}</span>
{#if item.departamento}
<span class="item-meta">{item.departamento} · {item.provincia}</span>
{/if}
</button>
{/each}
{/if}
</div>
{/if}
</div>
</div>
</div>
</header>
<!-- Breadcrumb -->
<nav class="breadcrumb">
<a href="/">Inicio</a>
<span class="sep">/</span>
<a href="/ubicacion">Ubicación geográfica</a>
<span class="sep">/</span>
<span>{nombre}</span>
</nav>
<!-- Historia temporal (todos los años) -->
<section class="seccion">
<div class="seccion-titulo-row">
<h2 class="seccion-titulo">Gasto público ejecutado en esta geografía</h2>
<div class="percapita-toggle">
<button class="percapita-btn" class:active={!modoPerCapita} onclick={() => { modoPerCapita = false; }}>Total</button>
<button class="percapita-btn" class:active={modoPerCapita} onclick={() => { modoPerCapita = true; }}>Per cápita</button>
</div>
</div>
<div class="historia-mapa-grid">
{#if tieneIngresos}
<div class="historia-card" bind:this={chartRefA}>
<div class="historia-header">
<span class="historia-label">Ingresos</span>
{#if historiaIngresos.length > 0}
<span class="historia-monto ingresos">
{#if hoveredIngresos}
{hoveredIngresos.gestion} · {fmt(hoveredIngresos.devengado)} {unidadMonto}
{:else}
{#if modoPerCapita}
{formatearPerCapita(perCapitaGlobal('ingresos'))} Bs por persona al año · promedio del periodo
{:else}
{formatearMonto(historiaIngresos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos
{/if}
{/if}
</span>
{/if}
</div>
{#if xScaleIngresos && yScaleIngresos}
<svg class="historia-svg" viewBox="0 0 {chartWidth} {chartHeight}" preserveAspectRatio="xMidYMid meet"
onmouseleave={() => { hoveredIngresos = null; }}>
<g transform="translate({chartMargin.left},{chartMargin.top})">
{#each yTicksIngresos as tick}
<line x1="0" x2={innerW} y1={yScaleIngresos(tick)} y2={yScaleIngresos(tick)} class="grid-line" />
<text x="-8" y={yScaleIngresos(tick)} class="axis-label y-label">{formatearMontoCorto(tick)}</text>
{/each}
<path d={areaIngresos} class="area-path" />
<path d={lineIngresos} class="line-path" fill="none" stroke-width="1.5" />
{#each historiaIngresos as d}
<circle cx={xScaleIngresos(d.gestion)} cy={yScaleIngresos(d.devengado)}
r={hoveredIngresos?.gestion === d.gestion ? 4 : 2} class="dot" class:active={hoveredIngresos?.gestion === d.gestion} />
{/each}
{#each historiaIngresos as d}
<rect x={xScaleIngresos(d.gestion) - innerW / historiaIngresos.length / 2} y="0"
width={innerW / historiaIngresos.length} height={innerH}
fill="transparent"
onmouseenter={() => { hoveredIngresos = d; }}
/>
{/each}
{#if hoveredIngresos && xScaleIngresos}
<line x1={xScaleIngresos(hoveredIngresos.gestion)} x2={xScaleIngresos(hoveredIngresos.gestion)}
y1="0" y2={innerH} class="hover-line" />
<text x={xScaleIngresos(hoveredIngresos.gestion)} y={innerH + 16} class="axis-label x-label active">{hoveredIngresos.gestion}</text>
{:else}
{#each historiaIngresos as d, i}
{#if i === 0 || i === historiaIngresos.length - 1 || i === Math.floor(historiaIngresos.length / 2)}
<text x={xScaleIngresos(d.gestion)} y={innerH + 16} class="axis-label x-label">{d.gestion}</text>
{/if}
{/each}
{/if}
</g>
</svg>
{/if}
</div>
{/if}
<div class="historia-card" bind:this={chartRefB}>
<div class="historia-header">
<span class="historia-label">Gastos</span>
{#if historiaGastos.length > 0}
<span class="historia-monto gastos">
{#if hoveredGastos}
{hoveredGastos.gestion} · {fmt(hoveredGastos.devengado)} {unidadMonto}
{:else}
{#if modoPerCapita}
{formatearPerCapita(perCapitaGlobal('gastos'))} Bs por persona al año · promedio del periodo
{:else}
{formatearMonto(historiaGastos.reduce((s, d) => s + d.devengado, 0))} de Bolivianos
{/if}
{/if}
</span>
{/if}
</div>
{#if xScaleGastos && yScaleGastos}
<svg class="historia-svg" width="100%" height={chartHeight} viewBox="0 0 {chartWidth} {chartHeight}"
onmouseleave={() => { hoveredGastos = null; }}>
<g transform="translate({chartMargin.left},{chartMargin.top})">
{#each yTicksGastos as tick}
<line x1="0" x2={innerW} y1={yScaleGastos(tick)} y2={yScaleGastos(tick)} class="grid-line" />
<text x="-8" y={yScaleGastos(tick)} class="axis-label y-label">{formatearMontoCorto(tick)}</text>
{/each}
<path d={areaGastos} class="area-path" />
<path d={lineGastos} class="line-path" fill="none" stroke-width="1.5" />
{#each historiaGastos as d}
<circle cx={xScaleGastos(d.gestion)} cy={yScaleGastos(d.devengado)}
r={hoveredGastos?.gestion === d.gestion ? 4 : 2} class="dot" class:active={hoveredGastos?.gestion === d.gestion} />
{/each}
{#each historiaGastos as d}
<rect x={xScaleGastos(d.gestion) - innerW / historiaGastos.length / 2} y="0"
width={innerW / historiaGastos.length} height={innerH}
fill="transparent"
onmouseenter={() => { hoveredGastos = d; }}
/>
{/each}
{#if hoveredGastos && xScaleGastos}
<line x1={xScaleGastos(hoveredGastos.gestion)} x2={xScaleGastos(hoveredGastos.gestion)}
y1="0" y2={innerH} class="hover-line" />
<text x={xScaleGastos(hoveredGastos.gestion)} y={innerH + 16} class="axis-label x-label active">{hoveredGastos.gestion}</text>
{:else}
{#each historiaGastos as d, i}
{#if i === 0 || i === historiaGastos.length - 1 || i === Math.floor(historiaGastos.length / 2)}
<text x={xScaleGastos(d.gestion)} y={innerH + 16} class="axis-label x-label">{d.gestion}</text>
{/if}
{/each}
{/if}
</g>
</svg>
{/if}
</div>
<!-- Mapa -->
<a href="/ubicacion" class="mapa-card" style="text-decoration:none;"
data-sveltekit-preload-data="hover"
onmouseenter={() => { mapaHovered = true; }}
onmouseleave={() => { mapaHovered = false; }}
>
{#if mapaBoliviaPath}
<svg viewBox="0 0 200 240" class="mapa-svg" style="cursor:pointer;">
<path d={mapaBoliviaPath} class="mapa-pais" />
<g class="mapa-capas-base" class:ocultar={mapaHovered}>
{#if mapaDepartamentoPath}
<path d={mapaDepartamentoPath} class="mapa-departamento" />
{/if}
{#if mapaMunicipioPath}
<path d={mapaMunicipioPath} class="mapa-municipio" />
{/if}
</g>
{#if mapaAllMunisPaths.length > 0}
<g class="mapa-munis-layer" class:visible={mapaHovered}>
{#each mapaAllMunisPaths as m}
<path d={m.path} class="mapa-muni-hover" />
{/each}
</g>
{/if}
</svg>
{/if}
<span class="mapa-label">{nombre}</span>
{#if poblacionMap[gestionSeleccionada]}
<span class="mapa-poblacion">{poblacionMap[gestionSeleccionada].toLocaleString('es-BO')} hab. ({gestionSeleccionada})</span>
{/if}
<span class="mapa-cta">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Comparar municipios
</span>
</a>
</div>
</section>
<!-- Sección por año -->
<section class="seccion seccion-anual">
<h2 class="seccion-titulo">Gestión
<div class="gestion-selector">
<button class="gestion-btn" onclick={() => { gestionDropdownOpen = !gestionDropdownOpen; }}>
<span>{gestionSeleccionada}</span>
<svg class="gestion-chevron" class:open={gestionDropdownOpen} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
{#if gestionDropdownOpen}
<div class="gestion-dropdown">
{#each gestiones() as g}
<button
class="gestion-option"
class:active={gestionSeleccionada === g}
onclick={() => { gestionSeleccionada = g; gestionDropdownOpen = false; }}
>
{g}
</button>
{/each}
</div>
{/if}
</div>
</h2>
<!-- Ranking (ancho completo, barras reales) -->
{#if rankingGastos()}
{@const r = rankingGastos()}
{@const total = parseInt(r.total) || 342}
{@const barCount = Math.min(total, 342)}
{@const barIdx = Math.max(0, Math.min(barCount - 1, barCount - Math.round((r.posicion / total) * barCount)))}
<div class="ranking-card ranking-card-full">
<div class="ranking-headline">
<span class="ranking-pos-big">#{r.posicion}</span>
<span class="ranking-pos-context">de <span class="ranking-total-num">{total}</span> geografías · {r.posicion <= total / 2 ? 'entre las que más reciben gasto' : 'entre las que menos reciben gasto'}</span>
</div>
<div class="ranking-bars">
{#each Array(barCount) as _, i}
<div class="ranking-bar-tick"></div>
{/each}
<div class="ranking-marker" style="left: {(barIdx / barCount) * 100}%"></div>
</div>
<div class="ranking-extremos">
<span class="ranking-extremo">Menos gasto</span>
<span class="ranking-extremo">Más gasto</span>
</div>
</div>
{/if}
<!-- Composición del gasto -->
{#if clasificadoresGasto.length > 0}
<div class="seccion-sub">
<h3 class="seccion-subtitulo">¿En qué gasta?</h3>
<div class="clasificadores-grid">
{#each clasificadoresGasto as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
<div class="barra-container" bind:this={barContainers[key]}></div>
</div>
{/each}
</div>
</div>
{/if}
<!-- Origen del dinero -->
{#if tieneIngresos && clasificadoresIngreso.length > 0}
<div class="seccion-sub">
<h3 class="seccion-subtitulo">Fuentes de ingreso</h3>
<div class="clasificadores-grid">
{#each clasificadoresIngreso as { key, label, data }}
{@const displayItem = getDisplayItem(key, data)}
{@const totalClasif = getTotal(data)}
{@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0}
<div class="clasificador-card">
<div class="clasificador-header">
<div class="clasificador-info">
<div class="clasificador-nombres">
<span class="clasificador-padre">{displayItem?.padre || ''}</span>
<span class="clasificador-nombre">{displayItem?.nombre || ''}</span>
</div>
<div class="clasificador-valores">
<span class="clasificador-monto">{fmt(displayItem?.monto || 0)} {unidadMonto}</span>
<span class="clasificador-pct">{pct}%</span>
</div>
</div>
<span class="clasificador-label">{label}</span>
</div>
<div class="barra-container" bind:this={barContainers[key]}></div>
</div>
{/each}
</div>
</div>
{/if}
</section>
</div>
{/if}
<style>
.dashboard {
min-height: 100vh;
background: var(--theme-body);
color: var(--theme-texto);
padding: 2rem;
padding-top: 0;
font-family: var(--font-sans);
}
.dashboard > :not(.nav-spacer) {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
:global(html:not(.dark)) .dashboard {
background: #f5f5f7;
}
/* Spacer para empujar debajo del navbar fixed */
.nav-spacer {
height: 3.5rem;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 50;
padding: 0.75rem 2rem;
margin: 0 -2rem 1rem -2rem;
background: var(--theme-body);
border-bottom: 1px solid var(--theme-borde);
}
:global(html:not(.dark)) .sticky-header {
background: #f5f5f7;
}
.sticky-row {
display: flex;
align-items: center;
gap: 1rem;
}
.sticky-left {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex: 1;
}
.entidad-nombre {
font-size: 1.4rem;
font-weight: 700;
color: var(--theme-titulo);
margin: 0;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.entidad-meta-text {
font-size: 0.75rem;
color: var(--theme-texto);
opacity: 0.6;
white-space: nowrap;
}
/* DA titulo jerárquico */
.da-titulo {
display: flex;
align-items: baseline;
gap: 0.4rem;
min-width: 0;
}
.entidad-madre-link {
font-size: 0.85rem;
font-weight: 500;
color: var(--theme-texto);
text-decoration: none;
opacity: 0.6;
transition: opacity 0.15s;
white-space: nowrap;
}
.entidad-madre-link:hover {
opacity: 1;
text-decoration: underline;
}
.da-separador {
font-size: 0.85rem;
color: var(--theme-texto);
opacity: 0.35;
}
.da-nombre {
font-size: 1.1rem;
font-weight: 700;
color: var(--theme-titulo);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Buscador de entidades (estilo ObjetoSearch) */
.entidad-search {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--search-bg, #f5f5f5);
border: 1px solid var(--theme-borde);
border-radius: 8px;
padding: 0.5rem 0.75rem;
transition: box-shadow 0.2s, border-color 0.2s;
width: 240px;
flex-shrink: 0;
}
:global(html.dark) .entidad-search {
--search-bg: rgba(255, 255, 255, 0.08);
}
.entidad-search.focused {
box-shadow: 0 0 0 3px rgba(107, 159, 212, 0.15);
}
:global(html:not(.dark)) .entidad-search.focused {
box-shadow: 0 0 0 3px rgba(196, 137, 125, 0.15);
}
.search-icon {
color: var(--theme-texto);
opacity: 0.5;
flex-shrink: 0;
}
.entidad-search input {
flex: 1;
border: none;
background: transparent;
color: var(--theme-titulo);
font-size: 0.8125rem;
outline: none;
min-width: 100px;
font-family: var(--font-sans);
}
.entidad-search input::placeholder {
color: var(--theme-texto);
opacity: 0.5;
}
.search-clear {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-radius: 4px;
border: none;
background: transparent;
color: var(--theme-texto);
cursor: pointer;
transition: background 0.2s;
}
.search-clear:hover {
background: var(--theme-borde);
}
.search-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
max-height: 280px;
overflow-y: auto;
z-index: 100;
min-width: 320px;
}
.search-msg {
padding: 0.75rem 1rem;
font-size: 0.8125rem;
color: var(--theme-texto);
opacity: 0.7;
}
.search-item {
display: flex;
flex-direction: column;
gap: 0.1rem;
width: 100%;
padding: 0.625rem 1rem;
border: none;
background: transparent;
text-align: left;
cursor: pointer;
transition: background 0.15s;
font-family: var(--font-sans);
}
.search-item:hover,
.search-item.selected {
background: var(--theme-surface-hover);
}
.item-name {
font-size: 0.8125rem;
color: var(--theme-titulo);
line-height: 1.3;
}
.item-name :global(mark) {
background: rgba(196, 137, 125, 0.25);
color: inherit;
border-radius: 2px;
padding: 0 1px;
}
:global(html.dark) .item-name :global(mark) {
background: rgba(107, 159, 212, 0.3);
}
.item-meta {
font-size: 0.7rem;
color: var(--theme-texto);
opacity: 0.5;
}
/* Breadcrumb */
.breadcrumb {
font-size: 0.75rem;
color: var(--theme-texto);
opacity: 0.6;
margin-bottom: 1.5rem;
}
.breadcrumb a {
color: var(--theme-texto);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb .sep {
margin: 0 0.35rem;
}
/* Toggle per cápita */
.seccion-titulo-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.seccion-titulo-row .seccion-titulo {
margin-bottom: 0;
}
.percapita-toggle {
display: flex;
gap: 2px;
background: rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 3px;
flex-shrink: 0;
}
:global(html:not(.dark)) .percapita-toggle {
background: rgba(0, 0, 0, 0.05);
}
.percapita-btn {
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-sans);
border: none;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: var(--theme-texto);
opacity: 0.6;
transition: all 0.2s;
}
.percapita-btn:hover {
opacity: 0.8;
}
.percapita-btn.active {
opacity: 1;
background: rgba(255, 255, 255, 0.12);
color: var(--theme-titulo);
}
:global(html:not(.dark)) .percapita-btn.active {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
/* Historia + Mapa grid */
.historia-mapa-grid {
display: grid;
grid-template-columns: 1fr 0.4fr;
gap: 1.5rem;
}
.ranking-card-full {
width: 100%;
margin-bottom: 1.5rem;
}
.mapa-card {
background: var(--theme-surface);
border-radius: 16px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
cursor: pointer;
transition: box-shadow 0.2s;
}
.mapa-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.mapa-svg {
width: 100%;
max-width: 200px;
height: auto;
margin: 0 auto;
}
.mapa-pais {
fill: #e8e6e1;
stroke: #c4c0b8;
stroke-width: 0.5;
}
:global(html.dark) .mapa-pais {
fill: rgba(255, 255, 255, 0.06);
stroke: rgba(255, 255, 255, 0.12);
}
.mapa-departamento {
fill: #d5d0c8;
stroke: #b8b3aa;
stroke-width: 0.5;
}
:global(html.dark) .mapa-departamento {
fill: rgba(255, 255, 255, 0.1);
stroke: rgba(255, 255, 255, 0.15);
}
.mapa-municipio {
fill: #c4897d;
stroke: #a87068;
stroke-width: 1;
}
:global(html.dark) .mapa-municipio {
fill: #D4A574;
stroke: #b8884e;
stroke-width: 1;
}
.mapa-munis-layer {
opacity: 0;
transition: opacity 0.4s ease;
}
.mapa-munis-layer.visible {
opacity: 1;
}
.mapa-capas-base {
transition: opacity 0.3s ease;
}
.mapa-capas-base.ocultar {
opacity: 0;
}
.mapa-muni-hover {
fill: #e8e6e1;
stroke: #b8b3aa;
stroke-width: 0.5;
}
:global(html.dark) .mapa-muni-hover {
fill: rgba(255, 255, 255, 0.06);
stroke: rgba(255, 255, 255, 0.15);
stroke-width: 0.5;
}
.mapa-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--theme-titulo);
text-align: center;
}
.mapa-poblacion {
font-size: 0.65rem;
color: var(--theme-texto);
opacity: 0.6;
text-align: center;
}
.mapa-cta {
font-size: 0.7rem;
color: var(--theme-accent);
opacity: 0.8;
text-align: center;
transition: opacity 0.2s;
display: flex;
align-items: center;
gap: 0.25rem;
}
.mapa-card:hover .mapa-cta {
opacity: 1;
}
/* Ranking visual - barritas con marcador animado */
.ranking-bars {
display: flex;
height: 18px;
position: relative;
gap: 0;
}
.ranking-bar-tick {
flex: 1;
height: 100%;
background: var(--theme-borde);
opacity: 0.5;
border-right: 1px solid var(--theme-surface);
}
:global(html:not(.dark)) .ranking-bar-tick {
background: #d0cec9;
opacity: 0.6;
}
.ranking-marker {
position: absolute;
top: -2px;
bottom: -2px;
width: 3px;
background: #c4897d;
border-radius: 1.5px;
transition: left 0.5s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
:global(html.dark) .ranking-marker {
background: #D4A574;
}
.ranking-extremos {
display: flex;
justify-content: space-between;
margin-top: 0.25rem;
}
.ranking-extremo {
font-size: 0.6rem;
color: var(--theme-texto);
opacity: 0.4;
}
.ranking-headline {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.ranking-pos-big {
font-size: 1.5rem;
font-weight: 700;
color: var(--theme-titulo);
line-height: 1;
}
.ranking-pos-context {
font-size: 0.8rem;
color: var(--theme-texto);
opacity: 0.7;
}
.ranking-total-num {
font-weight: 700;
color: var(--theme-titulo);
opacity: 1;
}
/* Secciones */
.seccion {
margin-bottom: 2.5rem;
}
.seccion-titulo {
font-size: 0.9rem;
font-weight: 600;
color: var(--theme-titulo);
margin: 0 0 1rem 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Sección anual */
.seccion-anual {
border-top: 1px solid var(--theme-borde);
padding-top: 2rem;
}
/* Dropdown de gestión */
.gestion-selector {
position: relative;
display: inline-flex;
}
.gestion-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.125rem 0;
background: transparent;
border: none;
border-bottom: 1.5px dotted var(--theme-texto);
color: var(--theme-titulo);
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: border-color 0.2s;
font-family: var(--font-sans);
}
.gestion-btn:hover {
border-bottom-color: var(--theme-accent);
}
.gestion-chevron {
color: var(--theme-texto);
opacity: 0.5;
transition: transform 0.2s;
flex-shrink: 0;
}
.gestion-chevron.open {
transform: rotate(180deg);
}
.gestion-dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
min-width: 100px;
max-height: 240px;
overflow-y: auto;
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 100;
}
.gestion-option {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: var(--theme-titulo);
font-size: 0.8125rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: background 0.15s;
font-family: var(--font-sans);
}
.gestion-option:hover {
background: var(--theme-surface-hover);
}
.gestion-option.active {
background: rgba(107, 159, 212, 0.1);
color: #6B9FD4;
}
:global(html:not(.dark)) .gestion-option.active {
background: rgba(90, 157, 191, 0.1);
color: #3D7A9C;
}
.seccion-sub {
margin-top: 1.5rem;
}
.seccion-subtitulo {
font-size: 0.9rem;
font-weight: 600;
color: var(--theme-titulo);
margin: 0 0 0.75rem 0;
opacity: 0.8;
}
/* Rankings grid */
.ranking-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.ranking-grid.single {
grid-template-columns: 1fr;
max-width: 50%;
}
.ranking-card {
background: var(--theme-surface);
border-radius: 16px;
padding: 1.25rem 1.5rem;
}
.ranking-card-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--theme-texto);
opacity: 0.7;
display: block;
margin-bottom: 0.5rem;
}
/* Historia */
.historia-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.historia-grid.single {
grid-template-columns: 1fr;
}
.historia-card {
background: var(--theme-surface);
border-radius: 16px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
min-height: 200px;
}
.historia-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.75rem;
}
.historia-label {
font-size: 0.85rem;
font-weight: 500;
color: var(--theme-texto);
}
.historia-monto {
font-size: 0.8rem;
font-weight: 600;
}
.historia-monto.ingresos,
.historia-monto.gastos {
color: #3D7A9C;
}
:global(html.dark) .historia-monto.ingresos,
:global(html.dark) .historia-monto.gastos {
color: #D4A574;
}
/* SVG chart */
.historia-svg {
display: block;
overflow: visible;
cursor: default;
width: 100%;
height: 100%;
flex: 1;
}
.grid-line {
stroke: var(--theme-texto);
stroke-width: 0.5;
stroke-dasharray: 2 3;
opacity: 0.15;
}
.axis-label {
font-size: 0.55rem;
fill: var(--theme-texto);
opacity: 0.6;
font-family: var(--font-sans);
}
.y-label {
text-anchor: end;
dominant-baseline: middle;
}
.x-label {
text-anchor: middle;
dominant-baseline: hanging;
}
.area-path {
fill: rgba(61, 122, 156, 0.4);
}
.line-path {
stroke: #3D7A9C;
opacity: 0.9;
}
.dot {
fill: #3D7A9C;
opacity: 0.9;
transition: r 0.15s;
}
.dot.active {
fill: #3D7A9C;
}
.hover-line {
stroke: var(--theme-texto);
stroke-width: 0.5;
stroke-dasharray: 3 3;
opacity: 0.3;
}
.x-label.active {
font-weight: 600;
opacity: 1;
}
:global(html.dark) .area-path {
fill: rgba(212, 165, 116, 0.12);
}
:global(html.dark) .line-path {
stroke: #D4A574;
}
:global(html.dark) .dot {
fill: #D4A574;
}
:global(html.dark) .dot.active {
fill: #D4A574;
}
/* Clasificadores */
.clasificadores-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.clasificador-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: var(--theme-surface);
border-radius: 16px;
padding: 1.25rem 1.5rem;
}
.clasificador-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.clasificador-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
overflow: hidden;
}
.clasificador-nombres {
display: flex;
align-items: baseline;
gap: 0.4rem;
overflow: hidden;
}
.clasificador-valores {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.clasificador-monto {
font-size: 0.95rem;
font-weight: 600;
color: var(--theme-titulo);
white-space: nowrap;
flex-shrink: 0;
}
.clasificador-pct {
font-size: 0.8rem;
font-weight: 600;
color: var(--theme-texto);
opacity: 0.5;
white-space: nowrap;
flex-shrink: 0;
}
.clasificador-padre {
font-size: 0.8rem;
font-weight: 400;
color: var(--theme-texto);
opacity: 0.5;
white-space: nowrap;
flex-shrink: 0;
}
.clasificador-nombre {
font-size: 0.85rem;
font-weight: 500;
color: var(--theme-titulo);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.clasificador-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--theme-texto);
opacity: 0.6;
flex-shrink: 0;
}
.barra-container {
width: 100%;
height: 48px;
border-radius: 4px;
overflow: hidden;
}
/* Responsive */
@media (max-width: 768px) {
.dashboard {
padding: 1rem;
padding-top: 0.5rem;
}
.nav-spacer {
height: 3rem;
}
.sticky-header {
margin: 0 -1rem 0.75rem -1rem;
padding: 0.5rem 1rem;
}
.sticky-row {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.sticky-left {
flex-direction: column;
gap: 0.25rem;
}
.da-titulo {
flex-direction: column;
gap: 0.15rem;
}
.entidad-nombre {
font-size: 0.95rem;
}
.da-nombre {
font-size: 0.95rem;
}
.historia-mapa-grid {
grid-template-columns: 1fr;
}
.entidad-search {
width: 100%;
}
.historia-grid {
grid-template-columns: 1fr;
}
.ranking-grid {
grid-template-columns: 1fr;
}
.ranking-grid.single {
max-width: 100%;
}
}
</style>
This diff could not be displayed because it is too large.
codigo_ine,gestion,poblacion
10101,2016,289621
10101,2017,294228
10101,2018,298377
10101,2019,302052
10101,2020,305505
10101,2021,307966
10101,2022,309436
10101,2023,310903
10101,2024,311295
10101,2025,311032
10102,2016,10026
10102,2017,10060
10102,2018,10079
10102,2019,10082
10102,2020,10076
10102,2021,10033
10102,2022,9953
10102,2023,9866
10102,2024,9735
10102,2025,9578
10103,2016,16884
10103,2017,16693
10103,2018,16464
10103,2019,16201
10103,2020,15919
10103,2021,15583
10103,2022,15198
10103,2023,14817
10103,2024,14391
10103,2025,13966
10201,2016,10582
10201,2017,10355
10201,2018,10104
10201,2019,9834
10201,2020,9558
10201,2021,9258
10201,2022,8942
10201,2023,8645
10201,2024,8340
10201,2025,8056
10202,2016,15580
10202,2017,15889
10202,2018,16214
10202,2019,16541
10202,2020,16873
10202,2021,17151
10202,2022,17362
10202,2023,17547
10202,2024,17631
10202,2025,17632
10301,2016,12884
10301,2017,13152
10301,2018,13392
10301,2019,13606
10301,2020,13811
10301,2021,13975
10301,2022,14102
10301,2023,14241
10301,2024,14348
10301,2025,14438
10302,2016,13506
10302,2017,13594
10302,2018,13637
10302,2019,13640
10302,2020,13620
10302,2021,13549
10302,2022,13433
10302,2023,13323
10302,2024,13179
10302,2025,13030
10303,2016,8811
10303,2017,8944
10303,2018,9072
10303,2019,9193
10303,2020,9310
10303,2021,9397
10303,2022,9451
10303,2023,9498
10303,2024,9502
10303,2025,9473
10304,2016,8530
10304,2017,8693
10304,2018,8861
10304,2019,9027
10304,2020,9195
10304,2021,9336
10304,2022,9444
10304,2023,9544
10304,2024,9599
10304,2025,9613
10401,2016,10615
10401,2017,10557
10401,2018,10493
10401,2019,10419
10401,2020,10339
10401,2021,10224
10401,2022,10069
10401,2023,9902
10401,2024,9683
10401,2025,9436
10402,2016,9009
10402,2017,9059
10402,2018,9102
10402,2019,9134
10402,2020,9159
10402,2021,9151
10402,2022,9106
10402,2023,9050
10402,2024,8947
10402,2025,8817
10403,2016,7714
10403,2017,7722
10403,2018,7716
10403,2019,7696
10403,2020,7667
10403,2021,7609
10403,2022,7522
10403,2023,7430
10403,2024,7305
10403,2025,7167
10404,2016,5013
10404,2017,4927
10404,2018,4821
10404,2019,4698
10404,2020,4567
10404,2021,4421
10404,2022,4266
10404,2023,4121
10404,2024,3976
10404,2025,3845
10405,2016,4985
10405,2017,5112
10405,2018,5244
10405,2019,5378
10405,2020,5514
10405,2021,5637
10405,2022,5742
10405,2023,5845
10405,2024,5921
10405,2025,5971
10501,2016,25779
10501,2017,25942
10501,2018,26090
10501,2019,26210
10501,2020,26313
10501,2021,26321
10501,2022,26221
10501,2023,26084
10501,2024,25807
10501,2025,25441
10502,2016,8489
10502,2017,8422
10502,2018,8350
10502,2019,8269
10502,2020,8184
10502,2021,8071
10502,2022,7928
10502,2023,7777
10502,2024,7588
10502,2025,7380
10601,2016,16919
10601,2017,16639
10601,2018,16335
10601,2019,16011
10601,2020,15681
10601,2021,15308
10601,2022,14895
10601,2023,14493
10601,2024,14052
10601,2025,13608
10602,2016,10390
10602,2017,10288
10602,2018,10160
10602,2019,10009
10602,2020,9845
10602,2021,9647
10602,2022,9418
10602,2023,9192
10602,2024,8939
10602,2025,8684
10701,2016,16568
10701,2017,16575
10701,2018,16542
10701,2019,16471
10701,2020,16376
10701,2021,16218
10701,2022,15999
10701,2023,15773
10701,2024,15486
10701,2025,15179
10702,2016,33276
10702,2017,32888
10702,2018,32413
10702,2019,31862
10702,2020,31272
10702,2021,30575
10702,2022,29787
10702,2023,29015
10702,2024,28169
10702,2025,27334
10703,2016,14823
10703,2017,15207
10703,2018,15581
10703,2019,15939
10703,2020,16293
10703,2021,16597
10703,2022,16846
10703,2023,17090
10703,2024,17267
10703,2025,17395
10704,2016,17073
10704,2017,16987
10704,2018,16834
10704,2019,16624
10704,2020,16383
10704,2021,16081
10704,2022,15732
10704,2023,15400
10704,2024,15041
10704,2025,14696
10801,2016,11297
10801,2017,11156
10801,2018,10997
10801,2019,10819
10801,2020,10632
10801,2021,10410
10801,2022,10153
10801,2023,9897
10801,2024,9604
10801,2025,9307
10901,2016,4167
10901,2017,4355
10901,2018,4550
10901,2019,4749
10901,2020,4950
10901,2021,5139
10901,2022,5311
10901,2023,5478
10901,2024,5617
10901,2025,5732
10902,2016,18220
10902,2017,18040
10902,2018,17815
10902,2019,17549
10902,2020,17260
10902,2021,16910
10902,2022,16505
10902,2023,16104
10902,2024,15653
10902,2025,15203
10903,2016,4594
10903,2017,4701
10903,2018,4806
10903,2019,4906
10903,2020,5004
10903,2021,5087
10903,2022,5152
10903,2023,5215
10903,2024,5257
10903,2025,5284
11001,2016,11031
11001,2017,11405
11001,2018,11802
11001,2019,12210
11001,2020,12627
11001,2021,13011
11001,2022,13345
11001,2023,13662
11001,2024,13900
11001,2025,14067
11002,2016,2717
11002,2017,2730
11002,2018,2738
11002,2019,2742
11002,2020,2743
11002,2021,2736
11002,2022,2719
11002,2023,2702
11002,2024,2677
11002,2025,2644
11003,2016,7869
11003,2017,7896
11003,2018,7911
11003,2019,7912
11003,2020,7904
11003,2021,7867
11003,2022,7798
11003,2023,7722
11003,2024,7611
11003,2025,7482
20101,2016,807311
20101,2017,809186
20101,2018,810405
20101,2019,810900
20101,2020,811307
20101,2021,809537
20101,2022,805573
20101,2023,801926
20101,2024,795746
20101,2025,788297
20102,2016,18904
20102,2017,19433
20102,2018,19963
20102,2019,20489
20102,2020,21023
20102,2021,21508
20102,2022,21937
20102,2023,22374
20102,2024,22738
20102,2025,23038
20103,2016,18101
20103,2017,18586
20103,2018,19057
20103,2019,19511
20103,2020,19965
20103,2021,20368
20103,2022,20721
20103,2023,21089
20103,2024,21400
20103,2025,21665
20104,2016,29866
20104,2017,32241
20104,2018,34694
20104,2019,37179
20104,2020,39683
20104,2021,42062
20104,2022,44257
20104,2023,46363
20104,2024,48176
20104,2025,49644
20105,2016,924282
20105,2017,931574
20105,2018,936522
20105,2019,939416
20105,2020,941397
20105,2021,940467
20105,2022,937049
20105,2023,934526
20105,2024,930046
20105,2025,925014
20201,2016,49174
20201,2017,49493
20201,2018,49763
20201,2019,49977
20201,2020,50170
20201,2021,50209
20201,2022,50087
20201,2023,49958
20201,2024,49640
20201,2025,49209
20202,2016,14585
20202,2017,14982
20202,2018,15411
20202,2019,15857
20202,2020,16320
20202,2021,16747
20202,2022,17119
20202,2023,17478
20202,2024,17747
20202,2025,17934
20204,2016,8559
20204,2017,8658
20204,2018,8756
20204,2019,8850
20204,2020,8946
20204,2021,9016
20204,2022,9060
20204,2023,9101
20204,2024,9106
20204,2025,9083
20205,2016,9313
20205,2017,9422
20205,2018,9520
20205,2019,9605
20205,2020,9685
20205,2021,9734
20205,2022,9752
20205,2023,9768
20205,2024,9748
20205,2025,9708
20206,2016,4515
20206,2017,4641
20206,2018,4764
20206,2019,4881
20206,2020,4997
20206,2021,5100
20206,2022,5189
20206,2023,5281
20206,2024,5357
20206,2025,5421
20203,2016,5746
20203,2017,5969
20203,2018,6208
20203,2019,6456
20203,2020,6712
20203,2021,6953
20203,2022,7171
20203,2023,7383
20203,2024,7555
20203,2025,7687
20301,2016,11810
20301,2017,12111
20301,2018,12433
20301,2019,12767
20301,2020,13113
20301,2021,13430
20301,2022,13708
20301,2023,13979
20301,2024,14186
20301,2025,14334
20302,2016,16004
20302,2017,16153
20302,2018,16269
20302,2019,16353
20302,2020,16424
20302,2021,16444
20302,2022,16418
20302,2023,16403
20302,2024,16345
20302,2025,16269
20303,2016,11049
20303,2017,11298
20303,2018,11546
20303,2019,11790
20303,2020,12038
20303,2021,12258
20303,2022,12446
20303,2023,12640
20303,2024,12795
20303,2025,12917
20304,2016,4391
20304,2017,4525
20304,2018,4665
20304,2019,4810
20304,2020,4960
20304,2021,5102
20304,2022,5232
20304,2023,5364
20304,2024,5477
20304,2025,5569
20305,2016,3691
20305,2017,3794
20305,2018,3896
20305,2019,3997
20305,2020,4099
20305,2021,4191
20305,2022,4271
20305,2023,4352
20305,2024,4418
20305,2025,4472
20306,2016,5932
20306,2017,6076
20306,2018,6198
20306,2019,6303
20306,2020,6401
20306,2021,6484
20306,2022,6557
20306,2023,6648
20306,2024,6741
20306,2025,6840
20307,2016,1507
20307,2017,1847
20307,2018,2205
20307,2019,2572
20307,2020,2941
20307,2021,3294
20307,2022,3622
20307,2023,3925
20307,2024,4181
20307,2025,4376
20308,2016,7834
20308,2017,7934
20308,2018,8038
20308,2019,8140
20308,2020,8245
20308,2021,8326
20308,2022,8379
20308,2023,8426
20308,2024,8433
20308,2025,8408
20401,2016,12390
20401,2017,12673
20401,2018,12981
20401,2019,13302
20401,2020,13639
20401,2021,13947
20401,2022,14215
20401,2023,14477
20401,2024,14672
20401,2025,14802
20402,2016,16970
20402,2017,17234
20402,2018,17509
20402,2019,17784
20402,2020,18064
20402,2021,18293
20402,2022,18461
20402,2023,18615
20402,2024,18680
20402,2025,18679
20403,2016,16062
20403,2017,16374
20403,2018,16692
20403,2019,17007
20403,2020,17326
20403,2021,17600
20403,2022,17818
20403,2023,18031
20403,2024,18169
20403,2025,18244
20404,2016,5716
20404,2017,5768
20404,2018,5818
20404,2019,5864
20404,2020,5910
20404,2021,5939
20404,2022,5949
20404,2023,5956
20404,2024,5940
20404,2025,5906
20405,2016,7974
20405,2017,8155
20405,2018,8340
20405,2019,8525
20405,2020,8714
20405,2021,8881
20405,2022,9021
20405,2023,9158
20405,2024,9257
20405,2025,9324
20501,2016,13460
20501,2017,14093
20501,2018,14771
20501,2019,15476
20501,2020,16203
20501,2021,16892
20501,2022,17523
20501,2023,18129
20501,2024,18630
20501,2025,19019
20502,2016,9082
20502,2017,9176
20502,2018,9263
20502,2019,9340
20502,2020,9415
20502,2021,9462
20502,2022,9479
20502,2023,9496
20502,2024,9477
20502,2025,9436
20503,2016,6192
20503,2017,6325
20503,2018,6451
20503,2019,6569
20503,2020,6687
20503,2021,6788
20503,2022,6872
20503,2023,6964
20503,2024,7039
20503,2025,7100
20601,2016,26695
20601,2017,27386
20601,2018,28065
20601,2019,28727
20601,2020,29394
20601,2021,29989
20601,2022,30507
20601,2023,31044
20601,2024,31489
20601,2025,31861
20602,2016,16281
20602,2017,16489
20602,2018,16665
20602,2019,16814
20602,2020,16953
20602,2021,17045
20602,2022,17094
20602,2023,17159
20602,2024,17186
20602,2025,17194
20603,2016,9148
20603,2017,9321
20603,2018,9481
20603,2019,9631
20603,2020,9781
20603,2021,9907
20603,2022,10011
20603,2023,10128
20603,2024,10224
20603,2025,10304
20604,2016,3193
20604,2017,3346
20604,2018,3506
20604,2019,3671
20604,2020,3840
20604,2021,4000
20604,2022,4147
20604,2023,4290
20604,2024,4413
20604,2025,4511
20605,2016,3992
20605,2017,3989
20605,2018,3971
20605,2019,3943
20605,2020,3908
20605,2021,3862
20605,2022,3808
20605,2023,3760
20605,2024,3710
20605,2025,3665
20606,2016,10070
20606,2017,9879
20606,2018,9658
20606,2019,9415
20606,2020,9167
20606,2021,8899
20606,2022,8622
20606,2023,8373
20606,2024,8132
20606,2025,7925
20607,2016,16827
20607,2017,17624
20607,2018,18429
20607,2019,19234
20607,2020,20046
20607,2021,20808
20607,2022,21508
20607,2023,22206
20607,2024,22818
20607,2025,23337
20608,2016,10979
20608,2017,11391
20608,2018,11804
20608,2019,12214
20608,2020,12628
20608,2021,13008
20608,2022,13352
20608,2023,13695
20608,2024,13989
20608,2025,14238
20701,2016,21846
20701,2017,21830
20701,2018,21724
20701,2019,21545
20701,2020,21332
20701,2021,21050
20701,2022,20722
20701,2023,20436
20701,2024,20141
20701,2025,19881
20702,2016,7809
20702,2017,8044
20702,2018,8276
20702,2019,8502
20702,2020,8731
20702,2021,8938
20702,2022,9123
20702,2023,9314
20702,2024,9478
20702,2025,9618
20801,2016,95383
20801,2017,98723
20801,2018,101945
20801,2019,105056
20801,2020,108160
20801,2021,110999
20801,2022,113581
20801,2023,116282
20801,2024,118724
20801,2025,120926
20802,2016,7992
20802,2017,8145
20802,2018,8302
20802,2019,8457
20802,2020,8614
20802,2021,8747
20802,2022,8851
20802,2023,8950
20802,2024,9007
20802,2025,9032
20803,2016,13325
20803,2017,13513
20803,2018,13691
20803,2019,13855
20803,2020,14016
20803,2021,14136
20803,2022,14213
20803,2023,14290
20803,2024,14316
20803,2025,14309
20804,2016,7782
20804,2017,7900
20804,2018,8000
20804,2019,8084
20804,2020,8160
20804,2021,8211
20804,2022,8238
20804,2023,8271
20804,2024,8283
20804,2025,8288
20805,2016,6894
20805,2017,7082
20805,2018,7279
20805,2019,7479
20805,2020,7683
20805,2021,7869
20805,2022,8030
20805,2023,8188
20805,2024,8308
20805,2025,8395
20806,2016,16521
20806,2017,16766
20806,2018,16994
20806,2019,17204
20806,2020,17409
20806,2021,17566
20806,2022,17673
20806,2023,17786
20806,2024,17844
20806,2025,17860
20807,2016,7277
20807,2017,7396
20807,2018,7508
20807,2019,7611
20807,2020,7712
20807,2021,7788
20807,2022,7839
20807,2023,7889
20807,2024,7908
20807,2025,7909
20901,2016,11893
20901,2017,11903
20901,2018,11879
20901,2019,11825
20901,2020,11758
20901,2021,11654
20901,2022,11519
20901,2023,11396
20901,2024,11252
20901,2025,11108
20902,2016,14126
20902,2017,14532
20902,2018,14950
20902,2019,15374
20902,2020,15812
20902,2021,16216
20902,2022,16580
20902,2023,16949
20902,2024,17258
20902,2025,17507
20903,2016,8828
20903,2017,9323
20903,2018,9850
20903,2019,10395
20903,2020,10955
20903,2021,11488
20903,2022,11978
20903,2023,12447
20903,2024,12840
20903,2025,13149
20904,2016,5628
20904,2017,5660
20904,2018,5675
20904,2019,5676
20904,2020,5671
20904,2021,5650
20904,2022,5617
20904,2023,5593
20904,2024,5562
20904,2025,5532
20905,2016,12537
20905,2017,12790
20905,2018,13047
20905,2019,13302
20905,2020,13561
20905,2021,13783
20905,2022,13961
20905,2023,14135
20905,2024,14250
20905,2025,14317
21001,2016,16342
21001,2017,16759
21001,2018,17201
21001,2019,17656
21001,2020,18128
21001,2021,18556
21001,2022,18927
21001,2023,19287
21001,2024,19557
21001,2025,19747
21002,2016,9406
21002,2017,9679
21002,2018,9956
21002,2019,10231
21002,2020,10511
21002,2021,10764
21002,2022,10988
21002,2023,11211
21002,2024,11393
21002,2025,11537
21003,2016,12431
21003,2017,12948
21003,2018,13470
21003,2019,13989
21003,2020,14513
21003,2021,15000
21003,2022,15444
21003,2023,15886
21003,2024,16269
21003,2025,16592
21004,2016,21758
21004,2017,22197
21004,2018,22632
21004,2019,23057
21004,2020,23484
21004,2021,23848
21004,2022,24140
21004,2023,24431
21004,2024,24631
21004,2025,24760
21005,2016,8820
21005,2017,8999
21005,2018,9174
21005,2019,9342
21005,2020,9512
21005,2021,9658
21005,2022,9780
21005,2023,9908
21005,2024,10006
21005,2025,10081
21006,2016,6565
21006,2017,6811
21006,2018,7046
21006,2019,7270
21006,2020,7492
21006,2021,7694
21006,2022,7876
21006,2023,8065
21006,2024,8235
21006,2025,8392
21101,2016,19370
21101,2017,19490
21101,2018,19556
21101,2019,19576
21101,2020,19576
21101,2021,19517
21101,2022,19411
21101,2023,19330
21101,2024,19219
21101,2025,19102
21102,2016,20069
21102,2017,20687
21102,2018,21283
21102,2019,21859
21102,2020,22435
21102,2021,22957
21102,2022,23425
21102,2023,23916
21102,2024,24350
21102,2025,24736
21103,2016,7218
21103,2017,7348
21103,2018,7464
21103,2019,7567
21103,2020,7668
21103,2021,7752
21103,2022,7820
21103,2023,7903
21103,2024,7976
21103,2025,8045
21104,2016,27376
21104,2017,27703
21104,2018,27956
21104,2019,28145
21104,2020,28308
21104,2021,28389
21104,2022,28403
21104,2023,28456
21104,2024,28466
21104,2025,28470
21105,2016,45454
21105,2017,46101
21105,2018,46567
21105,2019,46892
21105,2020,47161
21105,2021,47304
21105,2022,47373
21105,2023,47568
21105,2024,47776
21105,2025,48055
21201,2016,36791
21201,2017,38759
21201,2018,40791
21201,2019,42852
21201,2020,44945
21201,2021,46923
21201,2022,48743
21201,2023,50524
21201,2024,52059
21201,2025,53318
21202,2016,29790
21202,2017,31205
21202,2018,32630
21202,2019,34050
21202,2020,35482
21202,2021,36823
21202,2022,38057
21202,2023,39290
21202,2024,40376
21202,2025,41303
21203,2016,18915
21203,2017,19281
21203,2018,19678
21203,2019,20090
21203,2020,20516
21203,2021,20887
21203,2022,21183
21203,2023,21452
21203,2024,21603
21203,2025,21651
21204,2016,7078
21204,2017,7228
21204,2018,7392
21204,2019,7565
21204,2020,7746
21204,2021,7910
21204,2022,8050
21204,2023,8183
21204,2024,8275
21204,2025,8327
21301,2016,34143
21301,2017,34519
21301,2018,34841
21301,2019,35111
21301,2020,35360
21301,2021,35504
21301,2022,35547
21301,2023,35605
21301,2024,35559
21301,2025,35457
21302,2016,9428
21302,2017,9481
21302,2018,9532
21302,2019,9577
21302,2020,9621
21302,2021,9639
21302,2022,9626
21302,2023,9612
21302,2024,9560
21302,2025,9479
21303,2016,9094
21303,2017,9445
21303,2018,9806
21303,2019,10172
21303,2020,10544
21303,2021,10891
21303,2022,11204
21303,2023,11512
21303,2024,11771
21303,2025,11981
21304,2016,13803
21304,2017,14114
21304,2018,14432
21304,2019,14751
21304,2020,15077
21304,2021,15367
21304,2022,15615
21304,2023,15863
21304,2024,16051
21304,2025,16190
21305,2016,24973
21305,2017,25283
21305,2018,25561
21305,2019,25805
21305,2020,26039
21305,2021,26197
21305,2022,26280
21305,2023,26373
21305,2024,26384
21305,2025,26345
21306,2016,11189
21306,2017,11469
21306,2018,11742
21306,2019,12008
21306,2020,12273
21306,2021,12507
21306,2022,12707
21306,2023,12913
21306,2024,13077
21306,2025,13211
21307,2016,5629
21307,2017,5699
21307,2018,5748
21307,2019,5781
21307,2020,5806
21307,2021,5813
21307,2022,5805
21307,2023,5805
21307,2024,5798
21307,2025,5792
21401,2016,22934
21401,2017,23776
21401,2018,24602
21401,2019,25409
21401,2020,26218
21401,2021,26964
21401,2022,27643
21401,2023,28344
21401,2024,28969
21401,2025,29522
21402,2016,19811
21402,2017,20484
21402,2018,21144
21402,2019,21789
21402,2020,22437
21402,2021,23030
21402,2022,23565
21402,2023,24118
21402,2024,24606
21402,2025,25034
21501,2016,10913
21501,2017,11245
21501,2018,11562
21501,2019,11865
21501,2020,12165
21501,2021,12437
21501,2022,12681
21501,2023,12940
21501,2024,13174
21501,2025,13386
21502,2016,9737
21502,2017,9901
21502,2018,10046
21502,2019,10173
21502,2020,10295
21502,2021,10388
21502,2022,10455
21502,2023,10534
21502,2024,10591
21502,2025,10638
21601,2016,14711
21601,2017,15018
21601,2018,15300
21601,2019,15558
21601,2020,15809
21601,2021,16017
21601,2022,16185
21601,2023,16368
21601,2024,16513
21601,2025,16640
21602,2016,4330
21602,2017,4661
21602,2018,5002
21602,2019,5348
21602,2020,5698
21602,2021,6031
21602,2022,6339
21602,2023,6637
21602,2024,6895
21602,2025,7107
21701,2016,16404
21701,2017,16695
21701,2018,16985
21701,2019,17270
21701,2020,17558
21701,2021,17801
21701,2022,17991
21701,2023,18182
21701,2024,18304
21701,2025,18368
21702,2016,7114
21702,2017,7431
21702,2018,7763
21702,2019,8103
21702,2020,8450
21702,2021,8775
21702,2022,9068
21702,2023,9350
21702,2024,9579
21702,2025,9759
21703,2016,7216
21703,2017,7354
21703,2018,7462
21703,2019,7548
21703,2020,7625
21703,2021,7683
21703,2022,7731
21703,2023,7803
21703,2024,7879
21703,2025,7970
21801,2016,9555
21801,2017,9632
21801,2018,9695
21801,2019,9743
21801,2020,9783
21801,2021,9791
21801,2022,9767
21801,2023,9741
21801,2024,9681
21801,2025,9604
21802,2016,7431
21802,2017,7430
21802,2018,7411
21802,2019,7375
21802,2020,7330
21802,2021,7263
21802,2022,7174
21802,2023,7092
21802,2024,6992
21802,2025,6890
21803,2016,2155
21803,2017,2163
21803,2018,2167
21803,2019,2167
21803,2020,2166
21803,2021,2162
21803,2022,2155
21803,2023,2154
21803,2024,2152
21803,2025,2151
21901,2016,5372
21901,2017,5595
21901,2018,5827
21901,2019,6065
21901,2020,6308
21901,2021,6535
21901,2022,6742
21901,2023,6944
21901,2024,7113
21901,2025,7249
21902,2016,3711
21902,2017,3959
21902,2018,4211
21902,2019,4464
21902,2020,4718
21902,2021,4959
21902,2022,5181
21902,2023,5396
21902,2024,5584
21902,2025,5741
22001,2016,55975
22001,2017,57041
22001,2018,58061
22001,2019,59030
22001,2020,59991
22001,2021,60790
22001,2022,61422
22001,2023,62077
22001,2024,62537
22001,2025,62857
22002,2016,11971
22002,2017,11960
22002,2018,11906
22002,2019,11818
22002,2020,11716
22002,2021,11578
22002,2022,11415
22002,2023,11275
22002,2024,11127
22002,2025,10993
30101,2016,685658
30101,2017,691116
30101,2018,695217
30101,2019,698136
30101,2020,700677
30101,2021,701301
30101,2022,700266
30101,2023,700040
30101,2024,698431
30101,2025,696554
30201,2016,24426
30201,2017,24514
30201,2018,24598
30201,2019,24667
30201,2020,24734
30201,2021,24725
30201,2022,24632
30201,2023,24523
30201,2024,24302
30201,2025,24007
30202,2016,7362
30202,2017,7358
30202,2018,7326
30202,2019,7269
30202,2020,7202
30202,2021,7111
30202,2022,7003
30202,2023,6908
30202,2024,6808
30202,2025,6717
30203,2016,6167
30203,2017,6282
30203,2018,6403
30203,2019,6526
30203,2020,6655
30203,2021,6768
30203,2022,6862
30203,2023,6955
30203,2024,7020
30203,2025,7058
30301,2016,24469
30301,2017,24406
30301,2018,24326
30301,2019,24223
30301,2020,24109
30301,2021,23917
30301,2022,23642
30301,2023,23352
30301,2024,22958
30301,2025,22509
30302,2016,13650
30302,2017,13875
30302,2018,14134
30302,2019,14413
30302,2020,14708
30302,2021,14966
30302,2022,15170
30302,2023,15349
30302,2024,15436
30302,2025,15441
30303,2016,19591
30303,2017,19809
30303,2018,20010
30303,2019,20190
30303,2020,20364
30303,2021,20480
30303,2022,20534
30303,2023,20591
30303,2024,20576
30303,2025,20518
30401,2016,9359
30401,2017,9668
30401,2018,9995
30401,2019,10331
30401,2020,10678
30401,2021,11000
30401,2022,11288
30401,2023,11568
30401,2024,11793
30401,2025,11966
30402,2016,7515
30402,2017,7559
30402,2018,7612
30402,2019,7666
30402,2020,7723
30402,2021,7758
30402,2022,7763
30402,2023,7757
30402,2024,7707
30402,2025,7623
30403,2016,21681
30403,2017,22835
30403,2018,23996
30403,2019,25159
30403,2020,26341
30403,2021,27471
30403,2022,28541
30403,2023,29635
30403,2024,30650
30403,2025,31577
30404,2016,4736
30404,2017,4810
30404,2018,4886
30404,2019,4963
30404,2020,5042
30404,2021,5108
30404,2022,5157
30404,2023,5205
30404,2024,5229
30404,2025,5234
30501,2016,10299
30501,2017,10495
30501,2018,10709
30501,2019,10930
30501,2020,11159
30501,2021,11357
30501,2022,11513
30501,2023,11652
30501,2024,11725
30501,2025,11742
30502,2016,9078
30502,2017,9062
30502,2018,9061
30502,2019,9067
30502,2020,9079
30502,2021,9066
30502,2022,9020
30502,2023,8960
30502,2024,8845
30502,2025,8687
30601,2016,11151
30601,2017,11197
30601,2018,11242
30601,2019,11280
30601,2020,11318
30601,2021,11323
30601,2022,11291
30601,2023,11255
30601,2024,11171
30601,2025,11054
30602,2016,10876
30602,2017,10965
30602,2018,11057
30602,2019,11146
30602,2020,11237
30602,2021,11296
30602,2022,11316
30602,2023,11329
30602,2024,11288
30602,2025,11209
30701,2016,20873
30701,2017,20961
30701,2018,21004
30701,2019,21005
30701,2020,20986
30701,2021,20900
30701,2022,20751
30701,2023,20612
30701,2024,20415
30701,2025,20200
30702,2016,7236
30702,2017,7391
30702,2018,7549
30702,2019,7705
30702,2020,7865
30702,2021,8004
30702,2022,8120
30702,2023,8235
30702,2024,8318
30702,2025,8373
30703,2016,4138
30703,2017,4177
30703,2018,4200
30703,2019,4212
30703,2020,4218
30703,2021,4212
30703,2022,4196
30703,2023,4188
30703,2024,4177
30703,2025,4168
30801,2016,23256
30801,2017,23289
30801,2018,23272
30801,2019,23209
30801,2020,23124
30801,2021,22965
30801,2022,22738
30801,2023,22522
30801,2024,22242
30801,2025,21943
30802,2016,7588
30802,2017,7650
30802,2018,7702
30802,2019,7745
30802,2020,7784
30802,2021,7800
30802,2022,7793
30802,2023,7785
30802,2024,7751
30802,2025,7703
30803,2016,6580
30803,2017,6891
30803,2018,7217
30803,2019,7552
30803,2020,7897
30803,2021,8223
30803,2022,8525
30803,2023,8821
30803,2024,9074
30803,2025,9286
30901,2016,154428
30901,2017,157696
30901,2018,160769
30901,2019,163650
30901,2020,166495
30901,2021,168900
30901,2022,170877
30901,2023,172989
30901,2024,174658
30901,2025,176045
30902,2016,47969
30902,2017,49445
30902,2018,50901
30902,2019,52330
30902,2020,53769
30902,2021,55078
30902,2022,56248
30902,2023,57452
30902,2024,58486
30902,2025,59383
30903,2016,60144
30903,2017,61097
30903,2018,61919
30903,2019,62626
30903,2020,63293
30903,2021,63783
30903,2022,64121
30903,2023,64535
30903,2024,64826
30903,2025,65073
30904,2016,57602
30904,2017,58199
30904,2018,58600
30904,2019,58843
30904,2020,59018
30904,2021,59021
30904,2022,58898
30904,2023,58883
30904,2024,58818
30904,2025,58797
30905,2016,59048
30905,2017,60586
30905,2018,62095
30905,2019,63565
30905,2020,65042
30905,2021,66354
30905,2022,67492
30905,2023,68661
30905,2024,69619
30905,2025,70415
31001,2016,197244
31001,2017,202205
31001,2018,206882
31001,2019,211296
31001,2020,215664
31001,2021,219482
31001,2022,222783
31001,2023,226315
31001,2024,229346
31001,2025,232058
31002,2016,21173
31002,2017,21493
31002,2018,21794
31002,2019,22073
31002,2020,22346
31002,2021,22555
31002,2022,22697
31002,2023,22843
31002,2024,22911
31002,2025,22928
31003,2016,81935
31003,2017,84258
31003,2018,86524
31003,2019,88725
31003,2020,90936
31003,2021,92926
31003,2022,94688
31003,2023,96521
31003,2024,98094
31003,2025,99463
31101,2016,26047
31101,2017,26160
31101,2018,26255
31101,2019,26324
31101,2020,26385
31101,2021,26364
31101,2022,26257
31101,2023,26142
31101,2024,25920
31101,2025,25634
31201,2016,15733
31201,2017,15813
31201,2018,15862
31201,2019,15884
31201,2020,15893
31201,2021,15853
31201,2022,15767
31201,2023,15690
31201,2024,15570
31201,2025,15432
31202,2016,11031
31202,2017,11227
31202,2018,11436
31202,2019,11649
31202,2020,11870
31202,2021,12060
31202,2022,12211
31202,2023,12354
31202,2024,12438
31202,2025,12471
31203,2016,11506
31203,2017,11684
31203,2018,11884
31203,2019,12095
31203,2020,12317
31203,2021,12508
31203,2022,12656
31203,2023,12790
31203,2024,12854
31203,2025,12859
31204,2016,24834
31204,2017,25452
31204,2018,26033
31204,2019,26581
31204,2020,27123
31204,2021,27596
31204,2022,28003
31204,2023,28438
31204,2024,28811
31204,2025,29142
31205,2016,53785
31205,2017,55614
31205,2018,57480
31205,2019,59355
31205,2020,61267
31205,2021,63034
31205,2022,64625
31205,2023,66216
31205,2024,67557
31205,2025,68671
31206,2016,37235
31206,2017,38654
31206,2018,40072
31206,2019,41480
31206,2020,42907
31206,2021,44240
31206,2022,45465
31206,2023,46722
31206,2024,47845
31206,2025,48843
31301,2016,20970
31301,2017,21060
31301,2018,21138
31301,2019,21200
31301,2020,21256
31301,2021,21247
31301,2022,21169
31301,2023,21082
31301,2024,20907
31301,2025,20677
31302,2016,5815
31302,2017,5820
31302,2018,5809
31302,2019,5786
31302,2020,5756
31302,2021,5707
31302,2022,5643
31302,2023,5584
31302,2024,5513
31302,2025,5441
31303,2016,3556
31303,2017,3576
31303,2018,3605
31303,2019,3638
31303,2020,3674
31303,2021,3699
31303,2022,3710
31303,2023,3713
31303,2024,3690
31303,2025,3649
31304,2016,7824
31304,2017,7948
31304,2018,8057
31304,2019,8154
31304,2020,8245
31304,2021,8313
31304,2022,8359
31304,2023,8413
31304,2024,8446
31304,2025,8472
31401,2016,32219
31401,2017,32917
31401,2018,33609
31401,2019,34285
31401,2020,34967
31401,2021,35559
31401,2022,36052
31401,2023,36551
31401,2024,36923
31401,2025,37196
31402,2016,8906
31402,2017,8983
31402,2018,9035
31402,2019,9067
31402,2020,9091
31402,2021,9088
31402,2022,9064
31402,2023,9051
31402,2024,9025
31402,2025,8995
31403,2016,14367
31403,2017,14387
31403,2018,14379
31403,2019,14345
31403,2020,14296
31403,2021,14200
31403,2022,14057
31403,2023,13916
31403,2024,13728
31403,2025,13524
31404,2016,1390
31404,2017,1392
31404,2018,1391
31404,2019,1388
31404,2020,1384
31404,2021,1376
31404,2022,1367
31404,2023,1360
31404,2024,1351
31404,2025,1343
31405,2016,3223
31405,2017,3329
31405,2018,3434
31405,2019,3537
31405,2020,3641
31405,2021,3737
31405,2022,3824
31405,2023,3915
31405,2024,3996
31405,2025,4069
31501,2016,7830
31501,2017,7950
31501,2018,8081
31501,2019,8216
31501,2020,8356
31501,2021,8475
31501,2022,8565
31501,2023,8647
31501,2024,8685
31501,2025,8686
31601,2016,21962
31601,2017,21802
31601,2018,21592
31601,2019,21337
31601,2020,21061
31601,2021,20715
31601,2022,20308
31601,2023,19910
31601,2024,19458
31601,2025,19004
31602,2016,23933
31602,2017,24593
31602,2018,25229
31602,2019,25841
31602,2020,26453
31602,2021,27001
31602,2022,27484
31602,2023,27993
31602,2024,28434
31602,2025,28824
40101,2016,293419
40101,2017,297672
40101,2018,301349
40101,2019,304501
40101,2020,307451
40101,2021,309498
40101,2022,310720
40101,2023,312184
40101,2024,312889
40101,2025,313210
40102,2016,24693
40102,2017,24779
40102,2018,24815
40102,2019,24803
40102,2020,24767
40102,2021,24650
40102,2022,24458
40102,2023,24274
40102,2024,24017
40102,2025,23730
40103,2016,9781
40103,2017,9940
40103,2018,10074
40103,2019,10184
40103,2020,10285
40103,2021,10355
40103,2022,10397
40103,2023,10450
40103,2024,10482
40103,2025,10507
40104,2016,14198
40104,2017,14527
40104,2018,14870
40104,2019,15218
40104,2020,15572
40104,2021,15881
40104,2022,16134
40104,2023,16369
40104,2024,16519
40104,2025,16598
40201,2016,32479
40201,2017,33209
40201,2018,33917
40201,2019,34595
40201,2020,35267
40201,2021,35839
40201,2022,36305
40201,2023,36775
40201,2024,37115
40201,2025,37354
40202,2016,4604
40202,2017,4726
40202,2018,4844
40202,2019,4959
40202,2020,5073
40202,2021,5173
40202,2022,5257
40202,2023,5340
40202,2024,5405
40202,2025,5452
40301,2016,11476
40301,2017,12190
40301,2018,12933
40301,2019,13686
40301,2020,14442
40301,2021,15145
40301,2022,15774
40301,2023,16358
40301,2024,16824
40301,2025,17161
40302,2016,2138
40302,2017,2210
40302,2018,2282
40302,2019,2354
40302,2020,2424
40302,2021,2486
40302,2022,2538
40302,2023,2587
40302,2024,2622
40302,2025,2647
40401,2016,4943
40401,2017,5203
40401,2018,5484
40401,2019,5775
40401,2020,6072
40401,2021,6349
40401,2022,6595
40401,2023,6819
40401,2024,6988
40401,2025,7100
40402,2016,5740
40402,2017,5819
40402,2018,5888
40402,2019,5947
40402,2020,6002
40402,2021,6038
40402,2022,6058
40402,2023,6081
40402,2024,6087
40402,2025,6087
40501,2016,1772
40501,2017,2084
40501,2018,2417
40501,2019,2761
40501,2020,3105
40501,2021,3430
40501,2022,3723
40501,2023,3981
40501,2024,4181
40501,2025,4313
40502,2016,4725
40502,2017,4738
40502,2018,4720
40502,2019,4680
40502,2020,4629
40502,2021,4567
40502,2022,4503
40502,2023,4463
40502,2024,4439
40502,2025,4441
40503,2016,2067
40503,2017,2032
40503,2018,1982
40503,2019,1922
40503,2020,1858
40503,2021,1787
40503,2022,1716
40503,2023,1654
40503,2024,1599
40503,2025,1559
40504,2016,660
40504,2017,699
40504,2018,738
40504,2019,775
40504,2020,811
40504,2021,843
40504,2022,870
40504,2023,896
40504,2024,915
40504,2025,930
40505,2016,3265
40505,2017,3386
40505,2018,3499
40505,2019,3605
40505,2020,3708
40505,2021,3800
40505,2022,3885
40505,2023,3974
40505,2024,4058
40505,2025,4134
40601,2016,8513
40601,2017,8694
40601,2018,8866
40601,2019,9029
40601,2020,9190
40601,2021,9326
40601,2022,9434
40601,2023,9546
40601,2024,9628
40601,2025,9683
40602,2016,5909
40602,2017,5754
40602,2018,5580
40602,2019,5392
40602,2020,5202
40602,2021,5006
40602,2022,4810
40602,2023,4639
40602,2024,4483
40602,2025,4355
40603,2016,3542
40603,2017,3592
40603,2018,3642
40603,2019,3691
40603,2020,3740
40603,2021,3780
40603,2022,3807
40603,2023,3834
40603,2024,3845
40603,2025,3843
40701,2016,25445
40701,2017,25086
40701,2018,24628
40701,2019,24097
40701,2020,23540
40701,2021,22926
40701,2022,22286
40701,2023,21722
40701,2024,21186
40701,2025,20733
40702,2016,5451
40702,2017,5592
40702,2018,5732
40702,2019,5869
40702,2020,6006
40702,2021,6127
40702,2022,6229
40702,2023,6331
40702,2024,6409
40702,2025,6466
40801,2016,13776
40801,2017,14214
40801,2018,14641
40801,2019,15053
40801,2020,15460
40801,2021,15820
40801,2022,16129
40801,2023,16435
40801,2024,16679
40801,2025,16868
40802,2016,3208
40802,2017,3242
40802,2018,3275
40802,2019,3305
40802,2020,3334
40802,2021,3352
40802,2022,3359
40802,2023,3365
40802,2024,3356
40802,2025,3338
40901,2016,10568
40901,2017,11343
40901,2018,12130
40901,2019,12914
40901,2020,13693
40901,2021,14416
40901,2022,15068
40901,2023,15681
40901,2024,16189
40901,2025,16576
40902,2016,1089
40902,2017,1139
40902,2018,1189
40902,2019,1239
40902,2020,1290
40902,2021,1338
40902,2022,1383
40902,2023,1428
40902,2024,1469
40902,2025,1502
40903,2016,2227
40903,2017,2271
40903,2018,2314
40903,2019,2354
40903,2020,2392
40903,2021,2422
40903,2022,2443
40903,2023,2462
40903,2024,2470
40903,2025,2471
41001,2016,11403
41001,2017,11637
41001,2018,11854
41001,2019,12056
41001,2020,12254
41001,2021,12417
41001,2022,12547
41001,2023,12683
41001,2024,12785
41001,2025,12861
41101,2016,5604
41101,2017,5636
41101,2018,5664
41101,2019,5686
41101,2020,5704
41101,2021,5703
41101,2022,5682
41101,2023,5657
41101,2024,5607
41101,2025,5541
41201,2016,5829
41201,2017,5955
41201,2018,6077
41201,2019,6194
41201,2020,6310
41201,2021,6408
41201,2022,6485
41201,2023,6562
41201,2024,6614
41201,2025,6647
41202,2016,2402
41202,2017,2506
41202,2018,2612
41202,2019,2718
41202,2020,2826
41202,2021,2928
41202,2022,3022
41202,2023,3116
41202,2024,3198
41202,2025,3261
41301,2016,6158
41301,2017,6284
41301,2018,6407
41301,2019,6526
41301,2020,6644
41301,2021,6743
41301,2022,6822
41301,2023,6901
41301,2024,6955
41301,2025,6986
41401,2016,14389
41401,2017,14471
41401,2018,14513
41401,2019,14521
41401,2020,14513
41401,2021,14460
41401,2022,14367
41401,2023,14289
41401,2024,14182
41401,2025,14067
41501,2016,739
41501,2017,818
41501,2018,900
41501,2019,983
41501,2020,1065
41501,2021,1141
41501,2022,1209
41501,2023,1270
41501,2024,1317
41501,2025,1353
41502,2016,892
41502,2017,935
41502,2018,977
41502,2019,1017
41502,2020,1058
41502,2021,1095
41502,2022,1129
41502,2023,1162
41502,2024,1191
41502,2025,1214
41503,2016,981
41503,2017,1008
41503,2018,1033
41503,2019,1056
41503,2020,1079
41503,2021,1102
41503,2022,1124
41503,2023,1151
41503,2024,1179
41503,2025,1206
41601,2016,5920
41601,2017,5988
41601,2018,6056
41601,2019,6121
41601,2020,6187
41601,2021,6236
41601,2022,6264
41601,2023,6291
41601,2024,6292
41601,2025,6271
50101,2016,211612
50101,2017,214659
50101,2018,217365
50101,2019,219794
50101,2020,222208
50101,2021,224130
50101,2022,225644
50101,2023,227552
50101,2024,229153
50101,2025,230710
50102,2016,29542
50102,2017,29783
50102,2018,29967
50102,2019,30106
50102,2020,30236
50102,2021,30294
50102,2022,30294
50102,2023,30346
50102,2024,30358
50102,2025,30374
50103,2016,10292
50103,2017,10624
50103,2018,10964
50103,2019,11306
50103,2020,11657
50103,2021,11981
50103,2022,12273
50103,2023,12568
50103,2024,12816
50103,2025,13024
50104,2016,3058
50104,2017,3101
50104,2018,3138
50104,2019,3170
50104,2020,3201
50104,2021,3223
50104,2022,3238
50104,2023,3258
50104,2024,3272
50104,2025,3285
50201,2016,23705
50201,2017,23845
50201,2018,23941
50201,2019,24001
50201,2020,24053
50201,2021,24045
50201,2022,23986
50201,2023,23961
50201,2024,23895
50201,2025,23829
50202,2016,17191
50202,2017,17235
50202,2018,17246
50202,2019,17229
50202,2020,17204
50202,2021,17134
50202,2022,17026
50202,2023,16941
50202,2024,16826
50202,2025,16711
50203,2016,43890
50203,2017,44066
50203,2018,44174
50203,2019,44223
50203,2020,44263
50203,2021,44196
50203,2022,44037
50203,2023,43938
50203,2024,43761
50203,2025,43566
50204,2016,8622
50204,2017,8659
50204,2018,8679
50204,2019,8684
50204,2020,8689
50204,2021,8674
50204,2022,8647
50204,2023,8638
50204,2024,8624
50204,2025,8612
50301,2016,35244
50301,2017,35165
50301,2018,35057
50301,2019,34918
50301,2020,34778
50301,2021,34549
50301,2022,34231
50301,2023,33935
50301,2024,33539
50301,2025,33112
50302,2016,10376
50302,2017,10354
50302,2018,10315
50302,2019,10260
50302,2020,10200
50302,2021,10112
50302,2022,9998
50302,2023,9892
50302,2024,9763
50302,2025,9630
50303,2016,12960
50303,2017,13217
50303,2018,13491
50303,2019,13775
50303,2020,14076
50303,2021,14351
50303,2022,14593
50303,2023,14843
50303,2024,15041
50303,2025,15194
50401,2016,16491
50401,2017,16642
50401,2018,16774
50401,2019,16889
50401,2020,17003
50401,2021,17075
50401,2022,17107
50401,2023,17156
50401,2024,17165
50401,2025,17162
50402,2016,21437
50402,2017,21075
50402,2018,20660
50402,2019,20206
50402,2020,19745
50402,2021,19246
50402,2022,18726
50402,2023,18264
50402,2024,17809
50402,2025,17411
50403,2016,29685
50403,2017,30343
50403,2018,30974
50403,2019,31579
50403,2020,32190
50403,2021,32731
50403,2022,33205
50403,2023,33721
50403,2024,34169
50403,2025,34577
50404,2016,16479
50404,2017,16373
50404,2018,16261
50404,2019,16140
50404,2020,16024
50404,2021,15872
50404,2022,15682
50404,2023,15505
50404,2024,15284
50404,2025,15047
50405,2016,21511
50405,2017,21668
50405,2018,21791
50405,2019,21888
50405,2020,21981
50405,2021,22023
50405,2022,22021
50405,2023,22051
50405,2024,22043
50405,2025,22027
50501,2016,31579
50501,2017,31531
50501,2018,31422
50501,2019,31261
50501,2020,31084
50501,2021,30825
50501,2022,30497
50501,2023,30209
50501,2024,29867
50501,2025,29529
50502,2016,11484
50502,2017,11503
50502,2018,11509
50502,2019,11500
50502,2020,11491
50502,2021,11452
50502,2022,11387
50502,2023,11333
50502,2024,11254
50502,2025,11165
50601,2016,33685
50601,2017,33555
50601,2018,33310
50601,2019,32975
50601,2020,32605
50601,2021,32144
50601,2022,31625
50601,2023,31178
50601,2024,30724
50601,2025,30331
50602,2016,11117
50602,2017,11112
50602,2018,11097
50602,2019,11073
50602,2020,11046
50602,2021,10990
50602,2022,10904
50602,2023,10822
50602,2024,10705
50602,2025,10575
50701,2016,20265
50701,2017,20102
50701,2018,19894
50701,2019,19651
50701,2020,19398
50701,2021,19094
50701,2022,18752
50701,2023,18440
50701,2024,18104
50701,2025,17788
50702,2016,9142
50702,2017,9152
50702,2018,9154
50702,2019,9146
50702,2020,9136
50702,2021,9100
50702,2022,9038
50702,2023,8978
50702,2024,8889
50702,2025,8790
50801,2016,48080
50801,2017,48301
50801,2018,48430
50801,2019,48482
50801,2020,48512
50801,2021,48417
50801,2022,48217
50801,2023,48082
50801,2024,47863
50801,2025,47641
50802,2016,11638
50802,2017,11519
50802,2018,11364
50802,2019,11181
50802,2020,10988
50802,2021,10768
50802,2022,10532
50802,2023,10323
50802,2024,10114
50802,2025,9929
50901,2016,13960
50901,2017,13982
50901,2018,13961
50901,2019,13906
50901,2020,13839
50901,2021,13733
50901,2022,13601
50901,2023,13497
50901,2024,13382
50901,2025,13287
50902,2016,1245
50902,2017,1295
50902,2018,1346
50902,2019,1399
50902,2020,1456
50902,2021,1513
50902,2022,1569
50902,2023,1630
50902,2024,1690
50902,2025,1743
51001,2016,3580
51001,2017,3570
51001,2018,3548
51001,2019,3517
51001,2020,3482
51001,2021,3439
51001,2022,3391
51001,2023,3350
51001,2024,3310
51001,2025,3277
51002,2016,1253
51002,2017,1244
51002,2018,1228
51002,2019,1208
51002,2020,1185
51002,2021,1158
51002,2022,1129
51002,2023,1104
51002,2024,1079
51002,2025,1061
51003,2016,2383
51003,2017,2357
51003,2018,2321
51003,2019,2279
51003,2020,2233
51003,2021,2182
51003,2022,2128
51003,2023,2081
51003,2024,2038
51003,2025,2003
51101,2016,21659
51101,2017,21164
51101,2018,20628
51101,2019,20066
51101,2020,19509
51101,2021,18923
51101,2022,18326
51101,2023,17792
51101,2024,17274
51101,2025,16825
51102,2016,13040
51102,2017,13160
51102,2018,13267
51102,2019,13361
51102,2020,13456
51102,2021,13521
51102,2022,13557
51102,2023,13609
51102,2024,13633
51102,2025,13645
51103,2016,16306
51103,2017,16218
51103,2018,16113
51103,2019,15990
51103,2020,15866
51103,2021,15701
51103,2022,15499
51103,2023,15310
51103,2024,15081
51103,2025,14844
51201,2016,33431
51201,2017,34035
51201,2018,34566
51201,2019,35039
51201,2020,35502
51201,2021,35886
51201,2022,36211
51201,2023,36612
51201,2024,36984
51201,2025,37366
51202,2016,9188
51202,2017,9116
51202,2018,9022
51202,2019,8911
51202,2020,8796
51202,2021,8660
51202,2022,8512
51202,2023,8383
51202,2024,8251
51202,2025,8131
51203,2016,12689
51203,2017,13069
51203,2018,13427
51203,2019,13769
51203,2020,14110
51203,2021,14422
51203,2022,14710
51203,2023,15025
51203,2024,15322
51203,2025,15604
51204,2016,6339
51204,2017,6347
51204,2018,6339
51204,2019,6318
51204,2020,6294
51204,2021,6253
51204,2022,6200
51204,2023,6159
51204,2024,6112
51204,2025,6071
51301,2016,4230
51301,2017,4192
51301,2018,4152
51301,2019,4111
51301,2020,4071
51301,2021,4024
51301,2022,3968
51301,2023,3917
51301,2024,3856
51301,2025,3793
51302,2016,6128
51302,2017,5977
51302,2018,5806
51302,2019,5622
51302,2020,5437
51302,2021,5242
51302,2022,5047
51302,2023,4875
51302,2024,4715
51302,2025,4584
51401,2016,4492
51401,2017,4509
51401,2018,4511
51401,2019,4503
51401,2020,4491
51401,2021,4467
51401,2022,4437
51401,2023,4416
51401,2024,4395
51401,2025,4381
51402,2016,1723
51402,2017,1713
51402,2018,1704
51402,2019,1695
51402,2020,1688
51402,2021,1677
51402,2022,1662
51402,2023,1647
51402,2024,1626
51402,2025,1602
51501,2016,49092
51501,2017,49632
51501,2018,50094
51501,2019,50490
51501,2020,50878
51501,2021,51148
51501,2022,51317
51501,2023,51565
51501,2024,51732
51501,2025,51888
51601,2016,1885
51601,2017,1932
51601,2018,1980
51601,2019,2029
51601,2020,2078
51601,2021,2122
51601,2022,2160
51601,2023,2198
51601,2024,2226
51601,2025,2248
60101,2016,230705
60101,2017,235146
60101,2018,239101
60101,2019,242551
60101,2020,245694
60101,2021,247910
60101,2022,249195
60101,2023,250344
60101,2024,250495
60101,2025,250057
60201,2016,19727
60201,2017,19774
60201,2018,19790
60201,2019,19768
60201,2020,19719
60201,2021,19587
60201,2022,19367
60201,2023,19115
60201,2024,18758
60201,2025,18344
60202,2016,37516
60202,2017,37981
60202,2018,38400
60202,2019,38757
60202,2020,39070
60202,2021,39226
60202,2022,39211
60202,2023,39135
60202,2024,38847
60202,2025,38427
60301,2016,100453
60301,2017,101584
60301,2018,102539
60301,2019,103292
60301,2020,103909
60301,2021,104104
60301,2022,103861
60301,2023,103488
60301,2024,102606
60301,2025,101432
60302,2016,16954
60302,2017,17076
60302,2018,17125
60302,2019,17109
60302,2020,17053
60302,2021,16927
60302,2022,16744
60302,2023,16569
60302,2024,16358
60302,2025,16151
60303,2016,45186
60303,2017,46021
60303,2018,46711
60303,2019,47268
60303,2020,47748
60303,2021,48046
60303,2022,48182
60303,2023,48328
60303,2024,48335
60303,2025,48291
60401,2016,16235
60401,2017,16441
60401,2018,16613
60401,2019,16748
60401,2020,16859
60401,2021,16903
60401,2022,16880
60401,2023,16841
60401,2024,16728
60401,2025,16574
60402,2016,5723
60402,2017,5691
60402,2018,5642
60402,2019,5577
60402,2020,5502
60402,2021,5404
60402,2022,5283
60402,2023,5158
60402,2024,5013
60402,2025,4861
60501,2016,27085
60501,2017,27833
60501,2018,28575
60501,2019,29293
60501,2020,29998
60501,2021,30598
60501,2022,31078
60501,2023,31520
60501,2024,31800
60501,2025,31954
60502,2016,11754
60502,2017,11653
60502,2018,11516
60502,2019,11346
60502,2020,11156
60502,2021,10919
60502,2022,10642
60502,2023,10362
60502,2024,10045
60502,2025,9723
60601,2016,23457
60601,2017,23503
60601,2018,23484
60601,2019,23402
60601,2020,23277
60601,2021,23053
60601,2022,22735
60601,2023,22398
60601,2024,21965
60601,2025,21496
70101,2016,1589815
70101,2017,1610316
70101,2018,1628034
70101,2019,1643322
70101,2020,1658020
70101,2021,1668431
70101,2022,1675059
70101,2023,1683733
70101,2024,1689137
70101,2025,1693583
70102,2016,71625
70102,2017,76531
70102,2018,81604
70102,2019,86776
70102,2020,92058
70102,2021,97154
70102,2022,101977
70102,2023,106780
70102,2024,111138
70102,2025,114964
70103,2016,18238
70103,2017,19005
70103,2018,19782
70103,2019,20562
70103,2020,21357
70103,2021,22107
70103,2022,22806
70103,2023,23517
70103,2024,24158
70103,2025,24728
70104,2016,111040
70104,2017,116694
70104,2018,122302
70104,2019,127858
70104,2020,133479
70104,2021,138836
70104,2022,143918
70104,2023,149188
70104,2024,154152
70104,2025,158768
70105,2016,54790
70105,2017,55523
70105,2018,56156
70105,2019,56701
70105,2020,57225
70105,2021,57602
70105,2022,57851
70105,2023,58175
70105,2024,58393
70105,2025,58586
70201,2016,117872
70201,2017,123213
70201,2018,128462
70201,2019,133628
70201,2020,138843
70201,2021,143777
70201,2022,148436
70201,2023,153311
70201,2024,157905
70201,2025,162215
70202,2016,12864
70202,2017,12737
70202,2018,12576
70202,2019,12390
70202,2020,12194
70202,2021,11964
70202,2022,11710
70202,2023,11476
70202,2024,11228
70202,2025,10996
70301,2016,59370
70301,2017,60868
70301,2018,62340
70301,2019,63786
70301,2020,65260
70301,2021,66600
70301,2022,67802
70301,2023,69085
70301,2024,70212
70301,2025,71231
70302,2016,12893
70302,2017,13281
70302,2018,13679
70302,2019,14084
70302,2020,14502
70302,2021,14893
70302,2022,15250
70302,2023,15617
70302,2024,15936
70302,2025,16211
70303,2016,7158
70303,2017,7421
70303,2018,7691
70303,2019,7964
70303,2020,8244
70303,2021,8510
70303,2022,8757
70303,2023,9011
70303,2024,9239
70303,2025,9442
70401,2016,13741
70401,2017,13847
70401,2018,13951
70401,2019,14049
70401,2020,14153
70401,2021,14226
70401,2022,14264
70401,2023,14314
70401,2024,14321
70401,2025,14309
70402,2016,21263
70402,2017,21218
70402,2018,21116
70402,2019,20969
70402,2020,20808
70402,2021,20593
70402,2022,20341
70402,2023,20130
70402,2024,19906
70402,2025,19707
70403,2016,56507
70403,2017,57329
70403,2018,58003
70403,2019,58561
70403,2020,59089
70403,2021,59475
70403,2022,59762
70403,2023,60178
70403,2024,60558
70403,2025,60979
70404,2016,9923
70404,2017,10038
70404,2018,10151
70404,2019,10263
70404,2020,10381
70404,2021,10477
70404,2022,10551
70404,2023,10636
70404,2024,10692
70404,2025,10734
70501,2016,34128
70501,2017,35314
70501,2018,36465
70501,2019,37587
70501,2020,38721
70501,2021,39779
70501,2022,40767
70501,2023,41824
70501,2024,42818
70501,2025,43754
70502,2016,44375
70502,2017,46013
70502,2018,47666
70502,2019,49321
70502,2020,51012
70502,2021,52601
70502,2022,54072
70502,2023,55588
70502,2024,56949
70502,2025,58164
70503,2016,17239
70503,2017,17571
70503,2018,17908
70503,2019,18245
70503,2020,18594
70503,2021,18903
70503,2022,19166
70503,2023,19440
70503,2024,19652
70503,2025,19821
70601,2016,19669
70601,2017,19994
70601,2018,20310
70601,2019,20614
70601,2020,20921
70601,2021,21177
70601,2022,21378
70601,2023,21592
70601,2024,21739
70601,2025,21849
70602,2016,20381
70602,2017,20332
70602,2018,20220
70602,2019,20060
70602,2020,19881
70602,2021,19649
70602,2022,19381
70602,2023,19155
70602,2024,18918
70602,2025,18713
70603,2016,6391
70603,2017,6405
70603,2018,6415
70603,2019,6419
70603,2020,6422
70603,2021,6410
70603,2022,6380
70603,2023,6355
70603,2024,6310
70603,2025,6259
70701,2016,5800
70701,2017,5867
70701,2018,5934
70701,2019,5998
70701,2020,6066
70701,2021,6120
70701,2022,6160
70701,2023,6205
70701,2024,6234
70701,2025,6254
70702,2016,35948
70702,2017,36608
70702,2018,37223
70702,2019,37800
70702,2020,38383
70702,2021,38883
70702,2022,39312
70702,2023,39804
70702,2024,40233
70702,2025,40635
70703,2016,29666
70703,2017,30338
70703,2018,31002
70703,2019,31655
70703,2020,32323
70703,2021,32924
70703,2022,33454
70703,2023,34019
70703,2024,34501
70703,2025,34928
70704,2016,5525
70704,2017,5566
70704,2018,5590
70704,2019,5602
70704,2020,5611
70704,2021,5607
70704,2022,5594
70704,2023,5595
70704,2024,5596
70704,2025,5607
70705,2016,13513
70705,2017,13748
70705,2018,13979
70705,2019,14204
70705,2020,14434
70705,2021,14629
70705,2022,14788
70705,2023,14955
70705,2024,15077
70705,2025,15168
70706,2016,35660
70706,2017,35622
70706,2018,35516
70706,2019,35352
70706,2020,35174
70706,2021,34906
70706,2022,34565
70706,2023,34273
70706,2024,33926
70706,2025,33593
70707,2016,5567
70707,2017,5628
70707,2018,5681
70707,2019,5726
70707,2020,5771
70707,2021,5803
70707,2022,5825
70707,2023,5857
70707,2024,5883
70707,2025,5910
70801,2016,18232
70801,2017,18289
70801,2018,18327
70801,2019,18347
70801,2020,18365
70801,2021,18337
70801,2022,18267
70801,2023,18212
70801,2024,18113
70801,2025,18004
70802,2016,2306
70802,2017,2334
70802,2018,2360
70802,2019,2386
70802,2020,2413
70802,2021,2434
70802,2022,2449
70802,2023,2465
70802,2024,2474
70802,2025,2480
70803,2016,2879
70803,2017,2888
70803,2018,2899
70803,2019,2912
70803,2020,2927
70803,2021,2937
70803,2022,2939
70803,2023,2941
70803,2024,2932
70803,2025,2915
70804,2016,2368
70804,2017,2316
70804,2018,2259
70804,2019,2200
70804,2020,2140
70804,2021,2078
70804,2022,2014
70804,2023,1958
70804,2024,1903
70804,2025,1853
70805,2016,2139
70805,2017,2137
70805,2018,2138
70805,2019,2139
70805,2020,2142
70805,2021,2141
70805,2022,2135
70805,2023,2129
70805,2024,2116
70805,2025,2099
70901,2016,11442
70901,2017,11605
70901,2018,11762
70901,2019,11910
70901,2020,12060
70901,2021,12179
70901,2022,12266
70901,2023,12360
70901,2024,12416
70901,2025,12451
70902,2016,9926
70902,2017,9999
70902,2018,10056
70902,2019,10101
70902,2020,10144
70902,2021,10163
70902,2022,10161
70902,2023,10174
70902,2024,10168
70902,2025,10160
70903,2016,11494
70903,2017,11756
70903,2018,12010
70903,2019,12254
70903,2020,12502
70903,2021,12724
70903,2022,12922
70903,2023,13138
70903,2024,13328
70903,2025,13505
70904,2016,3254
70904,2017,3269
70904,2018,3275
70904,2019,3274
70904,2020,3270
70904,2021,3260
70904,2022,3246
70904,2023,3241
70904,2024,3237
70904,2025,3238
71001,2016,121836
71001,2017,123773
71001,2018,125498
71001,2019,127049
71001,2020,128576
71001,2021,129802
71001,2022,130774
71001,2023,131956
71001,2024,132942
71001,2025,133885
71002,2016,15379
71002,2017,15647
71002,2018,15938
71002,2019,16239
71002,2020,16555
71002,2021,16830
71002,2022,17054
71002,2023,17266
71002,2024,17397
71002,2025,17462
71003,2016,24808
71003,2017,24865
71003,2018,24872
71003,2019,24837
71003,2020,24792
71003,2021,24685
71003,2022,24529
71003,2023,24414
71003,2024,24267
71003,2025,24132
71004,2016,16167
71004,2017,16169
71004,2018,16122
71004,2019,16037
71004,2020,15937
71004,2021,15795
71004,2022,15626
71004,2023,15490
71004,2024,15348
71004,2025,15231
71005,2016,19691
71005,2017,19394
71005,2018,19013
71005,2019,18568
71005,2020,18097
71005,2021,17576
71005,2022,17031
71005,2023,16539
71005,2024,16067
71005,2025,15661
71101,2016,22693
71101,2017,23793
71101,2018,24926
71101,2019,26076
71101,2020,27254
71101,2021,28379
71101,2022,29432
71101,2023,30492
71101,2024,31444
71101,2025,32278
71102,2016,14837
71102,2017,14991
71102,2018,15123
71102,2019,15237
71102,2020,15351
71102,2021,15431
71102,2022,15482
71102,2023,15558
71102,2024,15609
71102,2025,15659
71104,2016,50320
71104,2017,50242
71104,2018,50021
71104,2019,49688
71104,2020,49316
71104,2021,48813
71104,2022,48220
71104,2023,47724
71104,2024,47194
71104,2025,46731
71105,2016,6847
71105,2017,6858
71105,2018,6860
71105,2019,6854
71105,2020,6846
71105,2021,6820
71105,2022,6777
71105,2023,6740
71105,2024,6686
71105,2025,6628
71103,2016,8305
71103,2017,8430
71103,2018,8540
71103,2019,8636
71103,2020,8730
71103,2021,8800
71103,2022,8850
71103,2023,8910
71103,2024,8953
71103,2025,8990
71106,2016,25112
71106,2017,25413
71106,2018,25666
71106,2019,25878
71106,2020,26081
71106,2021,26217
71106,2022,26296
71106,2023,26413
71106,2024,26485
71106,2025,26543
71201,2016,15586
71201,2017,15709
71201,2018,15814
71201,2019,15904
71201,2020,15995
71201,2021,16050
71201,2022,16072
71201,2023,16117
71201,2024,16130
71201,2025,16135
71301,2016,17193
71301,2017,17356
71301,2018,17503
71301,2019,17636
71301,2020,17771
71301,2021,17865
71301,2022,17921
71301,2023,17998
71301,2024,18034
71301,2025,18056
71302,2016,7677
71302,2017,7577
71302,2018,7442
71302,2019,7282
71302,2020,7111
71302,2021,6920
71302,2022,6720
71302,2023,6541
71302,2024,6371
71302,2025,6226
71401,2016,20815
71401,2017,20662
71401,2018,20433
71401,2019,20144
71401,2020,19830
71401,2021,19459
71401,2022,19052
71401,2023,18691
71401,2024,18328
71401,2025,18014
71402,2016,18175
71402,2017,18343
71402,2018,18474
71402,2019,18578
71402,2020,18676
71402,2021,18732
71402,2022,18754
71402,2023,18812
71402,2024,18848
71402,2025,18895
71403,2016,7349
71403,2017,7593
71403,2018,7838
71403,2019,8083
71403,2020,8334
71403,2021,8569
71403,2022,8786
71403,2023,9012
71403,2024,9216
71403,2025,9399
71501,2016,30895
71501,2017,31612
71501,2018,32275
71501,2019,32897
71501,2020,33519
71501,2021,34071
71501,2022,34567
71501,2023,35132
71501,2024,35660
71501,2025,36182
71502,2016,7422
71502,2017,7405
71502,2018,7368
71502,2019,7315
71502,2020,7255
71502,2021,7175
71502,2022,7077
71502,2023,6990
71502,2024,6892
71502,2025,6803
71503,2016,16090
71503,2017,16407
71503,2018,16690
71503,2019,16947
71503,2020,17204
71503,2021,17426
71503,2022,17624
71503,2023,17864
71503,2024,18097
71503,2025,18336
80101,2016,119359
80101,2017,121646
80101,2018,123791
80101,2019,125821
80101,2020,127876
80101,2021,129671
80101,2022,131237
80101,2023,133029
80101,2024,134627
80101,2025,136139
80102,2016,6387
80102,2017,6656
80102,2018,6917
80102,2019,7170
80102,2020,7421
80102,2021,7652
80102,2022,7863
80102,2023,8078
80102,2024,8270
80102,2025,8441
80201,2016,99657
80201,2017,101848
80201,2018,104014
80201,2019,106146
80201,2020,108332
80201,2021,110296
80201,2022,112029
80201,2023,113882
80201,2024,115460
80201,2025,116844
80202,2016,44233
80202,2017,44342
80202,2018,44407
80202,2019,44437
80202,2020,44478
80202,2021,44428
80202,2022,44300
80202,2023,44243
80202,2024,44114
80202,2025,43984
80301,2016,13749
80301,2017,13617
80301,2018,13444
80301,2019,13239
80301,2020,13025
80301,2021,12781
80301,2022,12520
80301,2023,12295
80301,2024,12075
80301,2025,11887
80302,2016,44749
80302,2017,45318
80302,2018,45835
80302,2019,46305
80302,2020,46778
80302,2021,47145
80302,2022,47417
80302,2023,47755
80302,2024,48004
80302,2025,48215
80303,2016,10396
80303,2017,10569
80303,2018,10743
80303,2019,10917
80303,2020,11097
80303,2021,11255
80303,2022,11389
80303,2023,11534
80303,2024,11650
80303,2025,11745
80304,2016,21156
80304,2017,21400
80304,2018,21596
80304,2019,21755
80304,2020,21907
80304,2021,22007
80304,2022,22070
80304,2023,22177
80304,2024,22265
80304,2025,22359
80401,2016,17841
80401,2017,17963
80401,2018,18116
80401,2019,18290
80401,2020,18491
80401,2021,18660
80401,2022,18785
80401,2023,18915
80401,2024,18971
80401,2025,18974
80402,2016,7047
80402,2017,7211
80402,2018,7385
80402,2019,7564
80402,2020,7753
80402,2021,7929
80402,2022,8086
80402,2023,8249
80402,2024,8384
80402,2025,8491
80501,2016,20869
80501,2017,21145
80501,2018,21418
80501,2019,21686
80501,2020,21961
80501,2021,22184
80501,2022,22350
80501,2023,22526
80501,2024,22630
80501,2025,22685
80601,2016,4146
80601,2017,4200
80601,2018,4253
80601,2019,4305
80601,2020,4358
80601,2021,4400
80601,2022,4430
80601,2023,4461
80601,2024,4477
80601,2025,4483
80602,2016,14028
80602,2017,14345
80602,2018,14659
80602,2019,14967
80602,2020,15282
80602,2021,15564
80602,2022,15812
80602,2023,16076
80602,2024,16298
80602,2025,16493
80701,2016,7381
80701,2017,7386
80701,2018,7372
80701,2019,7345
80701,2020,7316
80701,2021,7271
80701,2022,7217
80701,2023,7182
80701,2024,7146
80701,2025,7122
80702,2016,5254
80702,2017,5310
80702,2018,5374
80702,2019,5444
80702,2020,5522
80702,2021,5591
80702,2022,5649
80702,2023,5710
80702,2024,5752
80702,2025,5776
80703,2016,1022
80703,2017,1038
80703,2018,1054
80703,2019,1071
80703,2020,1089
80703,2021,1105
80703,2022,1118
80703,2023,1132
80703,2024,1143
80703,2025,1153
80801,2016,12448
80801,2017,12612
80801,2018,12764
80801,2019,12905
80801,2020,13046
80801,2021,13157
80801,2022,13239
80801,2023,13335
80801,2024,13402
80801,2025,13455
80802,2016,6682
80802,2017,6836
80802,2018,6991
80802,2019,7146
80802,2020,7305
80802,2021,7451
80802,2022,7581
80802,2023,7718
80802,2024,7836
80802,2025,7937
80803,2016,4492
80803,2017,4552
80803,2018,4609
80803,2019,4663
80803,2020,4718
80803,2021,4761
80803,2022,4793
80803,2023,4829
80803,2024,4854
80803,2025,4873
80502,2016,3682
80502,2017,3763
80502,2018,3836
80502,2019,3903
80502,2020,3967
80502,2021,4021
80502,2022,4066
80502,2023,4117
80502,2024,4161
80502,2025,4201
90101,2016,52269
90101,2017,53085
90101,2018,53752
90101,2019,54325
90101,2020,54912
90101,2021,55438
90101,2022,55968
90101,2023,56746
90101,2024,57643
90101,2025,58663
90102,2016,8965
90102,2017,9095
90102,2018,9198
90102,2019,9283
90102,2020,9369
90102,2021,9445
90102,2022,9522
90102,2023,9642
90102,2024,9784
90102,2025,9952
90103,2016,2425
90103,2017,2454
90103,2018,2475
90103,2019,2491
90103,2020,2506
90103,2021,2515
90103,2022,2522
90103,2023,2537
90103,2024,2553
90103,2025,2577
90104,2016,4172
90104,2017,4151
90104,2018,4112
90104,2019,4061
90104,2020,4007
90104,2021,3946
90104,2022,3885
90104,2023,3843
90104,2024,3811
90104,2025,3796
90201,2016,6946
90201,2017,7046
90201,2018,7135
90201,2019,7217
90201,2020,7305
90201,2021,7387
90201,2022,7469
90201,2023,7581
90201,2024,7705
90201,2025,7839
90202,2016,3392
90202,2017,3438
90202,2018,3473
90202,2019,3499
90202,2020,3526
90202,2021,3549
90202,2022,3575
90202,2023,3621
90202,2024,3679
90202,2025,3750
90203,2016,6754
90203,2017,6971
90203,2018,7181
90203,2019,7386
90203,2020,7595
90203,2021,7794
90203,2022,7984
90203,2023,8196
90203,2024,8404
90203,2025,8610
90301,2016,9869
90301,2017,10278
90301,2018,10678
90301,2019,11070
90301,2020,11469
90301,2021,11849
90301,2022,12212
90301,2023,12601
90301,2024,12977
90301,2025,13334
90302,2016,8832
90302,2017,9037
90302,2018,9220
90302,2019,9389
90302,2020,9562
90302,2021,9721
90302,2022,9877
90302,2023,10071
90302,2024,10277
90302,2025,10498
90303,2016,9611
90303,2017,9819
90303,2018,9991
90303,2019,10141
90303,2020,10291
90303,2021,10430
90303,2022,10572
90303,2023,10768
90303,2024,10995
90303,2025,11253
90401,2016,2651
90401,2017,2700
90401,2018,2748
90401,2019,2796
90401,2020,2847
90401,2021,2893
90401,2022,2935
90401,2023,2982
90401,2024,3024
90401,2025,3065
90402,2016,2019
90402,2017,2113
90402,2018,2206
90402,2019,2298
90402,2020,2390
90402,2021,2477
90402,2022,2558
90402,2023,2640
90402,2024,2715
90402,2025,2785
90501,2016,2172
90501,2017,2130
90501,2018,2073
90501,2019,2005
90501,2020,1933
90501,2021,1858
90501,2022,1786
90501,2023,1727
90501,2024,1682
90501,2025,1657
90502,2016,3014
90502,2017,2988
90502,2018,2944
90502,2019,2887
90502,2020,2827
90502,2021,2763
90502,2022,2703
90502,2023,2661
90502,2024,2636
90502,2025,2631
90503,2016,2429
90503,2017,2444
90503,2018,2446
90503,2019,2440
90503,2020,2432
90503,2021,2423
90503,2022,2418
90503,2023,2430
90503,2024,2456
90503,2025,2496