refactor(mapa): simplificat de 15 a 3 capes (dropdown -> botons grans)

L'usuaria no entenia el mapa: 15 variables, 2 capes redundants
(consumo_p10_muy_bajo + consumo_p10_bajo pintaven el MATEIX camp), i
noms quasi-iguals ('Cua de baix consum' vs 'Cua de consum baix').
Decidim tornar a 0.

3 capes simples, 3 botons grans (no dropdown):

1. 'Habitatges totals'   (capa original)
2. 'Possibles vacants'   (renova 4 capes consumo + viviendas_vacantes,
                           pintant consumo_p10_kwh amb semafor vermell)
3. 'Pressio turistica'   (capa composta nova: airbnb_listings a
                           Mallorca/Menorca + hut_plazas a Eivissa,
                           pintades simultaniament)

Treu:
- 14 entrades de variableOptions
- 13 entrades de LAYERS
- 18 claus i18n obsoletes (group_consumo/presion/hut/avanzado +
  variables buides)
- El format dropdown + optgroup

AFEGEIX:
- HTML <button> amb aria-pressed (estil actiu automatic via <style>)
- Listener click en lloc de change event
- Llegenda custom per 'pressio_turistica' amb 2 mini-barres
  (groc-taronya Airbnb, blau HUT) perque la capa composta no
  es pot representar amb un sol degradat
- formatTick especial per a possibles_vacants (kWh)

Net: -404 linies a Mapa.astro, -42 a cada i18n (-488 total).

