amc-console / index.html
clyxconsultingdev's picture
Deploy the site from pipeline commit 7d4731dcd0cb
b753a2f verified
Raw History Blame Contribute Delete
37.9 kB
<!doctype html>
<html lang="es" data-root="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Google Search Console ownership (2026-10-20). Public by design; removing it un-verifies the site. -->
<meta name="google-site-verification" content="j0VyZ0gw5m_kBinHxAbWR2v8R-8j4RUIslnYk1xEG5g">
<title>Empresas de Colombia · AMC</title>
<meta name="description" content="Cuántas empresas hay en Colombia, a qué se dedican, dónde están y qué cuentan 36 años de registro empresarial — depurado, verificado y abierto.">
<meta property="og:title" content="AMC · Las empresas de Colombia, en datos abiertos">
<meta property="og:type" content="website">
<meta property="og:description" content="Cuántas empresas siguen de verdad activas en Colombia, qué hacen y dónde están. El registro empresarial depurado y verificado: historias, explorador, ejercicios de SQL y datos abiertos, gratis.">
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<meta property="og:image" content="https://amc.clyxconsulting.com/assets/og/inicio.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="Analíticas Mercantiles Colombianas">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://amc.clyxconsulting.com/assets/og/inicio.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/amc.css">
<link rel="stylesheet" href="assets/nav.css">
<link rel="canonical" href="https://amc.clyxconsulting.com/">
<meta property="og:url" content="https://amc.clyxconsulting.com/">
<meta property="og:locale" content="es_CO">
<meta property="og:image:type" content="image/png">
<meta property="og:image:alt" content="AMC · Las empresas de Colombia, en datos abiertos">
</head>
<body>
<main class="wrap" id="app"><p class="loading">Cargando el panorama…</p></main>
<noscript><p class="wrap">Esta página necesita JavaScript para dibujar sus gráficos.</p></noscript>
<script type="module">
// Everything on this page comes from summary.json and the stories' files,
// which the publisher builds next to the data and verifies against it. No
// figure here is typed by hand, so none of them can go stale.
import * as A from "./assets/amc.js";
import { sparkRaster } from "./assets/charts.js";
import { STORIES, storyCard } from "./assets/catalog.js";
const { el, n, rich, tableView, tipOn, title } = A;
const pct = (a, b) => A.pct(b ? a / b : 0);
A.chrome("inicio");
const app = document.getElementById("app");
// ---------------------------------------------------------------- hero
// The "latido" heatmap + its caption, shared by every variant.
function heroRibbon() {
const ribbon = el("div", { class: "ribbon" });
const art = el("a", { href: "historias/latido/index.html", "aria-label": "El latido del país: cada semana desde 2006", style: "display:block;height:150px;position:relative;border-radius:10px;overflow:hidden;background:var(--surface)" });
ribbon.append(art, el("div", { class: "cap" },
el("span", { text: "Cada cuadro es una semana desde 2006 y cada fila un año: más intenso, más empresas creadas." }),
el("a", { href: "historias/latido/index.html", text: "Ver el latido completo →" })));
A.story("latido").then((st) => { const [d] = st.q("dias"); sparkRaster(art, A.parseDate(d.inicio), d.creadas, { fromYear: 2006, pad: 0 }); }).catch(() => ribbon.remove());
return ribbon;
}
function heroButtons() {
return el("div", { style: "display:flex;gap:10px;flex-wrap:wrap;margin-top:24px" },
el("a", { class: "btn primary", href: "explorar/index.html" }, "Explorar una actividad", A.icon("arrow")),
el("a", { class: "btn", href: "historias/index.html" }, "Leer las historias"),
el("a", { class: "btn", href: "consulta/index.html" }, A.icon("sql"), "Consultar con SQL"));
}
// "Es un mínimo…" plus, where there is one, the previous snapshot's figure.
function heroMinimoNote(s) {
const prev = (s.history || []).length > 1 ? s.history[s.history.length - 2] : null;
const note = rich("p", { class: "note" }, ["Es un mínimo: las renovaciones llegan durante todo el año, así que la cifra crece hasta diciembre.",
...(prev && prev.live ? [" El ", { b: A.dayMonth(prev.snapshot) }, " eran ", { b: n(prev.live) }, "."] : [])]);
note.append(" ", el("a", { href: "metodologia/index.html#minimo", text: "Por qué" }));
return note;
}
// The reframe: the big number stays the centrepiece, but it now answers "how
// many are REALLY active", and the note carries the project's finding — the
// official status column marks more than double as "ACTIVA". The note needs
// says_activa, which enters summary.json on the next publish; until then it
// falls back to the "es un mínimo" honesty note, so a deploy is always valid.
function hero(s) {
const h = s.headline;
const note = h.says_activa
? rich("p", { class: "note" }, ["El registro da por «activas» a ", { b: n(h.says_activa) },
" —más del doble—, pero muchas llevan años sin renovar. Aquí la vigencia se mide por la renovación, no por el estado del registro. ",
el("a", { href: "metodologia/index.html#activa", text: "Cómo se mide" })])
: heroMinimoNote(s);
return el("section", { class: "hero" },
el("p", { class: "kicker", text: "El registro empresarial de Colombia, depurado y verificado" }),
el("h1", { text: n(h.live) }),
el("p", { class: "line", text: `empresas y entidades están de verdad activas en ${s.year}: renovaron o abrieron su matrícula.` }),
// The split that makes the headline checkable against the RUES's own
// "empresas activas", which counts sole traders and sociedades only.
// Absent from summaries published before live_mercantile existed.
h.live_mercantile ? rich("p", { class: "note" }, [{ b: n(h.live_mercantile) }, " son empresas, comerciantes y sociedades: lo que el RUES cuenta como «empresas activas». ",
{ b: n(h.live - h.live_mercantile) }, " son entidades sin ánimo de lucro, cooperativas y otras entidades."]) : null,
note, heroButtons(), heroRibbon());
}
// ---------------------------------------------------------------- stories
function stories() {
const [first, ...rest] = STORIES;
return el("section", { class: "block", id: "historias" },
el("div", { class: "sec-head" }, el("div", {}, el("p", { class: "kicker", text: "Historias" }), el("h2", { text: "Lo que cuentan 36 años de registro" }),
el("p", { text: "Preguntas que cualquiera se hace, respondidas con los datos abiertos. Cada respuesta dice también lo que esos datos no permiten concluir." })),
el("a", { class: "btn ghost", href: "historias/index.html", text: "Todas las historias →" })),
el("div", { class: "stories" }, storyCard(first, { feature: true })),
// Six on the home page, chosen for range; the hub has them all.
el("div", { class: "stories three", style: "margin-top:16px" },
...["latido", "sas", "abril-2020", "negocios", "tu-empresa", "regiones"].map((k) => rest.find((x) => x.slug === k)).filter(Boolean).map((x) => storyCard(x))),
el("div", { style: "margin-top:20px;text-align:center" }, el("a", { class: "btn", href: "historias/index.html", text: `Ver las ${STORIES.length} historias →` })));
}
// "¿Sabías que…?": a pool of facts, each computed from a story's own
// figures, never typed. Three a day, the same three for every reader that day
// (a shared screenshot shows what the next reader sees), and "Otros tres
// datos" brings the next three. A fact whose story is missing is skipped.
// A company's name without its legal form ("S.A.", "S.A.S.", "LTDA"), as a sentence names it.
const firm = (name) => title(name.replace(/[\s,]+(S\.?\s?A\.?(\s?S\.?)?|LTDA\.?|LIMITADA)$/i, ""));
const yearOf = (d) => String(d).slice(0, 4);
const FACTS = [
["abril-2020", (st) => {
const d = st.q("diario");
const sum = (a, b) => d.filter((r) => r.fecha >= a && r.fecha < b).reduce((t, r) => t + r.creadas, 0);
const a19 = sum("2019-04-01", "2019-05-01"), a20 = sum("2020-04-01", "2020-05-01");
return [A.signedPct(a20 / a19 - 1), ["En ", { b: "abril de 2020" }, `, primer mes de la cuarentena, se crearon ${n(a20)} empresas; un año antes, ${n(a19)}.`], "historias/abril-2020/index.html", "Abril de 2020 →"];
}],
["sas", (st, s) => {
const r = st.q("anual").find((x) => x.anio === s.year - 1);
return [A.pct(r.sas / r.sociedades), ["de las sociedades creadas en ", { b: String(r.anio) }, " fueron SAS, el tipo de sociedad que creó la Ley 1258 de 2008."], "historias/sas/index.html", "La revolución SAS →"];
}],
["bancos", (st) => {
const B = st.q("bancos"), y = Math.max(...B.map((r) => r.anio_fiscal)), rows = B.filter((r) => r.anio_fiscal === y);
const top = rows.reduce((m, r) => (r.depositos > m.depositos ? r : m));
return [A.pct(top.depositos / rows.reduce((t, r) => t + r.depositos, 0)), ["de lo que el público guarda en los bancos está en uno solo: ", { b: firm(top.nombre) }, ` (diciembre de ${y}).`], "historias/bancos/index.html", "Diez años de bancos →"];
}],
["latido", (st) => {
const [r] = st.q("mas_activos");
const opened = A.newChamberNear(r.camara_principal, r.fecha);
return [n(r.creadas), ["empresas se registraron el ", { b: A.date(r.fecha) }, `, el día récord desde 1990, y ${n(r.en_esa_camara)} de ellas en una sola cámara: ${title(r.camara_principal)}${opened ? ", que abría su registro tras crearse ese año" : ""}.`], "historias/latido/index.html", "El latido del país →"];
}],
["de-cien", (st, s) => {
const y = s.year - 10, rr = st.q("nacional").filter((r) => r.anio === y);
const k = Math.round((100 * rr.reduce((t, r) => t + r.activas, 0)) / rr.reduce((t, r) => t + r.empresas, 0));
return [`${k} de 100`, ["empresas creadas en ", { b: String(y) }, " siguen activas hoy."], "historias/de-cien/index.html", "De 100 que abren →"];
}],
["turismo", (st) => {
const C = st.q("categorias"), c = (k) => C.filter((r) => r.categoria === k).reduce((t, r) => t + r.registros_empresas + r.registros_otros, 0);
return [A.n1(c("VIVIENDAS TURÍSTICAS") / c("ESTABLECIMIENTOS DE ALOJAMIENTO TURÍSTICO")), ["viviendas turísticas hay inscritas en el Registro Nacional de Turismo por cada hotel, hostal u otro establecimiento de alojamiento."], "historias/turismo/index.html", "Quién recibe a los turistas →"];
}],
["gobiernos", (st) => {
const P = st.q("periodos").filter((p) => !p.en_curso);
const a = P[0], b = P[P.length - 1];
return [`${A.n1((b.creadas / b.dias) / (a.creadas / a.dias))}×`, ["Entre ", { b: `${a.inicio.slice(0, 4)} y ${b.fin.slice(0, 4)}` }, ` el país pasó de crear ${n(a.creadas / a.dias)} empresas al día a ${n(b.creadas / b.dias)}.`], "historias/gobiernos/index.html", "Ocho presidentes →"];
}],
["margenes", (st) => {
const A_ = st.q("anual"), r = A_[A_.length - 1], [p] = st.q("panel");
return [A.pct(r.con_perdidas / r.empresas), ["de las ", { b: n(r.empresas) }, ` empresas que reportan a la Supersociedades todos los años desde ${p.desde} cerraron ${r.anio_fiscal} con pérdidas.`], "historias/margenes/index.html", "Márgenes →"];
}],
["contratos", (st) => {
const P = st.q("primeros"), R = st.q("registro"), p = P[P.length - 1], r = R.find((x) => x.anio === p.anio);
return [`${A.n1(p.edad_mediana)} años`, ["tiene, en la mediana, una sociedad cuando el Estado la contrata por primera vez; las del registro tienen ", { b: `${A.n1(r.edad_mediana)} años` }, ` (${p.anio}).`], "historias/contratos/index.html", "El primer contrato →"];
}],
["duracion", (st, s) => {
// The story's cohorts: created twelve to four years before the current year.
const T = st.q("total"), soc = T.find((r) => !r.es_natural), nat = T.find((r) => r.es_natural);
return [A.pct(soc.siguen_3 / soc.observables), [`de las sociedades creadas entre ${s.year - 12} y ${s.year - 4} seguían renovando su matrícula tres años después de crearse; de los comerciantes personas naturales, `, { b: A.pct(nat.siguen_3 / nat.observables) }, "."], "historias/duracion/index.html", "¿Qué negocios duran más? →"];
}],
["compras", (st) => {
const D = st.q("departamentos").filter((r) => r.dp !== "11");
return [A.pct(D.reduce((t, r) => t + r.bogota, 0) / D.reduce((t, r) => t + r.contratos, 0)), ["de los contratos que firman las entidades públicas fuera de Bogotá van a empresas registradas en Bogotá."], "historias/compras/index.html", "A quién le compra el Estado →"];
}],
["nombres", (st) => {
const [w] = st.q("comunes");
return [A.cap(w.palabra.toLowerCase()), ["es la palabra más común en los nombres de las empresas: la llevan ", { b: n(w.empresas) }, "."], "historias/nombres/index.html", "Lo que dicen los nombres →"];
}],
["concentracion", (st) => {
const M = st.q("anual").filter((r) => r.grupo === "mismas"), r = M[M.length - 1];
return [A.pct(r.parte_10), ["de los ingresos de las mismas ", { b: n(r.empresas) }, ` empresas fue en ${r.anio_fiscal} para el 10% más grande de ellas.`], "historias/concentracion/index.html", "¿Más concentradas? →"];
}],
["marzo", (st, s) => {
const C = st.q("camaras"), t = C.reduce((a, r) => a + r.total, 0);
return [A.pct(C.reduce((a, r) => a + r.ultima_semana, 0) / t), ["de las renovaciones de ", { b: String(s.year) }, " llegaron en la última semana de marzo, antes del plazo del 31."], "historias/marzo/index.html", "La hora de la verdad →"];
}],
["piramide", (st) => {
const E = [...st.q("edades")].sort((a, b) => a.edad - b.edad), all = E.reduce((t, r) => t + r.naturales + r.juridicas, 0);
let acc = 0; const m = E.find((r) => (acc += r.naturales + r.juridicas) >= all / 2).edad;
return [`${m} años`, ["es la edad mediana de las empresas activas: la mitad tiene menos, la otra mitad más."], "historias/piramide/index.html", "La pirámide empresarial →"];
}],
["migracion", (st) => {
const D = st.q("documentos"), m = (ks) => D.filter((r) => ks.includes(r.documento)).reduce((t, r) => t + r.matriculas, 0);
const permits = m(["PERMISO ESPECIAL DE PERMANENCIA", "PERMISO POR PROTECCION TEMPORAL"]);
return [n(m(["CEDULA DE EXTRANJERIA", "PASAPORTE", "DOCUMENTO EXTRANJERO"]) + permits), ["matrículas de comerciantes con un documento extranjero; ", { b: n(permits) }, " con el PEP o el PPT, los permisos creados para la migración venezolana."], "historias/migracion/index.html", "Emprender migrando →"];
}],
["peso", (st) => {
const [c] = st.q("concentracion"), [a] = st.q("anio");
return [A.pct(c.top_100 / c.total), ["de los ingresos de las ", { b: n(c.empresas) }, ` sociedades que reportan a la Supersociedades fue en ${a.anio_fiscal} para solo 100 de ellas.`], "historias/peso/index.html", "El peso de las empresas →"];
}],
["pandemia", (st) => {
const S = st.q("serie"), v = (y) => S.find((r) => r.anio_fiscal === y)?.ingresos_constantes;
return [A.signedPct(v(2020) / v(2019) - 1), ["cambiaron en 2020 los ingresos de las mismas empresas frente a 2019, en pesos constantes", v(2021) > v(2019) ? "; en 2021 ya estaban por encima." : "."], "historias/pandemia/index.html", "La pandemia en los ingresos →"];
}],
["software", (st) => {
const R = st.q("anual"), y1 = Math.max(...R.map((r) => r.anio)), y0 = y1 - 10;
const at = (y) => new Map(R.filter((r) => r.anio === y).map((r) => [r.camara, r.creadas]));
const b0 = at(y0), b1 = at(y1);
const top = [...b1].filter(([c]) => b0.get(c) > 0).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([c]) => c);
const f = top.reduce((x, c) => (b1.get(c) / b0.get(c) > b1.get(x) / b0.get(x) ? c : x), top[0]);
return [`${A.n1(b1.get(f) / b0.get(f))}×`, ["se multiplicó en diez años la creación de empresas de software en ", { b: title(f) }, `: de ${n(b0.get(f))} en ${y0} a ${n(b1.get(f))} en ${y1}.`], "historias/software/index.html", "El mapa del software →"];
}],
["regiones", (st) => {
const r = [...st.q("mas_antiguas")].sort((a, b) => String(a.desde).localeCompare(String(b.desde)))[0];
return [yearOf(r.desde), ["es el año de matrícula de la empresa activa más antigua del registro: ", { b: firm(r.razon_social) }, "."], "historias/regiones/index.html", "Tu región en cifras →"];
}],
["negocios", (st) => {
const C = st.q("clases");
return [A.pct(C.reduce((t, r) => t + r.activas_naturales, 0) / C.reduce((t, r) => t + r.activas, 0)), ["de las empresas activas son comerciantes personas naturales; el resto, sociedades y entidades."], "historias/negocios/index.html", "¿Qué montan los colombianos? →"];
}],
["depuraciones", (st) => {
const r = st.q("anual").reduce((m, x) => (x.canceladas > m.canceladas ? x : m));
return [n(r.canceladas), ["matrículas se cancelaron en ", { b: String(r.anio) }, `; ${A.pct(r.en_dias_masivos / r.canceladas)} en días de depuración masiva del registro, no en cierres de cada día.`], "historias/depuraciones/index.html", "Las grandes depuraciones →"];
}],
];
function facts(s) {
const box = el("div", { class: "facts", "aria-live": "polite" });
const more = el("button", { class: "btn ghost", type: "button", text: "Otros tres datos" });
const sec = el("section", { class: "block", id: "datos" },
el("div", { class: "sec-head" }, el("div", {}, el("p", { class: "kicker", text: "¿Sabías que…?" }), el("h2", { text: "Tres datos para empezar" })), more), box);
const fact = ([big, parts, href, link]) => el("div", { class: "fact reveal" }, el("p", { class: "big", text: big }), rich("p", {}, parts), el("a", { href, text: link }));
// Bogotá's calendar day, so the set changes at its midnight.
let at = (Math.floor((Date.now() - 5 * 3600e3) / 864e5) * 3) % FACTS.length;
async function show() {
// Five candidates fetched at once; the first three that compute are shown.
const cand = Array.from({ length: 5 }, (_, k) => FACTS[(at + k) % FACTS.length]);
const got = await Promise.all(cand.map(([slug, f]) => A.story(slug).then((st) => f(st, s)).catch(() => null)));
let used = 0;
const picks = [];
for (const g of got) { used++; if (g) picks.push(g); if (picks.length === 3) break; }
at = (at + used) % FACTS.length;
box.replaceChildren(...picks.map(fact));
A.reveal(box);
}
more.addEventListener("click", show);
show();
return sec;
}
// "Para quién": one way in for each kind of reader the site is built for.
function paths(s) {
const path = (href, who, t, d) => el("a", { href }, el("p", { class: "kicker", style: "margin:0 0 6px", text: who }), el("div", { class: "t", text: t }), el("div", { class: "d", text: d }));
return el("section", { class: "block", id: "para-quien" },
el("div", { class: "sec-head" }, el("div", {}, el("p", { class: "kicker", text: "Para quién" }), el("h2", { text: "Por dónde empezar" })),
el("a", { class: "btn ghost", href: "para-quien/index.html", text: "Todo lo que puedes hacer →" })),
el("div", { class: "cta" },
path("metodologia/index.html#citar", "Periodistas", "Citar una cifra →", "Cada cifra dice de qué corte es y qué no permite concluir. La metodología explica cómo citarla."),
path("consulta/index.html", "Docentes y estudiantes", "Aprender con datos reales →", `${A.words(s.headline.companies)} de empresas para practicar SQL en el navegador, con ejemplos y un enlace para compartir cada consulta.`),
path("historias/tu-empresa/index.html", "Emprendedores y empresas", "Ver tu empresa entre las suyas →", "Busca cualquier sociedad del país y compárala con las de su sector y su cámara de comercio.")));
}
// ---------------------------------------------------------------- panorama
// "Lo que está cambiando": the panorama's counts say how much; these say
// what moves. Six comparisons, each a sentence computed from the latest
// bulletin's tables (twelve months against the twelve before, per business
// day, as the bulletin compares) or from a story's file, and each linked to
// the page that explains it. Loaded when the panorama comes into view, so the
// top of the page never waits for them.
function changing(s) {
const box = el("div", { class: "facts" });
const wrap = el("div", { style: "margin-top:28px" }, el("h3", { text: "Lo que está cambiando", style: "margin:0 0 14px" }), box);
const card = (kicker, big, parts, href, link) => el("div", { class: "fact reveal" }, el("p", { class: "kicker", style: "margin:0", text: kicker }),
el("p", { class: "big", text: big }), rich("p", {}, parts), el("a", { href, text: link }));
const load = async () => {
const [index, tiempo] = await Promise.all([A.getJSON("boletin/index.json"), A.story("regiones-tiempo").catch(() => null)]);
const latest = [...index].sort((x, y) => y.mes.localeCompare(x.mes))[0];
const b = await A.getJSON(`boletin/${latest.mes}.json`);
const c = b.cifras, rows = (k) => { const q = b.queries[k]; return q.rows.map((r) => Object.fromEntries(q.columns.map((col, i) => [col, r[i]]))); };
const perDay = c.dias_habiles_12m_anterior / c.dias_habiles_12m; // the bulletin's per-business-day comparison
const growth = (now, before) => (now / before) * perDay - 1;
const mes = A.monthYear(`${latest.mes}-01`), href = `boletin/${latest.mes}/index.html`;
const cards = [];
cards.push(card("El ritmo", A.signedPct(growth(c.sociedades_12m, c.sociedades_12m_anterior)),
["cambió la creación de sociedades en los doce meses hasta ", { b: mes }, `, frente a los doce anteriores y por día hábil: es ${c.posicion_sociedades}. La de comerciantes, ${A.signedPct(growth(c.comerciantes_12m, c.comerciantes_12m_anterior))}.`],
href, "El boletín →"));
const act = rows("actividades").filter((r) => r.sociedades_anterior >= 500).sort((x, y) => y.sociedades / y.sociedades_anterior - x.sociedades / x.sociedades_anterior)[0];
if (act) cards.push(card("La actividad que más crece", A.signedPct(growth(act.sociedades, act.sociedades_anterior)),
[{ b: A.cap(A.activityName(act.descripcion)) }, `: ${n(act.sociedades)} sociedades nuevas en doce meses, frente a ${n(act.sociedades_anterior)}.`],
`explorar/index.html#clase=${act.clase}`, "Explorar la actividad →"));
const tot = (r) => r.sociedades + r.comerciantes, ant = (r) => r.sociedades_anterior + r.comerciantes_anterior;
const sec = rows("sectores").filter((r) => r.section && ant(r) >= 500).sort((x, y) => tot(y) / ant(y) - tot(x) / ant(x))[0];
if (sec) cards.push(card("El sector que más crece", A.signedPct(growth(tot(sec), ant(sec))),
[{ b: sec.label }, `: ${n(tot(sec))} empresas nuevas en doce meses, frente a ${n(ant(sec))}.`],
`explorar/index.html#clase=${sec.section}`, "Explorar el sector →"));
const reg = rows("regiones").filter((r) => ant(r) >= 3000).sort((x, y) => tot(y) / ant(y) - tot(x) / ant(x))[0];
if (reg) cards.push(card("La región que más crece", A.signedPct(growth(tot(reg), ant(reg))),
[{ b: reg.region }, `: ${n(tot(reg))} empresas nuevas en doce meses, frente a ${n(ant(reg))}.`], href, "El boletín →"));
if (tiempo) {
const H = tiempo.q("hoy"), per = (r) => (1e5 * r.activas) / r.poblacion;
const top = [...H].sort((x, y) => per(y) - per(x))[0];
const nat = (1e5 * H.reduce((t, r) => t + r.activas, 0)) / H.reduce((t, r) => t + r.poblacion, 0);
cards.push(card("Donde hay más empresas", n(per(top)),
["empresas activas por cada 100.000 habitantes en ", { b: top.region }, `, la región con más; en el país, ${n(nat)}.`],
"historias/regiones-tiempo/index.html", "Las regiones en movimiento →"));
}
cards.push(card("La primera renovación", A.pct(c.primera_renovacion_sociedades),
["de las sociedades creadas en ", { b: String(c.cohorte) }, ` ya renovaron su matrícula en ${c.cohorte + 1}; de los comerciantes, ${A.pct(c.primera_renovacion_comerciantes)}.`],
href, "El boletín →"));
box.replaceChildren(...cards);
A.reveal(box);
};
// When it comes into view; at once where there is no observer.
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { io.disconnect(); load().catch(() => wrap.remove()); } }, { rootMargin: "400px" });
io.observe(wrap);
} else load().catch(() => wrap.remove());
return wrap;
}
function tiles(s) {
const h = s.headline;
const tile = (label, value, sub) => el("div", { class: "tile" }, el("p", { class: "label", text: label }), el("p", { class: "value", text: value }), el("p", { class: "sub", text: sub }));
return el("div", { class: "tiles" },
tile("Sociedades y entidades activas", n(h.live_with_charter), `con personería jurídica · ${pct(h.live_with_charter, h.live)} de las activas`),
tile("Comerciantes personas naturales", n(h.live_natural), `activos · ${pct(h.live_natural, h.live)} de las activas`),
tile(`Creadas en ${s.year}`, n(h.formed_this_year), `empresas nuevas hasta el ${A.dayMonth(s.data_through)}`),
tile("Registradas en total", n(h.companies), `empresas en ${h.chambers} cámaras de comercio, activas o no`));
}
function sectors(s) {
const rows = s.by_sector.filter((r) => r.section);
const total = s.by_sector.reduce((a, r) => a + r.juridicas + r.naturales, 0);
const top = rows[0], max = Math.max(...rows.map((r) => r.juridicas + r.naturales));
const card = el("div", { class: "card" }, A.legend([{ label: "Sociedades y entidades", color: "var(--c1)" }, { label: "Comerciantes personas naturales", color: "var(--c2)" }]));
const grid = el("div", { class: "hbars" });
for (const r of rows) {
const t = r.juridicas + r.naturales;
grid.append(el("div", { class: "lab" }, el("span", { class: "code", text: r.section }), r.label));
const bar = el("div", { class: "bar", style: `--f:${(t / max)}` });
const segJ = el("div", { class: "seg", style: `flex:${r.juridicas} 1 0;background:var(--c1)` });
const segN = el("div", { class: "seg", style: `flex:${r.naturales} 1 0;background:var(--c2)` });
const tip = { title: `${r.section} · ${r.label}`, rows: [{ label: "Sociedades y entidades", value: n(r.juridicas), color: A.css("--c1") },
{ label: "Personas naturales", value: n(r.naturales), color: A.css("--c2") }, { label: "Total", value: n(t) }] };
tipOn(segJ, tip); tipOn(segN, tip);
bar.append(segJ, segN);
grid.append(el("div", { class: "track" }, bar, el("span", { class: "val", text: n(t) })));
}
card.append(grid);
const unc = s.by_sector.find((r) => !r.section);
if (unc) card.append(el("p", { class: "lede", style: "margin:14px 0 0;font-size:13.5px", text: `Además, ${n(unc.juridicas + unc.naturales)} empresas activas no tienen un sector clasificable.` }));
card.append(tableView("Empresas activas por sector", ["Sector", "Sociedades y entidades", "Personas naturales", "Total"],
s.by_sector.map((r) => [r.section ? `${r.section} · ${r.label}` : r.label, n(r.juridicas), n(r.naturales), n(r.juridicas + r.naturales)])));
return el("section", { class: "block", id: "sectores" },
el("div", { class: "sec-head" }, el("div", {}, el("h2", { text: "¿A qué se dedican?" }),
rich("p", {}, ["Empresas activas por sector económico (CIIU). ", { b: top.label }, " concentra el ", { b: pct(top.juridicas + top.naturales, total) },
" de todas; en ese sector, el ", { b: pct(top.naturales, top.juridicas + top.naturales) }, " son comerciantes personas naturales."]))), card);
}
function years(s) {
const data = s.by_year;
const ref = data.find((d) => d.year === s.year - 10) || data[0];
const refTotal = ref.live + ref.not_live;
const card = el("div", { class: "card" }, A.legend([{ label: "Siguen activas", color: "var(--c1)" }, { label: "Ya no renuevan", color: "var(--deemph)" }]));
const holder = el("div");
card.append(holder);
columnsStacked(holder, data, s);
card.append(el("p", { class: "lede", style: "margin:10px 0 0;font-size:13.5px", text: `${s.year} es el año en curso: cuenta las matrículas hasta el ${A.dayMonth(s.data_through)}.` }),
tableView("Empresas creadas por año", ["Año", "Creadas", "Siguen activas", "Ya no renuevan"], data.map((d) => [String(d.year), n(d.live + d.not_live), n(d.live), n(d.not_live)])));
return el("section", { class: "block", id: "creacion" },
el("div", { class: "sec-head" }, el("div", {}, el("h2", { text: "¿Cuántas nacen y cuántas siguen?" }),
rich("p", {}, ["Empresas creadas cada año, y cuántas siguen renovando su matrícula. De las ", { b: n(refTotal) }, ` empresas creadas en ${ref.year}, el `,
{ b: pct(ref.live, refTotal) }, ` sigue activo ${s.year - ref.year} años después.`]))), card);
}
// Stacked columns: two segments with the 2px surface gap, rounded on the data end only.
function columnsStacked(holder, data, s) {
const draw = () => {
const W = Math.max(320, holder.clientWidth), H = W < 600 ? 240 : 300, L = W < 600 ? 40 : 50, R = 8, T = 18, B = 30;
const max = Math.max(...data.map((d) => d.live + d.not_live));
const p = 10 ** Math.floor(Math.log10(max / 4)), f = max / 4 / p, step = (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * p, top = Math.ceil(max / step) * step;
const band = (W - L - R) / data.length, bw = Math.min(24, band - 2);
const y = (v) => T + (H - T - B) * (1 - v / top);
const NS = "http://www.w3.org/2000/svg";
const mk = (tag, at) => { const e = document.createElementNS(NS, tag); for (const k in at) e.setAttribute(k, at[k]); return e; };
const svg = mk("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": "Empresas creadas por año y cuántas siguen activas" });
for (let v = 0; v <= top; v += step) {
svg.append(mk("line", { x1: L, x2: W - R, y1: y(v), y2: y(v), class: v === 0 ? "base" : "grid" }));
const t = mk("text", { x: L - 8, y: y(v) + 4, "text-anchor": "end", class: "tick" }); t.textContent = A.compact(v); svg.append(t);
}
const c1 = A.css("--c1"), cd = A.css("--deemph");
const path = (x, yy, w, h, r, fill) => { r = Math.min(r, h, w / 2); return mk("path", { fill, d: `M${x},${yy + h}V${yy + r}Q${x},${yy} ${x + r},${yy}H${x + w - r}Q${x + w},${yy} ${x + w},${yy + r}V${yy + h}Z` }); };
const box = el("div", { class: "chart" }, svg);
data.forEach((d, i) => {
const cx = L + band * i + band / 2, total = d.live + d.not_live, yl = y(d.live), yt = y(total), gap = d.not_live > 0 && d.live > 0 ? 2 : 0;
const g = mk("g", {});
g.append(path(cx - bw / 2, yl, bw, y(0) - yl, d.not_live > 0 ? 0 : 4, c1));
if (d.not_live > 0) g.append(path(cx - bw / 2, yt, bw, Math.max(0, yl - yt - gap), 4, cd));
// The hit band sits on top of the marks and is wider than them.
const hit = mk("rect", { x: L + band * i, y: T, width: band, height: H - T - B, class: "hit" });
svg.append(g, hit);
tipOn(hit, { title: d.year === s.year ? `${d.year} (hasta el ${A.dayMonth(s.data_through)})` : String(d.year), rows: [{ label: "Creadas", value: n(total) },
{ label: "Siguen activas", value: `${n(d.live)} · ${pct(d.live, total)}`, color: c1 }, { label: "Ya no renuevan", value: n(d.not_live), color: cd }] },
() => g.classList.add("mark-hover"), () => g.classList.remove("mark-hover"));
if ((d.year % 5 === 0 && data.length - 1 - i > 2) || i === data.length - 1) { const t = mk("text", { x: cx, y: H - 10, "text-anchor": "middle", class: "tick" }); t.textContent = d.year; svg.append(t); }
});
holder.replaceChildren(box);
};
new ResizeObserver(draw).observe(holder);
window.addEventListener("amc-theme", draw);
}
function chambers(s) {
const rows = s.by_chamber, total = rows.reduce((a, r) => a + r.live, 0);
const named = rows.filter((r) => !r.other), max = Math.max(...named.map((r) => r.live));
const top = named[0];
const card = el("div", { class: "card" });
const grid = el("div", { class: "hbars" });
for (const r of named) {
grid.append(el("div", { class: "lab", text: title(r.chamber) }));
const seg = el("div", { class: "seg", style: "flex:1 1 0;background:var(--c1)" });
tipOn(seg, { title: title(r.chamber), rows: [{ label: "Empresas activas", value: n(r.live) }, { label: "Del total", value: pct(r.live, total) }] });
grid.append(el("div", { class: "track" }, el("div", { class: "bar", style: `--f:${(r.live / max)}` }, seg), el("span", { class: "val", text: n(r.live) })));
}
card.append(grid);
const other = rows.find((r) => r.other);
if (other) card.append(el("p", { class: "lede", style: "margin:14px 0 0;font-size:13.5px", text: `Las ${other.chamber.replace(/^Otras /, "otras ")} suman ${n(other.live)} empresas activas (${pct(other.live, total)}).` }));
card.append(tableView("Empresas activas por cámara", ["Cámara de comercio", "Empresas activas", "Del total"], rows.map((r) => [r.other ? r.chamber : title(r.chamber), n(r.live), pct(r.live, total)])));
return el("section", { class: "block", id: "regiones" },
el("div", { class: "sec-head" }, el("div", {}, el("h2", { text: "¿Dónde están?" }),
rich("p", {}, ["Empresas activas por cámara de comercio. ", { b: title(top.chamber) }, " registra el ", { b: pct(top.live, total) },
" de las empresas activas del país; las doce cámaras más grandes, el ", { b: pct(named.reduce((a, r) => a + r.live, 0), total) }, "."]))), card);
}
function changes(s) {
const c = s.changes;
if (!c) return null;
const item = (value, label, sub) => el("div", { class: "change" }, el("p", { class: "value", text: n(value) }), el("p", { class: "label", text: label }), el("p", { class: "sub", text: sub }));
return el("section", { class: "block", id: "cambios" },
el("div", { class: "sec-head" }, el("div", {}, el("h2", { text: `¿Qué cambió entre el ${A.dayMonth(c.from)} y el ${A.dayMonth(c.to)}?` }),
el("p", { text: "El registro se publica aproximadamente cada mes. Entre un corte y el siguiente, matrícula por matrícula:" }))),
el("div", { class: "changes" },
item(c.new_registrations, "matrículas nuevas", "empresas y comerciantes que se registraron por primera vez"),
item(c.renewed_this_year, `renovaciones de ${s.year}`, "matrículas que renovaron en estas semanas, después del plazo de marzo"),
item(c.cancelled, "cancelaciones", "matrículas que pasaron a estado cancelado"),
item(c.activity_changed, "cambios de actividad", "empresas que cambiaron su actividad económica principal"),
item(c.renamed, "cambios de razón social", "empresas que cambiaron su nombre registrado"),
item(c.moved_away, "traslados de domicilio", "matrículas canceladas para registrarse en otra cámara")));
}
function explore(s) {
return el("section", { class: "block", id: "explorar" },
el("div", { class: "sec-head" }, el("div", {}, el("p", { class: "kicker", text: "Los datos" }), el("h2", { text: "Haz tus propias preguntas" }),
el("p", { text: "Los mismos datos que usan las historias, empresa por empresa: sin escribir código, con SQL en tu navegador o descargados." }))),
el("div", { class: "cta" },
el("a", { class: "primary", href: "explorar/index.html" }, el("div", { class: "t", text: "Explorar una actividad →" }), el("div", { class: "d", text: "Cuántas empresas hay en tu actividad y tu región, cuántas se crean cada mes y cuáles venden más. Sin SQL." })),
el("a", { href: "consulta/index.html" }, el("div", { class: "t", text: "Consulta SQL →" }), el("div", { class: "d", text: `${A.words(s.headline.companies)} de empresas, en tu navegador, sin instalar nada` })),
el("a", { href: "datos/abrir/index.html" }, el("div", { class: "t", text: "Datos abiertos →" }), el("div", { class: "d", text: "Paso a paso, cómo llevarlos a Excel, Power BI, Python o R. Licencia CC BY-SA 4.0" }))));
}
function trust(s) {
const col = (h, p) => el("div", {}, el("h3", { text: h }), el("p", { text: p }));
return el("section", { class: "block", id: "confianza" },
el("div", { class: "sec-head" }, el("div", {}, el("h2", { text: "Cómo sabemos que es correcto" }))),
el("div", { class: "trust" },
col("Verificado antes de publicar", `Cada noche, ${n(s.checks_run_nightly)} comprobaciones automáticas revisan los datos. Solo se publica lo que las pasa todas; esta edición, después de la verificación n.º ${s.verified_by_gate}.`),
col("Las personas se cuentan, no se nombran", "Los comerciantes personas naturales aparecen en todas las cifras, pero nunca su nombre ni su número de identificación (Ley 1581 de 2012)."),
col("Reproducible por cualquiera", "Cada gráfico de las historias trae el SQL que lo produjo, sobre los mismos archivos públicos. Se puede ejecutar en la consulta y obtener la misma cifra.")));
}
try {
const s = await A.summary();
app.replaceChildren(hero(s), stories(), facts(s), paths(s),
el("section", { class: "block", id: "panorama" }, el("div", { class: "sec-head", style: "margin-bottom:16px" }, el("div", {},
el("p", { class: "kicker", text: "El panorama de este corte" }), el("h2", { text: "Las empresas de Colombia, hoy" }))), tiles(s), changing(s)),
...[sectors(s), years(s), chambers(s), changes(s), explore(s), trust(s)].filter(Boolean));
// The headline counts live companies: their band, not the whole registry's.
A.footer(s, { resolution: "vivas" });
A.reveal();
window.__amcReady = true;
} catch (e) { A.fail(app, e, "el panorama"); }
</script>
</body>
</html>