Showing
14 changed files
with
311 additions
and
39 deletions
This diff is collapsed. Click to expand it.
| 1 | import { writable } from 'svelte/store'; | 1 | import { writable } from 'svelte/store'; |
| 2 | 2 | ||
| 3 | // Persiste el estado de búsqueda del landing entre navegaciones | 3 | // Persiste el estado de búsqueda del landing entre navegaciones |
| 4 | -export const landingSearchMode = writable('programas'); // 'programas' | 'clasificadores' | 4 | +export const landingSearchMode = writable('todo'); // 'todo' | 'programas' | 'clasificadores' |
| 5 | export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...] | 5 | export const landingSelectedClassifiers = writable([]); // ['entidad', 'objeto', ...] |
| 6 | export const landingSearchQuery = writable(''); | 6 | export const landingSearchQuery = writable(''); | ... | ... |
src/lib/stores/mapaCache.js
0 → 100644
This diff is collapsed. Click to expand it.
| 1 | // Proxy para evitar CORS | 1 | // Proxy para evitar CORS |
| 2 | -const API_BASE = 'http://34.66.240.236/api/proyecto/search'; | 2 | +const API_BASE = 'http://34.171.18.2/api/proyecto/search'; |
| 3 | 3 | ||
| 4 | export async function GET({ url }) { | 4 | export async function GET({ url }) { |
| 5 | const q = url.searchParams.get('q') || ''; | 5 | const q = url.searchParams.get('q') || ''; |
| 6 | - const is_class = url.searchParams.get('is_class') || 'false'; | 6 | + const is_class = url.searchParams.get('is_class'); // null si no viene |
| 7 | - const mode = url.searchParams.get('mode') || 'gastos'; // 'gastos' | 'ingresos' | 7 | + const mode = url.searchParams.get('mode') || ''; |
| 8 | - const class_ = url.searchParams.get('class_') || ''; // comma-separated: entidad, objeto, finfun, acteco | 8 | + const class_ = url.searchParams.get('class_') || ''; |
| 9 | - const page = url.searchParams.get('page') || '1'; // Paginación | 9 | + const page = url.searchParams.get('page') || '1'; |
| 10 | - const per_page = url.searchParams.get('per_page') || ''; // Resultados por página | 10 | + const per_page = url.searchParams.get('per_page') || ''; |
| 11 | 11 | ||
| 12 | - // Build URL with all parameters | ||
| 13 | const apiUrl = new URL(API_BASE); | 12 | const apiUrl = new URL(API_BASE); |
| 14 | apiUrl.searchParams.set('q', q); | 13 | apiUrl.searchParams.set('q', q); |
| 15 | - apiUrl.searchParams.set('is_class', is_class); | ||
| 16 | apiUrl.searchParams.set('page', page); | 14 | apiUrl.searchParams.set('page', page); |
| 15 | + | ||
| 16 | + // Solo enviar is_class si viene explícitamente | ||
| 17 | + if (is_class !== null) { | ||
| 18 | + apiUrl.searchParams.set('is_class', is_class); | ||
| 19 | + } | ||
| 17 | if (per_page) apiUrl.searchParams.set('per_page', per_page); | 20 | if (per_page) apiUrl.searchParams.set('per_page', per_page); |
| 18 | if (mode) apiUrl.searchParams.set('mode', mode); | 21 | if (mode) apiUrl.searchParams.set('mode', mode); |
| 19 | if (class_) apiUrl.searchParams.set('class_', class_); | 22 | if (class_) apiUrl.searchParams.set('class_', class_); | ... | ... |
| ... | @@ -5,21 +5,29 @@ | ... | @@ -5,21 +5,29 @@ |
| 5 | import { supabase } from '$lib/supabase'; | 5 | import { supabase } from '$lib/supabase'; |
| 6 | 6 | ||
| 7 | let { data } = $props(); | 7 | let { data } = $props(); |
| 8 | - const entidad = data.entidad; | 8 | + let entidad = $derived(data.entidad); |
| 9 | - const isDA = data.isDA; | 9 | + let isDA = $derived(data.isDA); |
| 10 | - const nombreDA = data.nombreDA; | 10 | + let nombreDA = $derived(data.nombreDA); |
| 11 | - const nombreEntidadMadre = data.nombreEntidadMadre; | 11 | + let nombreEntidadMadre = $derived(data.nombreEntidadMadre); |
| 12 | - const codigoEntidadPadre = data.codigoEntidadPadre; | 12 | + let codigoEntidadPadre = $derived(data.codigoEntidadPadre); |
| 13 | 13 | ||
| 14 | // Datos desde el loader (Supabase) | 14 | // Datos desde el loader (Supabase) |
| 15 | let codigoSeleccionado = $derived($page.params.codigo); | 15 | let codigoSeleccionado = $derived($page.params.codigo); |
| 16 | let gestionSeleccionada = $state(data.gestionInicial); | 16 | let gestionSeleccionada = $state(data.gestionInicial); |
| 17 | - let resumenData = $state(data.resumenData); | 17 | + let resumenData = $derived(data.resumenData); |
| 18 | let distribucionesData = $state(data.distribucionesData); | 18 | let distribucionesData = $state(data.distribucionesData); |
| 19 | let cargando = $state(false); | 19 | let cargando = $state(false); |
| 20 | let distCache = $state({ [data.gestionInicial]: data.distribucionesData }); | 20 | let distCache = $state({ [data.gestionInicial]: data.distribucionesData }); |
| 21 | let cargandoDist = $state(false); | 21 | let cargandoDist = $state(false); |
| 22 | 22 | ||
| 23 | + // Resetear al cambiar de entidad | ||
| 24 | + $effect(() => { | ||
| 25 | + const g = data.gestionInicial; | ||
| 26 | + gestionSeleccionada = g; | ||
| 27 | + distribucionesData = data.distribucionesData; | ||
| 28 | + distCache = { [g]: data.distribucionesData }; | ||
| 29 | + }); | ||
| 30 | + | ||
| 23 | async function cargarDistribuciones(gestion) { | 31 | async function cargarDistribuciones(gestion) { |
| 24 | if (distCache[gestion]) { | 32 | if (distCache[gestion]) { |
| 25 | distribucionesData = distCache[gestion]; | 33 | distribucionesData = distCache[gestion]; |
| ... | @@ -148,14 +156,21 @@ | ... | @@ -148,14 +156,21 @@ |
| 148 | return years; | 156 | return years; |
| 149 | }); | 157 | }); |
| 150 | 158 | ||
| 159 | + function parseRanking(rankingStr) { | ||
| 160 | + if (!rankingStr) return null; | ||
| 161 | + const parts = String(rankingStr).split('/'); | ||
| 162 | + if (parts.length !== 2) return null; | ||
| 163 | + return { posicion: parseInt(parts[0]), total: parseInt(parts[1]) }; | ||
| 164 | + } | ||
| 165 | + | ||
| 151 | let rankingGastos = $derived(() => { | 166 | let rankingGastos = $derived(() => { |
| 152 | const row = historiaGastos.find(d => d.gestion === gestionSeleccionada); | 167 | const row = historiaGastos.find(d => d.gestion === gestionSeleccionada); |
| 153 | - return row ? { posicion: row.ranking, total: '~600' } : null; | 168 | + return row ? parseRanking(row.ranking) : null; |
| 154 | }); | 169 | }); |
| 155 | 170 | ||
| 156 | let rankingIngresos = $derived(() => { | 171 | let rankingIngresos = $derived(() => { |
| 157 | const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada); | 172 | const row = historiaIngresos.find(d => d.gestion === gestionSeleccionada); |
| 158 | - return row ? { posicion: row.ranking, total: '~600' } : null; | 173 | + return row ? parseRanking(row.ranking) : null; |
| 159 | }); | 174 | }); |
| 160 | 175 | ||
| 161 | let distFiltradas = $derived(distribucionesData); | 176 | let distFiltradas = $derived(distribucionesData); |
| ... | @@ -672,34 +687,42 @@ | ... | @@ -672,34 +687,42 @@ |
| 672 | {#if rankingGastos()} | 687 | {#if rankingGastos()} |
| 673 | {@const r = rankingGastos()} | 688 | {@const r = rankingGastos()} |
| 674 | {@const total = parseInt(r.total) || 600} | 689 | {@const total = parseInt(r.total) || 600} |
| 675 | - {@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))} | 690 | + {@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))} |
| 676 | <div class="ranking-card"> | 691 | <div class="ranking-card"> |
| 677 | - <span class="ranking-card-label">Ranking en gasto</span> | 692 | + <div class="ranking-headline"> |
| 693 | + <span class="ranking-pos-big">#{r.posicion}</span> | ||
| 694 | + <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> | ||
| 695 | + </div> | ||
| 678 | <div class="ranking-bars"> | 696 | <div class="ranking-bars"> |
| 679 | {#each Array(80) as _, i} | 697 | {#each Array(80) as _, i} |
| 680 | <div class="ranking-bar-tick"></div> | 698 | <div class="ranking-bar-tick"></div> |
| 681 | {/each} | 699 | {/each} |
| 682 | <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> | 700 | <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> |
| 683 | </div> | 701 | </div> |
| 684 | - <div class="ranking-caption"> | 702 | + <div class="ranking-extremos"> |
| 685 | - <span class="ranking-pos">#{r.posicion}</span> de {total} entidades | 703 | + <span class="ranking-extremo">Menos gasto</span> |
| 704 | + <span class="ranking-extremo">Más gasto</span> | ||
| 686 | </div> | 705 | </div> |
| 687 | </div> | 706 | </div> |
| 688 | {/if} | 707 | {/if} |
| 689 | {#if tieneIngresos && rankingIngresos()} | 708 | {#if tieneIngresos && rankingIngresos()} |
| 690 | {@const r = rankingIngresos()} | 709 | {@const r = rankingIngresos()} |
| 691 | {@const total = parseInt(r.total) || 600} | 710 | {@const total = parseInt(r.total) || 600} |
| 692 | - {@const barIdx = Math.max(0, Math.min(79, Math.round((r.posicion / total) * 80)))} | 711 | + {@const barIdx = Math.max(0, Math.min(79, 80 - Math.round((r.posicion / total) * 80)))} |
| 693 | <div class="ranking-card"> | 712 | <div class="ranking-card"> |
| 694 | - <span class="ranking-card-label">Ranking en ingresos</span> | 713 | + <div class="ranking-headline"> |
| 714 | + <span class="ranking-pos-big">#{r.posicion}</span> | ||
| 715 | + <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> | ||
| 716 | + </div> | ||
| 695 | <div class="ranking-bars"> | 717 | <div class="ranking-bars"> |
| 696 | {#each Array(80) as _, i} | 718 | {#each Array(80) as _, i} |
| 697 | <div class="ranking-bar-tick"></div> | 719 | <div class="ranking-bar-tick"></div> |
| 698 | {/each} | 720 | {/each} |
| 699 | <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> | 721 | <div class="ranking-marker" style="left: {barIdx * 1.25}%"></div> |
| 700 | </div> | 722 | </div> |
| 701 | - <div class="ranking-caption"> | 723 | + <div class="ranking-extremos"> |
| 702 | - <span class="ranking-pos">#{r.posicion}</span> de {total} entidades | 724 | + <span class="ranking-extremo">Menos ingresos</span> |
| 725 | + <span class="ranking-extremo">Más ingresos</span> | ||
| 703 | </div> | 726 | </div> |
| 704 | </div> | 727 | </div> |
| 705 | {/if} | 728 | {/if} |
| ... | @@ -712,13 +735,20 @@ | ... | @@ -712,13 +735,20 @@ |
| 712 | <div class="clasificadores-grid"> | 735 | <div class="clasificadores-grid"> |
| 713 | {#each clasificadoresGasto as { key, label, data }} | 736 | {#each clasificadoresGasto as { key, label, data }} |
| 714 | {@const displayItem = getDisplayItem(key, data)} | 737 | {@const displayItem = getDisplayItem(key, data)} |
| 738 | + {@const totalClasif = getTotal(data)} | ||
| 739 | + {@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0} | ||
| 715 | <div class="clasificador-card"> | 740 | <div class="clasificador-card"> |
| 716 | <div class="clasificador-header"> | 741 | <div class="clasificador-header"> |
| 717 | <div class="clasificador-info"> | 742 | <div class="clasificador-info"> |
| 718 | - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> | 743 | + <div class="clasificador-nombres"> |
| 719 | <span class="clasificador-padre">{displayItem?.padre || ''}</span> | 744 | <span class="clasificador-padre">{displayItem?.padre || ''}</span> |
| 720 | <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> | 745 | <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> |
| 721 | </div> | 746 | </div> |
| 747 | + <div class="clasificador-valores"> | ||
| 748 | + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> | ||
| 749 | + <span class="clasificador-pct">{pct}%</span> | ||
| 750 | + </div> | ||
| 751 | + </div> | ||
| 722 | <span class="clasificador-label">{label}</span> | 752 | <span class="clasificador-label">{label}</span> |
| 723 | </div> | 753 | </div> |
| 724 | <div class="barra-container" bind:this={barContainers[key]}></div> | 754 | <div class="barra-container" bind:this={barContainers[key]}></div> |
| ... | @@ -735,13 +765,20 @@ | ... | @@ -735,13 +765,20 @@ |
| 735 | <div class="clasificadores-grid"> | 765 | <div class="clasificadores-grid"> |
| 736 | {#each clasificadoresIngreso as { key, label, data }} | 766 | {#each clasificadoresIngreso as { key, label, data }} |
| 737 | {@const displayItem = getDisplayItem(key, data)} | 767 | {@const displayItem = getDisplayItem(key, data)} |
| 768 | + {@const totalClasif = getTotal(data)} | ||
| 769 | + {@const pct = totalClasif > 0 && displayItem ? Math.round((displayItem.monto / totalClasif) * 100) : 0} | ||
| 738 | <div class="clasificador-card"> | 770 | <div class="clasificador-card"> |
| 739 | <div class="clasificador-header"> | 771 | <div class="clasificador-header"> |
| 740 | <div class="clasificador-info"> | 772 | <div class="clasificador-info"> |
| 741 | - <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> | 773 | + <div class="clasificador-nombres"> |
| 742 | <span class="clasificador-padre">{displayItem?.padre || ''}</span> | 774 | <span class="clasificador-padre">{displayItem?.padre || ''}</span> |
| 743 | <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> | 775 | <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> |
| 744 | </div> | 776 | </div> |
| 777 | + <div class="clasificador-valores"> | ||
| 778 | + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} de Bolivianos</span> | ||
| 779 | + <span class="clasificador-pct">{pct}%</span> | ||
| 780 | + </div> | ||
| 781 | + </div> | ||
| 745 | <span class="clasificador-label">{label}</span> | 782 | <span class="clasificador-label">{label}</span> |
| 746 | </div> | 783 | </div> |
| 747 | <div class="barra-container" bind:this={barContainers[key]}></div> | 784 | <div class="barra-container" bind:this={barContainers[key]}></div> |
| ... | @@ -1039,14 +1076,39 @@ | ... | @@ -1039,14 +1076,39 @@ |
| 1039 | background: #D4A574; | 1076 | background: #D4A574; |
| 1040 | } | 1077 | } |
| 1041 | 1078 | ||
| 1042 | - .ranking-caption { | 1079 | + .ranking-extremos { |
| 1043 | - margin-top: 0.35rem; | 1080 | + display: flex; |
| 1044 | - font-size: 0.7rem; | 1081 | + justify-content: space-between; |
| 1082 | + margin-top: 0.25rem; | ||
| 1083 | + } | ||
| 1084 | + | ||
| 1085 | + .ranking-extremo { | ||
| 1086 | + font-size: 0.6rem; | ||
| 1045 | color: var(--theme-texto); | 1087 | color: var(--theme-texto); |
| 1046 | - opacity: 0.6; | 1088 | + opacity: 0.4; |
| 1089 | + } | ||
| 1090 | + | ||
| 1091 | + .ranking-headline { | ||
| 1092 | + display: flex; | ||
| 1093 | + align-items: baseline; | ||
| 1094 | + gap: 0.5rem; | ||
| 1095 | + margin-bottom: 0.75rem; | ||
| 1047 | } | 1096 | } |
| 1048 | 1097 | ||
| 1049 | - .ranking-pos { | 1098 | + .ranking-pos-big { |
| 1099 | + font-size: 1.5rem; | ||
| 1100 | + font-weight: 700; | ||
| 1101 | + color: var(--theme-titulo); | ||
| 1102 | + line-height: 1; | ||
| 1103 | + } | ||
| 1104 | + | ||
| 1105 | + .ranking-pos-context { | ||
| 1106 | + font-size: 0.8rem; | ||
| 1107 | + color: var(--theme-texto); | ||
| 1108 | + opacity: 0.7; | ||
| 1109 | + } | ||
| 1110 | + | ||
| 1111 | + .ranking-total-num { | ||
| 1050 | font-weight: 700; | 1112 | font-weight: 700; |
| 1051 | color: var(--theme-titulo); | 1113 | color: var(--theme-titulo); |
| 1052 | opacity: 1; | 1114 | opacity: 1; |
| ... | @@ -1150,7 +1212,7 @@ | ... | @@ -1150,7 +1212,7 @@ |
| 1150 | 1212 | ||
| 1151 | :global(html:not(.dark)) .gestion-option.active { | 1213 | :global(html:not(.dark)) .gestion-option.active { |
| 1152 | background: rgba(90, 157, 191, 0.1); | 1214 | background: rgba(90, 157, 191, 0.1); |
| 1153 | - color: #5A9DBF; | 1215 | + color: #3D7A9C; |
| 1154 | } | 1216 | } |
| 1155 | 1217 | ||
| 1156 | .seccion-sub { | 1218 | .seccion-sub { |
| ... | @@ -1230,7 +1292,7 @@ | ... | @@ -1230,7 +1292,7 @@ |
| 1230 | 1292 | ||
| 1231 | .historia-monto.ingresos, | 1293 | .historia-monto.ingresos, |
| 1232 | .historia-monto.gastos { | 1294 | .historia-monto.gastos { |
| 1233 | - color: #5A9DBF; | 1295 | + color: #3D7A9C; |
| 1234 | } | 1296 | } |
| 1235 | 1297 | ||
| 1236 | :global(html.dark) .historia-monto.ingresos, | 1298 | :global(html.dark) .historia-monto.ingresos, |
| ... | @@ -1270,20 +1332,22 @@ | ... | @@ -1270,20 +1332,22 @@ |
| 1270 | } | 1332 | } |
| 1271 | 1333 | ||
| 1272 | .area-path { | 1334 | .area-path { |
| 1273 | - fill: rgba(90, 157, 191, 0.12); | 1335 | + fill: rgba(61, 122, 156, 0.4); |
| 1274 | } | 1336 | } |
| 1275 | 1337 | ||
| 1276 | .line-path { | 1338 | .line-path { |
| 1277 | - stroke: #5A9DBF; | 1339 | + stroke: #3D7A9C; |
| 1340 | + opacity: 0.9; | ||
| 1278 | } | 1341 | } |
| 1279 | 1342 | ||
| 1280 | .dot { | 1343 | .dot { |
| 1281 | - fill: #5A9DBF; | 1344 | + fill: #3D7A9C; |
| 1345 | + opacity: 0.9; | ||
| 1282 | transition: r 0.15s; | 1346 | transition: r 0.15s; |
| 1283 | } | 1347 | } |
| 1284 | 1348 | ||
| 1285 | .dot.active { | 1349 | .dot.active { |
| 1286 | - fill: #5A9DBF; | 1350 | + fill: #3D7A9C; |
| 1287 | } | 1351 | } |
| 1288 | 1352 | ||
| 1289 | .hover-line { | 1353 | .hover-line { |
| ... | @@ -1339,12 +1403,25 @@ | ... | @@ -1339,12 +1403,25 @@ |
| 1339 | 1403 | ||
| 1340 | .clasificador-info { | 1404 | .clasificador-info { |
| 1341 | display: flex; | 1405 | display: flex; |
| 1406 | + flex-direction: column; | ||
| 1407 | + gap: 0.15rem; | ||
| 1408 | + min-width: 0; | ||
| 1409 | + overflow: hidden; | ||
| 1410 | + } | ||
| 1411 | + | ||
| 1412 | + .clasificador-nombres { | ||
| 1413 | + display: flex; | ||
| 1342 | align-items: baseline; | 1414 | align-items: baseline; |
| 1343 | gap: 0.4rem; | 1415 | gap: 0.4rem; |
| 1344 | - min-width: 0; | ||
| 1345 | overflow: hidden; | 1416 | overflow: hidden; |
| 1346 | } | 1417 | } |
| 1347 | 1418 | ||
| 1419 | + .clasificador-valores { | ||
| 1420 | + display: flex; | ||
| 1421 | + align-items: baseline; | ||
| 1422 | + gap: 0.4rem; | ||
| 1423 | + } | ||
| 1424 | + | ||
| 1348 | .clasificador-monto { | 1425 | .clasificador-monto { |
| 1349 | font-size: 0.95rem; | 1426 | font-size: 0.95rem; |
| 1350 | font-weight: 600; | 1427 | font-weight: 600; |
| ... | @@ -1353,6 +1430,15 @@ | ... | @@ -1353,6 +1430,15 @@ |
| 1353 | flex-shrink: 0; | 1430 | flex-shrink: 0; |
| 1354 | } | 1431 | } |
| 1355 | 1432 | ||
| 1433 | + .clasificador-pct { | ||
| 1434 | + font-size: 0.8rem; | ||
| 1435 | + font-weight: 600; | ||
| 1436 | + color: var(--theme-texto); | ||
| 1437 | + opacity: 0.5; | ||
| 1438 | + white-space: nowrap; | ||
| 1439 | + flex-shrink: 0; | ||
| 1440 | + } | ||
| 1441 | + | ||
| 1356 | .clasificador-padre { | 1442 | .clasificador-padre { |
| 1357 | font-size: 0.8rem; | 1443 | font-size: 0.8rem; |
| 1358 | font-weight: 400; | 1444 | font-weight: 400; | ... | ... |
src/routes/test/mapa/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + import * as d3 from 'd3'; | ||
| 4 | + | ||
| 5 | + let mapaData = $state(null); | ||
| 6 | + let boliviaPath = $state(''); | ||
| 7 | + let deptoPath = $state(''); | ||
| 8 | + let muniPath = $state(''); | ||
| 9 | + let error = $state(''); | ||
| 10 | + let debug = $state(''); | ||
| 11 | + | ||
| 12 | + onMount(async () => { | ||
| 13 | + try { | ||
| 14 | + const res = await fetch('/mapa.json'); | ||
| 15 | + mapaData = await res.json(); | ||
| 16 | + | ||
| 17 | + debug = `bolivia: ${mapaData.bolivia.features.length} features, `; | ||
| 18 | + debug += `deptos: ${mapaData.departamentos.features.length}, `; | ||
| 19 | + debug += `munis: ${mapaData.municipios.features.length}`; | ||
| 20 | + | ||
| 21 | + // Quitar CRS y corregir winding order | ||
| 22 | + delete mapaData.bolivia.crs; | ||
| 23 | + delete mapaData.departamentos.crs; | ||
| 24 | + delete mapaData.municipios.crs; | ||
| 25 | + | ||
| 26 | + // Corregir winding order: invertir todos los anillos | ||
| 27 | + function fixWinding(geojson) { | ||
| 28 | + const fix = (coords, isHole) => { | ||
| 29 | + // Para geo coords: exterior debe ser counterclockwise (area < 0 en D3) | ||
| 30 | + // Simplemente invertimos todos los anillos exteriores | ||
| 31 | + coords.forEach((ring, i) => { | ||
| 32 | + if (i === 0) ring.reverse(); // exterior | ||
| 33 | + // holes quedan como están | ||
| 34 | + }); | ||
| 35 | + }; | ||
| 36 | + for (const feat of geojson.features || [geojson]) { | ||
| 37 | + const geom = (feat.geometry || feat); | ||
| 38 | + if (geom.type === 'Polygon') { | ||
| 39 | + fix(geom.coordinates); | ||
| 40 | + } else if (geom.type === 'MultiPolygon') { | ||
| 41 | + geom.coordinates.forEach(poly => fix(poly)); | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + fixWinding(mapaData.bolivia); | ||
| 47 | + fixWinding(mapaData.departamentos); | ||
| 48 | + fixWinding(mapaData.municipios); | ||
| 49 | + | ||
| 50 | + const bounds = d3.geoBounds(mapaData.bolivia); | ||
| 51 | + debug += ` | bounds: ${JSON.stringify(bounds)}`; | ||
| 52 | + | ||
| 53 | + const projection = d3.geoMercator().fitSize([380, 480], mapaData.bolivia); | ||
| 54 | + const pathGen = d3.geoPath(projection); | ||
| 55 | + | ||
| 56 | + boliviaPath = pathGen(mapaData.bolivia) || ''; | ||
| 57 | + debug += ` | boliviaPath length: ${boliviaPath.length}`; | ||
| 58 | + | ||
| 59 | + deptoPath = mapaData.departamentos.features.map(f => pathGen(f)).join(' '); | ||
| 60 | + debug += ` | deptoPath length: ${deptoPath.length}`; | ||
| 61 | + | ||
| 62 | + const muni = mapaData.municipios.features[0]; | ||
| 63 | + muniPath = pathGen(muni) || ''; | ||
| 64 | + debug += ` | muni code: ${muni.properties.codigo}`; | ||
| 65 | + | ||
| 66 | + } catch (e) { | ||
| 67 | + error = e.message; | ||
| 68 | + } | ||
| 69 | + }); | ||
| 70 | +</script> | ||
| 71 | + | ||
| 72 | +<div style="padding: 2rem; background: #1C1C1A; color: #F5F0E8; min-height: 100vh;"> | ||
| 73 | + <h1>Test Mapa</h1> | ||
| 74 | + <p style="font-size: 12px; color: #888;">{debug}</p> | ||
| 75 | + {#if error} | ||
| 76 | + <p style="color: red;">{error}</p> | ||
| 77 | + {/if} | ||
| 78 | + | ||
| 79 | + <svg width="400" height="500" viewBox="0 0 400 500" style="border: 1px solid #333;"> | ||
| 80 | + {#if boliviaPath} | ||
| 81 | + <path d={boliviaPath} fill="none" stroke="#555" stroke-width="1" /> | ||
| 82 | + {/if} | ||
| 83 | + {#if deptoPath} | ||
| 84 | + <path d={deptoPath} fill="rgba(255,255,255,0.1)" stroke="#777" stroke-width="0.5" /> | ||
| 85 | + {/if} | ||
| 86 | + {#if muniPath} | ||
| 87 | + <path d={muniPath} fill="#D4A574" stroke="#D4A574" stroke-width="1" /> | ||
| 88 | + {/if} | ||
| 89 | + </svg> | ||
| 90 | +</div> |
src/routes/ubicacion/+page.js
0 → 100644
| 1 | +import { supabase } from '$lib/supabase'; | ||
| 2 | + | ||
| 3 | +export async function load({ fetch }) { | ||
| 4 | + // Cargar resumen y población en paralelo | ||
| 5 | + const [resumenRes, pobRes] = await Promise.all([ | ||
| 6 | + supabase | ||
| 7 | + .schema('ppto') | ||
| 8 | + .from('entidad_resumen') | ||
| 9 | + .select('codigo, desc, gestion, devengado, ranking') | ||
| 10 | + .eq('tipo', 'gastos') | ||
| 11 | + .eq('tipo_codigo', 'municipio_ubigeo') | ||
| 12 | + .eq('gestion', 2025) | ||
| 13 | + .order('devengado', { ascending: false }) | ||
| 14 | + .limit(500), | ||
| 15 | + | ||
| 16 | + fetch('/poblacion.csv').then(r => r.text()) | ||
| 17 | + ]); | ||
| 18 | + | ||
| 19 | + const poblacionMap = {}; | ||
| 20 | + pobRes.split('\n').slice(1).forEach(line => { | ||
| 21 | + const [cod, gestion, pob] = line.split(','); | ||
| 22 | + if (parseInt(gestion) === 2025) { | ||
| 23 | + poblacionMap[cod] = parseInt(pob); | ||
| 24 | + } | ||
| 25 | + }); | ||
| 26 | + | ||
| 27 | + const gestiones = [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]; | ||
| 28 | + | ||
| 29 | + return { | ||
| 30 | + resumen: resumenRes.data || [], | ||
| 31 | + poblacionMap, | ||
| 32 | + gestiones, | ||
| 33 | + gestionInicial: 2025 | ||
| 34 | + }; | ||
| 35 | +} |
src/routes/ubicacion/+page.svelte
0 → 100644
This diff is collapsed. Click to expand it.
src/routes/ubicacion/[ubigeo]/+page.js
0 → 100644
| 1 | +import { supabase } from '$lib/supabase'; | ||
| 2 | +import { error } from '@sveltejs/kit'; | ||
| 3 | + | ||
| 4 | +export async function load({ params, fetch }) { | ||
| 5 | + const codigo = params.ubigeo; | ||
| 6 | + | ||
| 7 | + // Cargar resumen y población en paralelo | ||
| 8 | + const [resumenRes, pobRes] = await Promise.all([ | ||
| 9 | + supabase | ||
| 10 | + .schema('ppto') | ||
| 11 | + .from('entidad_resumen') | ||
| 12 | + .select('tipo, tipo_codigo, codigo, desc, desc_padre, gestion, devengado, ranking') | ||
| 13 | + .eq('codigo', codigo), | ||
| 14 | + | ||
| 15 | + fetch('/poblacion.csv').then(r => r.text()) | ||
| 16 | + ]); | ||
| 17 | + | ||
| 18 | + if (resumenRes.error || !resumenRes.data?.length) { | ||
| 19 | + throw error(404, 'Ubicación no encontrada'); | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + // Parsear población para este código | ||
| 23 | + const poblacionMap = {}; | ||
| 24 | + pobRes.split('\n').slice(1).forEach(line => { | ||
| 25 | + const [cod, gestion, pob] = line.split(','); | ||
| 26 | + if (cod === codigo) { | ||
| 27 | + poblacionMap[parseInt(gestion)] = parseInt(pob); | ||
| 28 | + } | ||
| 29 | + }); | ||
| 30 | + | ||
| 31 | + // Determinar última gestión | ||
| 32 | + const gestiones = [...new Set(resumenRes.data.map(d => d.gestion))].sort((a, b) => b - a); | ||
| 33 | + const ultimaGestion = gestiones[0] || 2025; | ||
| 34 | + | ||
| 35 | + // Cargar distribuciones de la última gestión | ||
| 36 | + const distRes = await supabase | ||
| 37 | + .schema('ppto') | ||
| 38 | + .from('entidad_distribuciones') | ||
| 39 | + .select('tipo, dimension, gestion, padre, desc_padre, hijo, desc_hijo, devengado') | ||
| 40 | + .eq('codigo', codigo) | ||
| 41 | + .eq('gestion', ultimaGestion); | ||
| 42 | + | ||
| 43 | + const firstRow = resumenRes.data[0]; | ||
| 44 | + | ||
| 45 | + return { | ||
| 46 | + nombre: firstRow.desc || `Ubicación ${codigo}`, | ||
| 47 | + nombrePadre: firstRow.desc_padre || null, | ||
| 48 | + codigo, | ||
| 49 | + resumenData: resumenRes.data || [], | ||
| 50 | + distribucionesData: distRes.data || [], | ||
| 51 | + gestionInicial: ultimaGestion, | ||
| 52 | + poblacionMap | ||
| 53 | + }; | ||
| 54 | +} |
src/routes/ubicacion/[ubigeo]/+page.svelte
0 → 100644
This diff is collapsed. Click to expand it.
static/mapa.json
0 → 100644
This diff could not be displayed because it is too large.
static/poblacion.csv
0 → 100644
This diff is collapsed. Click to expand it.
static/screen.png
deleted
100644 → 0
52.3 KB
-
Please register or login to post a comment