astro-check: 0 errors nous a Mapa.astro.
This commit is contained in:
2026-07-16 10:46:42 +02:00
parent b3510b3f2f
commit 4a8e2d42b8
3 changed files with 163 additions and 333 deletions
+149 -263
View File
@@ -12,26 +12,10 @@ interface VariableOption {
}
const variableOptions: VariableOption[] = [
// Grupo 1: Vivienda (INE Censo 2021)
// 3 capas simples. Sin sub-grupos.
{ value: "viviendas_totales", labelKey: "map.variable_viviendas_totales", descKey: "map.variable_viviendas_totales_desc", scope: "all", groupKey: "map.group_vivienda" },
{ value: "viviendas_vacantes", labelKey: "map.variable_viviendas_vacantes", descKey: "map.variable_viviendas_vacantes_desc", scope: "all", groupKey: "map.group_vivienda" },
// Grupo 2: Consumo eléctrico
{ value: "consumo_p10_muy_bajo", labelKey: "map.variable_consumo_p10_muy_bajo", descKey: "map.variable_consumo_p10_muy_bajo_desc", scope: "all", groupKey: "map.group_consumo" },
{ value: "consumo_p10_bajo", labelKey: "map.variable_consumo_p10_bajo", descKey: "map.variable_consumo_p10_bajo_desc", scope: "all", groupKey: "map.group_consumo" },
{ value: "consumo_p50_medio", labelKey: "map.variable_consumo_p50_medio", descKey: "map.variable_consumo_p50_medio_desc", scope: "all", groupKey: "map.group_consumo" },
{ value: "consumo_p90_alto", labelKey: "map.variable_consumo_p90_alto", descKey: "map.variable_consumo_p90_alto_desc", scope: "all", groupKey: "map.group_consumo" },
// Grupo 3: Presión Airbnb
{ value: "airbnb_listings", labelKey: "map.variable_airbnb_listings", descKey: "map.variable_airbnb_listings_desc", scope: "airbnb", groupKey: "map.group_presion" },
{ value: "airbnb_entire_homes", labelKey: "map.variable_airbnb_entire", descKey: "map.variable_airbnb_entire_desc", scope: "airbnb", groupKey: "map.group_presion" },
{ value: "airbnb_revenue_total", labelKey: "map.variable_airbnb_revenue", descKey: "map.variable_airbnb_revenue_desc", scope: "airbnb", groupKey: "map.group_presion" },
{ value: "airbnb_per_1000", labelKey: "map.variable_airbnb_per_1000", descKey: "map.variable_airbnb_per_1000_desc", scope: "airbnb", groupKey: "map.group_presion" },
// Grupo 4: Vivienda turística legal (HUT)
{ value: "hut_registros", labelKey: "map.variable_hut_registros", descKey: "map.variable_hut_registros_desc", scope: "hut", groupKey: "map.group_hut" },
{ value: "hut_plazas", labelKey: "map.variable_hut_plazas", descKey: "map.variable_hut_plazas_desc", scope: "hut", groupKey: "map.group_hut" },
// Grupo 5: Avanzado
{ value: "airbnb_con_licencia", labelKey: "map.variable_airbnb_con_licencia", descKey: "map.variable_airbnb_con_licencia_desc", scope: "airbnb", groupKey: "map.group_avanzado" },
{ value: "ratio_p10_p50_consumo", labelKey: "map.variable_ratio_p10_p50", descKey: "map.variable_ratio_p10_p50_desc", scope: "all", groupKey: "map.group_avanzado" },
{ value: "consumo_p50_kwh", labelKey: "map.variable_consumo_p50", descKey: "map.variable_consumo_p50_desc", scope: "all", groupKey: "map.group_avanzado" },
{ value: "possibles_vacants", labelKey: "map.variable_possibles_vacants", descKey: "map.variable_possibles_vacants_desc", scope: "all", groupKey: "map.group_vivienda" },
{ value: "pressio_turistica", labelKey: "map.variable_pressio_turistica", descKey: "map.variable_pressio_turistica_desc", scope: "all", groupKey: "map.group_vivienda" },
];
const popupTitleTmpl = t(lang, "map.popup_title");
@@ -66,29 +50,40 @@ const popupTitleTmpl = t(lang, "map.popup_title");
class="absolute top-3 left-3 z-20 bg-white/95 backdrop-blur border border-stone-200 rounded-lg shadow-sm p-3 w-64 max-w-[calc(100vw-1.5rem)]"
>
<div class="mb-2">
<label for="variable-select" class="block text-xs font-medium text-stone-700 mb-1">
<p class="text-xs font-medium text-stone-700 mb-2">
{t(lang, "map.variable_label")}
</label>
<select
id="variable-select"
class="w-full text-sm border border-stone-300 rounded px-2 py-1 bg-white"
>
{(() => {
const groups = Array.from(new Set(variableOptions.map((o) => o.groupKey)));
return groups.map((g) => (
<optgroup label={t(lang, g)}>
{variableOptions
.filter((o) => o.groupKey === g)
.map((o) => (
<option value={o.value} data-scope={o.scope}>{t(lang, o.labelKey)}</option>
))}
</optgroup>
));
})()}
</select>
<p id="variable-desc" class="text-[10px] text-stone-500 mt-1 leading-tight"></p>
</p>
<div class="space-y-2">
{variableOptions.map((o) => (
<button
type="button"
data-variable={o.value}
data-scope={o.scope}
class="variable-btn w-full text-left border border-stone-300 rounded px-3 py-2 bg-white hover:bg-stone-50 active:bg-stone-100 transition-colors"
aria-pressed="false"
>
<span class="font-medium text-sm text-stone-900 block">{t(lang, o.labelKey)}</span>
<span class="text-[11px] text-stone-500 block mt-0.5 leading-snug">{t(lang, o.descKey)}</span>
</button>
))}
</div>
<p id="variable-desc" class="text-[10px] text-stone-500 mt-2 leading-tight"></p>
</div>
<style>
.variable-btn[aria-pressed="true"] {
background: #1c1917;
border-color: #1c1917;
color: #fafaf9;
}
.variable-btn[aria-pressed="true"] span {
color: inherit;
}
.variable-btn[aria-pressed="true"] .text-stone-500 {
color: #d6d3d1 !important;
}
</style>
<div class="border-t border-stone-200 pt-2">
<p class="text-xs font-medium text-stone-700 mb-1">
{t(lang, "map.layer_label")}
@@ -134,18 +129,15 @@ const popupTitleTmpl = t(lang, "map.popup_title");
listings: "Listings Airbnb",
entire_homes: lang === "es" ? "Pisos enteros" : "Pisos sencers",
revenue: lang === "es" ? "Ingresos L365d" : "Ingressos L365d",
con_licencia: lang === "es" ? "Con licencia Airbnb" : "Amb llicència Airbnb",
hut_registros: lang === "es" ? "Registros HUT" : "Registres HUT",
hut_plazas: lang === "es" ? "Plazas HUT" : "Places HUT",
viviendas_totales: lang === "es" ? "Viviendas totales" : "Habitatges totals",
consumo_p50: lang === "es" ? "Mediana consumo kWh" : "Mediana consum kWh",
viviendas_vacantes: lang === "es" ? "Viv. vacantes (estim.)" : "Hab. vacants (estim.)",
consumo_p10: lang === "es" ? "Consumo p10 (kWh)" : "Consum p10 (kWh)",
consumo_p10_muy_bajo: lang === "es" ? "Cola bajo consumo (p10)" : "Cua de baix consum (p10)",
viviendas_vacantes_label: lang === "es" ? "Viv. vacantes" : "Hab. vacants",
consumo_p10_bajo: lang === "es" ? "Cola consumo bajo (p10)" : "Cua de consum baix (p10)",
consumo_p50_medio: lang === "es" ? "Consumo medio (p50)" : "Consum mitjà (p50)",
consumo_p90_alto: lang === "es" ? "Cola consumo alto (p90)" : "Cua de consum alt (p90)",
airbnb_per_1000: lang === "es" ? "Airbnb / 1000 viv." : "Airbnb / 1000 hab.",
consumo_p50: lang === "es" ? "Mediana consumo kWh" : "Mediana consum kWh",
ratio_p10_p50: lang === "es" ? "Ratio p10/p50" : "Ratio p10/p50",
possibles_vacants: lang === "es" ? "Posibles vacantes" : "Possibles vacants",
pressio_turistica: lang === "es" ? "Presión turística" : "Pressió turística",
granularitat: lang === "es" ? "Granularidad" : "Granularitat",
seccion_censal: lang === "es" ? "sección censal" : "secció censal",
municipio: "municipi",
@@ -155,50 +147,30 @@ const popupTitleTmpl = t(lang, "map.popup_title");
legend_islands_mm: lang === "es" ? "Mallorca / Menorca (sección censal)" : "Mallorca / Menorca (secció censal)",
legend_islands_ef: lang === "es" ? "Ibiza / Formentera (municipio)" : "Eivissa / Formentera (municipi)",
legend_islands_all_mun: lang === "es" ? "Todas (municipio)" : "Totes (municipi)",
legend_islands_all_dis: lang === "es" ? "Todas (distrito)" : "Totes (districte)",
legend_islands_tourism: lang === "es" ? "Airbnb + HUT simultáneos" : "Airbnb + HUT simultanis",
};
const DESC: Record<string, string> = {
airbnb_listings:
lang === "es"
? "Total de listings activos por sección censal"
: "Total de listings actius per secció censal",
airbnb_entire_homes:
lang === "es"
? "Listings que son pisos completos — proxy de conversión residencial → turístico"
: "Listings que són pisos complets — proxy de conversió residencial → turístic",
airbnb_revenue_total:
lang === "es"
? "Suma estimada de ingresos de los últimos 365 días por sección"
: "Suma estimada dels ingressos dels darrers 365 dies per secció",
airbnb_con_licencia:
lang === "es"
? "Listings con son campo `license` rellenado (autorreportado, no validado)"
: "Listings amb camp `license` emplenat (autorreportat, no validat)",
airbnb_per_1000:
lang === "es"
? "Listings Airbnb por cada 1000 viviendas del municipio"
: "Listings Airbnb per cada 1000 habitatges del municipi",
hut_plazas:
lang === "es"
? "Plazas registradas en HUT del Consell d'Eivissa (municipio)"
: "Places registrades al HUT del Consell d'Eivissa (municipi)",
hut_registros:
lang === "es"
? "Número de registros HUT activos (municipio)"
: "Nombre de registres HUT actius (municipi)",
viviendas_totales:
lang === "es"
? "Total de viviendas por municipio (Censo 2021, replicado a todas las secciones del municipio)"
: "Total d'habitatges per municipi (Cens 2021, replicat a totes les seccions del municipi)",
consumo_p50_kwh:
possibles_vacants:
lang === "es"
? "Percentil 50 de consumo eléctrico anual por distrito (Censo 2021)"
: "Percentil 50 de consum elèctric anual per districte (Cens 2021)",
ratio_p10_p50:
? "Secciones donde la cola baja del consumo eléctrico es muy baja (p10). Proxy Holm 2025: cuanto más bajo el p10, más probable que haya viviendas vacías o 2ª residencia."
: "Seccions on la cua baixa del consum elèctric és molt baixa (p10). Proxy Holm 2025: com més baix el p10, més probable que hi hagi habitatges buits o 2a residència.",
pressio_turistica:
lang === "es"
? "Cociente entre el percentil 10 y la mediana del consumo eléctrico del distrito (Censo 2021). Bajo (<0.3) = grupo de casas con consumo muy bajo (posibles vacías). Alto (~0.6+) = uso residencial estable."
: "Quocient entre el percentil 10 i la mediana del consum elèctric del districte (Cens 2021). Baix (<0.3) = grup de cases amb consum molt baix. Alt (~0.6+) = ús residencial estable.",
? "Mallorca y Menorca: listings Airbnb activos (junio 2026). Eivissa: plazas de vivienda turística legal — HUT (julio 2026)."
: "Mallorca i Menorca: listings d'Airbnb actius (juny 2026). Eivissa: places d'habitatge turístic legal — HUT (juliol 2026).",
airbnb_listings:
lang === "es"
? "Total de listings activos por sección censal"
: "Total de listings actius per secció censal",
hut_plazas:
lang === "es"
? "Plazas registradas en HUT del Consell d'Eivissa (municipio)"
: "Places registrades al HUT del Consell d'Eivissa (municipi)",
};
const LAYERS: Record<string, {
@@ -211,6 +183,44 @@ const popupTitleTmpl = t(lang, "map.popup_title");
desc: string;
appliesTo: string;
}> = {
viviendas_totales: {
scope: "all",
field: "viviendas_totales",
colorExpr: [
"step", ["coalesce", ["get", "viviendas_totales"], 0],
"#dcfce7", 500, "#bbf7d0", 2000, "#86efac", 5000, "#22c55e", 15000, "#14532d",
],
stops: [0, 500, 2000, 5000, 15000],
colors: ["#dcfce7", "#bbf7d0", "#86efac", "#22c55e", "#14532d", "#14532d"],
label: T.viviendas_totales,
desc: DESC.viviendas_totales,
appliesTo: T.legend_islands_all_mun,
},
possibles_vacants: {
scope: "all",
field: "consumo_p10_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p10_kwh"], 0],
"#7f1d1d", 600, "#dc2626", 1000, "#f97316", 1500, "#fbbf24", 2200, "#fde68a",
],
stops: [0, 600, 1000, 1500, 2200],
colors: ["#7f1d1d", "#dc2626", "#f97316", "#fbbf24", "#fde68a", "#fde68a"],
label: T.possibles_vacants,
desc: DESC.possibles_vacants,
appliesTo: T.legend_islands_all_mun,
},
pressio_turistica: {
// Capa compuesta: usa los fill-colors de airbnb_listings y hut_plazas
// directamente (ver applyVariable). Este entry existe solo para legend/UI.
scope: "all",
field: "",
colorExpr: NO_DATA_COLOR,
stops: [0],
colors: ["#fde68a", "#7c2d12", "#7dd3fc", "#0c4a6e", "#fde68a", "#0c4a6e"],
label: T.pressio_turistica,
desc: DESC.pressio_turistica,
appliesTo: T.legend_islands_tourism,
},
airbnb_listings: {
scope: "airbnb",
field: "airbnb_listings",
@@ -224,46 +234,6 @@ const popupTitleTmpl = t(lang, "map.popup_title");
desc: DESC.airbnb_listings,
appliesTo: T.legend_islands_mm,
},
airbnb_entire_homes: {
scope: "airbnb",
field: "airbnb_entire_homes",
colorExpr: [
"step", ["coalesce", ["get", "airbnb_entire_homes"], 0],
"#fef3c7", 1, "#fde68a", 5, "#fbbf24", 30, "#f97316", 100, "#c2410c", 250, "#7c2d12",
],
stops: [0, 1, 5, 30, 100, 250],
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
label: T.entire_homes,
desc: DESC.airbnb_entire_homes,
appliesTo: T.legend_islands_mm,
},
airbnb_revenue_total: {
scope: "airbnb",
field: "airbnb_revenue_total",
colorExpr: [
"step", ["coalesce", ["get", "airbnb_revenue_total"], 0],
"#fef3c7", 100000, "#fde68a", 500000, "#fbbf24",
2000000, "#f97316", 5000000, "#c2410c", 10000000, "#7c2d12",
],
stops: [0, 100000, 500000, 2000000, 5000000, 10000000],
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
label: T.revenue,
desc: DESC.airbnb_revenue_total,
appliesTo: T.legend_islands_mm,
},
airbnb_con_licencia: {
scope: "airbnb",
field: "airbnb_con_licencia",
colorExpr: [
"step", ["coalesce", ["get", "airbnb_con_licencia"], 0],
"#fef3c7", 1, "#fde68a", 10, "#fbbf24", 50, "#f97316", 150, "#c2410c", 400, "#7c2d12",
],
stops: [0, 1, 10, 50, 150, 400],
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
label: T.con_licencia,
desc: DESC.airbnb_con_licencia,
appliesTo: T.legend_islands_mm,
},
hut_plazas: {
scope: "hut",
field: "hut_plazas",
@@ -277,136 +247,6 @@ const popupTitleTmpl = t(lang, "map.popup_title");
desc: DESC.hut_plazas,
appliesTo: T.legend_islands_ef,
},
hut_registros: {
scope: "hut",
field: "hut_registros",
colorExpr: [
"step", ["coalesce", ["get", "hut_registros"], 0],
"#e0f2fe", 1, "#7dd3fc", 200, "#0ea5e9", 1000, "#0369a1", 2000, "#0c4a6e",
],
stops: [0, 1, 200, 1000, 2000],
colors: ["#e0f2fe", "#7dd3fc", "#0ea5e9", "#0369a1", "#0c4a6e", "#0c4a6e"],
label: T.hut_registros,
desc: DESC.hut_registros,
appliesTo: T.legend_islands_ef,
},
airbnb_per_1000: {
scope: "airbnb",
field: "airbnb_listings_por_1000_viviendas",
colorExpr: [
"step", ["coalesce", ["get", "airbnb_listings_por_1000_viviendas"], 0],
"#ede9fe", 5, "#c4b5fd", 20, "#a78bfa", 50, "#8b5cf6", 100, "#6d28d9",
],
stops: [0, 5, 20, 50, 100],
colors: ["#ede9fe", "#c4b5fd", "#a78bfa", "#8b5cf6", "#6d28d9", "#6d28d9"],
label: T.airbnb_per_1000,
desc: DESC.airbnb_per_1000,
appliesTo: T.legend_islands_mm,
},
viviendas_totales: {
scope: "all",
field: "viviendas_totales",
colorExpr: [
"step", ["coalesce", ["get", "viviendas_totales"], 0],
"#dcfce7", 500, "#bbf7d0", 2000, "#86efac", 5000, "#22c55e", 15000, "#14532d",
],
stops: [0, 500, 2000, 5000, 15000],
colors: ["#dcfce7", "#bbf7d0", "#86efac", "#22c55e", "#14532d", "#14532d"],
label: T.viviendas_totales,
desc: DESC.viviendas_totales,
appliesTo: T.legend_islands_all_mun,
},
viviendas_vacantes: {
scope: "all",
field: "viviendas_vacantes_estimadas",
colorExpr: [
"step", ["coalesce", ["get", "viviendas_vacantes_estimadas"], 0],
"#fee2e2", 100, "#fca5a5", 500, "#ef4444", 1500, "#b91c1c", 5000, "#7f1d1d",
],
stops: [0, 100, 500, 1500, 5000],
colors: ["#fee2e2", "#fca5a5", "#ef4444", "#b91c1c", "#7f1d1d", "#7f1d1d"],
label: T.viviendas_vacantes,
desc: DESC.viviendas_vacantes,
appliesTo: T.legend_islands_all_mun,
},
consumo_p10_muy_bajo: {
scope: "all",
field: "consumo_p10_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p10_kwh"], 0],
"#7f1d1d", 800, "#dc2626", 1200, "#f97316", 1800, "#fbbf24", 2500, "#22c55e",
],
stops: [0, 800, 1200, 1800, 2500],
colors: ["#7f1d1d", "#dc2626", "#f97316", "#fbbf24", "#22c55e", "#22c55e"],
label: T.consumo_p10_muy_bajo,
desc: DESC.consumo_p10_muy_bajo,
appliesTo: T.legend_islands_all_dis,
},
consumo_p10_bajo: {
scope: "all",
field: "consumo_p10_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p10_kwh"], 0],
"#7f1d1d", 1000, "#dc2626", 1500, "#f97316", 2000, "#fbbf24", 2500, "#22c55e",
],
stops: [0, 1000, 1500, 2000, 2500],
colors: ["#7f1d1d", "#dc2626", "#f97316", "#fbbf24", "#22c55e", "#22c55e"],
label: T.consumo_p10_bajo,
desc: DESC.consumo_p10_bajo,
appliesTo: T.legend_islands_all_dis,
},
consumo_p50_medio: {
scope: "all",
field: "consumo_p50_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p50_kwh"], 0],
"#7f1d1d", 2500, "#dc2626", 3000, "#facc15", 3500, "#86efac", 4000, "#22c55e",
],
stops: [0, 2500, 3000, 3500, 4000],
colors: ["#7f1d1d", "#dc2626", "#facc15", "#86efac", "#22c55e", "#22c55e"],
label: T.consumo_p50_medio,
desc: DESC.consumo_p50_medio,
appliesTo: T.legend_islands_all_dis,
},
consumo_p90_alto: {
scope: "all",
field: "consumo_p90_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p90_kwh"], 0],
"#7f1d1d", 3000, "#facc15", 4000, "#86efac", 5000, "#22c55e", 6000, "#15803d",
],
stops: [0, 3000, 4000, 5000, 6000],
colors: ["#7f1d1d", "#facc15", "#86efac", "#22c55e", "#15803d", "#15803d"],
label: T.consumo_p90_alto,
desc: DESC.consumo_p90_alto,
appliesTo: T.legend_islands_all_dis,
},
consumo_p50_kwh: {
scope: "all",
field: "consumo_p50_kwh",
colorExpr: [
"step", ["coalesce", ["get", "consumo_p50_kwh"], 0],
"#fee2e2", 1500, "#fecaca", 2500, "#fca5a5", 3500, "#ef4444", 4500, "#7f1d1d",
],
stops: [0, 1500, 2500, 3500, 4500],
colors: ["#fee2e2", "#fecaca", "#fca5a5", "#ef4444", "#7f1d1d", "#7f1d1d"],
label: T.consumo_p50,
desc: DESC.consumo_p50_kwh,
appliesTo: T.legend_islands_all_dis,
},
ratio_p10_p50_consumo: {
scope: "all",
field: "ratio_p10_p50_consumo",
colorExpr: [
"step", ["coalesce", ["get", "ratio_p10_p50_consumo"], 0],
"#7c2d12", 0.15, "#c2410c", 0.25, "#f97316", 0.35, "#fbbf24", 0.5, "#86efac",
],
stops: [0, 0.15, 0.25, 0.35, 0.5],
colors: ["#7c2d12", "#c2410c", "#f97316", "#fbbf24", "#86efac", "#86efac"],
label: T.ratio_p10_p50,
desc: DESC.ratio_p10_p50,
appliesTo: T.legend_islands_all_dis,
},
};
const fmt = (n: number) =>
@@ -553,14 +393,27 @@ const popupTitleTmpl = t(lang, "map.popup_title");
if (loading) loading.style.display = "none";
}
let activeVariable = "airbnb_listings";
let activeVariable = "viviendas_totales";
function applyVariable(variable: string) {
const cfg = LAYERS[variable];
if (!cfg) return;
activeVariable = variable;
if (cfg.scope === "airbnb") {
if (variable === "pressio_turistica") {
// Capa compuesta: Airbnb en Mallorca/Menorca + HUT en Eivissa, simultáneas.
map.setPaintProperty(
"secciones-airbnb-fill",
"fill-color",
LAYERS.airbnb_listings.colorExpr
);
map.setPaintProperty(
"secciones-hut-fill",
"fill-color",
LAYERS.hut_plazas.colorExpr
);
map.setPaintProperty("secciones-all-fill", "fill-color", NO_DATA_COLOR);
} else if (cfg.scope === "airbnb") {
map.setPaintProperty("secciones-airbnb-fill", "fill-color", cfg.colorExpr);
map.setPaintProperty("secciones-hut-fill", "fill-color", NO_DATA_COLOR);
map.setPaintProperty("secciones-all-fill", "fill-color", NO_DATA_COLOR);
@@ -575,10 +428,11 @@ const popupTitleTmpl = t(lang, "map.popup_title");
map.setPaintProperty("secciones-hut-fill", "fill-color", NO_DATA_COLOR);
}
// Auto-ocultar círculos de Airbnb cuando la variable no es Airbnb.
// Auto-ocultar círculos de Airbnb cuando la variable no es Airbnb/turística.
// Antes: los círculos mostraban siempre el mismo dato (count de listings)
// independientemente de la variable, lo que confundía al usuario.
const showAirbnbClusters = cfg.scope === "airbnb";
const showAirbnbClusters =
variable === "airbnb_listings" || variable === "pressio_turistica";
const clusterVis: "visible" | "none" = showAirbnbClusters ? "visible" : "none";
["airbnb-clusters", "airbnb-cluster-count", "airbnb-unclustered"].forEach((id) => {
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", clusterVis);
@@ -587,6 +441,14 @@ const popupTitleTmpl = t(lang, "map.popup_title");
const clusterToggle = document.getElementById("toggle-clusters") as HTMLInputElement | null;
if (clusterToggle) clusterToggle.checked = showAirbnbClusters;
// aria-pressed en los botones según cuál está activo (estilo visual)
Array.from(document.querySelectorAll<HTMLButtonElement>(".variable-btn")).forEach((b) => {
b.setAttribute(
"aria-pressed",
b.dataset.variable === variable ? "true" : "false"
);
});
updateLegend(cfg);
const desc = document.getElementById("variable-desc");
if (desc) desc.textContent = cfg.desc;
@@ -594,16 +456,35 @@ const popupTitleTmpl = t(lang, "map.popup_title");
function formatTick(n: number): string {
if (n === 0) return "0";
if (activeVariable === "airbnb_revenue_total") return fmtMoney(n);
if (activeVariable === "consumo_p50_kwh") return `${fmt(n)} kWh`;
if (activeVariable === "airbnb_per_1000") return n.toFixed(0);
if (activeVariable === "ratio_p10_p50_consumo") return n.toFixed(2);
if (activeVariable === "possibles_vacants") return `${fmt(Math.round(n))} kWh`;
return fmtShort(n);
}
function updateLegend(cfg: typeof LAYERS[string]) {
const el = document.getElementById("legend-content");
if (!el) return;
// Caso especial: la capa composta 'pressio_turistica' usa dos fill-colors
// distintos geográficamente (Airbnb en Mallorca/Menorca, HUT en Eivissa).
// El degradado único del LAYERS no representa eso; mostramos dos mini-leyendas.
if (activeVariable === "pressio_turistica") {
const airbnbBar = LAYERS.airbnb_listings.colors
.map((c) => `<span class="flex-1" style="background:${c}"></span>`)
.join("");
const hutBar = LAYERS.hut_plazas.colors
.map((c) => `<span class="flex-1" style="background:${c}"></span>`)
.join("");
el.innerHTML = `
<p class="font-semibold text-stone-700 mb-1">${escape(cfg.label)}</p>
<p class="text-stone-500 mb-2 leading-tight">${escape(cfg.desc)}</p>
<p class="text-xs font-medium text-stone-700 mb-1">Airbnb — Mallorca / Menorca</p>
<div class="flex items-center gap-0.5 h-2 rounded overflow-hidden mb-2">${airbnbBar}</div>
<p class="text-xs font-medium text-stone-700 mb-1">HUT — Eivissa</p>
<div class="flex items-center gap-0.5 h-2 rounded overflow-hidden mb-2">${hutBar}</div>
`;
return;
}
const swatches = cfg.colors
.map((c) => `<span class="flex-1" style="background:${c}"></span>`)
.join("");
@@ -628,9 +509,14 @@ const popupTitleTmpl = t(lang, "map.popup_title");
}
map.on("load", () => {
const variableSelect = document.getElementById("variable-select") as HTMLSelectElement;
variableSelect?.addEventListener("change", () => applyVariable(variableSelect.value));
if (variableSelect) applyVariable(variableSelect.value);
const variableBtns = Array.from(
document.querySelectorAll<HTMLButtonElement>(".variable-btn")
);
variableBtns.forEach((btn) => {
btn.addEventListener("click", () => applyVariable(btn.dataset.variable ?? ""));
});
const initial = variableBtns[0]?.dataset.variable ?? "viviendas_totales";
if (variableBtns[0]) applyVariable(initial);
const toggleChoro = document.getElementById("toggle-choropleth") as HTMLInputElement;
const toggleClu = document.getElementById("toggle-clusters") as HTMLInputElement;
+7 -35
View File
@@ -69,41 +69,13 @@
"layer_choropleth": "Plànol coroplètic",
"layer_clusters": "Punts Airbnb (clústers)",
"variable_label": "Variable",
"group_vivienda": "Habitatge (INE Cens 2021)",
"group_consumo": "Consum elèctric (Cens 2021)",
"group_presion": "Pressió Airbnb (juny 2026)",
"group_hut": "Habitatge turístic legal — HUT (juliol 2026)",
"group_avanzado": "Avançat",
"variable_airbnb_listings": "Listings Airbnb",
"variable_airbnb_listings_desc": "Total de listings actius per secció censal",
"variable_airbnb_entire": "Pisos sencers (entire home/apt)",
"variable_airbnb_entire_desc": "Listings que són pisos complets — proxy de conversió residencial → turístic",
"variable_airbnb_revenue": "Ingressos Airbnb L365d",
"variable_airbnb_revenue_desc": "Suma estimada dels ingressos dels darrers 365 dies per secció",
"variable_airbnb_con_licencia": "Listings amb llicència Airbnb",
"variable_airbnb_con_licencia_desc": "Listings amb camp `license` emplenat (autorreportat, no validat pel Govern)",
"variable_hut_plazas": "Places HUT legals",
"variable_hut_plazas_desc": "Places registrades al registre HUT del Consell d'Eivissa (municipi)",
"variable_hut_registros": "Registres HUT",
"variable_hut_registros_desc": "Nombre de registres HUT actius (municipi)",
"variable_viviendas_totales": "Habitatges totals (INE)",
"variable_viviendas_totales_desc": "Total d'habitatges per municipi (Cens 2021, replicat a totes les seccions del municipi)",
"variable_viviendas_vacantes": "Habitatges vacants (estimació per municipi)",
"variable_viviendas_vacantes_desc": "Estimació del nombre d'habitatges vacants per municipi, calculada com el 16,2% del total (taxa CCAA de l'INE, Cens 2021). És un proxy: la taxa real varia per municipi (més alta a zones turístiques). Total Balears: 105.632 vacants.",
"variable_consumo_p50": "Mediana consum elèctric (kWh)",
"variable_consumo_p50_desc": "Percentil 50 de consum elèctric anual per districte (Cens 2021). Mediana del que consumeix una llar al districte.",
"variable_consumo_p10_muy_bajo": "Cola de baix consum (p10)",
"variable_consumo_p10_muy_bajo_desc": "Districte amb percentil 10 de consum elèctric inferior a 1.000 kWh/any. Una p10 baixa indica que hi ha un grup nombrós de cases amb consum molt reduït (vacants, segones residències, consum testimonial). Vermell = risc elevat de semi-vaciació.",
"variable_consumo_p10_bajo": "Cola de consum baix (p10)",
"variable_consumo_p10_bajo_desc": "Districte amb p10 entre 1.000 i 2.000 kWh/any. Taronja = possible ús estacional o semi-vaciació parcial.",
"variable_consumo_p50_medio": "Consum mitjà (p50)",
"variable_consumo_p50_medio_desc": "Districte amb p50 entre 2.500 i 3.500 kWh/any. Groc = consum residencial estàndard. La mediana de Balears és 2.869 kWh.",
"variable_consumo_p90_alto": "Cua de consum alt (p90)",
"variable_consumo_p90_alto_desc": "Districte amb p90 superior a 4.500 kWh/any. Verd = sense cues de baix consum, perfil residencial estable amb AC intensiu o família nombrosa.",
"variable_ratio_p10_p50": "Ratio p10/p50 consum elèctric",
"variable_ratio_p10_p50_desc": "Quocient entre el percentil 10 i la mediana del consum elèctric del districte (Cens 2021). Baix (<0.3) = grup de cases amb consum molt baix, possibles vacants o segones residències. Alt (~0.6+) = distribució més homogènia d'ús residencial estable.",
"variable_airbnb_per_1000": "Pressió Airbnb (per 1000 habitatges)",
"variable_airbnb_per_1000_desc": "Listings Airbnb per cada 1000 habitatges del municipi. Mesura relativa de la pressió turística sobre el parc residencial.",
"group_vivienda": "Habitatge i pressió",
"variable_viviendas_totales": "Habitatges totals",
"variable_viviendas_totales_desc": "Total d'habitatges censats per secció censal (INE 2021).",
"variable_possibles_vacants": "Possibles vacants",
"variable_possibles_vacants_desc": "Seccions on la cua baixa del consum elèctric és molt baixa (p10). Com més baix el p10, més probable que hi hagi habitatges buits o 2a residència (proxy Holm 2025).",
"variable_pressio_turistica": "Pressió turística",
"variable_pressio_turistica_desc": "Mallorca i Menorca: listings d'Airbnb actius (juny 2026). Eivissa: places d'habitatge turístic legal — HUT (juliol 2026).",
"legend_title": "Llegenda",
"legend_scale_label": "Escala",
"legend_applies_to": "Aplica a",
+7 -35
View File
@@ -69,41 +69,13 @@
"layer_choropleth": "Plano coroplético",
"layer_clusters": "Puntos Airbnb (clústeres)",
"variable_label": "Variable",
"group_vivienda": "Vivienda (INE Censo 2021)",
"group_consumo": "Consumo eléctrico (Censo 2021)",
"group_presion": "Presión Airbnb (junio 2026)",
"group_hut": "Vivienda turística legal — HUT (julio 2026)",
"group_avanzado": "Avanzado",
"variable_airbnb_listings": "Listings Airbnb",
"variable_airbnb_listings_desc": "Total de listings activos por sección censal",
"variable_airbnb_entire": "Pisos enteros (entire home/apt)",
"variable_airbnb_entire_desc": "Listings que son pisos completos — proxy de conversión residencial → turístico",
"variable_airbnb_revenue": "Ingresos Airbnb L365d",
"variable_airbnb_revenue_desc": "Suma estimada de ingresos de los últimos 365 días por sección",
"variable_airbnb_con_licencia": "Listings con licencia Airbnb",
"variable_airbnb_con_licencia_desc": "Listings con campo `license` rellenado (autorreportado, no validado por Govern)",
"variable_hut_plazas": "Plazas HUT legales",
"variable_hut_plazas_desc": "Plazas registradas en el registro HUT del Consell d'Eivissa (municipio)",
"variable_hut_registros": "Registros HUT",
"variable_hut_registros_desc": "Número de registros HUT activos (municipio)",
"variable_viviendas_totales": "Viviendas totales (INE)",
"variable_viviendas_totales_desc": "Total de viviendas por municipio (Censo 2021, replicado en todas las secciones del municipio)",
"variable_viviendas_vacantes": "Viviendas vacantes (estimación por municipio)",
"variable_viviendas_vacantes_desc": "Estimación del número de viviendas vacantes por municipio, calculada como el 16,2% del total (tasa CCAA del INE, Censo 2021). Es un proxy: la tasa real varía por municipio (más alta en zonas turísticas). Total Baleares: 105.632 vacantes.",
"variable_consumo_p50": "Mediana consumo eléctrico (kWh)",
"variable_consumo_p50_desc": "Percentil 50 de consumo eléctrico anual por distrito (Censo 2021). Mediana de lo que consume un hogar en el distrito.",
"variable_consumo_p10_muy_bajo": "Cola de bajo consumo (p10)",
"variable_consumo_p10_muy_bajo_desc": "Distrito con percentil 10 de consumo eléctrico inferior a 1.000 kWh/año. Una p10 baja indica que hay un grupo numeroso de casas con consumo muy reducido (vacías, segundas residencias, consumo testimonial). Rojo = riesgo elevado de semi-vacío.",
"variable_consumo_p10_bajo": "Cola de consumo bajo (p10)",
"variable_consumo_p10_bajo_desc": "Distrito con p10 entre 1.000 y 2.000 kWh/año. Naranja = posible uso estacional o semi-vacío parcial.",
"variable_consumo_p50_medio": "Consumo medio (p50)",
"variable_consumo_p50_medio_desc": "Distrito con p50 entre 2.500 y 3.500 kWh/año. Amarillo = consumo residencial estándar. La mediana de Baleares es 2.869 kWh.",
"variable_consumo_p90_alto": "Cola de consumo alto (p90)",
"variable_consumo_p90_alto_desc": "Distrito con p90 superior a 4.500 kWh/año. Verde = sin colas de bajo consumo, perfil residencial estable con AC intensivo o familia numerosa.",
"variable_airbnb_per_1000": "Presión Airbnb (por 1000 viviendas)",
"variable_airbnb_per_1000_desc": "Listings Airbnb por cada 1000 viviendas del municipio. Medida relativa de la presión turística sobre el parque residencial.",
"variable_ratio_p10_p50": "Ratio p10/p50 consumo eléctrico (proxy semi-vacío)",
"variable_ratio_p10_p50_desc": "Cociente entre el percentil 10 y la mediana del consumo eléctrico del distrito (Censo 2021). Bajo (<0.3) = hay un grupo numeroso de casas con consumo muy bajo, posibles vacías o segundas residencias. Alto (~0.6+) = distribución más homogénea de uso residencial estable.",
"group_vivienda": "Vivienda y presión",
"variable_viviendas_totales": "Viviendas totales",
"variable_viviendas_totales_desc": "Total de viviendas censadas por sección censal (INE 2021).",
"variable_possibles_vacants": "Posibles vacantes",
"variable_possibles_vacants_desc": "Secciones donde la cola baja del consumo eléctrico es muy baja (p10). Cuanto más bajo el p10, más probable que haya viviendas vacías o 2ª residencia (proxy Holm 2025).",
"variable_pressio_turistica": "Presión turística",
"variable_pressio_turistica_desc": "Mallorca y Menorca: listings de Airbnb activos (junio 2026). Eivissa: plazas de vivienda turística legal — HUT (julio 2026).",
"legend_title": "Leyenda",
"legend_scale_label": "Escala",
"legend_applies_to": "Aplica a",