Merge pull request #1 from itziarZG/fase-3-informe

feat(fase-3): informe Holm, mapa simplificat, transparència de fonts
This commit is contained in:
Itziar ZG
2026-07-16 16:20:40 +02:00
committed by GitHub
21 changed files with 749 additions and 328 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Estadisticas descriptivas - Cases Tancades
Generado: 2026-07-15 12:24
Generado: 2026-07-15 17:32
## Cobertura geografica
Binary file not shown.
+22 -22
View File
@@ -1,5 +1,27 @@
{
"resumen_por_isla": [
{
"isla": "Eivissa",
"secciones": 76,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 32953,
"hut_plazas": 252243
},
{
"isla": "Formentera",
"secciones": 6,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 8250,
"hut_plazas": 89610
},
{
"isla": "Mallorca",
"secciones": 545,
@@ -21,28 +43,6 @@
"airbnb_con_licencia": 3379,
"hut_registros": 0,
"hut_plazas": 0
},
{
"isla": "Eivissa",
"secciones": 76,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 2364,
"hut_plazas": 18184
},
{
"isla": "Formentera",
"secciones": 6,
"airbnb_listings": 0,
"airbnb_entire_homes": 0,
"airbnb_revenue_total": 0,
"airbnb_hosts_unicos": 0,
"airbnb_con_licencia": 0,
"hut_registros": 1375,
"hut_plazas": 14935
}
]
}
Binary file not shown.
+4
View File
@@ -300,6 +300,10 @@ def main() -> None:
np.nan,
)
# Estimación de viviendas vacantes por municipio (proxy a la tasa CCAA del 16,2%).
# Sección censal hereda el valor del municipio.
gdf["viviendas_vacantes_estimadas"] = (gdf["viviendas_totales"] * 0.162).round().astype(int)
# Viviendas INE a nivel CCAA (documentado en /metodologia)
gdf["viviendas_vacias_pct_ccaa"] = 16.2
gdf["viviendas_uso_esporadico_pct_ccaa"] = 6.9
+1
View File
@@ -42,6 +42,7 @@ def main() -> None:
"consumo_p75_kwh", "consumo_p90_kwh",
"airbnb_listings_por_1000_viviendas",
"ratio_p10_p50_consumo",
"viviendas_vacantes_estimadas",
"geometry",
]
cols = [c for c in cols if c in gdf.columns]
Binary file not shown.
@@ -0,0 +1,60 @@
---
import { t, tArray, type Lang } from "../i18n";
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const fmt = new Intl.NumberFormat(lang === "ca" ? "ca-ES" : "es-ES");
const buckets = tArray<{
label: string;
viviendas: number;
pct: number;
interpretacion: string;
}>(lang, "map.consumo_distribucion_buckets");
const totalViviendas = buckets.reduce((s, b) => s + b.viviendas, 0);
const maxPct = buckets.length ? Math.max(...buckets.map((b) => b.pct)) : 1;
---
<section class="max-w-6xl mx-auto px-4 py-12">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-2">
{t(lang, "map.consumo_distribucion_title")}
</h2>
<p class="text-stone-600 mb-6 max-w-3xl" set:html={t(lang, "map.consumo_distribucion_intro")} />
<div class="bg-white border border-stone-200 rounded-lg p-6 mb-4">
<div class="flex items-baseline gap-3 mb-6">
<span class="text-sm text-stone-500">
{t(lang, "map.consumo_distribucion_total_label")}:
</span>
<span class="text-3xl font-bold text-stone-900 tabular-nums">
{fmt.format(totalViviendas)}
</span>
</div>
<ul class="space-y-4">
{buckets.map((b) => (
<li>
<div class="flex items-baseline justify-between mb-1 gap-3 flex-wrap">
<span class="text-sm font-medium text-stone-800">{b.label}</span>
<span class="text-sm text-stone-600 tabular-nums">
<strong class="text-stone-900">{fmt.format(b.viviendas)}</strong>
<span class="text-stone-400"> · </span>
{b.pct.toFixed(1)}%
</span>
</div>
<div class="h-7 bg-stone-100 rounded relative overflow-hidden">
<div
class="absolute inset-y-0 left-0 bg-orange-600"
style={`width: ${(b.pct / maxPct) * 100}%`}
>
</div>
</div>
<p class="text-xs text-stone-600 mt-1" set:html={b.interpretacion} />
</li>
))}
</ul>
</div>
<p class="text-xs text-stone-500 italic" set:html={t(lang, "map.consumo_distribucion_footer")} />
</section>
+1
View File
@@ -27,5 +27,6 @@ const year = new Date().getFullYear();
© {year} Cases Tancades · CC BY-SA 4.0 ·
<a href="https://github.com/itziarZG/Vivienda" class="hover:text-orange-700 underline underline-offset-2">codi font</a>
</p>
<p class="text-xs text-stone-500 mt-2 italic" set:html={t(lang, "footer.based_on")} />
</div>
</footer>
+10 -10
View File
@@ -4,7 +4,7 @@ import LanguageToggle from "./LanguageToggle.astro";
interface Props {
lang?: Lang;
active: "home" | "map" | "methodology" | "islands" | "data" | "press" | "about";
active: "home" | "map" | "methodology" | "islands" | "data" | "press" | "about" | "informe";
}
const { lang: langProp, active } = Astro.props;
@@ -15,10 +15,10 @@ const isMap = path.startsWith("/mapa");
const isMetod = path.startsWith("/metodologia");
const isIslands = path.startsWith("/illes");
const isData = path.startsWith("/dades");
const isPress = path.startsWith("/premsa");
const isAbout = path.startsWith("/qui-som");
const isInforme = path.startsWith("/informe");
const navLink = (key: "home" | "map" | "methodology" | "islands" | "data" | "press" | "about") =>
const navLink = (key: "home" | "map" | "methodology" | "islands" | "data" | "press" | "about" | "informe") =>
`text-sm font-medium px-3 py-1.5 rounded transition-colors ${
active === key
? "text-orange-700 bg-orange-50"
@@ -51,6 +51,13 @@ const navLink = (key: "home" | "map" | "methodology" | "islands" | "data" | "pre
>
{t(lang, "nav.map")}
</a>
<a
href="/informe"
class={navLink("informe")}
aria-current={isInforme ? "page" : undefined}
>
{t(lang, "nav.informe")}
</a>
<a
href="/illes"
class={navLink("islands")}
@@ -65,13 +72,6 @@ const navLink = (key: "home" | "map" | "methodology" | "islands" | "data" | "pre
>
{t(lang, "nav.data")}
</a>
<a
href="/premsa"
class={navLink("press")}
aria-current={isPress ? "page" : undefined}
>
{t(lang, "nav.press")}
</a>
<a
href="/qui-som"
class={navLink("about")}
+281 -211
View File
@@ -8,37 +8,32 @@ interface VariableOption {
labelKey: string;
descKey: string;
scope: "airbnb" | "hut" | "all";
groupKey: string;
}
const variableOptions: VariableOption[] = [
{ value: "airbnb_listings", labelKey: "map.variable_airbnb_listings", descKey: "map.variable_airbnb_listings_desc", scope: "airbnb" },
{ value: "airbnb_entire_homes", labelKey: "map.variable_airbnb_entire", descKey: "map.variable_airbnb_entire_desc", scope: "airbnb" },
{ value: "airbnb_revenue_total", labelKey: "map.variable_airbnb_revenue", descKey: "map.variable_airbnb_revenue_desc", scope: "airbnb" },
{ value: "airbnb_con_licencia", labelKey: "map.variable_airbnb_con_licencia", descKey: "map.variable_airbnb_con_licencia_desc", scope: "airbnb" },
{ value: "airbnb_per_1000", labelKey: "map.variable_airbnb_per_1000", descKey: "map.variable_airbnb_per_1000_desc", scope: "airbnb" },
{ value: "hut_plazas", labelKey: "map.variable_hut_plazas", descKey: "map.variable_hut_plazas_desc", scope: "hut" },
{ value: "hut_registros", labelKey: "map.variable_hut_registros", descKey: "map.variable_hut_registros_desc", scope: "hut" },
{ value: "viviendas_totales", labelKey: "map.variable_viviendas_totales", descKey: "map.variable_viviendas_totales_desc", scope: "all" },
{ value: "consumo_p50_kwh", labelKey: "map.variable_consumo_p50", descKey: "map.variable_consumo_p50_desc", scope: "all" },
{ value: "ratio_p10_p50_consumo", labelKey: "map.variable_ratio_p10_p50", descKey: "map.variable_ratio_p10_p50_desc", scope: "all" },
// 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: "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");
---
<section class="border-b border-stone-200 bg-white">
<div class="max-w-6xl mx-auto px-4 py-8">
<h1 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-2">
<div class="max-w-6xl mx-auto px-4 py-2 sm:py-3">
<h1 class="text-lg sm:text-xl font-bold text-stone-900 leading-tight mb-0.5">
{t(lang, "map.title")}
</h1>
<p class="text-stone-600">{t(lang, "map.lead")}</p>
<p class="text-xs sm:text-sm text-stone-600 leading-snug">{t(lang, "map.lead")}</p>
</div>
</section>
<div
id="map-shell"
class="relative w-full"
style="height: calc(100vh - 60px); min-height: 480px;"
style="height: clamp(420px, 62vh, 640px);"
data-lang={lang}
data-popup-title-tmpl={popupTitleTmpl}
>
@@ -52,23 +47,43 @@ const popupTitleTmpl = t(lang, "map.popup_title");
</div>
<div
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)]"
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)] max-h-[calc(100%-1.5rem)] overflow-y-auto"
>
<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"
>
</p>
<div class="space-y-2">
{variableOptions.map((o) => (
<option value={o.value} data-scope={o.scope}>{t(lang, o.labelKey)}</option>
<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>
))}
</select>
<p id="variable-desc" class="text-[10px] text-stone-500 mt-1 leading-tight"></p>
</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")}
@@ -85,7 +100,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
</div>
<div
class="absolute bottom-3 right-3 z-20 bg-white/95 backdrop-blur border border-stone-200 rounded-lg shadow-sm p-3 max-w-xs text-xs"
class="absolute bottom-3 right-3 z-20 bg-white/95 backdrop-blur border border-stone-200 rounded-lg shadow-sm p-3 max-w-xs text-xs max-h-[calc(100%-1.5rem)] overflow-y-auto"
>
<p class="font-medium text-stone-800 mb-2">
{t(lang, "map.legend_title")}
@@ -114,12 +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",
viviendas_vacantes: lang === "es" ? "Viv. vacantes (estim.)" : "Hab. vacants (estim.)",
consumo_p10: lang === "es" ? "Consumo p10 (kWh)" : "Consum p10 (kWh)",
consumo_p50: lang === "es" ? "Mediana consumo kWh" : "Mediana consum kWh",
airbnb_per_1000: lang === "es" ? "Airbnb / 1000 viv." : "Airbnb / 1000 hab.",
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",
@@ -129,50 +147,34 @@ 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",
popup_source_ine: lang === "es" ? "Fuente: INE, Censo 2021, tabla 59531" : "Font: INE, Cens 2021, taula 59531",
popup_source_airbnb: lang === "es" ? "Fuente: Inside Airbnb (Mallorca y Menorca, junio 2026)" : "Font: Inside Airbnb (Mallorca i Menorca, juny 2026)",
popup_source_hut_eivissa: lang === "es" ? "Fuente: Registro HUT, Consell d'Eivissa (julio 2026)" : "Font: Registre HUT, Consell d'Eivissa (juliol 2026)",
popup_source_ibestat_formentera: lang === "es" ? "Fuente: IBESTAT 2019 (agregado isla, Formentera)" : "Font: IBESTAT 2019 (agregat illa, Formentera)",
};
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, {
@@ -185,6 +187,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",
@@ -198,46 +238,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",
@@ -251,71 +251,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,
},
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) =>
@@ -462,14 +397,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);
@@ -484,6 +432,27 @@ 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/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 =
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);
});
// Mantener el toggle visual en sync con la realidad
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;
@@ -491,16 +460,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("");
@@ -525,9 +513,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;
@@ -580,16 +573,6 @@ const popupTitleTmpl = t(lang, "map.popup_title");
const gran = String(p.granularidad ?? "—");
const granLabel =
gran === "seccion_censal" ? T.seccion_censal : gran === "municipio" ? T.municipio : gran;
const listings = Number(p.airbnb_listings ?? 0);
const entire = Number(p.airbnb_entire_homes ?? 0);
const conLic = Number(p.airbnb_con_licencia ?? 0);
const plazas = Number(p.airbnb_accommodates_total ?? 0);
const revenue = Number(p.airbnb_revenue_total ?? 0);
const regs = Number(p.hut_registros ?? 0);
const hPlazas = Number(p.hut_plazas ?? 0);
const viviendas = Number(p.viviendas_totales ?? 0);
const consumoP50 = Number(p.consumo_p50_kwh ?? 0);
const airbnbPer1000 = Number(p.airbnb_listings_por_1000_viviendas ?? 0);
const lines: string[] = [];
lines.push(`<div class="font-bold text-sm mb-1">${escape(title)}</div>`);
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.municipi}:</span> ${escape(String(p.NMUN ?? "—"))}</div>`);
@@ -598,35 +581,122 @@ const popupTitleTmpl = t(lang, "map.popup_title");
const sectionTitle = (label: string) =>
`<div class="text-[10px] uppercase tracking-wide text-stone-500 mt-2 mb-0.5 border-t border-stone-200 pt-1">${label}</div>`;
// Always show INE data (universal)
if (viviendas) {
lines.push(sectionTitle("INE (Cens 2021)"));
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.viviendas_totales}:</span> <b>${fmt(viviendas)}</b></div>`);
if (consumoP50) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.consumo_p50}:</span> <b>${fmt(consumoP50)} kWh</b></div>`);
const cfg = LAYERS[activeVariable];
const cfgField = cfg?.field ?? "";
type FieldMeta = { label: { ca: string; es: string }; unit?: string; hint: { ca: string; es: string } };
const FIELD: Record<string, FieldMeta> = {
viviendas_totales: {
label: { ca: "Habitatges totals", es: "Viviendas totales" },
hint: { ca: "cens INE 2021, replicat a cada secció del municipi", es: "censo INE 2021, replicado en cada sección del municipio" },
},
viviendas_vacantes_estimadas: {
label: { ca: "Possibles vacants (estim.)", es: "Posibles vacantes (estim.)" },
hint: { ca: "estimació = 16,2% × habitatges (taxa CCAA)", es: "estimación = 16,2% × viviendas (tasa CCAA)" },
},
consumo_p10_kwh: {
label: { ca: "Consum p10", es: "Consumo p10" },
unit: "kWh/any",
hint: { ca: "el 10% de llars d'aquesta secció consumeix menys", es: "el 10% de hogares de esta sección consume menos" },
},
consumo_p50_kwh: {
label: { ca: "Mediana consum", es: "Mediana consumo" },
unit: "kWh/any",
hint: { ca: "mediana del consum anual de la secció", es: "mediana del consumo anual de la sección" },
},
ratio_p10_p50_consumo: {
label: { ca: "Ratio p10/p50", es: "Ratio p10/p50" },
hint: { ca: "<0,3 = quasi-vacant · >0,6 = ús estable", es: "<0,3 = casi vacía · >0,6 = uso estable" },
},
airbnb_listings: {
label: { ca: "Listings Airbnb", es: "Listings Airbnb" },
hint: { ca: "anuncis actius (juny 2026, Inside Airbnb)", es: "anuncios activos (junio 2026, Inside Airbnb)" },
},
airbnb_entire_homes: {
label: { ca: "Pisos sencers (entire home/apt)", es: "Pisos enteros (entire home/apt)" },
hint: { ca: "pisos complets — proxy de conversió residencial → turístic", es: "pisos completos — proxy de conversión residencial → turístico" },
},
airbnb_accommodates_total: {
label: { ca: "Places Airbnb", es: "Plazas Airbnb" },
hint: { ca: "places ofertes pels anuncis", es: "plazas ofertadas por los anuncios" },
},
airbnb_revenue_total: {
label: { ca: "Ingressos L365d", es: "Ingresos L365d" },
unit: "€",
hint: { ca: "suma estimada últims 365 dies per secció", es: "suma estimada últimos 365 días por sección" },
},
hut_registros: {
label: { ca: "Registres HUT", es: "Registros HUT" },
hint: { ca: "registres oficials (Consell d'Eivissa, juliol 2026)", es: "registros oficiales (Consell d'Eivissa, julio 2026)" },
},
hut_plazas: {
label: { ca: "Places HUT", es: "Plazas HUT" },
hint: { ca: "places legals ofertes (municipi)", es: "plazas legales ofertadas (municipio)" },
},
};
const row = (field: string, v: number) => {
if (!v) return "";
if (field === cfgField) return "";
const info = FIELD[field];
if (!info) return "";
const label = info.label[lang];
const unit = info.unit ? ` <span class="text-stone-400 text-[10px]">${escape(info.unit)}</span>` : "";
const hint = `<div class="text-[10px] text-stone-400 leading-snug">${escape(info.hint[lang])}</div>`;
return `<div class="mt-1.5"><span class="text-stone-500 text-xs">${escape(label)}:</span> <b class="text-xs">${fmt(v)}</b>${unit}${hint}</div>`;
};
const highlight = (field: string, v: number) => {
if (!v) return "";
const info = FIELD[field];
const label = info ? info.label[lang] : cfg?.label ?? field;
const unit = info?.unit ? ` <span class="text-stone-300 text-xs">${escape(info.unit)}</span>` : "";
const hint = info ? `<div class="text-[11px] text-stone-300 leading-snug mt-0.5">${escape(info.hint[lang])}</div>` : "";
return `<div class="bg-stone-900 text-stone-50 rounded p-2 mt-2"><div class="text-[11px] uppercase tracking-wide opacity-70">${escape(label)}</div><div class="text-xl font-bold">${fmt(v)}${unit}</div>${hint}</div>`;
};
if (kind === "all") {
const cfgVal = Number(p[cfgField] ?? 0);
lines.push(highlight(cfgField, cfgVal));
const remaining = [
"viviendas_totales",
"viviendas_vacantes_estimadas",
"consumo_p10_kwh",
"consumo_p50_kwh",
"ratio_p10_p50_consumo",
].filter((f) => f !== cfgField);
lines.push(sectionTitle("Més dades INE (Cens 2021)"));
for (const f of remaining) lines.push(row(f, Number(p[f] ?? 0)));
} else if (kind === "airbnb") {
const cfgVal = Number(p[cfgField] ?? 0);
lines.push(highlight(cfgField, cfgVal));
const remaining = [
"airbnb_listings",
"airbnb_entire_homes",
"airbnb_accommodates_total",
"airbnb_revenue_total",
].filter((f) => f !== cfgField);
lines.push(sectionTitle("Més dades Airbnb (juny 2026)"));
for (const f of remaining) lines.push(row(f, Number(p[f] ?? 0)));
} else if (kind === "hut") {
const cfgVal = Number(p[cfgField] ?? 0);
lines.push(highlight(cfgField, cfgVal));
const remaining = ["hut_registros", "hut_plazas"].filter((f) => f !== cfgField);
lines.push(sectionTitle("Més dades HUT (juliol 2026)"));
for (const f of remaining) lines.push(row(f, Number(p[f] ?? 0)));
}
if (kind === "airbnb") {
lines.push(sectionTitle("Airbnb"));
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
if (entire) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.entire_homes}:</span> <b>${fmt(entire)}</b></div>`);
if (conLic) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.con_licencia}:</span> <b>${fmt(conLic)}</b></div>`);
if (plazas) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings} (plazas):</span> <b>${fmt(plazas)}</b></div>`);
if (revenue) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.revenue}:</span> <b>${fmtMoney(revenue)}</b></div>`);
if (airbnbPer1000) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.airbnb_per_1000}:</span> <b>${airbnbPer1000.toFixed(1)}</b></div>`);
} else if (kind === "hut") {
lines.push(sectionTitle("HUT"));
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(hPlazas)}</b></div>`);
} else {
// kind === "all" — show both Airbnb and HUT data if present
if (listings || hPlazas) {
lines.push(sectionTitle("Llocs actius"));
if (listings) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
if (regs) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
if (hPlazas) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(hPlazas)}</b></div>`);
let sourceKey = "popup_source_ine";
if (activeVariable === "pressio_turistica") {
if (kind === "airbnb") sourceKey = "popup_source_airbnb";
else if (kind === "hut") {
sourceKey = String(p.isla) === "Formentera"
? "popup_source_ibestat_formentera"
: "popup_source_hut_eivissa";
}
}
lines.push(`<div class="text-[10px] text-stone-500 mt-1 italic">${T.granularitat}: ${granLabel}</div>`);
lines.push(`<div class="text-[10px] text-stone-500 mt-1.5 leading-snug">${escape(T[sourceKey] ?? "")}</div>`);
lines.push(`<div class="text-[10px] text-stone-400 italic">${T.granularitat}: ${granLabel}</div>`);
new Popup({ closeButton: true })
.setLngLat(e.lngLat)
.setHTML(lines.join(""))
+97 -23
View File
@@ -12,7 +12,8 @@
"islands": "Per illa",
"data": "Dades",
"press": "Premsa",
"about": "Qui som"
"about": "Qui som",
"informe": "Informe"
},
"lang_toggle": {
"label": "Idioma",
@@ -42,7 +43,7 @@
"narrative_title": "El que sabem, el que no sabem",
"narrative_p1": "Hi ha 18.175 pisos oferts a Airbnb només a Mallorca i Menorca. Si cada pis allotja 4 persones de mitjana, parlem de 105.738 places turístiques — gairebé la meitat de la població de Palma.",
"narrative_p2": "A Eivissa, el Govern reconeix 18.184 places amb llicència HUT. Però el 40% es concentren a Sant Josep de sa Talaia, un municipi rural. A Eivissa capital — 50.000 habitants — només 41 registres amb 236 places.",
"narrative_p3": "La pregunta no és si hi ha pisos buits. La pregunta és on són les 7.000 places que s'ofereixen a Airbnb a Eivissa si el registre legal només en compta 236 al casc urbà.",
"narrative_p3": "A Balears **sobran pisos**. 105.632 habitatges estan buits i 102.597 més s'ofereixen a l'estranger sense tributar aquí. El problema no és construir més, és mobilitzar els que ja existeixen. (Holm, 2025)",
"section_per_island_title": "Per illa",
"col_island": "Illa",
"col_sections": "Seccions",
@@ -68,26 +69,25 @@
"layer_choropleth": "Plànol coroplètic",
"layer_clusters": "Punts Airbnb (clústers)",
"variable_label": "Variable",
"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_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_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": "Cada illa té la seva font oficial. Mallorca i Menorca → Airbnb (Inside Airbnb, juny 2026). Eivissa → HUT legal (Consell d'Eivissa, juliol 2026). Formentera → IBESTAT 2019 (única font pública disponible).",
"popup_source_ine": "Font: INE, Cens de Població i Vivendes 2021, taula 59531",
"popup_source_airbnb": "Font: Inside Airbnb (Mallorca i Menorca, juny 2026)",
"popup_source_hut_eivissa": "Font: Registre HUT, Consell d'Eivissa (juliol 2026)",
"popup_source_ibestat_formentera": "Font: IBESTAT, establiments turístics (2019, agregat illa)",
"sources_strip_title": "Per què cada illa té una capa diferent?",
"sources_strip_intro": "Cada illa té un ventall de fonts públiques diferent. No és una tria editorial — és el que hi ha disponible. Resum:",
"sources_strip_pm": "Mallorca i Menorca",
"sources_strip_pm_desc": "Airbnb (Inside Airbnb, juny 2026)",
"sources_strip_eivissa": "Eivissa",
"sources_strip_eivissa_desc": "HUT legal (Consell d'Eivissa, juliol 2026)",
"sources_strip_formentera": "Formentera",
"sources_strip_formentera_desc": "IBESTAT 2019 (única publicació, agregat illa)",
"legend_title": "Llegenda",
"legend_scale_label": "Escala",
"legend_applies_to": "Aplica a",
@@ -96,6 +96,17 @@
"legend_islands_all_municipio": "Totes (municipi)",
"legend_islands_all_distrito": "Totes (districte)",
"legend_islands_mallorca_menorca_only": "Només Mallorca / Menorca (cal Airbnb per calcular)",
"consumo_distribucion_title": "Distribució del consum elèctric a les Balears (Cens 2021)",
"consumo_distribucion_intro": "Aquestes dades provenen de l'informe Holm 2025, basat en l'INE 59531. Són a nivell de **tota la comunitat autònoma** (no per municipi), perquè el tram de consum s'agrega al nivell de secció censal a la publicació original. Per això apareixen com a context al costat del mapa, no com a capa pintable.",
"consumo_distribucion_total_label": "Total d'habitatges a Balears",
"consumo_distribucion_footer": "Font: INE, Cens de Població i Vivendes 2021, taula 59531. Interpretació: el 16,2% d'habitatges amb consum 0 són probablement vacants; el 12,4% amb consum < 1.000 kWh són probablement ús esporàdic (2a residència o estacional); només el 71% té consum estable. (Holm, 2025)",
"consumo_distribucion_buckets": [
{ "label": "Consum 0 (sense subministrament)", "viviendas": 105632, "pct": 16.2, "interpretacion": "<strong>Vacants reals</strong>: habitatges sense contracte de llum actiu. Són el gruix de la bossa de pisos buits." },
{ "label": "Menys de 250 kWh/any", "viviendas": 13466, "pct": 2.1, "interpretacion": "<strong>Consum molt baix</strong>: ús testimonial (persones grans que quasi no viuen a la casa, o visites molt esporàdiques)." },
{ "label": "Entre 251 i 1.000 kWh/any", "viviendas": 67202, "pct": 10.3, "interpretacion": "<strong>Ús esporàdic</strong>: 2a residència de temporada (sobretot estiu). Aquesta bossa xuta amb el turisme." },
{ "label": "Entre 1.001 i 4.000 kWh/any", "viviendas": 298871, "pct": 45.8, "interpretacion": "<strong>Consum mitjà</strong>: residència habitual estable. La mediana (2.869 kWh) cau en aquesta banda." },
{ "label": "Més de 4.000 kWh/any", "viviendas": 166952, "pct": 25.6, "interpretacion": "<strong>Consum alt</strong>: família nombrosa, AC intensiu, o doble ús (residencial + part del dia com a oficina)." }
],
"legend_empty": "Sense dades per a aquesta variable en aquesta illa",
"legend_airbnb_title": "Mallorca / Menorca",
"legend_airbnb_desc": "Listings actius a Airbnb per secció censal",
@@ -151,7 +162,8 @@
"license_body": "Codi i dades publicats sota CC BY-SA 4.0. Pots reutilitzar, adaptar i difondre amb atribució i la mateixa llicència.",
"contact": "Contacte",
"snap_grid_label": "Dada",
"snap_grid_value": "Data"
"snap_grid_value": "Data",
"report_context": "Aquest projecte es basa en l'informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (desembre 2025 - gener 2026). Les dades quantitatives provenen de fonts oficials (INE, Ministeri de Transports, MIVAU, Govern Balear). Les conclusions qualitatives i propostes polítiques són de l'informe de Holm, pendent de revisió per l'autora. L'informe encara no està publicat; totes les cites ho refereixen amb atribució."
},
"common": {
"and": "i",
@@ -243,6 +255,18 @@
"lead": "Material per a periodistes i mitjans que vulguin cobrir el tema. Cifres citables, bio del projecte i contacte.",
"section_quoteable_title": "Cifres citables",
"section_quoteable_lead": "Una frase per dada. Totes amb font pública. Pots reproduir-les lliurement amb atribució a Cases Tancades.",
"quoteables": [
{ "number": "18.175", "text": "Listings actius a Airbnb a Mallorca i Menorca", "source": "Inside Airbnb, juny 2026" },
{ "number": "18.184", "text": "Places turístiques legals (HUT) a Eivissa", "source": "Registre HUT Consell d'Eivissa, 14 juliol 2026" },
{ "number": "40%", "text": "De les places HUT d'Eivissa es concentren a Sant Josep de sa Talaia", "source": "Elaboració pròpia a partir del registre HUT" },
{ "number": "16,2%", "text": "D'habitatges buits a Balears", "source": "INE, 2024" },
{ "number": "$258M", "text": "Ingressos estimats d'Airbnb els darrers 365 dies, Mallorca + Menorca", "source": "Inside Airbnb, juny 2026" },
{ "number": "95,4%", "text": "Dels listings actius són pisos sencers (Entire home/apt)", "source": "Inside Airbnb, juny 2026" },
{ "number": "102.597", "text": "Habitatges de Balears oferts a l'estranger sense llicència ni tributar", "source": "Holm 2025 (4 cerques VPN a Holidu, Hometogo, Feriebolig-spania)" },
{ "number": "4.924M €", "text": "Renta anual estimada que NO tributa a Balears per aquests lloguers opacs al fisc", "source": "Holm 2025 (càlcul: 102.597 × 2.000€/set × 24 set)" },
{ "number": "1.181M €", "text": "Pèrdua impositiva estimada per a Balears (24% sobre ingressos bruts)", "source": "Holm 2025" },
{ "number": "208.470", "text": "Habitatges entre vacies (105.632) + ofertes a l'estranger (102.597) que NO estan al mercat per a residents", "source": "Holm 2025 (suma de 105.632 + 102.597)" }
],
"section_bio_title": "Sobre el projecte",
"section_bio_body": "Cases Tancades és una iniciativa ciutadana independent que creua dades oficials d'habitatge semi-buit amb lloguer turístic a les Illes Balears. El projecte no té afiliació política ni financament públic o privat. El codi i les dades es publiquen sota llicència oberta per facilitar la verificació i la difusió.",
"section_contact_title": "Contacte",
@@ -283,5 +307,55 @@
"contribute_sources": " Proposar noves fonts de dades públiques que es puguin afegir",
"contribute_share": " Difondre el projecte a través de xarxes o mitjans",
"section_methodology_link": "Per saber com hem fet el càlcul, consulta la"
},
"informe": {
"title": "L'informe de la vivienda a Balears",
"eyebrow": "Anàlisi basada en",
"lead": "Aquesta web es basa en l'informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (desembre 2025 - gener 2026), un treball d'anàlisi que combina dades oficials amb recerca específica sobre l'activitat especulativa a les illes. L'informe encara no està publicat; aquesta pàgina en resumeix les conclusions principals, totes elles referenciades amb atribució.",
"resumen_title": "Resum executiu",
"resumen_p1": "A les Balears hi ha **105.632 habitatges vacais** (16,2% del parc), segons dades de l'INE basades en el consum elèctric del Cens de Població 2021. A aquestes s'hi sumen **102.597 habitatges oferts a l'estranger** a través de portals com Holidu.dk, Hometogo.se, Feriebolig-spania.no i Hometogo.de, segons una recerca específica de l'autora amb VPN per evitar el filtratge geogràfic. (Holm, 2025)",
"resumen_p2": "El **preu mitjà de la vivenda en venda** a les Balears va assolir els **5.114 €/m²** al novembre de 2025, un 9,1% més que l'any anterior, segons Idealista. El **preu de lloguer** se situa al voltant dels **19,1 €/m²**. Les Balears són la comunitat autònoma amb el major percentatge de pisos turístics sobre el total de vivendes censades: un **3,84%** reconegut oficialment, que en termes absoluts suposa **32.606 habitatges**, sense comptar els il·legals que no figuren en cap registre. (Idealista; INE 2024)",
"resumen_p3": "Aquestes xifres dibuixen un patró: **el 60% de les compravendes les fan persones jurídiques**, i **el 25% dels compradors ja no resideix a les illes** (era el 10% el 2007). El **46% dels propietaris** tenen més d'un habitatge. La taxa d'esforç (el percentatge d'ingressos que es dedica a habitatge) arriba al **29,35%** a les Balears, gairebé 10 punts per sobre de la mitjana nacional.",
"resumen_p4": "L'informe conclou que **a les Balears sobren pisos** — el dèficit no és de construcció, sinó d'accés al mercat. El pla del Govern Balear (Llei 4/25 de Projectes Residencials Estratègics) aposta per construir 27.000 vivendes addicionals, però si les 208.470 vivendes detectades com a vacants o opaques al fisc s'incorporessin al mercat, la pressió sobre els preus baixaria.",
"resumen_p5": "L'autora proposa mesures fiscals (impostos progressius a vivendes vacants en mans de grans tenidors), una nova llei hipotecària (subhasta obligatòria i cancel·lació total del deute amb la dació en pagament), i un control estricte de l'alquiler turístic. La conclusió és clara: la solució a la crisi de l'habitatge a les Balears **no és construir més, sinó mobilitzar el que ja existeix**.",
"resumen_p6": "Les dades del Col·legi Oficial d'Arquitectes de les Illes Balears (COAIB, observatori semestral juliol 2025) confirmen aquesta dinàmica: en el primer semestre de 2025 es visaren **4.311 habitatges de nova creació** (+34,68% interanual), però les **vivendes protegides van caure un 33,87%**. És a dir, es construeix més, però la proporció d'habitatge assequible baixa. La pròpia memòria del COAIB conclou: 'la promoció d'habitatge lliure no podrà solucionar l'escassetat d'habitatge per a la població que més la pateix'.",
"cifras_title": "Cifres clau de l'informe",
"cifras_intro": "Aquestes són les xifres més impactants de l'anàlisi. Totes amb atribució completa a (Holm, 2025) o a la font original.",
"cifras": [
{ "value": "105.632", "label": "Habitatges vacais detectats per consum elèctric (Cens 2021)", "source": "INE (Cens 2021) / Holm, 2025" },
{ "value": "102.597", "label": "Habitatges oferts a l'estranger sense tributar a Espanya", "source": "4 cerques VPN (DK, SE, NO, DE) / Holm, 2025" },
{ "value": "208.470", "label": "Total vivendes 'fora del mercat' per a residents (105.632 + 102.597)", "source": "Suma Holm, 2025" },
{ "value": "4.924M €", "label": "Renta anual que no tributa per aquests lloguers opacs", "source": "Càlcul: 102.597 × 2.000€/setmana × 24 setmanes / Holm, 2025" },
{ "value": "1.181M €", "label": "Pèrdua impositiva estimada per a les Balears (24% sobre ingressos bruts)", "source": "Holm, 2025" },
{ "value": "5.114 €/m²", "label": "Preu mitjà de la vivenda en venda (novembre 2025)", "source": "Idealista" },
{ "value": "19,1 €/m²", "label": "Preu mitjà del lloguer (2025)", "source": "Idealista" },
{ "value": "32.606", "label": "Habitatges turístics reconeguts (>5% del parc censat, sense comptar els il·legals)", "source": "INE (2025M05)" },
{ "value": "29,35%", "label": "Taxa d'esforç econòmic: percentatge d'ingressos dedicat a habitatge (2023)", "source": "INE / Ministerio de Transportes" },
{ "value": "60% / 25%", "label": "Compradors persones jurídiques / compradors no residents", "source": "INE 2024 / Ministerio de Transportes" },
{ "value": "+34,68%", "label": "Augment interanual d'habitatges de nova creació visats (1r semestre 2025)", "source": "COAIB, observatori semestral juliol 2025" },
{ "value": "-33,87%", "label": "Caiguda interanual d'habitatges protegits visats (1r semestre 2025)", "source": "COAIB, observatori semestral juliol 2025" }
],
"fuentes_title": "Fonts utilitzades per l'informe",
"fuentes_intro": "L'informe combina dades oficials amb recerca específica via VPN per evitar el filtratge geogràfic dels portals d'alquiler turístic.",
"fuentes": [
{ "name": "INE", "desc": "Institut Nacional d'Estadística: Cens de Població i Vivendes 2021, valor de la vivenda, transaccions per residència del comprador" },
{ "name": "Ministeri de Transports, Mobilitat Sostenible", "desc": "Estadístiques de valor de la vivenda i taxa d'esforç" },
{ "name": "Ministeri de Vivenda i Agenda Urbana (MIVAU)", "desc": "Codi de Bones Pràctiques hipotecàries" },
{ "name": "Holidu.dk (Dinamarca)", "desc": "Recerca amb VPN: 11.075 habitatges oferts" },
{ "name": "Hometogo.se (Suècia)", "desc": "Recerca amb VPN: 38.036 habitatges oferts" },
{ "name": "Feriebolig-spania.no (Noruega)", "desc": "Recerca amb VPN: 12.787 cases de vacances" },
{ "name": "Hometogo.de (Alemanya)", "desc": "Recerca amb VPN: 40.699 ofertes" },
{ "name": "COAIB - Col·legi Oficial d'Arquitectes de les Illes Balears", "desc": "Observatori semestral juliol 2025: estadístiques de visat d'obra nova (per tipologia, demarcació, ús). Valida la tendència de construcció sense resolució de l'escassetat." }
],
"autora_title": "Sobre l'autora i l'informe",
"autora_p1": "**Lic. Emma C. Holm** ha elaborat l'informe 'Crisis de la vivienda en Baleares' entre desembre 2025 i gener 2026, amb dades oficials de l'INE, el Ministeri de Transports i la Conselleria d'Habitatge, combinades amb recerca específica en portals d'alquiler turístic estrangers. L'informe inclou anàlisi, conclusions i propostes.",
"autora_p2": "Aquesta web s'ha construït amb el permís de l'autora i serà revisada per ella abans de la publicació definitiva. Totes les xifres i arguments d'aquesta web que provenen de l'informe estan citats explícitament com a (Holm, 2025).",
"autora_p3": "L'informe **encara no està publicat** en cap altre mitjà. A mesura que es publiqui, afegirem aquí l'enllaç o la referència completa.",
"cta_title": "Vés més enllà",
"cta_map": "Veure les dades al mapa interactiu",
"cta_methodology": "Llegir la metodologia completa"
},
"footer": {
"based_on": "Anàlisi basat en l'informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (2025)."
}
}
+97 -23
View File
@@ -12,7 +12,8 @@
"islands": "Por isla",
"data": "Datos",
"press": "Prensa",
"about": "Quiénes somos"
"about": "Quiénes somos",
"informe": "Informe"
},
"lang_toggle": {
"label": "Idioma",
@@ -42,7 +43,7 @@
"narrative_title": "Lo que sabemos, lo que no sabemos",
"narrative_p1": "Hay 18.175 pisos ofertados en Airbnb solo en Mallorca y Menorca. Si cada piso aloja 4 personas de media, son 105.738 plazas turísticas — casi la mitad de la población de Palma.",
"narrative_p2": "En Ibiza, el Govern reconoce 18.184 plazas con licencia HUT. Pero el 40% se concentran en Sant Josep de sa Talaia, un municipio rural. En Ibiza capital — 50.000 habitantes — solo 41 registros con 236 plazas.",
"narrative_p3": "La pregunta no es si hay pisos vacíos. La pregunta es dónde están las 7.000 plazas que se ofertan en Airbnb en Ibiza si el registro legal solo cuenta 236 en el casco urbano.",
"narrative_p3": "En Balears **sobran pisos**. 105.632 viviendas están vacías y 102.597 más se ofertan en el extranjero sin tributar aquí. El problema no es construir más, es movilizar los que ya existen. (Holm, 2025)",
"section_per_island_title": "Por isla",
"col_island": "Isla",
"col_sections": "Secciones",
@@ -68,26 +69,25 @@
"layer_choropleth": "Plano coroplético",
"layer_clusters": "Puntos Airbnb (clústeres)",
"variable_label": "Variable",
"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_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_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": "Cada isla tiene su fuente oficial. Mallorca y Menorca → Airbnb (Inside Airbnb, junio 2026). Eivissa → HUT legal (Consell d'Eivissa, julio 2026). Formentera → IBESTAT 2019 (única fuente pública disponible).",
"popup_source_ine": "Fuente: INE, Censo de Población y Viviendas 2021, tabla 59531",
"popup_source_airbnb": "Fuente: Inside Airbnb (Mallorca y Menorca, junio 2026)",
"popup_source_hut_eivissa": "Fuente: Registro HUT, Consell d'Eivissa (julio 2026)",
"popup_source_ibestat_formentera": "Fuente: IBESTAT, establecimientos turísticos (2019, agregado isla)",
"sources_strip_title": "¿Por qué cada isla tiene una capa distinta?",
"sources_strip_intro": "Cada isla tiene un conjunto de fuentes públicas diferente. No es una elección editorial — es lo que hay disponible. Resumen:",
"sources_strip_pm": "Mallorca y Menorca",
"sources_strip_pm_desc": "Airbnb (Inside Airbnb, junio 2026)",
"sources_strip_eivissa": "Eivissa",
"sources_strip_eivissa_desc": "HUT legal (Consell d'Eivissa, julio 2026)",
"sources_strip_formentera": "Formentera",
"sources_strip_formentera_desc": "IBESTAT 2019 (única publicación, agregado isla)",
"legend_title": "Leyenda",
"legend_scale_label": "Escala",
"legend_applies_to": "Aplica a",
@@ -96,6 +96,17 @@
"legend_islands_all_municipio": "Todas (municipio)",
"legend_islands_all_distrito": "Todas (distrito)",
"legend_islands_mallorca_menorca_only": "Solo Mallorca / Menorca (hace falta Airbnb para calcular)",
"consumo_distribucion_title": "Distribución del consumo eléctrico en Baleares (Censo 2021)",
"consumo_distribucion_intro": "Estos datos provienen del informe Holm 2025, basado en el INE 59531. Son a nivel de **toda la comunidad autónoma** (no por municipio), porque el tramo de consumo se agrega al nivel de sección censal en la publicación original. Por eso aparecen como contexto junto al mapa, no como capa pintable.",
"consumo_distribucion_total_label": "Total de viviendas en Baleares",
"consumo_distribucion_footer": "Fuente: INE, Censo de Población y Viviendas 2021, tabla 59531. Interpretación: el 16,2% de viviendas con consumo 0 son probablemente vacantes; el 12,4% con consumo < 1.000 kWh son probablemente uso esporádico (2ª residencia o estacional); solo el 71% tiene consumo estable. (Holm, 2025)",
"consumo_distribucion_buckets": [
{ "label": "Consumo 0 (sin suministro)", "viviendas": 105632, "pct": 16.2, "interpretacion": "<strong>Vacantes reales</strong>: viviendas sin contrato de luz activo. Son el grueso de la bolsa de pisos vacíos." },
{ "label": "Menos de 250 kWh/año", "viviendas": 13466, "pct": 2.1, "interpretacion": "<strong>Consumo muy bajo</strong>: uso testimonial (personas mayores que casi no viven en la casa, o visitas muy esporádicas)." },
{ "label": "Entre 251 y 1.000 kWh/año", "viviendas": 67202, "pct": 10.3, "interpretacion": "<strong>Uso esporádico</strong>: 2ª residencia de temporada (sobre todo verano). Esta bolsa se mueve con el turismo." },
{ "label": "Entre 1.001 y 4.000 kWh/año", "viviendas": 298871, "pct": 45.8, "interpretacion": "<strong>Consumo medio</strong>: residencia habitual estable. La mediana (2.869 kWh) cae en esta banda." },
{ "label": "Más de 4.000 kWh/año", "viviendas": 166952, "pct": 25.6, "interpretacion": "<strong>Consumo alto</strong>: familia numerosa, AC intensivo, o doble uso (residencial + parte del día como oficina)." }
],
"legend_empty": "Sin datos para esta variable en esta isla",
"legend_airbnb_title": "Mallorca / Menorca",
"legend_airbnb_desc": "Listings activos en Airbnb por sección censal",
@@ -151,7 +162,8 @@
"license_body": "Código y datos publicados bajo CC BY-SA 4.0. Puedes reutilizar, adaptar y difundir con atribución y la misma licencia.",
"contact": "Contacto",
"snap_grid_label": "Dato",
"snap_grid_value": "Fecha"
"snap_grid_value": "Fecha",
"report_context": "Este proyecto se basa en el informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (diciembre 2025 - enero 2026). Los datos cuantitativos provienen de fuentes oficiales (INE, Ministerio de Transportes, MIVAU, Govern Balear). Las conclusiones cualitativas y propuestas políticas son del informe de Holm, pendiente de revisión por la autora. El informe aún no está publicado; todas las citas se refieren con atribución."
},
"common": {
"and": "y",
@@ -243,6 +255,18 @@
"lead": "Material para periodistas y medios que quieran cubrir el tema. Cifras citables, bio del proyecto y contacto.",
"section_quoteable_title": "Cifras citables",
"section_quoteable_lead": "Una frase por dato. Todas con fuente pública. Puedes reproducirlas libremente con atribución a Cases Tancades.",
"quoteables": [
{ "number": "18.175", "text": "Listings activos en Airbnb en Mallorca y Menorca", "source": "Inside Airbnb, junio 2026" },
{ "number": "18.184", "text": "Plazas turísticas legales (HUT) en Ibiza", "source": "Registro HUT Consell d'Eivissa, 14 julio 2026" },
{ "number": "40%", "text": "De las plazas HUT de Ibiza se concentran en Sant Josep de sa Talaia", "source": "Elaboración propia a partir del registro HUT" },
{ "number": "16,2%", "text": "De viviendas vacías en Baleares", "source": "INE, 2024" },
{ "number": "$258M", "text": "Ingresos estimados de Airbnb los últimos 365 días, Mallorca + Menorca", "source": "Inside Airbnb, junio 2026" },
{ "number": "95,4%", "text": "De los listings activos son pisos enteros (Entire home/apt)", "source": "Inside Airbnb, junio 2026" },
{ "number": "102.597", "text": "Viviendas de Baleares ofrecidas en el extranjero sin licencia ni tributar", "source": "Holm 2025 (4 búsquedas VPN en Holidu, Hometogo, Feriebolig-spania)" },
{ "number": "4.924M €", "text": "Renta anual estimada que NO tributa en Baleares por estos alquileres opacos al fisco", "source": "Holm 2025 (cálculo: 102.597 × 2.000€/sem × 24 sem)" },
{ "number": "1.181M €", "text": "Pérdida impositiva estimada para Baleares (24% sobre ingresos brutos)", "source": "Holm 2025" },
{ "number": "208.470", "text": "Viviendas entre vacías (105.632) + ofertadas en el extranjero (102.597) que NO están en el mercado para residentes", "source": "Holm 2025 (suma de 105.632 + 102.597)" }
],
"section_bio_title": "Sobre el proyecto",
"section_bio_body": "Cases Tancades es una iniciativa ciudadana independiente que cruza datos oficiales de vivienda semi-vacía con alquiler turístico en las Islas Baleares. El proyecto no tiene afiliación política ni financiación pública o privada. El código y los datos se publican bajo licencia abierta para facilitar la verificación y la difusión.",
"section_contact_title": "Contacto",
@@ -283,5 +307,55 @@
"contribute_sources": " Proponer nuevas fuentes de datos públicos que se puedan añadir",
"contribute_share": " Difundir el proyecto a través de redes o medios",
"section_methodology_link": "Para saber cómo hemos hecho el cálculo, consulta la"
},
"informe": {
"title": "El informe de la vivienda en Baleares",
"eyebrow": "Análisis basado en",
"lead": "Esta web se basa en el informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (diciembre 2025 - enero 2026), un trabajo de análisis que combina datos oficiales con investigación específica sobre la actividad especulativa en las islas. El informe aún no está publicado; esta página resume sus conclusiones principales, todas ellas referenciadas con atribución.",
"resumen_title": "Resumen ejecutivo",
"resumen_p1": "En Baleares hay **105.632 viviendas vacías** (16,2% del parque), según datos del INE basados en el consumo eléctrico del Censo de Población 2021. A estas se suman **102.597 viviendas ofrecidas en el extranjero** a través de portales como Holidu.dk, Hometogo.se, Feriebolig-spania.no y Hometogo.de, según una investigación específica de la autora con VPN para evitar el filtrado geográfico. (Holm, 2025)",
"resumen_p2": "El **precio medio de la vivienda en venta** en Baleares alcanzó los **5.114 €/m²** en noviembre de 2025, un 9,1% más que el año anterior, según Idealista. El **precio de alquiler** se sitúa en torno a **19,1 €/m²**. Baleares son la comunidad autónoma con el mayor porcentaje de pisos turísticos sobre el total de viviendas censadas: un **3,84%** reconocido oficialmente, que en términos absolutos supone **32.606 viviendas**, sin contar las ilegales que no figuran en ningún registro. (Idealista; INE 2024)",
"resumen_p3": "Estas cifras dibujan un patrón: **el 60% de las compraventas las hacen personas jurídicas**, y **el 25% de los compradores ya no reside en las islas** (era el 10% en 2007). El **46% de los propietarios** tienen más de una vivienda. La tasa de esfuerzo (el porcentaje de ingresos que se dedica a vivienda) llega al **29,35%** en Baleares, casi 10 puntos por encima de la media nacional.",
"resumen_p4": "El informe concluye que **en Baleares sobran pisos** — el déficit no es de construcción, sino de acceso al mercado. El plan del Govern Balear (Ley 4/25 de Proyectos Residenciales Estratégicos) apuesta por construir 27.000 viviendas adicionales, pero si las 208.470 viviendas detectadas como vacantes u opacas al fisco se incorporasen al mercado, la presión sobre los precios bajaría.",
"resumen_p5": "La autora propone medidas fiscales (impuestos progresivos a viviendas vacantes en manos de grandes tenedores), una nueva ley hipotecaria (subasta obligatoria y cancelación total de la deuda con la dación en pago), y un control estricto del alquiler turístico. La conclusión es clara: la solución a la crisis de la vivienda en Baleares **no es construir más, sino movilizar lo que ya existe**.",
"resumen_p6": "Los datos del Col·legi Oficial d'Arquitectes de les Illes Balears (COAIB, observatorio semestral julio 2025) confirman esta dinámica: en el primer semestre de 2025 se visaron **4.311 viviendas de nueva creación** (+34,68% interanual), pero las **viviendas protegidas cayeron un 33,87%**. Es decir, se construye más, pero la proporción de vivienda asequible baja. La propia memoria del COAIB concluye: 'la promoció d'habitatge lliure no podrà solucionar l'escassetat d'habitatge per a la població que més la pateix'.",
"cifras_title": "Cifras clave del informe",
"cifras_intro": "Estas son las cifras más impactantes del análisis. Todas con atribución completa a (Holm, 2025) o a la fuente original.",
"cifras": [
{ "value": "105.632", "label": "Viviendas vacías detectadas por consumo eléctrico (Censo 2021)", "source": "INE (Censo 2021) / Holm, 2025" },
{ "value": "102.597", "label": "Viviendas ofrecidas en el extranjero sin tributar en España", "source": "4 búsquedas VPN (DK, SE, NO, DE) / Holm, 2025" },
{ "value": "208.470", "label": "Total viviendas 'fuera del mercado' para residentes (105.632 + 102.597)", "source": "Suma Holm, 2025" },
{ "value": "4.924M €", "label": "Renta anual que no tributa por estos alquileres opacos", "source": "Cálculo: 102.597 × 2.000€/semana × 24 semanas / Holm, 2025" },
{ "value": "1.181M €", "label": "Pérdida impositiva estimada para Baleares (24% sobre ingresos brutos)", "source": "Holm, 2025" },
{ "value": "5.114 €/m²", "label": "Precio medio de la vivienda en venta (noviembre 2025)", "source": "Idealista" },
{ "value": "19,1 €/m²", "label": "Precio medio del alquiler (2025)", "source": "Idealista" },
{ "value": "32.606", "label": "Viviendas turísticas reconocidas (>5% del parque censado, sin contar las ilegales)", "source": "INE (2025M05)" },
{ "value": "29,35%", "label": "Tasa de esfuerzo económico: porcentaje de ingresos dedicado a vivienda (2023)", "source": "INE / Ministerio de Transportes" },
{ "value": "60% / 25%", "label": "Compradores personas jurídicas / compradores no residentes", "source": "INE 2024 / Ministerio de Transportes" },
{ "value": "+34,68%", "label": "Aumento interanual de viviendas de nueva creación visadas (1er semestre 2025)", "source": "COAIB, observatorio semestral julio 2025" },
{ "value": "-33,87%", "label": "Caída interanual de viviendas protegidas visadas (1er semestre 2025)", "source": "COAIB, observatorio semestral julio 2025" }
],
"fuentes_title": "Fuentes utilizadas por el informe",
"fuentes_intro": "El informe combina datos oficiales con investigación específica vía VPN para evitar el filtrado geográfico de los portales de alquiler turístico.",
"fuentes": [
{ "name": "INE", "desc": "Instituto Nacional de Estadística: Censo de Población y Viviendas 2021, valor de la vivienda, transacciones por residencia del comprador" },
{ "name": "Ministerio de Transportes, Movilidad Sostenible", "desc": "Estadísticas de valor de la vivienda y tasa de esfuerzo" },
{ "name": "Ministerio de Vivienda y Agenda Urbana (MIVAU)", "desc": "Código de Buenas Prácticas hipotecarias" },
{ "name": "Holidu.dk (Dinamarca)", "desc": "Investigación con VPN: 11.075 viviendas ofrecidas" },
{ "name": "Hometogo.se (Suecia)", "desc": "Investigación con VPN: 38.036 viviendas ofrecidas" },
{ "name": "Feriebolig-spania.no (Noruega)", "desc": "Investigación con VPN: 12.787 casas de vacaciones" },
{ "name": "Hometogo.de (Alemania)", "desc": "Investigación con VPN: 40.699 ofertas" },
{ "name": "COAIB - Col·legi Oficial d'Arquitectes de les Illes Balears", "desc": "Observatorio semestral julio 2025: estadísticas de visado de obra nueva (por tipología, demarcación, uso). Valida la tendencia de construcción sin resolución de la escasez." }
],
"autora_title": "Sobre la autora y el informe",
"autora_p1": "**Lic. Emma C. Holm** ha elaborado el informe 'Crisis de la vivienda en Baleares' entre diciembre 2025 y enero 2026, con datos oficiales del INE, el Ministerio de Transportes y la Conselleria d'Habitatge, combinados con investigación específica en portales de alquiler turístico extranjeros. El informe incluye análisis, conclusiones y propuestas.",
"autora_p2": "Esta web se ha construido con el permiso de la autora y será revisada por ella antes de la publicación definitiva. Todas las cifras y argumentos de esta web que provienen del informe están citados explícitamente como (Holm, 2025).",
"autora_p3": "El informe **aún no está publicado** en ningún otro medio. A medida que se publique, añadiremos aquí el enlace o la referencia completa.",
"cta_title": "Ve más allá",
"cta_map": "Ver los datos en el mapa interactivo",
"cta_methodology": "Leer la metodología completa"
},
"footer": {
"based_on": "Análisis basado en el informe 'Crisis de la vivienda en Baleares' de la **Lic. Emma C. Holm** (2025)."
}
}
+13 -2
View File
@@ -25,16 +25,27 @@ export function getLangFromAstroCookies(
export function t(lang: Lang, key: string): string {
const dict = DICTS[lang];
const cur = resolveKey(dict, key);
return typeof cur === "string" ? cur : key;
}
export function tArray<T = unknown>(lang: Lang, key: string): T[] {
const dict = DICTS[lang];
const cur = resolveKey(dict, key);
return Array.isArray(cur) ? (cur as T[]) : [];
}
function resolveKey(dict: Dict, key: string): unknown {
const parts = key.split(".");
let cur: unknown = dict;
for (const p of parts) {
if (cur && typeof cur === "object" && p in (cur as Record<string, unknown>)) {
cur = (cur as Record<string, unknown>)[p];
} else {
return key;
return undefined;
}
}
return typeof cur === "string" ? cur : key;
return cur;
}
export function format(lang: Lang, key: string, vars: Record<string, string | number>): string {
+1 -1
View File
@@ -8,7 +8,7 @@ interface Props {
title?: string;
description?: string;
image?: string;
active: "home" | "map" | "methodology";
active: "home" | "map" | "methodology" | "islands" | "data" | "press" | "about" | "informe";
lang?: Lang;
}
+1 -1
View File
@@ -7,7 +7,7 @@ const lang: Lang = getLangFromAstroCookies(Astro.cookies);
const description = t(lang, "site.description");
---
<Base lang={lang} title="404" description={description}>
<Base lang={lang} active="home" title="404" description={description}>
<section class="max-w-2xl mx-auto px-4 py-24 text-center">
<p class="text-8xl font-bold text-orange-700 mb-4">404</p>
<h1 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-4">
+3
View File
@@ -2,6 +2,7 @@
export const prerender = false;
import Base from "../layouts/Base.astro";
import CifraImpacto from "../components/CifraImpacto.astro";
import ConsumoDistribucion from "../components/ConsumoDistribucion.astro";
import { getDataset, totals } from "../data/dataset";
import { getLangFromAstroCookies, t, type Lang } from "../i18n";
@@ -128,6 +129,8 @@ const fmtPct = (x: number) => `${(x * 100).toFixed(0)}%`;
</div>
</section>
<ConsumoDistribucion lang={lang} />
<section class="max-w-6xl mx-auto px-4 py-12">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-6">
{t(lang, "home.section_story_title")}
+126
View File
@@ -0,0 +1,126 @@
---
export const prerender = false;
import Base from "../layouts/Base.astro";
import { getLangFromAstroCookies, t, tArray, type Lang } from "../i18n";
const lang: Lang = getLangFromAstroCookies(Astro.cookies);
const description = t(lang, "informe.lead").replace(/\*\*/g, "");
const cifras = tArray<{ value: string; label: string; source: string }>(lang, "informe.cifras");
const fuentes = tArray<{ name: string; desc: string }>(lang, "informe.fuentes");
---
<Base
lang={lang}
active="informe"
title={t(lang, "informe.title")}
description={description}
>
<section class="bg-gradient-to-b from-orange-50 to-stone-50 border-b border-stone-200">
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-16 text-center">
<p class="text-xs sm:text-sm font-medium text-orange-700 uppercase tracking-wider mb-3">
{t(lang, "informe.eyebrow")}
</p>
<h1 class="text-3xl sm:text-4xl font-bold text-stone-900 mb-4">
{t(lang, "informe.title")}
</h1>
<p class="text-base sm:text-lg text-stone-700 max-w-3xl mx-auto leading-relaxed">
{t(lang, "informe.lead")}
</p>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-6">
{t(lang, "informe.resumen_title")}
</h2>
<div class="prose prose-stone max-w-none space-y-4 text-stone-800 leading-relaxed">
<p set:html={t(lang, "informe.resumen_p1")} />
<p set:html={t(lang, "informe.resumen_p2")} />
<p set:html={t(lang, "informe.resumen_p3")} />
<p set:html={t(lang, "informe.resumen_p4")} />
<p set:html={t(lang, "informe.resumen_p5")} />
<p set:html={t(lang, "informe.resumen_p6")} />
</div>
</section>
<section class="max-w-5xl mx-auto px-4 py-12 border-t border-stone-200">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-2">
{t(lang, "informe.cifras_title")}
</h2>
<p class="text-stone-600 mb-6">{t(lang, "informe.cifras_intro")}</p>
<div class="overflow-x-auto border border-stone-200 rounded-lg bg-white">
<table class="w-full text-sm">
<thead class="bg-stone-100 text-stone-700">
<tr>
<th class="text-right px-4 py-2 font-medium w-32">Valor</th>
<th class="text-left px-4 py-2 font-medium">Indicador</th>
<th class="text-left px-4 py-2 font-medium hidden md:table-cell">Fuente</th>
</tr>
</thead>
<tbody>
{cifras.map((c) => (
<tr class="border-t border-stone-200">
<td class="px-4 py-3 text-right tabular-nums font-bold text-orange-700 align-top">
{c.value}
</td>
<td class="px-4 py-3 text-stone-900">
{c.label}
<p class="md:hidden text-xs text-stone-500 mt-1">{c.source}</p>
</td>
<td class="px-4 py-3 text-stone-600 text-xs hidden md:table-cell">
{c.source}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12 border-t border-stone-200">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-2">
{t(lang, "informe.fuentes_title")}
</h2>
<p class="text-stone-600 mb-6">{t(lang, "informe.fuentes_intro")}</p>
<ul class="space-y-3">
{fuentes.map((f) => (
<li class="flex gap-3 text-sm">
<span class="font-semibold text-stone-900 min-w-32 shrink-0">{f.name}</span>
<span class="text-stone-700">{f.desc}</span>
</li>
))}
</ul>
</section>
<section class="max-w-4xl mx-auto px-4 py-12 border-t border-stone-200">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-4">
{t(lang, "informe.autora_title")}
</h2>
<div class="prose prose-stone max-w-none space-y-4 text-stone-800 leading-relaxed">
<p set:html={t(lang, "informe.autora_p1")} />
<p set:html={t(lang, "informe.autora_p2")} />
<p set:html={t(lang, "informe.autora_p3")} />
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12 border-t border-stone-200">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-6 text-center">
{t(lang, "informe.cta_title")}
</h2>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<a
href="/mapa"
class="inline-flex items-center justify-center px-6 py-3 bg-orange-700 text-white font-medium rounded hover:bg-orange-800 transition-colors"
>
{t(lang, "informe.cta_map")}
</a>
<a
href="/metodologia"
class="inline-flex items-center justify-center px-6 py-3 border border-stone-300 text-stone-800 font-medium rounded hover:bg-stone-100 transition-colors"
>
{t(lang, "informe.cta_methodology")}
</a>
</div>
</section>
</Base>
+28 -1
View File
@@ -2,11 +2,38 @@
export const prerender = false;
import Base from "../layouts/Base.astro";
import Mapa from "../components/Mapa.astro";
import { getLangFromAstroCookies, type Lang } from "../i18n";
import ConsumoDistribucion from "../components/ConsumoDistribucion.astro";
import { getLangFromAstroCookies, t, type Lang } from "../i18n";
const lang: Lang = getLangFromAstroCookies(Astro.cookies);
---
<Base lang={lang} active="map" title={undefined} description={undefined}>
<Mapa />
<ConsumoDistribucion lang={lang} />
<section class="max-w-6xl mx-auto px-4 py-8">
<div class="bg-stone-50 border border-stone-200 rounded-lg p-4 sm:p-5">
<h3 class="text-sm font-bold text-stone-900 mb-1">
{t(lang, "map.sources_strip_title")}
</h3>
<p class="text-xs text-stone-600 mb-3 leading-snug max-w-3xl">
{t(lang, "map.sources_strip_intro")}
</p>
<dl class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<div class="bg-white rounded p-2.5 border border-stone-200">
<dt class="font-semibold text-stone-900 mb-0.5">{t(lang, "map.sources_strip_pm")}</dt>
<dd class="text-stone-600 leading-snug">{t(lang, "map.sources_strip_pm_desc")}</dd>
</div>
<div class="bg-white rounded p-2.5 border border-stone-200">
<dt class="font-semibold text-stone-900 mb-0.5">{t(lang, "map.sources_strip_eivissa")}</dt>
<dd class="text-stone-600 leading-snug">{t(lang, "map.sources_strip_eivissa_desc")}</dd>
</div>
<div class="bg-white rounded p-2.5 border border-stone-200">
<dt class="font-semibold text-stone-900 mb-0.5">{t(lang, "map.sources_strip_formentera")}</dt>
<dd class="text-stone-600 leading-snug">{t(lang, "map.sources_strip_formentera_desc")}</dd>
</div>
</dl>
</div>
</section>
</Base>
+1
View File
@@ -12,6 +12,7 @@ const lang: Lang = getLangFromAstroCookies(Astro.cookies);
{t(lang, "methodology.title")}
</h1>
<p class="text-stone-600 text-lg">{t(lang, "methodology.lead")}</p>
<p class="text-sm text-stone-600 mt-4 italic" set:html={t(lang, "methodology.report_context")} />
</section>
<section class="max-w-4xl mx-auto px-4 py-6">
+2 -33
View File
@@ -1,7 +1,7 @@
---
export const prerender = false;
import Base from "../layouts/Base.astro";
import { getLangFromAstroCookies, t, type Lang } from "../i18n";
import { getLangFromAstroCookies, t, tArray, type Lang } from "../i18n";
const lang: Lang = getLangFromAstroCookies(Astro.cookies);
@@ -14,38 +14,7 @@ interface Quoteable {
source: string;
}
const quoteables: Quoteable[] = [
{
number: "18.175",
text: "Listings actius a Airbnb a Mallorca i Menorca",
source: "Inside Airbnb, juny 2026",
},
{
number: "18.184",
text: "Places turístiques legals (HUT) a Eivissa",
source: "Registre HUT Consell d'Eivissa, 14 juliol 2026",
},
{
number: "40%",
text: "De les places HUT d'Eivissa es concentren a Sant Josep de sa Talaia",
source: "Elaboració pròpia a partir del registre HUT",
},
{
number: "16,2%",
text: "D'habitatges buits a Balears",
source: "INE, 2024",
},
{
number: "$258M",
text: "Ingressos estimats d'Airbnb els darrers 365 dies, Mallorca + Menorca",
source: "Inside Airbnb, juny 2026",
},
{
number: "95,4%",
text: "Dels listings actius són pisos sencers (Entire home/apt)",
source: "Inside Airbnb, juny 2026",
},
];
const quoteables = tArray<Quoteable>(lang, "press.quoteables");
const description = t(lang, "press.lead");
---