Spaces:
Running
Running
Deploy the site from pipeline commit 1f003bf5caec
Browse files- assets/amc.css +42 -18
- assets/nav.css +7 -0
- consulta/index.html +122 -40
assets/amc.css
CHANGED
|
@@ -22,7 +22,10 @@
|
|
| 22 |
--up: #006300; --down: #b4232f;
|
| 23 |
--shadow: 0 1px 2px rgba(0,0,0,.04), 0 6px 24px rgba(0,0,0,.05);
|
| 24 |
--shadow-lift: 0 2px 4px rgba(0,0,0,.05), 0 14px 40px rgba(0,0,0,.09);
|
| 25 |
-
--radius: 14px;
|
|
|
|
|
|
|
|
|
|
| 26 |
--sans: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 27 |
--display: "Inter Tight", Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 28 |
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
@@ -121,10 +124,10 @@ section.block { padding: 56px 0 8px; }
|
|
| 121 |
.sec-head p { color: var(--ink-2); margin: 8px 0 0; max-width: 64ch; }
|
| 122 |
.lede { color: var(--ink-2); margin: 0 0 20px; max-width: 70ch; }
|
| 123 |
.lede b { color: var(--ink); }
|
| 124 |
-
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding:
|
| 125 |
|
| 126 |
/* ---------------------------------------------------------------- story cards */
|
| 127 |
-
.stories { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:
|
| 128 |
.stories.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
| 129 |
@media (max-width: 960px) { .stories.three { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
| 130 |
@media (max-width: 680px) { .stories, .stories.three { grid-template-columns: minmax(0, 1fr); } }
|
|
@@ -133,7 +136,7 @@ section.block { padding: 56px 0 8px; }
|
|
| 133 |
.story-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); border-color: var(--border-strong); }
|
| 134 |
.story-card .thumb { aspect-ratio: 16 / 7.5; background: var(--surface-2); position: relative; border-bottom: 1px solid var(--border); }
|
| 135 |
.story-card .thumb svg, .story-card .thumb canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
| 136 |
-
.story-card .body { padding:
|
| 137 |
.story-card h3 { font-size: 21px; line-height: 1.2; margin: 0; }
|
| 138 |
.story-card p { margin: 0; color: var(--ink-2); font-size: 14.5px; }
|
| 139 |
.story-card .more { margin-top: auto; padding-top: 6px; font-weight: 600; font-size: 14px; color: var(--ink); }
|
|
@@ -147,10 +150,10 @@ section.block { padding: 56px 0 8px; }
|
|
| 147 |
}
|
| 148 |
|
| 149 |
/* ---------------------------------------------------------------- facts */
|
| 150 |
-
.facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:
|
| 151 |
@media (max-width: 860px) { .facts { grid-template-columns: minmax(0, 1fr); } }
|
| 152 |
-
.fact { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding:
|
| 153 |
-
display: flex; flex-direction: column; gap:
|
| 154 |
.fact .big { font: 800 clamp(34px, 4.6vw, 46px)/1 var(--display); letter-spacing: -.035em; margin: 0; }
|
| 155 |
.fact p { margin: 0; color: var(--ink-2); font-size: 15px; }
|
| 156 |
.fact p.big, .finding p.n { color: var(--ink); }
|
|
@@ -159,9 +162,9 @@ section.block { padding: 56px 0 8px; }
|
|
| 159 |
.fact a:hover { text-decoration: underline; }
|
| 160 |
|
| 161 |
/* ---------------------------------------------------------------- stat tiles */
|
| 162 |
-
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap:
|
| 163 |
@media (max-width: 900px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
| 164 |
-
.tile { background: var(--surface); border: 1px solid var(--border); border-radius:
|
| 165 |
.tile .label { font-size: 14px; color: var(--ink-2); margin: 0; }
|
| 166 |
.tile .value { font: 700 30px/1.15 var(--display); margin: 6px 0 4px; letter-spacing: -.02em; }
|
| 167 |
.tile .sub { font-size: 13px; color: var(--muted); margin: 0; }
|
|
@@ -177,10 +180,10 @@ section.block { padding: 56px 0 8px; }
|
|
| 177 |
.share:hover { color: var(--ink); border-color: var(--border-strong); }
|
| 178 |
.share svg { width: 13px; height: 13px; }
|
| 179 |
|
| 180 |
-
.findings { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:
|
| 181 |
@media (max-width: 820px) { .findings { grid-template-columns: minmax(0, 1fr); } }
|
| 182 |
-
.finding { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding:
|
| 183 |
-
.finding .n { font: 800 clamp(30px, 3.8vw, 40px)/1 var(--display); letter-spacing: -.03em; margin: 0 0
|
| 184 |
.finding p { margin: 0; color: var(--ink-2); font-size: 15px; line-height: 1.5; }
|
| 185 |
.finding p b { color: var(--ink); }
|
| 186 |
|
|
@@ -207,7 +210,7 @@ figure.fig.read { max-width: var(--read); }
|
|
| 207 |
|
| 208 |
/* "Cómo leerlo" — the nuance, next to the figure it qualifies */
|
| 209 |
aside.read-this { max-width: var(--read); margin: 28px auto; border-left: 3px solid var(--accent); background: var(--surface);
|
| 210 |
-
border-radius: 0
|
| 211 |
aside.read-this .t { font: 700 13px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; color: var(--accent); margin: 0 0 8px; }
|
| 212 |
aside.read-this p { margin: 0 0 .6em; color: var(--ink-2); }
|
| 213 |
aside.read-this p:last-child { margin-bottom: 0; }
|
|
@@ -296,16 +299,16 @@ details.tv th { background: var(--surface-2); color: var(--ink-2); font-weight:
|
|
| 296 |
details.tv td.n, details.tv th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
| 297 |
|
| 298 |
/* ---------------------------------------------------------------- what changed, cta, trust */
|
| 299 |
-
.changes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:
|
| 300 |
@media (max-width: 760px) { .changes { grid-template-columns: minmax(0, 1fr); } }
|
| 301 |
-
.change { background: var(--surface); border: 1px solid var(--border); border-radius:
|
| 302 |
.change .value { font: 700 26px/1.2 var(--display); margin: 0; letter-spacing: -.02em; }
|
| 303 |
.change .label { margin: 2px 0 0; font-weight: 600; }
|
| 304 |
.change .sub { margin: 4px 0 0; color: var(--ink-2); font-size: 14px; }
|
| 305 |
-
.cta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:
|
| 306 |
@media (max-width: 860px) { .cta { grid-template-columns: minmax(0, 1fr); } }
|
| 307 |
.cta a { display: block; text-decoration: none; background: var(--surface); border: 1px solid var(--border);
|
| 308 |
-
border-radius: var(--radius); padding:
|
| 309 |
.cta a:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); border-color: var(--border-strong); }
|
| 310 |
.cta a.primary { background: var(--ink); border-color: var(--ink); color: var(--page); }
|
| 311 |
.cta .t { font: 700 18px var(--display); letter-spacing: -.01em; }
|
|
@@ -357,7 +360,7 @@ nav.pager a { display: inline-block; padding: 10px 0; min-height: 24px; }
|
|
| 357 |
.scrolly .graphic { grid-row: 1; grid-column: 1; position: sticky; top: 76px; height: calc(100vh - 96px); display: flex; flex-direction: column; justify-content: center; }
|
| 358 |
.scrolly .graphic .chart.map svg { max-height: calc(100vh - 210px); }
|
| 359 |
.scrolly .steps { grid-row: 1; grid-column: 1; z-index: 1; padding: 62vh 0 30vh; pointer-events: none; }
|
| 360 |
-
.scrolly .step { pointer-events: auto; max-width: 440px; margin: 0 auto 80vh; padding:
|
| 361 |
border-radius: var(--radius); box-shadow: var(--shadow-lift); font-size: 16.5px; line-height: 1.55; opacity: .78; transition: opacity .3s; }
|
| 362 |
.scrolly .step.on { opacity: 1; }
|
| 363 |
.scrolly .step p { margin: 0; }
|
|
@@ -377,6 +380,27 @@ nav.pager a { display: inline-block; padding: 10px 0; min-height: 24px; }
|
|
| 377 |
.reveal.in { opacity: 1; transform: none; }
|
| 378 |
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } }
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
@media print {
|
| 381 |
nav.top, .share, .fig-foot .acts, footer.site { display: none; }
|
| 382 |
body { background: #fff; }
|
|
|
|
| 22 |
--up: #006300; --down: #b4232f;
|
| 23 |
--shadow: 0 1px 2px rgba(0,0,0,.04), 0 6px 24px rgba(0,0,0,.05);
|
| 24 |
--shadow-lift: 0 2px 4px rgba(0,0,0,.05), 0 14px 40px rgba(0,0,0,.09);
|
| 25 |
+
--radius: 14px; --radius-sm: 12px;
|
| 26 |
+
/* One spacing system for every box: its padding, the gap between boxes in a
|
| 27 |
+
row, and the gap between a box's number, title and text. */
|
| 28 |
+
--pad: 20px; --pad-tight: 16px; --gap: 16px; --gap-tight: 12px; --stack: 10px;
|
| 29 |
--sans: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 30 |
--display: "Inter Tight", Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 31 |
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
|
| 124 |
.sec-head p { color: var(--ink-2); margin: 8px 0 0; max-width: 64ch; }
|
| 125 |
.lede { color: var(--ink-2); margin: 0 0 20px; max-width: 70ch; }
|
| 126 |
.lede b { color: var(--ink); }
|
| 127 |
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--pad); box-shadow: var(--shadow); }
|
| 128 |
|
| 129 |
/* ---------------------------------------------------------------- story cards */
|
| 130 |
+
.stories { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap); }
|
| 131 |
.stories.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
| 132 |
@media (max-width: 960px) { .stories.three { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
| 133 |
@media (max-width: 680px) { .stories, .stories.three { grid-template-columns: minmax(0, 1fr); } }
|
|
|
|
| 136 |
.story-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); border-color: var(--border-strong); }
|
| 137 |
.story-card .thumb { aspect-ratio: 16 / 7.5; background: var(--surface-2); position: relative; border-bottom: 1px solid var(--border); }
|
| 138 |
.story-card .thumb svg, .story-card .thumb canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
| 139 |
+
.story-card .body { padding: var(--pad); display: flex; flex-direction: column; gap: var(--stack); flex: 1; }
|
| 140 |
.story-card h3 { font-size: 21px; line-height: 1.2; margin: 0; }
|
| 141 |
.story-card p { margin: 0; color: var(--ink-2); font-size: 14.5px; }
|
| 142 |
.story-card .more { margin-top: auto; padding-top: 6px; font-weight: 600; font-size: 14px; color: var(--ink); }
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
/* ---------------------------------------------------------------- facts */
|
| 153 |
+
.facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap); }
|
| 154 |
@media (max-width: 860px) { .facts { grid-template-columns: minmax(0, 1fr); } }
|
| 155 |
+
.fact { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--pad); box-shadow: var(--shadow);
|
| 156 |
+
display: flex; flex-direction: column; gap: var(--stack); }
|
| 157 |
.fact .big { font: 800 clamp(34px, 4.6vw, 46px)/1 var(--display); letter-spacing: -.035em; margin: 0; }
|
| 158 |
.fact p { margin: 0; color: var(--ink-2); font-size: 15px; }
|
| 159 |
.fact p.big, .finding p.n { color: var(--ink); }
|
|
|
|
| 162 |
.fact a:hover { text-decoration: underline; }
|
| 163 |
|
| 164 |
/* ---------------------------------------------------------------- stat tiles */
|
| 165 |
+
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gap-tight); margin: 0 0 8px; }
|
| 166 |
@media (max-width: 900px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
| 167 |
+
.tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--pad-tight); box-shadow: var(--shadow); }
|
| 168 |
.tile .label { font-size: 14px; color: var(--ink-2); margin: 0; }
|
| 169 |
.tile .value { font: 700 30px/1.15 var(--display); margin: 6px 0 4px; letter-spacing: -.02em; }
|
| 170 |
.tile .sub { font-size: 13px; color: var(--muted); margin: 0; }
|
|
|
|
| 180 |
.share:hover { color: var(--ink); border-color: var(--border-strong); }
|
| 181 |
.share svg { width: 13px; height: 13px; }
|
| 182 |
|
| 183 |
+
.findings { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap); margin: 36px 0 12px; counter-reset: f; }
|
| 184 |
@media (max-width: 820px) { .findings { grid-template-columns: minmax(0, 1fr); } }
|
| 185 |
+
.finding { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--pad); box-shadow: var(--shadow); }
|
| 186 |
+
.finding .n { font: 800 clamp(30px, 3.8vw, 40px)/1 var(--display); letter-spacing: -.03em; margin: 0 0 var(--stack); }
|
| 187 |
.finding p { margin: 0; color: var(--ink-2); font-size: 15px; line-height: 1.5; }
|
| 188 |
.finding p b { color: var(--ink); }
|
| 189 |
|
|
|
|
| 210 |
|
| 211 |
/* "Cómo leerlo" — the nuance, next to the figure it qualifies */
|
| 212 |
aside.read-this { max-width: var(--read); margin: 28px auto; border-left: 3px solid var(--accent); background: var(--surface);
|
| 213 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding: var(--pad-tight) var(--pad); box-shadow: var(--shadow); font-size: 15.5px; line-height: 1.6; }
|
| 214 |
aside.read-this .t { font: 700 13px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; color: var(--accent); margin: 0 0 8px; }
|
| 215 |
aside.read-this p { margin: 0 0 .6em; color: var(--ink-2); }
|
| 216 |
aside.read-this p:last-child { margin-bottom: 0; }
|
|
|
|
| 299 |
details.tv td.n, details.tv th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
| 300 |
|
| 301 |
/* ---------------------------------------------------------------- what changed, cta, trust */
|
| 302 |
+
.changes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-tight); }
|
| 303 |
@media (max-width: 760px) { .changes { grid-template-columns: minmax(0, 1fr); } }
|
| 304 |
+
.change { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--pad-tight); box-shadow: var(--shadow); }
|
| 305 |
.change .value { font: 700 26px/1.2 var(--display); margin: 0; letter-spacing: -.02em; }
|
| 306 |
.change .label { margin: 2px 0 0; font-weight: 600; }
|
| 307 |
.change .sub { margin: 4px 0 0; color: var(--ink-2); font-size: 14px; }
|
| 308 |
+
.cta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-tight); }
|
| 309 |
@media (max-width: 860px) { .cta { grid-template-columns: minmax(0, 1fr); } }
|
| 310 |
.cta a { display: block; text-decoration: none; background: var(--surface); border: 1px solid var(--border);
|
| 311 |
+
border-radius: var(--radius); padding: var(--pad); box-shadow: var(--shadow); transition: transform .2s, box-shadow .2s, border-color .2s; }
|
| 312 |
.cta a:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); border-color: var(--border-strong); }
|
| 313 |
.cta a.primary { background: var(--ink); border-color: var(--ink); color: var(--page); }
|
| 314 |
.cta .t { font: 700 18px var(--display); letter-spacing: -.01em; }
|
|
|
|
| 360 |
.scrolly .graphic { grid-row: 1; grid-column: 1; position: sticky; top: 76px; height: calc(100vh - 96px); display: flex; flex-direction: column; justify-content: center; }
|
| 361 |
.scrolly .graphic .chart.map svg { max-height: calc(100vh - 210px); }
|
| 362 |
.scrolly .steps { grid-row: 1; grid-column: 1; z-index: 1; padding: 62vh 0 30vh; pointer-events: none; }
|
| 363 |
+
.scrolly .step { pointer-events: auto; max-width: 440px; margin: 0 auto 80vh; padding: var(--pad-tight) var(--pad); background: var(--surface); border: 1px solid var(--border);
|
| 364 |
border-radius: var(--radius); box-shadow: var(--shadow-lift); font-size: 16.5px; line-height: 1.55; opacity: .78; transition: opacity .3s; }
|
| 365 |
.scrolly .step.on { opacity: 1; }
|
| 366 |
.scrolly .step p { margin: 0; }
|
|
|
|
| 380 |
.reveal.in { opacity: 1; transform: none; }
|
| 381 |
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; transition: none; } }
|
| 382 |
|
| 383 |
+
/* ---------------------------------------------------------------- typography
|
| 384 |
+
Text in boxes reads as one family on every screen. Headings and the short
|
| 385 |
+
lines of a card balance their lines; paragraphs avoid a lone last word.
|
| 386 |
+
Every page is lang="es", so the browser hyphenates by Spanish rules: in the
|
| 387 |
+
boxes always, in the prose only on a phone, where a long word otherwise
|
| 388 |
+
leaves a ragged gap. Figures that sit side by side use tabular digits.
|
| 389 |
+
Long prose is justified only where its column is wide enough to take it,
|
| 390 |
+
and always hyphenated; a card, a caption or a figure is never justified. */
|
| 391 |
+
h1, h2, h3, .hero .line, .story-head .dek, .fig-head h3, .story-card h3, .cta .t, .tile .label, .change .label { text-wrap: balance; }
|
| 392 |
+
p, li, .fig-head p, .cta .d { text-wrap: pretty; }
|
| 393 |
+
.card, .fact, .finding, .tile, .change, .story-card .body, .cta a, .trust p, .fig-head p, aside.read-this, .scrolly .step, #tip {
|
| 394 |
+
-webkit-hyphens: auto; hyphens: auto; hyphenate-limit-chars: 7 3 3; overflow-wrap: break-word; }
|
| 395 |
+
.fact .big, .finding .n, .tile .value, .change .value, .story-card .stat { font-variant-numeric: tabular-nums; -webkit-hyphens: manual; hyphens: manual; }
|
| 396 |
+
@media (max-width: 540px) {
|
| 397 |
+
.prose { -webkit-hyphens: auto; hyphens: auto; hyphenate-limit-chars: 7 3 3; }
|
| 398 |
+
}
|
| 399 |
+
@media (min-width: 900px) {
|
| 400 |
+
.prose > p { text-align: justify; -webkit-hyphens: auto; hyphens: auto; hyphenate-limit-chars: 7 3 3; }
|
| 401 |
+
figure .prose > p { text-align: start; }
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
@media print {
|
| 405 |
nav.top, .share, .fig-foot .acts, footer.site { display: none; }
|
| 406 |
body { background: #fff; }
|
assets/nav.css
CHANGED
|
@@ -18,6 +18,13 @@ nav.top .wrap { max-width: 1080px; margin: 0 auto; padding: 0 16px; display: fle
|
|
| 18 |
nav.top .brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; white-space: nowrap; color: var(--n-ink); }
|
| 19 |
nav.top .brand .mark { height: 30px; padding: 0 8px; border-radius: 8px; background: var(--n-mark); color: var(--n-mark-ink);
|
| 20 |
display: grid; place-items: center; font: 800 12.5px/1 "Inter Tight", Inter, system-ui, sans-serif; letter-spacing: .04em; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
nav.top .brand .name { font: 700 15px/1.1 "Inter Tight", Inter, system-ui, sans-serif; letter-spacing: -.01em; }
|
| 22 |
nav.top .brand .name small { display: block; font: 500 12px/1.2 Inter, system-ui, sans-serif; color: var(--n-muted); letter-spacing: 0; }
|
| 23 |
nav.top .links { display: flex; gap: 2px; margin-left: auto; align-items: center; }
|
|
|
|
| 18 |
nav.top .brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; white-space: nowrap; color: var(--n-ink); }
|
| 19 |
nav.top .brand .mark { height: 30px; padding: 0 8px; border-radius: 8px; background: var(--n-mark); color: var(--n-mark-ink);
|
| 20 |
display: grid; place-items: center; font: 800 12.5px/1 "Inter Tight", Inter, system-ui, sans-serif; letter-spacing: .04em; }
|
| 21 |
+
/* Colombia's colours, not its flag: a strip in the flag's bands (yellow half,
|
| 22 |
+
blue and red a quarter each) along the foot of the mark. The flag itself, or
|
| 23 |
+
its arms, would read as a government site, which AMC is careful not to be. */
|
| 24 |
+
nav.top .brand .mark { position: relative; overflow: hidden; box-sizing: border-box; padding-bottom: 4px; }
|
| 25 |
+
nav.top .brand .mark::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 4px;
|
| 26 |
+
background: linear-gradient(90deg, #fcd116 0 50%, #003893 50% 75%, #ce1126 75%); }
|
| 27 |
+
@media (forced-colors: active) { nav.top .brand .mark::after { display: none; } }
|
| 28 |
nav.top .brand .name { font: 700 15px/1.1 "Inter Tight", Inter, system-ui, sans-serif; letter-spacing: -.01em; }
|
| 29 |
nav.top .brand .name small { display: block; font: 500 12px/1.2 Inter, system-ui, sans-serif; color: var(--n-muted); letter-spacing: 0; }
|
| 30 |
nav.top .links { display: flex; gap: 2px; margin-left: auto; align-items: center; }
|
consulta/index.html
CHANGED
|
@@ -257,38 +257,78 @@ import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.32.0
|
|
| 257 |
const BASE = "https://huggingface.co/datasets/clyxconsultingdev/amc-rues/resolve/main";
|
| 258 |
const DISPLAY_CAP = 1000;
|
| 259 |
|
| 260 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
const EXAMPLES = [
|
| 262 |
-
["Empresas vigentes por sector",
|
| 263 |
-
"-- Personas jurídicas vigentes (la lista objetivo), por sección CIIU\nSELECT s.section, s.label, count(*) AS empresas\nFROM companies c JOIN sector_sections s USING (section)\nWHERE c.in_target_list\nGROUP BY ALL ORDER BY empresas DESC;"],
|
| 264 |
-
["
|
| 265 |
-
"--
|
| 266 |
-
["
|
| 267 |
-
"
|
| 268 |
-
["
|
| 269 |
-
"--
|
| 270 |
-
["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
"SELECT chamber AS camara, count(*) AS empresas\nFROM companies\nWHERE in_target_list\nGROUP BY chamber ORDER BY empresas DESC\nLIMIT 15;"],
|
| 272 |
-
["
|
| 273 |
-
"
|
| 274 |
-
["
|
| 275 |
-
"
|
| 276 |
-
["
|
| 277 |
-
"--
|
| 278 |
-
["
|
| 279 |
-
"
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
];
|
| 293 |
|
| 294 |
const $ = (id) => document.getElementById(id);
|
|
@@ -364,17 +404,41 @@ async function startDuckDB() {
|
|
| 364 |
// companies.parquet from Hugging Face before the first query (measured
|
| 365 |
// 2026-10-14). With ranges it reads the footer and the columns it needs.
|
| 366 |
await db.open({ query: { castDecimalToDouble: true }, filesystem: { forceFullHTTPReads: false } });
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
// One view per published file, named after it. The list comes from the
|
| 371 |
// manifest, so a table added to the edition appears here without an edit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
async function createViews(m) {
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
| 376 |
}
|
| 377 |
-
|
|
|
|
| 378 |
}
|
| 379 |
|
| 380 |
async function renderSchema(m, names) {
|
|
@@ -443,16 +507,29 @@ function cell(v, type, name = "") {
|
|
| 443 |
return { text: String(v), cls: "" };
|
| 444 |
}
|
| 445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
async function run() {
|
| 447 |
const sql = $("sql").value;
|
| 448 |
if (!sql.trim() || !conn) return;
|
|
|
|
| 449 |
$("run").disabled = true;
|
| 450 |
const t0 = performance.now();
|
| 451 |
const tick = setInterval(() => setStatus("busy", `Ejecutando… ${((performance.now() - t0) / 1000).toLocaleString("es-CO", { maximumFractionDigits: 1, minimumFractionDigits: 1 })} s`), 200);
|
| 452 |
setStatus("busy", "Ejecutando…");
|
| 453 |
try {
|
| 454 |
const q = displayable(sql);
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
const secs = ((performance.now() - t0) / 1000).toFixed(2);
|
| 457 |
const fields = res.schema.fields;
|
| 458 |
let rows = res.toArray().map((r) => r.toJSON());
|
|
@@ -464,13 +541,14 @@ async function run() {
|
|
| 464 |
$("csv").disabled = rows.length === 0;
|
| 465 |
history.replaceState(null, "", `#q=${encodeURIComponent(sql)}`);
|
| 466 |
} catch (e) {
|
|
|
|
| 467 |
lastRows = null;
|
| 468 |
$("results").replaceChildren(el("div", { class: "msg err", text: String(e.message || e) }));
|
| 469 |
setStatus("err", "Error en la consulta");
|
| 470 |
$("csv").disabled = true;
|
| 471 |
} finally {
|
| 472 |
clearInterval(tick);
|
| 473 |
-
$("run").disabled = false;
|
| 474 |
}
|
| 475 |
}
|
| 476 |
|
|
@@ -520,10 +598,14 @@ async function copyLink() {
|
|
| 520 |
}
|
| 521 |
|
| 522 |
// ---------------------------------------------------------------- wiring
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
$("examples").addEventListener("change", (e) => {
|
| 525 |
const ex = EXAMPLES[Number(e.target.value)];
|
| 526 |
-
if (ex) { $("sql").value = ex[
|
| 527 |
e.target.value = "";
|
| 528 |
});
|
| 529 |
$("run").addEventListener("click", run);
|
|
@@ -535,7 +617,7 @@ $("sql").addEventListener("keydown", (e) => {
|
|
| 535 |
});
|
| 536 |
|
| 537 |
const fromLink = new URLSearchParams(location.hash.slice(1)).get("q");
|
| 538 |
-
$("sql").value = fromLink || EXAMPLES[0][
|
| 539 |
|
| 540 |
(async () => {
|
| 541 |
try {
|
|
|
|
| 257 |
const BASE = "https://huggingface.co/datasets/clyxconsultingdev/amc-rues/resolve/main";
|
| 258 |
const DISPLAY_CAP = 1000;
|
| 259 |
|
| 260 |
+
// Thirty, from a first SELECT to window functions, grouped by what each one
|
| 261 |
+
// teaches. Each was run in this page, against the published files, before it
|
| 262 |
+
// went in the list (scratchpad check, 2026-10-16). The slow ones read the
|
| 263 |
+
// 8.3-million-row `companies`; where a summary table answers the same
|
| 264 |
+
// question, the example uses it and says so.
|
| 265 |
const EXAMPLES = [
|
| 266 |
+
["Primeros pasos", "Empresas vigentes por sector",
|
| 267 |
+
"-- SELECT, JOIN, WHERE, GROUP BY y ORDER BY en una sola consulta.\n-- Personas jurídicas vigentes (la lista objetivo), por sección CIIU\nSELECT s.section, s.label, count(*) AS empresas\nFROM companies c JOIN sector_sections s USING (section)\nWHERE c.in_target_list\nGROUP BY ALL ORDER BY empresas DESC;"],
|
| 268 |
+
["Primeros pasos", "¿Cuántas empresas llevan una palabra en el nombre?",
|
| 269 |
+
"-- name_words guarda cada palabra de cada razón social, en orden alfabético:\n-- buscar una palabra exacta solo lee el trozo del archivo donde está, y es rápido.\n-- Escríbela en mayúsculas y sin tildes, y prueba otras: SOFTWARE, TRANSPORTES, ARTESANIAS\nSELECT palabra, count(*) AS empresas,\n count(*) FILTER (WHERE vivo) AS vigentes,\n count(*) FILTER (WHERE primera) AS como_primera_palabra\nFROM name_words\nWHERE palabra = 'CAFE'\nGROUP BY palabra;"],
|
| 270 |
+
["Primeros pasos", "Buscar un fragmento de nombre con ILIKE",
|
| 271 |
+
"-- ILIKE busca un texto en cualquier parte, sin distinguir mayúsculas: '%' es cualquier cosa.\n-- Tiene que leer los 8,3 millones de nombres, así que tarda más que contar una palabra.\nSELECT display_name AS razon_social, nit, chamber AS camara, legal_form, first_registered, is_live AS vigente\nFROM companies\nWHERE display_name ILIKE '%alpina%'\nORDER BY is_live DESC, first_registered\nLIMIT 25;"],
|
| 272 |
+
["Primeros pasos", "Las sociedades más recientes",
|
| 273 |
+
"-- ORDER BY ... DESC y LIMIT: las últimas inscritas en el registro\nSELECT display_name AS razon_social, legal_form, chamber AS camara, section, first_registered\nFROM companies\nWHERE org_type IN ('sociedad', 'sin ánimo de lucro', 'economía solidaria')\nORDER BY first_registered DESC\nLIMIT 25;"],
|
| 274 |
+
["Primeros pasos", "Las leyes que cambiaron el registro",
|
| 275 |
+
"-- Una tabla pequeña para empezar: los hitos que explican saltos en las cifras\nSELECT fecha, titulo, detalle, fuente\nFROM milestones\nORDER BY fecha;"],
|
| 276 |
+
["Primeros pasos", "¿Qué código CIIU tiene una actividad?",
|
| 277 |
+
"-- OR combina condiciones; cada clase CIIU es una actividad económica\nSELECT clase, descripcion\nFROM activities\nWHERE descripcion ILIKE '%software%' OR descripcion ILIKE '%informática%'\nORDER BY clase;"],
|
| 278 |
+
|
| 279 |
+
["Contar y agrupar", "Quién está en el registro, por tipo de organización",
|
| 280 |
+
"-- count(*) FILTER (WHERE ...) cuenta solo una parte del grupo\nSELECT org_type, count(*) AS empresas,\n count(*) FILTER (WHERE is_live) AS vigentes\nFROM companies GROUP BY org_type ORDER BY empresas DESC;"],
|
| 281 |
+
["Contar y agrupar", "¿Dónde están? Por cámara de comercio",
|
| 282 |
"SELECT chamber AS camara, count(*) AS empresas\nFROM companies\nWHERE in_target_list\nGROUP BY chamber ORDER BY empresas DESC\nLIMIT 15;"],
|
| 283 |
+
["Contar y agrupar", "El estado no es lo mismo que la vigencia",
|
| 284 |
+
"-- Agrupar por dos columnas cruza sus valores.\n-- Millones de matrículas dicen ACTIVA sin haber renovado, y al revés\nSELECT status AS estado, is_live AS vigente, count(*) AS empresas\nFROM companies\nGROUP BY ALL ORDER BY empresas DESC\nLIMIT 12;"],
|
| 285 |
+
["Contar y agrupar", "Empresas creadas por año, y cuántas siguen",
|
| 286 |
+
"-- cohort es companies ya resumida por año de creación, cámara, sector y forma jurídica\n-- (las empresas del paso 3 de funnel): se suma con sum(), no se cuenta con count(),\n-- y responde en un instante\nSELECT formation_year AS anio,\n sum(companies) AS creadas,\n sum(live_companies) AS vigentes,\n round(100.0 * sum(live_companies) / sum(companies), 1) AS pct_vigentes\nFROM cohort\nWHERE formation_year BETWEEN 2000 AND 2026 AND is_juridical\nGROUP BY formation_year ORDER BY formation_year;"],
|
| 287 |
+
["Contar y agrupar", "Las formas jurídicas de las empresas nuevas",
|
| 288 |
+
"-- WHERE filtra filas antes de agrupar; HAVING filtra grupos después\nSELECT legal_form AS forma_juridica, sum(companies) AS creadas_desde_2020\nFROM cohort\nWHERE formation_year >= 2020 AND is_juridical\nGROUP BY legal_form\nHAVING sum(companies) >= 100\nORDER BY creadas_desde_2020 DESC;"],
|
| 289 |
+
["Contar y agrupar", "Cancelaciones de matrículas por año",
|
| 290 |
+
"-- year() saca el año de una fecha.\n-- 2011 y 2015: las depuraciones del registro, no cierres económicos\nSELECT year(fecha) AS anio, sum(matriculas) AS canceladas,\n sum(matriculas) FILTER (WHERE estado ILIKE '%1429%') AS por_ley_1429\nFROM registry_events\nWHERE evento = 'cancelación' AND fecha >= DATE '2000-01-01'\nGROUP BY 1 ORDER BY 1;"],
|
| 291 |
+
|
| 292 |
+
["Contar y agrupar", "Cuántas renovaron alguna vez, por año de creación",
|
| 293 |
+
"-- nullif() evita dividir por cero.\n-- De las que se sabe si renovaron, qué parte renovó después de su año de creación\nSELECT formation_year AS anio,\n sum(cohort_size) AS cohorte,\n round(100.0 * sum(renewed) / nullif(sum(renewed) + sum(never_renewed), 0), 1) AS pct_renovo\nFROM survival\nWHERE formation_year BETWEEN 2000 AND 2025\nGROUP BY formation_year ORDER BY formation_year;"],
|
| 294 |
+
["Unir tablas", "Las actividades que más empresas crean",
|
| 295 |
+
"-- JOIN trae la descripción de cada código desde otra tabla\nSELECT a.clase, a.descripcion, count(*) AS creadas_2025\nFROM companies c JOIN activities a ON a.clase = c.sector_code\nWHERE c.formation_year = 2025\nGROUP BY ALL ORDER BY creadas_2025 DESC\nLIMIT 20;"],
|
| 296 |
+
["Unir tablas", "Empresas creadas por día en cada gobierno",
|
| 297 |
+
"-- Una unión por rango de fechas, no por igualdad.\n-- El ritmo diario, porque los periodos no duran lo mismo; el actual va corriendo\nSELECT p.periodo, p.presidente,\n count(*) AS creadas,\n round(count(*) / (coalesce(p.fin, current_date) - p.inicio), 0) AS por_dia\nFROM periods p\nJOIN companies c ON c.first_registered >= p.inicio\n AND c.first_registered < coalesce(p.fin, current_date)\nGROUP BY p.periodo, p.presidente, p.inicio, p.fin\nORDER BY p.inicio;"],
|
| 298 |
+
["Unir tablas", "Empresas vigentes por cada mil habitantes",
|
| 299 |
+
"-- WITH nombra consultas intermedias. regions traduce cámaras y departamentos a una\n-- misma región, para poder cruzar empresas con la población del DANE\nWITH empresas AS (\n SELECT r.region, sum(c.live_companies) AS vigentes\n FROM cohort c JOIN regions r ON r.tipo = 'camara' AND r.codigo = c.codigo_camara\n WHERE c.is_juridical\n GROUP BY r.region\n), habitantes AS (\n SELECT r.region, sum(p.poblacion) AS poblacion\n FROM population p JOIN regions r ON r.tipo = 'departamento' AND r.codigo = p.dp\n WHERE p.anio = 2026\n GROUP BY r.region\n)\nSELECT region, vigentes, poblacion,\n round(1000.0 * vigentes / poblacion, 1) AS por_mil_habitantes\nFROM empresas JOIN habitantes USING (region)\nORDER BY por_mil_habitantes DESC;"],
|
| 300 |
+
["Unir tablas", "Las cámaras que comparten más empresas",
|
| 301 |
+
"-- La misma tabla unida dos veces, una por cada lado de la pareja.\n-- Una empresa inscrita en dos cámaras: casi siempre un traslado de domicilio\nWITH camaras AS (SELECT DISTINCT codigo_camara, chamber FROM cohort)\nSELECT a.chamber AS camara_a, b.chamber AS camara_b,\n l.empresas, l.con_traslado, l.misma_fecha\nFROM chamber_links l\nJOIN camaras a ON a.codigo_camara = l.camara_a\nJOIN camaras b ON b.codigo_camara = l.camara_b\nORDER BY l.empresas DESC\nLIMIT 15;"],
|
| 302 |
+
|
| 303 |
+
["Fechas", "Sociedades y comerciantes nuevos, mes a mes",
|
| 304 |
+
"-- Una subconsulta halla el último mes con datos; INTERVAL resta 24 meses.\n-- El último mes suele ir incompleto\nSELECT mes,\n sum(matriculas) FILTER (WHERE org_type = 'sociedad') AS sociedades,\n sum(matriculas) FILTER (WHERE org_type = 'persona natural') AS personas_naturales\nFROM registry_months\nWHERE evento = 'matrícula'\n AND mes > (SELECT max(mes) FROM registry_months) - INTERVAL 24 MONTH\nGROUP BY mes ORDER BY mes;"],
|
| 305 |
+
["Fechas", "¿Qué día de la semana se crean las sociedades?",
|
| 306 |
+
"-- isodow() numera los días de lunes (1) a domingo (7); una lista los nombra\nSELECT isodow(fecha) AS n,\n ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'][isodow(fecha)] AS dia,\n sum(matriculas) AS sociedades_inscritas\nFROM registry_events\nWHERE evento = 'matrícula' AND org_type = 'sociedad' AND fecha >= DATE '2015-01-01'\nGROUP BY ALL ORDER BY n;"],
|
| 307 |
+
["Fechas", "La pandemia, mes a mes (PIVOT)",
|
| 308 |
+
"-- PIVOT convierte los valores de una columna, aquí los años, en columnas\nPIVOT (\n SELECT month(mes) AS mes, year(mes) AS anio, matriculas\n FROM registry_months\n WHERE evento = 'matrícula' AND org_type = 'sociedad' AND year(mes) BETWEEN 2019 AND 2021\n) ON anio USING sum(matriculas)\nGROUP BY mes ORDER BY mes;"],
|
| 309 |
+
|
| 310 |
+
["Funciones de ventana", "Cuánto cambian las creaciones de un año al otro",
|
| 311 |
+
"-- lag() trae el valor de la fila anterior, en el orden que dice OVER\nWITH anual AS (\n SELECT formation_year AS anio, sum(companies) AS creadas\n FROM cohort\n WHERE is_juridical AND formation_year BETWEEN 2000 AND 2025\n GROUP BY formation_year\n)\nSELECT anio, creadas,\n lag(creadas) OVER (ORDER BY anio) AS el_anio_anterior,\n round(100.0 * (creadas / lag(creadas) OVER (ORDER BY anio) - 1), 1) AS cambio_pct\nFROM anual ORDER BY anio;"],
|
| 312 |
+
["Funciones de ventana", "Los tres sectores más grandes de cada cámara",
|
| 313 |
+
"-- row_number() numera dentro de cada cámara (PARTITION BY);\n-- QUALIFY filtra por ese número, como un WHERE para las ventanas\nSELECT chamber AS camara, section_label AS sector,\n sum(live_companies) AS vigentes,\n row_number() OVER (PARTITION BY chamber ORDER BY sum(live_companies) DESC) AS puesto\nFROM cohort\nWHERE is_juridical AND section IS NOT NULL\nGROUP BY chamber, section_label\nQUALIFY puesto <= 3\nORDER BY camara, puesto;"],
|
| 314 |
+
["Funciones de ventana", "¿Qué tan jóvenes son las empresas vigentes?",
|
| 315 |
+
"-- sum() OVER (ORDER BY ...) acumula fila a fila; sum() OVER () es el total.\n-- La fila donde el acumulado pasa del 50 % parte a las vigentes en dos mitades\nWITH anual AS (\n SELECT formation_year AS anio, sum(live_companies) AS vigentes\n FROM cohort WHERE is_juridical GROUP BY formation_year\n), acumulado AS (\n SELECT anio, vigentes,\n round(100.0 * sum(vigentes) OVER (ORDER BY anio DESC) / sum(vigentes) OVER (), 1) AS pct_creadas_desde_ese_anio\n FROM anual\n)\nSELECT * FROM acumulado WHERE anio >= 1990 ORDER BY anio DESC;"],
|
| 316 |
+
|
| 317 |
+
["Estados financieros", "Las empresas que más venden",
|
| 318 |
+
"-- Solo las empresas que reportan a la Superintendencia de Sociedades.\n-- Las cifras vienen en miles de pesos: entre 1e6 da miles de millones\nSELECT c.display_name AS razon_social, c.chamber AS camara, f.anio_fiscal,\n round(f.ingresos / 1e6, 1) AS ingresos_miles_de_millones\nFROM financials f JOIN companies c USING (nit)\nWHERE f.anio_fiscal = (SELECT max(anio_fiscal) FROM financials)\nORDER BY f.ingresos DESC NULLS LAST\nLIMIT 25;"],
|
| 319 |
+
["Estados financieros", "¿Qué edad tienen las empresas que reportan?",
|
| 320 |
+
"-- LEFT JOIN conserva las que no tienen pareja: su nit queda en NULL\nSELECT CASE WHEN f.nit IS NULL THEN 'no reportan' ELSE 'reportan' END AS grupo,\n count(*) AS sociedades,\n median(year(current_date) - c.formation_year) AS edad_mediana\nFROM companies c\nLEFT JOIN (SELECT DISTINCT nit FROM financials\n WHERE anio_fiscal = (SELECT max(anio_fiscal) FROM financials)) f USING (nit)\nWHERE c.is_live AND c.org_type = 'sociedad'\nGROUP BY ALL;"],
|
| 321 |
+
["Estados financieros", "El margen típico de cada sector",
|
| 322 |
+
"-- median() resiste a los valores extremos mejor que avg().\n-- Se dejan fuera los años marcados con una unidad dudosa\nSELECT s.label AS sector, count(*) AS empresas,\n round(100 * median(f.ganancia / f.ingresos), 1) AS margen_neto_mediano_pct\nFROM financials f\nJOIN companies c USING (nit)\nJOIN sector_sections s ON s.section = c.section\nWHERE f.anio_fiscal = 2024 AND f.ingresos > 0 AND NOT f.unidad_dudosa\nGROUP BY s.label\nHAVING count(*) >= 50\nORDER BY margen_neto_mediano_pct DESC;"],
|
| 323 |
+
["Estados financieros", "¿Cuánto deben las empresas? Cuartiles por año",
|
| 324 |
+
"-- quantile_cont() da cualquier percentil: aquí el 25 %, la mediana y el 75 %.\n-- Mira siempre con cuántas empresas se calcula cada cifra: si son pocas, pesa poco\nSELECT anio_fiscal, count(*) AS empresas,\n round(quantile_cont(pasivos / activos, 0.25), 2) AS p25,\n round(quantile_cont(pasivos / activos, 0.50), 2) AS mediana,\n round(quantile_cont(pasivos / activos, 0.75), 2) AS p75\nFROM financials\nWHERE activos > 0 AND NOT unidad_dudosa\nGROUP BY anio_fiscal ORDER BY anio_fiscal;"],
|
| 325 |
+
["Estados financieros", "Las grandes que más crecieron, descontando la inflación",
|
| 326 |
+
"-- La misma tabla dos veces, un año en cada lado; prices lleva los pesos de 2024 a\n-- pesos de 2025 con el IPC promedio. Grandes: más de 100.000 millones en 2024\nWITH f AS (SELECT nit, anio_fiscal, ingresos FROM financials WHERE NOT unidad_dudosa AND ingresos > 0),\nipc AS (SELECT max(ipc_promedio) FILTER (WHERE anio = 2025) / max(ipc_promedio) FILTER (WHERE anio = 2024) AS factor FROM prices)\nSELECT c.display_name AS razon_social,\n round(a.ingresos / 1e6, 1) AS ingresos_2024_miles_de_millones,\n round(b.ingresos / 1e6, 1) AS ingresos_2025_miles_de_millones,\n round(100 * (b.ingresos / (a.ingresos * ipc.factor) - 1), 1) AS crecimiento_real_pct\nFROM f a\nJOIN f b ON b.nit = a.nit AND b.anio_fiscal = 2025\nCROSS JOIN ipc\nJOIN companies c ON c.nit = a.nit\nWHERE a.anio_fiscal = 2024 AND a.ingresos >= 100000000\nORDER BY crecimiento_real_pct DESC\nLIMIT 20;"],
|
| 327 |
+
|
| 328 |
+
["Cómo se hizo", "De 8,3 millones a la lista objetivo",
|
| 329 |
+
"-- Cada paso que lleva del registro completo a las empresas que se analizan\nSELECT step AS paso, label, remaining AS quedan, lost_here AS salen, predicate\nFROM funnel ORDER BY step;"],
|
| 330 |
+
["Cómo se hizo", "¿Coinciden las cifras con las de Supersociedades?",
|
| 331 |
+
"-- Los estados financieros publicados aquí, comparados empresa por empresa\n-- con los de la Superintendencia de Sociedades\nSELECT anio, campo, comparadas,\n round(100.0 * coinciden / comparadas, 1) AS pct_iguales,\n round(100.0 * dentro_5 / comparadas, 1) AS pct_a_menos_de_5\nFROM supersociedades\nORDER BY anio, campo;"],
|
| 332 |
];
|
| 333 |
|
| 334 |
const $ = (id) => document.getElementById(id);
|
|
|
|
| 404 |
// companies.parquet from Hugging Face before the first query (measured
|
| 405 |
// 2026-10-14). With ranges it reads the footer and the columns it needs.
|
| 406 |
await db.open({ query: { castDecimalToDouble: true }, filesystem: { forceFullHTTPReads: false } });
|
| 407 |
+
const c = await db.connect();
|
| 408 |
+
// One read per row group for every column a query needs, rather than one per
|
| 409 |
+
// column: a third fewer requests over `companies` (measured 2026-10-16).
|
| 410 |
+
await c.query("SET prefetch_all_parquet_files = true");
|
| 411 |
+
return c;
|
| 412 |
}
|
| 413 |
|
| 414 |
// One view per published file, named after it. The list comes from the
|
| 415 |
// manifest, so a table added to the edition appears here without an edit.
|
| 416 |
+
//
|
| 417 |
+
// Each view reads the file's final address, not Hugging Face's. Hugging Face
|
| 418 |
+
// answers every request with a redirect to its CDN, and DuckDB reads a file in
|
| 419 |
+
// one request per row group and column, one after another: a query over
|
| 420 |
+
// `companies` made up to 360 requests, half of them redirects, and took up to a
|
| 421 |
+
// minute on a fast connection (measured 2026-10-16). The CDN's address is
|
| 422 |
+
// signed and expires within the hour, so it is looked up again before that,
|
| 423 |
+
// and once more if a read is refused.
|
| 424 |
+
const RESOLVE_EVERY = 30 * 60 * 1000;
|
| 425 |
+
let viewNames = [], resolvedAt = 0;
|
| 426 |
+
async function finalUrl(n) {
|
| 427 |
+
const url = `${BASE}/data/${n}.parquet`;
|
| 428 |
+
try {
|
| 429 |
+
const r = await fetch(url, { method: "HEAD", cache: "no-store" });
|
| 430 |
+
if (r.ok && r.url) return r.url;
|
| 431 |
+
} catch {}
|
| 432 |
+
return url;
|
| 433 |
+
}
|
| 434 |
async function createViews(m) {
|
| 435 |
+
if (m) viewNames = Object.keys(m.files).map((f) => f.replace(/\.parquet$/, ""));
|
| 436 |
+
const urls = await Promise.all(viewNames.map(finalUrl));
|
| 437 |
+
for (const [i, n] of viewNames.entries()) {
|
| 438 |
+
await conn.query(`CREATE OR REPLACE VIEW ${n} AS SELECT * FROM read_parquet('${urls[i].replace(/'/g, "''")}')`);
|
| 439 |
}
|
| 440 |
+
resolvedAt = Date.now();
|
| 441 |
+
return viewNames;
|
| 442 |
}
|
| 443 |
|
| 444 |
async function renderSchema(m, names) {
|
|
|
|
| 507 |
return { text: String(v), cls: "" };
|
| 508 |
}
|
| 509 |
|
| 510 |
+
// Each run gets a number. Queries on one connection run in turn, so picking a
|
| 511 |
+
// second example while the first is still reading would otherwise show the
|
| 512 |
+
// first's result last, under the second's SQL.
|
| 513 |
+
let runs = 0;
|
| 514 |
async function run() {
|
| 515 |
const sql = $("sql").value;
|
| 516 |
if (!sql.trim() || !conn) return;
|
| 517 |
+
const me = ++runs;
|
| 518 |
$("run").disabled = true;
|
| 519 |
const t0 = performance.now();
|
| 520 |
const tick = setInterval(() => setStatus("busy", `Ejecutando… ${((performance.now() - t0) / 1000).toLocaleString("es-CO", { maximumFractionDigits: 1, minimumFractionDigits: 1 })} s`), 200);
|
| 521 |
setStatus("busy", "Ejecutando…");
|
| 522 |
try {
|
| 523 |
const q = displayable(sql);
|
| 524 |
+
if (Date.now() - resolvedAt > RESOLVE_EVERY) await createViews();
|
| 525 |
+
let res;
|
| 526 |
+
try { res = await conn.query(q.sql); }
|
| 527 |
+
catch (e) {
|
| 528 |
+
if (!/HTTP|40[13]|expired/i.test(String(e.message || e))) throw e;
|
| 529 |
+
await createViews();
|
| 530 |
+
res = await conn.query(q.sql);
|
| 531 |
+
}
|
| 532 |
+
if (me !== runs) return;
|
| 533 |
const secs = ((performance.now() - t0) / 1000).toFixed(2);
|
| 534 |
const fields = res.schema.fields;
|
| 535 |
let rows = res.toArray().map((r) => r.toJSON());
|
|
|
|
| 541 |
$("csv").disabled = rows.length === 0;
|
| 542 |
history.replaceState(null, "", `#q=${encodeURIComponent(sql)}`);
|
| 543 |
} catch (e) {
|
| 544 |
+
if (me !== runs) return;
|
| 545 |
lastRows = null;
|
| 546 |
$("results").replaceChildren(el("div", { class: "msg err", text: String(e.message || e) }));
|
| 547 |
setStatus("err", "Error en la consulta");
|
| 548 |
$("csv").disabled = true;
|
| 549 |
} finally {
|
| 550 |
clearInterval(tick);
|
| 551 |
+
if (me === runs) $("run").disabled = false;
|
| 552 |
}
|
| 553 |
}
|
| 554 |
|
|
|
|
| 598 |
}
|
| 599 |
|
| 600 |
// ---------------------------------------------------------------- wiring
|
| 601 |
+
const groups = new Map();
|
| 602 |
+
for (const [i, [group, title]] of EXAMPLES.entries()) {
|
| 603 |
+
if (!groups.has(group)) $("examples").append(groups.set(group, el("optgroup", { label: group })).get(group));
|
| 604 |
+
groups.get(group).append(el("option", { value: String(i), text: title }));
|
| 605 |
+
}
|
| 606 |
$("examples").addEventListener("change", (e) => {
|
| 607 |
const ex = EXAMPLES[Number(e.target.value)];
|
| 608 |
+
if (ex) { $("sql").value = ex[2]; run(); }
|
| 609 |
e.target.value = "";
|
| 610 |
});
|
| 611 |
$("run").addEventListener("click", run);
|
|
|
|
| 617 |
});
|
| 618 |
|
| 619 |
const fromLink = new URLSearchParams(location.hash.slice(1)).get("q");
|
| 620 |
+
$("sql").value = fromLink || EXAMPLES[0][2];
|
| 621 |
|
| 622 |
(async () => {
|
| 623 |
try {
|