import { fmtFecha, htmlTabla, marcoFigura, parseFecha } from "../_js/portal.js"
AZUL = "#1a5276"
ROJO = "#C62828"
GRIS = "#bdbdbd"
hidro = (t) => (t.getMonth() + 1 >= 4 ? t.getFullYear() : t.getFullYear() - 1)
pct = (v) => v.toLocaleString("es-CL", { maximumFractionDigits: 1 })
crudo = FileAttachment("series/nieve_ndsi.csv").csv({typed: false})
serie = crudo.map((r) => ({ t: parseFecha(r.fecha), v: r.ndsi === "" ? null : +r.ndsi }))
.filter((d) => d.t && d.v != null)
.sort((a, b) => a.t - b.t)Nieve MODIS
Cobertura de nieve NDSI en la cuenca, en porcentaje, un valor por día. El umbral y la duración mínima se eligen aquí: el conteo de eventos se recalcula con esos dos números. El año hidrológico empieza en abril, igual que en las series de caudal.
viewof umbral = Inputs.range([0, 100], { label: "Umbral de cobertura (%)", step: 1, value: 25 })
viewof minDias = Inputs.range([1, 15], { label: "Duración mínima del evento (días)", step: 1, value: 5 })eventos = {
const lista = []
let ini = null
let fin = null
let n = 0
const cerrar = () => {
if (ini && n >= minDias) lista.push({ ini, fin, n })
ini = null
fin = null
n = 0
}
for (const d of serie) {
if (d.v >= umbral) {
if (!ini) ini = d.t
fin = d.t
n += 1
} else cerrar()
}
cerrar()
return lista
}
porAnio = {
const g = new Map()
for (const d of serie) {
const y = hidro(d.t)
let x = g.get(y)
if (!x) {
x = { anio: y, dias: 0, eventos: 0, primero: null, ultimo: null }
g.set(y, x)
}
if (d.v >= umbral) {
x.dias += 1
if (!x.primero) x.primero = d.t
x.ultimo = d.t
}
}
for (const e of eventos) {
const x = g.get(hidro(e.ini))
if (x) x.eventos += 1
}
return [...g.values()].sort((a, b) => a.anio - b.anio)
}html`<p><strong>Eventos</strong> ${eventos.length.toLocaleString("es-CL")} · umbral ${umbral.toLocaleString("es-CL")} % · al menos ${minDias.toLocaleString("es-CL")} días seguidos</p>`{
const fig = marcoFigura(Plot.plot({
width: 900,
height: 360,
marginBottom: 44,
color: { legend: true, domain: ["Cobertura", "Umbral"], range: [AZUL, ROJO] },
y: { grid: true, label: "Cobertura de nieve (%)" },
x: { label: "Fecha" },
marks: [
Plot.ruleY([{ v: umbral, serie: "Umbral" }], { y: "v", stroke: "serie" }),
Plot.lineY(serie, {
x: "t",
y: "v",
stroke: () => "Cobertura",
tip: true,
title: (d) => `${fmtFecha(d.t)}: ${pct(d.v)} %`
})
]
}), {
titulo: "Cobertura de nieve diaria y umbral",
archivo: "nieve_ndsi"
})
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "La línea roja es el umbral elegido. Un evento es una racha de días sobre ese umbral. La serie es el promedio de cuenca de NDSI Snow Cover, en porcentaje."
const caja = document.createElement("div")
caja.append(fig, p)
return caja
}clima = {
const g = new Map()
for (const d of serie) {
const k = (d.t.getMonth() + 1) * 100 + d.t.getDate()
let x = g.get(k)
if (!x) { x = []; g.set(k, x) }
x.push(d.v)
}
return [...g.entries()].map(([k, vals]) => {
vals.sort((a, b) => a - b)
const q = (p) => vals[Math.min(vals.length - 1, Math.floor(p * (vals.length - 1)))]
const mes = Math.floor(k / 100)
const dia = k % 100
return {
t: new Date(2000, mes - 1, dia),
media: vals.reduce((s, v) => s + v, 0) / vals.length,
p10: q(0.1),
p90: q(0.9)
}
}).sort((a, b) => a.t - b.t)
}
mesesEje = Array.from({ length: 12 }, (_, i) => new Date(2000, i, 15)){
const fig = marcoFigura(Plot.plot({
width: 900,
height: 340,
marginBottom: 44,
color: { legend: true, domain: ["P10–P90", "Media", "Umbral"], range: [GRIS, AZUL, ROJO] },
x: {
domain: [new Date(2000, 0, 1), new Date(2000, 11, 31)],
ticks: mesesEje,
tickFormat: (d) => d.toLocaleDateString("es-CL", { month: "short" }),
label: "Mes"
},
y: { grid: true, label: "Cobertura de nieve (%)" },
marks: [
Plot.areaY(clima, { x: "t", y1: "p10", y2: "p90", fill: () => "P10–P90", fillOpacity: 0.45 }),
Plot.lineY(clima, {
x: "t",
y: "media",
stroke: () => "Media",
tip: true,
title: (d) => `${d.t.toLocaleDateString("es-CL", { day: "numeric", month: "short" })}: media ${pct(d.media)} %, P10 ${pct(d.p10)} %, P90 ${pct(d.p90)} %`
}),
Plot.ruleY([{ v: umbral, serie: "Umbral" }], { y: "v", stroke: "serie" })
]
}), {
titulo: "Climatología de la cobertura de nieve",
archivo: "nieve_clima"
})
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Media diaria y banda P10–P90, con todos los años de la serie. La línea roja es el mismo umbral de la figura anterior."
const caja = document.createElement("div")
caja.append(fig, p)
return caja
}htmlTabla(
[
{ key: "anio", label: "Año hidrológico" },
{ key: "dias", label: "Días sobre el umbral", num: true, dec: 0 },
{ key: "eventos", label: "Eventos", num: true, dec: 0 },
{ key: "primero", label: "Primer día" },
{ key: "ultimo", label: "Último día" }
],
porAnio.map((d) => ({
anio: String(d.anio),
dias: d.dias,
eventos: d.eventos,
primero: fmtFecha(d.primero),
ultimo: fmtFecha(d.ultimo)
})),
"nieve_por_anio.csv"
)