{
const data = transpose(aseg_yearly_data)
const groupOrder = ["Mariguana", "Cocaina", "Opioides tradicionales", "Metanfetamina", "Fentanilo"]
const groupColors = grupoColors
// Deduplica metadata de drogas y ordena por grupo luego etiqueta
const drugs = [...new Map(data.map(d => [d.droga, d])).values()]
.sort((a, b) =>
groupOrder.indexOf(a.grupo) - groupOrder.indexOf(b.grupo) ||
a.etiqueta.localeCompare(b.etiqueta)
)
const gap = 18
// Celular y tableta (incluso en horizontal): un panel por fila a todo el ancho
const cols = vizWidth < 960 ? 1 : 2
const panelW = Math.floor((vizWidth - gap * (cols - 1)) / cols)
const panels = drugs.map(({droga, grupo, unidad}) => {
const subset = data.filter(d => d.droga === droga)
const color = groupColors[grupo] ?? "steelblue"
const plot = Plot.plot({
width: panelW, height: cols === 1 ? Math.min(360, Math.round(panelW * 0.62)) : Math.round(panelW * 0.55), marginBottom: 36, marginLeft: vizWidth < 420 ? 58 : 82,
style: plotStyle,
x: {label: null, domain: d3.range(1990, 2025), ticks: [1990, 2000, 2010, 2020], tickFormat: d => "'" + String(d).slice(2)},
y: {label: i18n.unidades[unidad] ?? unidad, grid: true, tickFormat: d => fmtNum(d)},
marks: [
Plot.barY(subset, {x: "anio", y: "total", fill: color, tip: true,
title: d => `${d.anio}: ${fmtNum(d.total, 2)} ${i18n.unidades[unidad] ?? unidad}`}),
Plot.ruleY([0])
]
})
const wrap = document.createElement("div")
const ttl = document.createElement("div")
ttl.style.cssText = "font-size:0.95em; font-weight:600; text-align:center; margin-bottom:2px;"
ttl.textContent = i18n.drogas[droga] ?? droga
const badge = document.createElement("span")
badge.style.cssText = `font-size:0.85em; color:${color}; display:block; text-align:center; margin-bottom:4px;`
badge.textContent = i18n.grupos[grupo] ?? grupo
wrap.appendChild(ttl)
wrap.appendChild(badge)
wrap.appendChild(plot)
return wrap
})
const container = document.createElement("div")
container.className = "viz-small"
container.style.cssText = `display:flex; flex-wrap:wrap; gap:${gap}px; align-items:flex-start; justify-content:center; width:${vizWidth}px; margin:0 auto;`
panels.forEach(p => container.appendChild(p))
return container
}Drug seizures in Mexico by substance, 1990–2024
brand = ({
purple: "#403A5E",
taupe: "#AEA793",
green: "#5D8E74",
blue: "#3D8CBC",
maroon: "#9F344D",
orange: "#E3A261"
})
grupoColors = ({
"Mariguana": brand.green,
"Cocaina": brand.blue,
"Opioides tradicionales": brand.orange,
"Metanfetamina": brand.purple,
"Fentanilo": brand.maroon
})
labColors = ({
"COCAINA": brand.blue,
"HEROINA": brand.orange,
"METANFETAMINA": brand.purple
})
metodoColors = ({
"Fumigación": brand.blue,
"Manual": brand.green
})
// Grupos presentes en incineraciones (sin fentanilo), en orden
incGrupos = ["Mariguana", "Cocaina", "Opioides tradicionales", "Metanfetamina"]
plotStyle = ({fontFamily: "Lato, system-ui, sans-serif", fontSize: "16px"})i18n = ({
es: {
locale: "es-MX",
period: "Período",
group: "Grupo de droga",
all: "Todos",
events: "eventos",
event: "evento",
year: "Año",
pctEvents:"% de eventos",
drugType: "Tipo de droga",
term: "Administración presidencial",
ops: "Operaciones militares",
download: "⬇ Descargar datos (CSV)",
grupos: {
"Mariguana": "Mariguana",
"Cocaina": "Cocaína",
"Opioides tradicionales": "Opioides tradicionales",
"Metanfetamina": "Metanfetamina",
"Fentanilo": "Fentanilo"
},
labs: {COCAINA: "Cocaína", HEROINA: "Heroína", METANFETAMINA: "Metanfetamina"},
drogas: {
mariguana: "Mariguana", semilla_de_mariguana: "Semilla de mariguana",
cocaina: "Cocaína", goma_de_opio: "Goma de opio", heroina: "Heroína",
semilla_de_amapola: "Semilla de amapola", metanfetamina: "Metanfetamina",
fentanilo: "Fentanilo", pastillas_de_fentanilo: "Pastillas de fentanilo",
ampolletas_de_fentanilo: "Ampolletas de fentanilo"
},
unidades: {kg: "kg", pastillas: "pastillas", ampolletas: "ampolletas"}
},
en: {
locale: "en-US",
period: "Period",
group: "Drug group",
all: "All",
events: "events",
event: "event",
year: "Year",
pctEvents:"% of events",
drugType: "Drug type",
term: "Presidential term",
ops: "Military operations",
download: "⬇ Download data (CSV)",
grupos: {
"Mariguana": "Marijuana",
"Cocaina": "Cocaine",
"Opioides tradicionales": "Traditional opioids",
"Metanfetamina": "Methamphetamine",
"Fentanilo": "Fentanyl"
},
labs: {COCAINA: "Cocaine", HEROINA: "Heroin", METANFETAMINA: "Methamphetamine"},
drogas: {
mariguana: "Marijuana", semilla_de_mariguana: "Marijuana seed",
cocaina: "Cocaine", goma_de_opio: "Opium gum", heroina: "Heroin",
semilla_de_amapola: "Poppy seed", metanfetamina: "Methamphetamine",
fentanilo: "Fentanyl", pastillas_de_fentanilo: "Fentanyl pills",
ampolletas_de_fentanilo: "Fentanyl ampoules"
},
unidades: {kg: "kg", pastillas: "pills", ampolletas: "ampoules"}
}
})[lang]
// Números: miles separados con espacio indivisible ("10 000") y punto decimal,
// igual en español e inglés. Sin coma de miles (estilo de la casa).
fmtNum = (v, maxDec = 2) => {
const s = Number(v).toLocaleString("en-US", {maximumFractionDigits: maxDec, useGrouping: false})
const [int, dec] = s.split(".")
const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, "\u00A0")
return dec ? `${grouped}.${dec}` : grouped
}
// Rangos con guion corto ("1990-2000") se muestran con raya ("1990–2000")
fmtRange = s => String(s).replace("-", "–")
// Ancho de las gráficas: llena el contenedor, con tope para pantallas grandes.
// Se mide directamente el ancho de la columna de contenido y se vuelve a avisar cada vez
// que cambia (p. ej. cuando el iframe se acomoda tarde en celular), para que las gráficas
// se redibujen al ancho real y no se queden con el ancho provisional del primer dibujo.
vizWidth = Generators.observe(notify => {
const main = document.querySelector("main.content") || document.body
let last = -1
const send = () => {
const w = Math.min(Math.floor(main.clientWidth || width), 1000)
if (w > 0 && w !== last) { last = w; notify(w) }
}
send()
const ro = new ResizeObserver(send)
ro.observe(main)
window.addEventListener("resize", send)
return () => { ro.disconnect(); window.removeEventListener("resize", send) }
})