import { fmt, fmtFecha, parseFecha, htmlTabla, perfilesEstacion, marcasLance, trimestreAustral, TRIMESTRE_NOMBRE } from "../_js/portal.js"
catalogo = FileAttachment("lago/ctd/catalogo.csv").csv()
nombres = new Map([
["Temperatura", "Temperatura (°C)"],
["Oxigeno", "Oxígeno (mg/L)"],
["OxigenoSat", "Oxígeno (% saturación)"],
["pH", "pH"],
["Conductividad", "Conductividad (µS/cm)"],
["Clorofila", "Clorofila (µg/L)"],
["Turbidez", "Turbidez (NTU)"]
])
claves = [...nombres.keys()]
viewof sitio = Inputs.select(catalogo, {
label: "Sitio",
format: (d) => `${d.nombre} (${d.n_fechas} ${+d.n_fechas === 1 ? "lance" : "lances"})`
})Perfiles CTD
Siete variables del control de calidad, en cada lance: temperatura, oxígeno, saturación, pH, conductividad, clorofila y turbidez. La profundidad crece hacia abajo. Primero se comparan lances entre sí, después el promedio de cada estación del año (se pueden ocultar estaciones y el rango entre lances), y al final un solo lance con sus siete variables.
crudo = await d3.csv(`lago/ctd/${sitio.id}.csv`)
filas = crudo.map((r) => {
const o = { fecha: r.fecha, t: parseFecha(r.fecha), z: +r.depth_m }
for (const k of claves) o[k] = r[k] === "" ? null : +r[k]
return o
}).filter((r) => Number.isFinite(r.z))
fechas = [...new Set(filas.map((r) => r.fecha))].sort()
viewof fechasElegidas = Inputs.checkbox(fechas, {
label: "Lances a comparar",
format: (f) => fmtFecha(parseFecha(f)),
value: fechas.slice(-3)
})
viewof variable = Inputs.select(claves, {
label: "Variable",
format: (k) => nombres.get(k),
value: "Temperatura"
}){
const elegidas = fechasElegidas || []
const perfil = filas
.filter((r) => elegidas.includes(r.fecha) && r[variable] != null)
.filter((r) => variable !== "Clorofila" || r.Clorofila >= 0)
.filter((r) => variable !== "Oxigeno" || r.Oxigeno >= 0)
.map((r) => ({ ...r, etiqueta: fmtFecha(r.t) }))
.sort((a, b) => a.etiqueta < b.etiqueta ? -1 : a.etiqueta > b.etiqueta ? 1 : a.z - b.z)
const caja = document.createElement("div")
const h = document.createElement("h2")
h.textContent = "Comparar lances"
caja.append(h)
if (!perfil.length) {
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Marque al menos un lance."
caja.append(p)
return caja
}
const fig = Plot.plot({
width: 860,
height: 520,
marginLeft: 64,
marginRight: 16,
marginBottom: 48,
color: { legend: true },
x: { grid: true, label: nombres.get(variable) },
y: { grid: true, reverse: true, label: "Profundidad (m)" },
marks: [
Plot.line(perfil, { x: variable, y: "z", z: "etiqueta", stroke: "etiqueta", strokeWidth: 1.8 })
]
})
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Cada color es un lance del sitio elegido. Marque o desmarque fechas para compararlos a la misma profundidad."
caja.append(fig, p)
return caja
}Promedio por estación del año
puntosEstacion = perfilesEstacion(filas, variable)
estacionesDisponibles = ["DEF", "MAM", "JJA", "SON"]
.filter((est) => puntosEstacion.some((p) => p.est === est))
.map((est) => ({ est, etiqueta: TRIMESTRE_NOMBRE[est] }))
viewof estacionesPerfil = Inputs.checkbox(estacionesDisponibles, {
label: "Estaciones en el gráfico",
format: (d) => d.etiqueta,
value: estacionesDisponibles
}){
const caja = document.createElement("div")
if (!puntosEstacion.length) {
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Este sitio no tiene lances con esta variable."
caja.append(p)
return caja
}
const codigos = new Set((estacionesPerfil || []).map((d) => d.est))
const visibles = puntosEstacion.filter((p) => codigos.has(p.est))
if (!visibles.length) {
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Marque al menos una estación del año."
caja.append(p)
return caja
}
const orden = []
const tonos = { DEF: "#d35400", MAM: "#b9770e", JJA: "#1a5276", SON: "#1e8449" }
const gama = []
for (const pto of visibles) {
if (orden.includes(pto.etiqueta)) continue
orden.push(pto.etiqueta)
gama.push(tonos[pto.est])
}
const zMaxEst = Math.max(...puntosEstacion.map((p) => p.z))
const marcas = [
Plot.line(visibles, { x: "v", y: "z", z: "etiqueta", stroke: "etiqueta", strokeWidth: 2.2, sort: "y" })
]
if (verRango) {
marcas.unshift(Plot.areaX(visibles, {
y: "z", x1: "vmin", x2: "vmax", z: "etiqueta",
fill: "etiqueta", fillOpacity: 0.18, sort: "y"
}))
}
const fig = Plot.plot({
width: 860,
height: 520,
marginLeft: 64,
marginRight: 16,
marginBottom: 48,
color: { legend: true, domain: orden, range: gama },
x: { grid: true, label: nombres.get(variable) },
y: { grid: true, reverse: true, label: "Profundidad (m)", domain: [0, zMaxEst] },
marks: marcas
})
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = verRango
? "La línea es el promedio del trimestre. La franja es el mínimo y el máximo de los lances en esa capa de 1 m. Las casillas eligen qué estaciones se dibujan. Verano es diciembre a febrero."
: "La línea es el promedio del trimestre, en capas de 1 m. El interruptor agrega la franja entre el lance más bajo y el más alto. Las casillas eligen qué estaciones se dibujan. Verano es diciembre a febrero."
caja.append(fig, p)
return caja
}viewof fechaUna = Inputs.select(fechas, {
label: "Lance de las siete variables",
format: (f) => fmtFecha(parseFecha(f)),
value: fechas[fechas.length - 1]
}){
const corte = filas.filter((r) => r.fecha === fechaUna)
const zMax = Math.max(...corte.map((r) => r.z))
const paneles = claves.map((k) => {
const rows = corte
.filter((r) => r[k] != null && (k !== "Clorofila" || r[k] >= 0) && (k !== "Oxigeno" || r[k] >= 0))
.sort((a, b) => a.z - b.z)
const tarjeta = document.createElement("div")
const tit = document.createElement("p")
tit.style.cssText = "margin:0 0 0.2rem;font-weight:600"
tit.textContent = nombres.get(k)
const fig = rows.length
? Plot.plot({
width: 320,
height: 420,
marginLeft: 46,
marginRight: 10,
marginTop: 8,
marginBottom: 28,
x: { grid: true, label: null },
y: { grid: true, reverse: true, label: "m", domain: [0, zMax] },
marks: [Plot.line(rows, { x: k, y: "z", stroke: "#1a5276", strokeWidth: 1.8 })]
})
: Object.assign(document.createElement("p"), { className: "portal-origen", textContent: "Este lance no tiene valores válidos." })
tarjeta.append(tit, fig)
return tarjeta
})
const caja = document.createElement("div")
const h = document.createElement("h2")
h.textContent = "Las siete variables del " + fmtFecha(parseFecha(fechaUna))
const rejilla = document.createElement("div")
rejilla.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1rem 1.2rem;align-items:start"
for (const tarjeta of paneles) rejilla.append(tarjeta)
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Misma fecha y la misma escala de profundidad en los siete paneles. Así se ve, por ejemplo, si el oxígeno baja donde la temperatura cambia."
caja.append(h, rejilla, p)
return caja
}{
const guion = (x, dec) => x == null ? "—" : fmt(x, dec)
const filasTabla = fechas.map((f) => {
const rows = filas.filter((r) => r.fecha === f)
const m = marcasLance(rows)
const t = parseFecha(f)
return {
fecha: fmtFecha(t),
trimestre: TRIMESTRE_NOMBRE[trimestreAustral(t)],
termoclina: guion(m.termoclina, 1),
o2: guion(m.o2, 2),
o2z: guion(m.o2z, 1),
clo: guion(m.clo, 2),
cloz: guion(m.cloz, 1)
}
})
const caja = document.createElement("div")
const h = document.createElement("h2")
h.textContent = "Resumen de cada lance"
const p = document.createElement("p")
p.className = "portal-origen"
p.textContent = "Sirve para recorrer el sitio sin abrir cada perfil. Cada fila es un lance. La termoclina es la profundidad, bajo 1 m, donde la temperatura cambia más por metro; se descartan saltos mayores que 5 °C/m. Al lado van el oxígeno más bajo y la clorofila más alta, con la profundidad a la que ocurren. Un guion significa que ese lance no permite calcularlo."
caja.append(h, p, htmlTabla([
{ key: "fecha", label: "Fecha" },
{ key: "trimestre", label: "Trimestre" },
{ key: "termoclina", label: "Termoclina (m)" },
{ key: "o2", label: "Oxígeno mínimo (mg/L)" },
{ key: "o2z", label: "Prof. del mínimo (m)" },
{ key: "clo", label: "Clorofila máxima (µg/L)" },
{ key: "cloz", label: "Prof. de la máxima (m)" }
], filasTabla, "resumen_lances_ctd.csv"))
return caja
}