Spaces:
Running
Running
Download index.html from clyxconsultingdev/amc-console: direct link, hf CLI and curl.
- Browser
- Download file 37.9 kB
-
https://huggingface.co/spaces/clyxconsultingdev/amc-console/resolve/main/index.html
- Command line
-
hf download hf://spaces/clyxconsultingdev/amc-console/index.html
-
curl -L -o index.html https://huggingface.co/spaces/clyxconsultingdev/amc-console/resolve/main/index.html
37.9 kB
| <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> | |