mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
feat(C207): projeção no último período do trio do gerador de gráficos (gate 2026-09-19)
This commit is contained in:
parent
5be9a1af4d
commit
ca59523759
10 changed files with 938 additions and 67 deletions
|
|
@ -56,8 +56,9 @@ lockup tipográfico); a paleta oficial é `#e4102f` (destaque/valência melhor),
|
|||
ultrapassagem que ganha anotação. A **extensão de três séries** (C205) usa o
|
||||
trio `--good=`/`--neutral=`/`--neutral-dark=` informado junto — melhor =
|
||||
vermelho/círculo/"↑ amplia", neutro = cinza/losango/"↗ cresce", neutro
|
||||
escuro = tinta/triângulo/"↘ diminui" —, só no feed e sem projeção nem
|
||||
cruzamento;
|
||||
escuro = tinta/triângulo/"↘ diminui" —, só no feed, sem cruzamento e com
|
||||
projeção no último período desde a C207 (marcadores finais vazados, faixa e
|
||||
tracejado próprios);
|
||||
- **duas medidas por categoria → `delta`**: barra de variação com a base
|
||||
(0 → inicial) num tom e a extensão (inicial → final) noutro, os dois valores
|
||||
numa gutter de duas colunas e o vermelho fora do plot (sem valência);
|
||||
|
|
@ -97,6 +98,16 @@ lockup tipográfico); a paleta oficial é `#e4102f` (destaque/valência melhor),
|
|||
--type=line --size=feed \
|
||||
--good="Hospital Estadual" --neutral="Rede municipal" --neutral-dark="Rede privada"
|
||||
```
|
||||
Três séries com o último período projetado (extensão C207, feed):
|
||||
```bash
|
||||
node scripts/build-chart-from-data.mjs --in=<arquivo> \
|
||||
--headline="Produção estadual cresce 8x enquanto a municipal cai pela metade" \
|
||||
--subtitle="Procedimentos ambulatoriais do SUS · Vitória da Conquista (BA) · 2015–2026" \
|
||||
--source="Ministério da Saúde — Sistema de Informações Ambulatoriais do SUS (SIA/SUS)" \
|
||||
--note="2026 é projeção pela média de janeiro a junho × 2." \
|
||||
--type=line --size=feed \
|
||||
--good="Estadual" --neutral="Rede privada" --neutral-dark="Municipal" --projected=2026
|
||||
```
|
||||
Barra de variação (base + extensão, tabela de rótulo + inicial + final):
|
||||
```bash
|
||||
node scripts/build-chart-from-data.mjs --in=<arquivo> --type=delta --size=feed \
|
||||
|
|
@ -176,7 +187,8 @@ sufixo aos rótulos de valor (ex.: `"%"`). O `kicker` (spec) segue a **relação
|
|||
dos dados, não o layout — colunas verticais de categorias leem "Comparação",
|
||||
poucos períodos leem "Poucos períodos". Em `series`, `tone` ∈
|
||||
`good | bad` (os dois tons ou nenhum) ou `good | neutral | neutral-dark` (o trio
|
||||
da extensão C205, só no feed, sem `projectedLabel` nem `crossingLabel`); as
|
||||
da extensão C205/C207, só no feed, sem `crossingLabel`; a projeção no último
|
||||
período é certificada desde a C207); as
|
||||
séries compartilham os mesmos rótulos temporais; `rows` e `highlight` não entram
|
||||
nessas variantes.
|
||||
`projectedLabel` marca a projeção no último período e `crossingLabel` só é aceito
|
||||
|
|
@ -237,10 +249,15 @@ O builder **recusa** em vez de desenhar algo enganoso:
|
|||
círculo, "↑ amplia"; neutro = cinza `#78716c`, losango, "↗ cresce"; neutro
|
||||
escuro = tinta `#1c1917`, triângulo, "↘ diminui" (direções factuais, nunca
|
||||
"recua"/perda) — e o azul `#184e92` não entra em dado nesta variante.
|
||||
- **Projeção** (`--projected=`) só no último período das duas séries (o trio de
|
||||
três séries não certifica projeção): último segmento tracejado, marcador final
|
||||
vazado e nota do método; sem projeção não há faixa, tracejado nem ressalva
|
||||
automática.
|
||||
- **Projeção** (`--projected=`) só no último período da linha multi-série
|
||||
(certificada no par de 2 séries e no trio de 3 desde a C207): último segmento
|
||||
tracejado, marcador final vazado (nas três formas do trio), faixa neutra com o
|
||||
rótulo "projeção \<período>" no topo e nota do método. No par, a variação da
|
||||
gutter fecha no último ponto **observado** ("±N% até \<período observado>");
|
||||
no trio (C207), a variação fecha no próprio ponto projetado — o mesmo par da
|
||||
manchete — também nomeado no rótulo superior (feedback do gate humano:
|
||||
porcentagem tem de bater com o valor exibido e com a manchete). Sem projeção
|
||||
não há faixa, tracejado nem ressalva automática.
|
||||
- **Cruzamento** (`--crossing=<período>`, só no par de 2 séries) só com
|
||||
confirmação textual e quando a série boa realmente ultrapassa a ruim naquele
|
||||
período; a anotação nunca é inferida sozinha.
|
||||
|
|
@ -271,10 +288,10 @@ O builder **recusa** em vez de desenhar algo enganoso:
|
|||
rótulo, valor não numérico, rótulo repetido. Pergunte à pessoa; não invente.
|
||||
- **"N pontos (> 7)"** → agrupe/remova categorias com a pessoa. Na linha
|
||||
multi-série o teto é 12 por série.
|
||||
- **Séries desalinhadas / tom ímpar (par ou trio incompletos) / projeção ou
|
||||
cruzamento na linha de três séries / três séries fora do feed / cruzamento não
|
||||
confirmado** → o builder pede a correção com mensagem acionável; nunca
|
||||
interpola, completa nem infere ultrapassagem.
|
||||
- **Séries desalinhadas / tom ímpar (par ou trio incompletos) / cruzamento na
|
||||
linha de três séries / três séries fora do feed / cruzamento não confirmado /
|
||||
projeção fora do último período** → o builder pede a correção com mensagem
|
||||
acionável; nunca interpola, completa nem infere ultrapassagem.
|
||||
- **"pizza não é gerada"** → ofereça barras horizontais.
|
||||
- **Número pt-BR:** `1.234,56` é lido como 1234,56; `1,5` como 1,5.
|
||||
**Separador único é sempre decimal:** `1.234` é lido como 1,234 e `1.500`
|
||||
|
|
@ -286,9 +303,9 @@ O builder **recusa** em vez de desenhar algo enganoso:
|
|||
|
||||
- Design aprovado: `docs/plans/graficos-dados-instagram-ui-design.html` (inclui
|
||||
a variação certificada de linha de duas séries, com valência bom/ruim; a
|
||||
extensão C205 de três séries observadas e a revisão C206 do rodapé/marca,
|
||||
ambas pendentes de gate humano; base C203 — marca e paleta oficiais do kit
|
||||
1313).
|
||||
extensão C205 de três séries observadas; a revisão C207 da projeção no trio,
|
||||
nova e aguardando gate humano; e a revisão C206 do rodapé/marca; base C203 —
|
||||
marca e paleta oficiais do kit 1313).
|
||||
- Plano de intenção: `docs/plans/graficos-dados-instagram.md`; impl:
|
||||
`docs/plans/graficos-dados-instagram-impl.md`.
|
||||
- Marca oficial: `docs/plans/graficos-dados-marca-kit-1313.md` (impl:
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
**C207 — Gráficos do Instagram: projeção no último período da linha de três séries (2026-09-19):** a skill `graficos-dados` (C191/C203/C205/C206) certifica a variante do trio no feed com o último período projetado. (1) **Contrato (C207):** `validateSeries` aceita `projectedLabel` na linha de 3 séries — o período tem de ser o último e comum às três; cruzamento e tamanhos ≠ feed seguem recusados — e `tripleLineChart` ganha a projeção: faixa neutra cobrindo o último passo com o rótulo "projeção \<período>" no topo (x=552/y=45), só o último segmento tracejado (`18 12`), os três marcadores finais vazados mantendo a forma do tom (círculo r=11, losango 28×28, triângulo ▼ 30×26, papel `#faf9f7` + stroke 5) e a gutter com o valor projetado + a variação 2015→projeção, com a terceira lane só a valência; aria-label anuncia as três projeções e a variação até o período projetado. (2) **Feedback do gate humano:** a copy troca "produção/procedimentos ambulatoriais" por "consultas e exames" (termo popular); a porcentagem passa a fechar no mesmo par da manchete (2015→2026 projetado: +706% / −49% / +3%) e o "até 2025" é aposentado; após a colisão medida no render (Chromium, getBBox) entre o literal e as palavras de valência, "projeção 2026" migra para o rótulo superior e a lane 3 fica limpa. (3) **Superfície:** `SKILL.md` atualizado (exemplo CLI com `--projected` no trio, guardrail da projeção e troubleshooting), hi-fi com a CENA 02E/C207 aprovada no gate de 2026-09-19. Peça de validação: produção ambulatorial do SUS (SIA/SUS) em Vitória da Conquista, 2015–2026 (2026 = projeção jan–jun ×2) — estadual 522.656→4.213.052 (+706%, 8,1x), municipal 2.904.003→1.492.020 (−49%) e rede privada (Empresariais + Sem Fins Lucrativos) 833.679→855.684 (+3%). Verificação: unit 4.158 verdes (specs do gerador estendidos em chartData/chartPrimitives/graficosInstagramRender/buildChartFromData), lint/format/typecheck limpos; sem migration.
|
||||
|
|
@ -508,6 +508,35 @@
|
|||
stroke-linecap: square;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
/* C207 · extensão projetada da C205. A faixa termina no limite do plot
|
||||
(x=560): nunca entra no corredor de leaders nem na gutter. */
|
||||
.triple-projection-band {
|
||||
fill: #a8a29e;
|
||||
fill-opacity: 0.16;
|
||||
}
|
||||
.triple-projection-label {
|
||||
fill: #78716c;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.triple-series-projection {
|
||||
stroke-dasharray: 18 12;
|
||||
}
|
||||
.triple-marker-projected {
|
||||
fill: var(--paper);
|
||||
stroke-width: 5px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.triple-projected .triple-end-metric {
|
||||
font-size: 30px;
|
||||
}
|
||||
.feed-canvas.triple-projected .chart-headline {
|
||||
max-width: 910px;
|
||||
font-size: 48px;
|
||||
}
|
||||
.feed-canvas.triple-projected .source-line {
|
||||
max-width: 620px;
|
||||
}
|
||||
.rule-code {
|
||||
display: inline-flex;
|
||||
padding: 2px 6px;
|
||||
|
|
@ -998,23 +1027,26 @@
|
|||
<div
|
||||
class="mb-6 rounded-2xl border border-[#184e92]/30 bg-[#184e92]/[0.05] p-5 sm:p-6"
|
||||
role="note"
|
||||
aria-label="Base C203 aprovada e revisões C205 e C206 aprovadas no gate de 2026-09-19"
|
||||
aria-label="Base C203 e revisões C205, C206 e C207 aprovadas no gate humano de 2026-09-19"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-x-5 gap-y-3">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md bg-[#184e92] px-3 py-1 text-sm font-black tracking-[0.14em] text-white"
|
||||
>BASE C203 APROVADA · C205/C206 APROVADAS</span
|
||||
>BASE C203 APROVADA · C205/C206/C207 APROVADAS</span
|
||||
>
|
||||
<div class="max-w-3xl">
|
||||
<p class="text-sm font-extrabold text-[#184e92]">
|
||||
Base C203 aprovada em 2026-09-19 · revisões C205 e C206 aprovadas no gate em
|
||||
Base C203 aprovada em 2026-09-19 · revisões C205, C206 e C207 aprovadas no gate em
|
||||
2026-09-19.
|
||||
</p>
|
||||
<p class="mt-1 text-sm leading-6 text-stone-700">
|
||||
O aceite C203 permanece intacto. A C205 revisa no lugar a CENA 02D — linha temporal de
|
||||
três séries — e supera a C204, que nunca foi certificada. A C206 revisa somente o
|
||||
rodapé compartilhado para ampliar a marca oficial. As duas são a fonte de verdade do
|
||||
port desde o gate humano de 2026-09-19.
|
||||
rodapé compartilhado para ampliar a marca oficial. A C207 estende a C205 com o último
|
||||
período projetado (CENA 02E).
|
||||
<strong
|
||||
>As três são a fonte de verdade do port desde o gate humano de 2026-09-19.</strong
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1023,7 +1055,7 @@
|
|||
<header class="max-w-4xl">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="scene-label border-blue-200 bg-blue-50 text-[#184e92]"
|
||||
>Revisões C205 + C206 · aprovadas no gate</span
|
||||
>C207 · aprovada no gate de 2026-09-19</span
|
||||
>
|
||||
<span class="scene-label">Base C203 aprovada · C205/C206 aprovadas</span>
|
||||
<span class="scene-label">Instagram feed + Stories</span>
|
||||
|
|
@ -1033,7 +1065,7 @@
|
|||
</h1>
|
||||
<p class="mt-4 max-w-3xl text-[15px] leading-6 text-stone-600">
|
||||
A superfície desenhada é a imagem exportada — não uma tela de app. Os exemplos-base são
|
||||
deliberadamente genéricos; a variante de duas séries usa o caso fornecido de Vitória da
|
||||
deliberadamente genéricos; as linhas multi-série usam casos fornecidos de Vitória da
|
||||
Conquista. As peças fixam hierarquia, grid, tratamento de marca e limites do gerador.
|
||||
</p>
|
||||
</header>
|
||||
|
|
@ -1173,6 +1205,34 @@
|
|||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="mt-4 rounded-2xl border border-blue-200 bg-blue-50/70 p-5 sm:p-6"
|
||||
aria-labelledby="revision-c207-title"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="max-w-3xl">
|
||||
<p class="text-xs font-extrabold tracking-[0.08em] text-[#184e92]">
|
||||
REVISÃO VISÍVEL · C207 · AJUSTE PÓS-FEEDBACK DO GATE · APROVADA NO GATE 2026-09-19
|
||||
</p>
|
||||
<h2 id="revision-c207-title" class="mt-1 text-lg font-bold tracking-tight">
|
||||
Três séries com o último período projetado
|
||||
</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-stone-700">
|
||||
A <strong>CENA 02E</strong> estende a C205 no feed, sem alterar a variante observada:
|
||||
aplica às três formas a linguagem de projeção já certificada na CENA 02B. Faixa,
|
||||
tracejado final, marcadores vazados, aria-label e nota tornam 2026 inequívoco. Após o
|
||||
feedback humano no gate, valor e variação da gutter agora fecham no mesmo ponto
|
||||
projetado de 2026. O ano migra para o rótulo superior “projeção 2026”; a terceira lane
|
||||
da gutter preserva somente a valência, sem colisão. A peça final foi aprovada pelo
|
||||
humano no gate de 2026-09-19.
|
||||
</p>
|
||||
</div>
|
||||
<span class="scene-label border-blue-200 bg-blue-50 text-[#184e92]"
|
||||
>APROVADA · GATE 2026-09-19</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="scene-card mt-9 p-4 sm:p-6 lg:p-8" aria-labelledby="main-scene-title">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
|
|
@ -1374,7 +1434,7 @@
|
|||
<div>
|
||||
<p class="text-xs font-bold text-brand-dark">CENA 02 · SISTEMA</p>
|
||||
<h2 id="variations-title" class="mt-1 text-xl font-bold tracking-tight">
|
||||
Cinco variações-base + duas extensões, um mesmo template
|
||||
Cinco variações-base + três extensões, um mesmo template
|
||||
</h2>
|
||||
</div>
|
||||
<p class="max-w-xl text-sm leading-5 text-stone-600">
|
||||
|
|
@ -2714,6 +2774,524 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CENA 02E · C207 — EXTENSÃO DA C205, APROVADA NO GATE 2026-09-19. -->
|
||||
<!-- Três séries; 2026 é projeção comum às três. CENA 02D não muda. -->
|
||||
<!-- ================================================================== -->
|
||||
<section
|
||||
class="scene-card mt-10 border-2 border-blue-200 p-4 sm:p-6 lg:p-8"
|
||||
aria-labelledby="triple-projected-title"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-bold text-brand-dark">
|
||||
CENA 02E · REVISÃO C207 · AJUSTE PÓS-FEEDBACK · APROVADA NO GATE 2026-09-19
|
||||
</p>
|
||||
<h2 id="triple-projected-title" class="mt-1 text-xl font-bold tracking-tight">
|
||||
Feed 4:5 · linha temporal de três séries com projeção final
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="measure-tag"><span class="measure-dot"></span>1080 × 1350</span>
|
||||
<span class="measure-tag">Plot SVG · viewBox 880 × 650</span>
|
||||
<span class="measure-tag">33 observadas + 3 projetadas</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid items-start gap-7 lg:grid-cols-[594px_1fr]">
|
||||
<div class="overflow-x-auto pb-2">
|
||||
<div class="feed-stage">
|
||||
<article
|
||||
class="feed-canvas triple-series triple-projected"
|
||||
aria-label="Consultas e exames estaduais crescem oito vezes enquanto os municipais caem pela metade em Vitória da Conquista entre 2015 e 2026. A faixa do último período está rotulada projeção 2026. Estadual: de 522.656 em 2015 para projeção de 4.213.052 em 2026, variação de mais 706 por cento até 2026 (projeção). Municipal: de 2.904.003 em 2015 para projeção de 1.492.020 em 2026, variação de menos 49 por cento até 2026 (projeção). Rede privada: de 833.679 em 2015 para projeção de 855.684 em 2026, variação de mais 3 por cento até 2026 (projeção). 2026 é projeção pela média de janeiro a junho multiplicada por dois. O último segmento e os três marcadores de 2026 são projetados."
|
||||
>
|
||||
<div class="feed-inner">
|
||||
<div class="top-rule"></div>
|
||||
<p class="chart-context">Comparação no tempo</p>
|
||||
<h3 class="chart-headline">
|
||||
Consultas e exames estaduais crescem 8x enquanto os municipais caem pela metade
|
||||
</h3>
|
||||
<p class="chart-subtitle">
|
||||
Consultas e exames do SUS · Vitória da Conquista (BA) · 2015–2026
|
||||
</p>
|
||||
|
||||
<div class="triple-line-plot tabular">
|
||||
<svg
|
||||
viewBox="0 0 880 650"
|
||||
role="img"
|
||||
aria-label="A faixa do último período está rotulada projeção 2026. Estadual: de 522.656 em 2015 para projeção de 4.213.052 em 2026, variação de mais 706 por cento até 2026 (projeção). Municipal: de 2.904.003 em 2015 para projeção de 1.492.020 em 2026, variação de menos 49 por cento até 2026 (projeção). Rede privada: de 833.679 em 2015 para projeção de 855.684 em 2026, variação de mais 3 por cento até 2026 (projeção). O último segmento e os três marcadores de 2026 são projetados."
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<rect
|
||||
class="triple-projection-band"
|
||||
x="514"
|
||||
y="0"
|
||||
width="46"
|
||||
height="544"
|
||||
fill="#a8a29e"
|
||||
fill-opacity="0.16"
|
||||
/>
|
||||
<line
|
||||
class="triple-grid-line"
|
||||
x1="52"
|
||||
y1="56"
|
||||
x2="560"
|
||||
y2="56"
|
||||
stroke="#a8a29e"
|
||||
stroke-opacity="0.32"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<line
|
||||
class="triple-grid-line"
|
||||
x1="52"
|
||||
y1="300"
|
||||
x2="560"
|
||||
y2="300"
|
||||
stroke="#a8a29e"
|
||||
stroke-opacity="0.32"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<line
|
||||
class="triple-zero-line"
|
||||
x1="52"
|
||||
y1="544"
|
||||
x2="560"
|
||||
y2="544"
|
||||
stroke="#a8a29e"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<text x="42" y="538" text-anchor="end" class="triple-axis-label">0</text>
|
||||
<text x="552" y="45" text-anchor="end" class="triple-projection-label">
|
||||
projeção 2026
|
||||
</text>
|
||||
|
||||
<polyline
|
||||
class="triple-series triple-series-good"
|
||||
points="52,493 98,491 144,465 191,399 237,277 283,235 329,188 375,181 421,186 468,204 514,176"
|
||||
fill="none"
|
||||
stroke="#e4102f"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<line
|
||||
class="triple-series-projection triple-series-good"
|
||||
x1="514"
|
||||
y1="176"
|
||||
x2="560"
|
||||
y2="133"
|
||||
stroke="#e4102f"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="18 12"
|
||||
/>
|
||||
<polyline
|
||||
class="triple-series triple-series-neutral-dark"
|
||||
points="52,261 98,316 144,336 191,348 237,352 283,425 329,380 375,362 421,347 468,343 514,355"
|
||||
fill="none"
|
||||
stroke="#1c1917"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<line
|
||||
class="triple-series-projection triple-series-neutral-dark"
|
||||
x1="514"
|
||||
y1="355"
|
||||
x2="560"
|
||||
y2="398"
|
||||
stroke="#1c1917"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="18 12"
|
||||
/>
|
||||
<polyline
|
||||
class="triple-series triple-series-neutral"
|
||||
points="52,463 98,475 144,477 191,469 237,474 283,494 329,487 375,476 421,457 468,458 514,455"
|
||||
fill="none"
|
||||
stroke="#78716c"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<line
|
||||
class="triple-series-projection triple-series-neutral"
|
||||
x1="514"
|
||||
y1="455"
|
||||
x2="560"
|
||||
y2="460"
|
||||
stroke="#78716c"
|
||||
stroke-width="7"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="18 12"
|
||||
/>
|
||||
|
||||
<g class="triple-markers-good" fill="#e4102f">
|
||||
<circle cx="52" cy="493" r="8" />
|
||||
<circle cx="98" cy="491" r="8" />
|
||||
<circle cx="144" cy="465" r="8" />
|
||||
<circle cx="191" cy="399" r="8" />
|
||||
<circle cx="237" cy="277" r="8" />
|
||||
<circle cx="283" cy="235" r="8" />
|
||||
<circle cx="329" cy="188" r="8" />
|
||||
<circle cx="375" cy="181" r="8" />
|
||||
<circle cx="421" cy="186" r="8" />
|
||||
<circle cx="468" cy="204" r="8" />
|
||||
<circle cx="514" cy="176" r="8" />
|
||||
</g>
|
||||
<g class="triple-markers-neutral-dark" fill="#1c1917">
|
||||
<path d="M42 253 L62 253 L52 271 Z" />
|
||||
<path d="M88 308 L108 308 L98 326 Z" />
|
||||
<path d="M134 328 L154 328 L144 346 Z" />
|
||||
<path d="M181 340 L201 340 L191 358 Z" />
|
||||
<path d="M227 344 L247 344 L237 362 Z" />
|
||||
<path d="M273 417 L293 417 L283 435 Z" />
|
||||
<path d="M319 372 L339 372 L329 390 Z" />
|
||||
<path d="M365 354 L385 354 L375 372 Z" />
|
||||
<path d="M411 339 L431 339 L421 357 Z" />
|
||||
<path d="M458 335 L478 335 L468 353 Z" />
|
||||
<path d="M504 347 L524 347 L514 365 Z" />
|
||||
</g>
|
||||
<g class="triple-markers-neutral" fill="#78716c">
|
||||
<rect x="45" y="456" width="14" height="14" transform="rotate(45 52 463)" />
|
||||
<rect x="91" y="468" width="14" height="14" transform="rotate(45 98 475)" />
|
||||
<rect
|
||||
x="137"
|
||||
y="470"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 144 477)"
|
||||
/>
|
||||
<rect
|
||||
x="184"
|
||||
y="462"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 191 469)"
|
||||
/>
|
||||
<rect
|
||||
x="230"
|
||||
y="467"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 237 474)"
|
||||
/>
|
||||
<rect
|
||||
x="276"
|
||||
y="487"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 283 494)"
|
||||
/>
|
||||
<rect
|
||||
x="322"
|
||||
y="480"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 329 487)"
|
||||
/>
|
||||
<rect
|
||||
x="368"
|
||||
y="469"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 375 476)"
|
||||
/>
|
||||
<rect
|
||||
x="414"
|
||||
y="450"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 421 457)"
|
||||
/>
|
||||
<rect
|
||||
x="461"
|
||||
y="451"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 468 458)"
|
||||
/>
|
||||
<rect
|
||||
x="507"
|
||||
y="448"
|
||||
width="14"
|
||||
height="14"
|
||||
transform="rotate(45 514 455)"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<circle
|
||||
class="triple-marker-projected triple-marker-projected-good"
|
||||
cx="560"
|
||||
cy="133"
|
||||
r="11"
|
||||
fill="#faf9f7"
|
||||
stroke="#e4102f"
|
||||
stroke-width="5"
|
||||
/>
|
||||
<path
|
||||
class="triple-marker-projected triple-marker-projected-neutral-dark"
|
||||
d="M545 387 L575 387 L560 413 Z"
|
||||
fill="#faf9f7"
|
||||
stroke="#1c1917"
|
||||
stroke-width="5"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
class="triple-marker-projected triple-marker-projected-neutral"
|
||||
d="M560 446 L574 460 L560 474 L546 460 Z"
|
||||
fill="#faf9f7"
|
||||
stroke="#78716c"
|
||||
stroke-width="5"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
|
||||
<polyline
|
||||
class="triple-end-leader"
|
||||
points="578,133 584,133 590,108 594,108"
|
||||
stroke="#e4102f"
|
||||
/>
|
||||
<text x="600" y="62" class="triple-end-name">Estadual</text>
|
||||
<text x="600" y="108" class="triple-end-metric">4.213.052</text>
|
||||
<text x="874" y="108" text-anchor="end" class="triple-end-metric">
|
||||
· +706%
|
||||
</text>
|
||||
<text x="600" y="150" class="triple-end-valence" fill="#e4102f">
|
||||
↑ amplia
|
||||
</text>
|
||||
|
||||
<polyline
|
||||
class="triple-end-leader"
|
||||
points="578,398 584,398 590,304 594,304"
|
||||
stroke="#1c1917"
|
||||
/>
|
||||
<text x="600" y="258" class="triple-end-name">Municipal</text>
|
||||
<text x="600" y="304" class="triple-end-metric">1.492.020</text>
|
||||
<text x="874" y="304" text-anchor="end" class="triple-end-metric">
|
||||
· −49%
|
||||
</text>
|
||||
<text x="600" y="346" class="triple-end-valence" fill="#1c1917">
|
||||
↘ diminui
|
||||
</text>
|
||||
|
||||
<polyline
|
||||
class="triple-end-leader"
|
||||
points="578,460 584,460 590,500 594,500"
|
||||
stroke="#78716c"
|
||||
/>
|
||||
<text x="600" y="454" class="triple-end-name">Rede privada</text>
|
||||
<text x="600" y="500" class="triple-end-metric">855.684</text>
|
||||
<text x="874" y="500" text-anchor="end" class="triple-end-metric">· +3%</text>
|
||||
<text x="600" y="542" class="triple-end-valence" fill="#78716c">
|
||||
↗ cresce
|
||||
</text>
|
||||
|
||||
<g class="triple-axis-label" text-anchor="middle">
|
||||
<text x="52" y="636">2015</text>
|
||||
<text x="144" y="636">2017</text>
|
||||
<text x="237" y="636">2019</text>
|
||||
<text x="329" y="636">2021</text>
|
||||
<text x="421" y="636">2023</text>
|
||||
<text x="560" y="636">2026</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<footer class="chart-footer">
|
||||
<p class="source-line">
|
||||
<strong>Fonte:</strong> Ministério da Saúde — Sistema de Informações
|
||||
Ambulatoriais do SUS (SIA/SUS)<br />Nota: 2026 é projeção pela média de
|
||||
janeiro a junho × 2. Rede privada = Empresariais + Sem Fins Lucrativos.
|
||||
</p>
|
||||
<div class="brand-logo-frame">
|
||||
<img
|
||||
src="../../public/campaign-kit/jorge-solla-positivo.png"
|
||||
alt="Jorge Solla — Deputado Federal"
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="grid gap-4">
|
||||
<div class="rounded-xl border border-amber-300 bg-amber-50/70 p-4">
|
||||
<p class="text-xs font-extrabold text-amber-800">
|
||||
PROJEÇÃO COMUM · TRÊS FORMAS VAZADAS
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-6 text-stone-700">
|
||||
Só 2026 é projetado. Círculo, triângulo e losango finais preservam a forma de cada
|
||||
tom, crescem e recebem papel <span class="rule-code">#faf9f7</span> + contorno de 5
|
||||
px. O vazio significa projeção em todas as séries; nunca ausência de dado.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-xs font-bold text-brand-dark">FAIXA NÃO VAZA PARA A GUTTER</p>
|
||||
<p class="mt-2 text-sm leading-6 text-stone-700">
|
||||
A faixa ocupa <span class="rule-code">x=514…560</span>, exatamente o último passo
|
||||
observado→projetado, e termina no limite do plot. “projeção 2026” ancora em
|
||||
x=552/y=45, text-anchor=end, acima do grid: não toca o eixo, o corredor de leaders
|
||||
nem o texto da gutter.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-xs font-bold text-brand-dark">VALOR E VARIAÇÃO FECHAM NA PROJEÇÃO</p>
|
||||
<p class="mt-2 text-sm leading-6 text-stone-700">
|
||||
+706%, −49% e +3% comparam 2015 ao ponto projetado de 2026 — o mesmo ponto dos
|
||||
valores grandes. O rótulo superior único “projeção 2026” nomeia a base final do
|
||||
cálculo para os três blocos; a terceira lane contém somente a palavra de valência.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-xs font-bold text-brand-dark">C205 CONTINUA INTACTA</p>
|
||||
<p class="mt-2 text-sm leading-6 text-stone-700">
|
||||
A CENA 02D segue sendo a variante de três séries totalmente observadas. A C207 só
|
||||
entra quando o rótulo projetado coincide com o último período das três séries e o
|
||||
método pode viajar na nota e no aria-label.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-7 border-t border-stone-200 pt-6"
|
||||
aria-labelledby="triple-projected-rules-title"
|
||||
>
|
||||
<p class="text-xs font-bold text-amber-800">
|
||||
REGRAS DA LINHA DE 3 SÉRIES + PROJEÇÃO · C207
|
||||
</p>
|
||||
<h3 id="triple-projected-rules-title" class="mt-1 text-lg font-bold tracking-tight">
|
||||
Contrato visual da extensão para o porte classe-a-classe
|
||||
</h3>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Aplicação e fail-closed</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Exatamente 3 séries, 2–12 períodos alinhados, feed 1080×1350 e tons distintos
|
||||
<span class="rule-code">good / neutral / neutral-dark</span>. O período informado em
|
||||
<span class="rule-code">projectedLabel</span> tem de ser o último e comum às três.
|
||||
Projeção ausente, parcial, intermediária ou sem método → perguntar; nunca inferir.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Faixa e label · geometria exata</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
No viewBox <span class="rule-code">880×650</span>, a faixa é
|
||||
<span class="rule-code">rect x=514 y=0 width=46 height=544</span>, fill
|
||||
<span class="rule-code">#a8a29e</span> a 16%. Ela cobre 2025→2026 e termina em
|
||||
x=560. Literal <span class="rule-code">projeção 2026</span> em
|
||||
<span class="rule-code">x=552 y=45 text-anchor=end</span>, 30/700 em #78716c; fica
|
||||
11 px acima do grid de topo y=56 e 8 px antes do limite do plot.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Segmentos sólidos × projetados</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Cada <span class="rule-code">triple-series-*</span> sólido termina em x=514 (2025).
|
||||
Somente x=514→560 usa <span class="rule-code">triple-series-projection</span>, linha
|
||||
7 px, round cap e <span class="rule-code">stroke-dasharray="18 12"</span>. Nenhum
|
||||
segmento anterior recebe dash; a escala compartilhada continua zero-based, 0…5 mi.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Vazados por tom · atributos exatos</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Good: círculo <span class="rule-code">cx=560 r=11</span>. Neutral: losango path
|
||||
<span class="rule-code">M560 446 L574 460 L560 474 L546 460 Z</span> (28×28).
|
||||
Neutral-dark: triângulo ▼
|
||||
<span class="rule-code">M545 387 L575 387 L560 413 Z</span> (30×26). Todos usam fill
|
||||
#faf9f7, stroke do tom, stroke-width 5 e linejoin round; são maiores que r=8,
|
||||
losango 14×14 rotado e triângulo 20×18 observados.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Tons e vocabulário preservados</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Melhor = <span class="rule-code">#e4102f + círculo + ↑ “amplia”</span>; neutro =
|
||||
<span class="rule-code">#78716c + losango + ↗ “cresce”</span>; neutro escuro =
|
||||
<span class="rule-code">#1c1917 + triângulo + ↘ “diminui”</span>. Cor nunca viaja
|
||||
sozinha; azul não entra nos dados; vermelho segue abaixo de 10% da peça.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Gutter, lanes e leaders</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Gutter x=600…880 e lanes C205 intactas: 62/108/150, 258/304/346 e 454/500/542,
|
||||
ordenadas pelo y projetado. O footprint dos vazados termina até x=577,5; por isso o
|
||||
leader começa em x=578, passa x=584, dobra em x=590 e termina x=594, com 6 px antes
|
||||
do texto. Faixa/label nunca entram no corredor x=560…600; ali só ficam o marcador
|
||||
final e seu leader, sem texto. A terceira lane usa somente a valência em x=600,
|
||||
30/850; não existe literal ancorado à direita nessa baseline.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Valor projetado + variação até a projeção</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Linha 1: nome. Linha 2: valor projetado de 2026 à esquerda + variação 2015→2026
|
||||
projetado à direita, no literal <span class="rule-code">valor · ±N%</span>, em 30 px
|
||||
tabular (redução controlada dos 34 px da C205 para os valores milionários). Linha 3:
|
||||
somente seta/palavra à esquerda, em 30/850:
|
||||
<span class="rule-code">x=600 y=150 ↑ amplia</span>,
|
||||
<span class="rule-code">x=600 y=346 ↘ diminui</span> e
|
||||
<span class="rule-code">x=600 y=542 ↗ cresce</span>. Não há literal à direita e não
|
||||
se cria quarta linha. Literais de métrica: <strong>4.213.052 · +706%</strong>;
|
||||
<strong>1.492.020 · −49%</strong>; <strong>855.684 · +3%</strong>. No aria-label,
|
||||
escrever por extenso:
|
||||
<strong>“para projeção de X em 2026, variação de Y até 2026 (projeção)”</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Eixo, aria e nota obrigatórios</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Eixo mantém 2015/2017/2019/2021/2023/2026 em x=52/144/237/329/421/560; “projeção
|
||||
2026” nunca desce para o eixo. O SVG tem role=img e aria-label que anuncia o rótulo
|
||||
da faixa, as três projeções e as três variações que incluem a projeção. A nota
|
||||
informa o método
|
||||
<strong>“2026 é projeção pela média de janeiro a junho × 2”</strong>, e a frase
|
||||
final confirma que o último segmento e os três marcadores de 2026 são projetados.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
<p class="text-sm font-bold">Copy e fonte · literais desta cena</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Manchete: “Consultas e exames estaduais crescem 8x enquanto os municipais caem pela
|
||||
metade”. Subtítulo: “Consultas e exames do SUS · Vitória da Conquista (BA) ·
|
||||
2015–2026”. Fonte: “Ministério da Saúde — Sistema de Informações Ambulatoriais do
|
||||
SUS (SIA/SUS)”. Nota: “2026 é projeção pela média de janeiro a junho × 2. Rede
|
||||
privada = Empresariais + Sem Fins Lucrativos.” Rótulo superior: “projeção 2026”.
|
||||
Lane 3 da gutter: somente “↑ amplia”, “↘ diminui” e “↗ cresce”.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-stone-200 bg-white p-4 md:col-span-2 xl:col-span-3"
|
||||
>
|
||||
<p class="text-sm font-bold">Mapa de classes C207</p>
|
||||
<p class="mt-2 text-sm leading-5 text-stone-600">
|
||||
Reusar a estrutura das classes C205
|
||||
<span class="rule-code">triple-line-plot</span>,
|
||||
<span class="rule-code">triple-series-*</span>,
|
||||
<span class="rule-code">triple-markers-*</span>,
|
||||
<span class="rule-code">triple-axis-label</span>,
|
||||
<span class="rule-code">triple-end-leader</span>,
|
||||
<span class="rule-code">triple-end-name</span>,
|
||||
<span class="rule-code">triple-end-metric</span> e
|
||||
<span class="rule-code">triple-end-valence</span>; nesta cena, métrica fica em 30 px
|
||||
e valência preserva 30/850, sozinha na terceira lane. C207 acrescenta
|
||||
<span class="rule-code">triple-projected</span> no canvas,
|
||||
<span class="rule-code">triple-projection-band</span>,
|
||||
<span class="rule-code">triple-projection-label</span>,
|
||||
<span class="rule-code">triple-series-projection</span>,
|
||||
<span class="rule-code">triple-marker-projected</span> e os modificadores
|
||||
<span class="rule-code">triple-marker-projected-good</span>,
|
||||
<span class="rule-code">-neutral</span> e
|
||||
<span class="rule-code">-neutral-dark</span>. Rodapé e marca seguem C206.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CENA 02C · PROPOSTA DEGRADED — variante "delta" (barra de -->
|
||||
<!-- variação por categoria). NÃO CERTIFICADA. Escrita no tier -->
|
||||
|
|
@ -3374,8 +3952,9 @@
|
|||
<p class="mt-1 text-sm leading-5 text-stone-600">
|
||||
Barra, coluna e linha simples: até 7 pontos. Exceção aprovada: linha de 2 séries,
|
||||
até 12 pontos alinhados por série. Revisão C205 aprovada: linha de 3 séries no feed,
|
||||
até 12 pontos alinhados por série (36 marcas). Story mantém 5 apenas para
|
||||
barra/coluna; C205 não certifica quadrado nem story.
|
||||
até 12 pontos alinhados por série (36 marcas). A C207 certifica o mesmo teto com o
|
||||
último período projetado, aprovada no gate de 2026-09-19. Story mantém 5 apenas para
|
||||
barra/coluna; C205/C207 não certificam quadrado nem story.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-stone-200 bg-white p-4">
|
||||
|
|
@ -3405,9 +3984,12 @@
|
|||
<span
|
||||
>base C203 <strong>aprovada</strong> · extensão C191
|
||||
<strong>certificada pelo designer e aprovada pelo humano</strong> · C205 (supera a C204) +
|
||||
C206 <strong>aprovadas no gate de 2026-09-19</strong></span
|
||||
C206 + C207 <strong>aprovadas no gate de 2026-09-19</strong></span
|
||||
>
|
||||
<span
|
||||
>Kit 1313 oficial · valência redundante · projeção explícita · marca ampliada · NEEDS
|
||||
ASSET resolvido</span
|
||||
>
|
||||
<span>Kit 1313 oficial · valência redundante · marca ampliada · NEEDS ASSET resolvido</span>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -423,8 +423,9 @@ export const classifyRelation = (rows, forcedType = null) => {
|
|||
/**
|
||||
* Guardrails of the multi-series time line (approved variants): aligned
|
||||
* periods, at most `MAX_POINTS_LINE` per series, honest zero base, the tone
|
||||
* pair (2 series) or triad (3 series, C204 — feed only, no projection, no
|
||||
* crossing) informed together and the projection only on the last period.
|
||||
* pair (2 series) or triad (3 series, C207 — feed only, projection on the last
|
||||
* period, no crossing) informed together and the projection only on the last
|
||||
* period.
|
||||
*/
|
||||
const validateSeries = (spec) => {
|
||||
const { series } = spec
|
||||
|
|
@ -440,9 +441,6 @@ const validateSeries = (spec) => {
|
|||
)
|
||||
}
|
||||
if (series.length === MAX_SERIES) {
|
||||
if (spec.projectedLabel) {
|
||||
throw new Error('a linha de três séries não certifica projeção — remova --projected.')
|
||||
}
|
||||
if (spec.crossingLabel) {
|
||||
throw new Error('a linha de três séries não certifica cruzamento — remova --crossing.')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,11 +541,12 @@ export const lineChart = ({
|
|||
}
|
||||
|
||||
/**
|
||||
* Geometry of the approved three-series time line (C205 extension, feed only).
|
||||
* The data box is narrower than the two-series one (x=52…560) so the wider end
|
||||
* gutter (x=600…880, 280px) holds three three-line blocks; x=560…600 belongs to
|
||||
* the leaders only. Lanes: name / metric / valence with 30–40px line boxes and
|
||||
* a 196px pitch (design: 62/108/150 · 258/304/346 · 454/500/542).
|
||||
* Geometry of the approved three-series time line (C205 extension, feed only;
|
||||
* projected final period added by C207). The data box is narrower than the
|
||||
* two-series one (x=52…560) so the wider end gutter (x=600…880, 280px) holds
|
||||
* three three-line blocks; x=560…600 belongs to the leaders only. Lanes:
|
||||
* name / metric / valence with 30–40px line boxes and a 196px pitch (design:
|
||||
* 62/108/150 · 258/304/346 · 454/500/542).
|
||||
*/
|
||||
const TRIPLE_GEOMETRY = {
|
||||
feed: {
|
||||
|
|
@ -561,31 +562,52 @@ const TRIPLE_GEOMETRY = {
|
|||
triangleWidth: 20,
|
||||
triangleTop: 8,
|
||||
triangleApex: 10,
|
||||
projectedDotRadius: 11,
|
||||
projectedDiamondSize: 28,
|
||||
projectedTriangleWidth: 30,
|
||||
projectedTriangleTop: 11,
|
||||
projectedTriangleApex: 15,
|
||||
axisSize: 30,
|
||||
axisY: 636,
|
||||
zeroX: 42,
|
||||
zeroY: 538,
|
||||
nameSize: 30,
|
||||
metricSize: 34,
|
||||
projectedMetricSize: 30,
|
||||
valenceSize: 30,
|
||||
changeX: 874,
|
||||
textX: 600,
|
||||
leader: { start: 570, rail: 580, bend: 590, end: 594 },
|
||||
projectedLeader: { start: 578, rail: 584, bend: 590, end: 594 },
|
||||
laneOffsets: [62, 108, 150],
|
||||
lanePitch: 196,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Three-series time line (C205 extension of the two-series variant): three
|
||||
* observed series on a shared zero-based scale and the triad of redundant tones
|
||||
* (melhor = red circle ↑ "amplia"; neutro = gray diamond ↗ "cresce"; neutro
|
||||
* escuro = ink triangle ↘ "diminui") with a three-line end gutter. Every point
|
||||
* is observed — no band, no dash, no hollow marker, no "projeção".
|
||||
* Three-series time line (C205 extension of the two-series variant, projected
|
||||
* final period certified by C207): three series on a shared zero-based scale
|
||||
* and the triad of redundant tones (melhor = red circle ↑ "amplia"; neutro =
|
||||
* gray diamond ↗ "cresce"; neutro escuro = ink triangle ↘ "diminui") with a
|
||||
* three-line end gutter. Every point observed — no band, no dash, no hollow
|
||||
* marker, no "projeção". When `projected` is on (last period only): the band
|
||||
* covers the final step, its top label names the projected period ("projeção
|
||||
* 2026"), only the last segment dashes and the three final markers are hollow,
|
||||
* and the gutter shows the projected value beside the variation that runs
|
||||
* first → projected period (human gate feedback on C207: the percentage must
|
||||
* close on the same pair as the headline).
|
||||
*
|
||||
* `series` items are `{ name, tone: 'good'|'neutral'|'neutral-dark', rows }`;
|
||||
* the caller owns the palette and the valence words (never color alone).
|
||||
*/
|
||||
export const tripleLineChart = ({ series, size = 'feed', colors, valence, format }) => {
|
||||
export const tripleLineChart = ({
|
||||
series,
|
||||
size = 'feed',
|
||||
projected = false,
|
||||
colors,
|
||||
valence,
|
||||
format,
|
||||
}) => {
|
||||
const cfg = TRIPLE_GEOMETRY[size] ?? TRIPLE_GEOMETRY.feed
|
||||
const count = series[0].rows.length
|
||||
const max = niceMax(Math.max(...series.flatMap((serie) => serie.rows.map((row) => row.value))))
|
||||
|
|
@ -606,6 +628,29 @@ export const tripleLineChart = ({ series, size = 'feed', colors, valence, format
|
|||
return `<circle cx="${x}" cy="${y}" r="${cfg.dotRadius}" />`
|
||||
}
|
||||
|
||||
/** Hollow final marker of the projected period (C207): same tone shape, big. */
|
||||
const projectedMarker = (tone, x, y) => {
|
||||
const color = colors[tone] ?? colors.neutral
|
||||
const className = `triple-marker-projected triple-marker-projected-${tone}`
|
||||
if (tone === 'neutral') {
|
||||
const half = cfg.projectedDiamondSize / 2
|
||||
return `<path class="${className}" d="M${x} ${y - half} L${x + half} ${y} L${x} ${y + half} L${x - half} ${y} Z" fill="${colors.paper}" stroke="${color}" stroke-width="5" stroke-linejoin="round" />`
|
||||
}
|
||||
if (tone === 'neutral-dark') {
|
||||
const half = cfg.projectedTriangleWidth / 2
|
||||
return `<path class="${className}" d="M${x - half} ${y - cfg.projectedTriangleTop} L${x + half} ${y - cfg.projectedTriangleTop} L${x} ${y + cfg.projectedTriangleApex} Z" fill="${colors.paper}" stroke="${color}" stroke-width="5" stroke-linejoin="round" />`
|
||||
}
|
||||
return `<circle class="${className}" cx="${x}" cy="${y}" r="${cfg.projectedDotRadius}" fill="${colors.paper}" stroke="${color}" stroke-width="5" />`
|
||||
}
|
||||
|
||||
const band = projected
|
||||
? `<rect class="triple-projection-band" x="${xAt(count - 2)}" y="0" width="${xAt(count - 1) - xAt(count - 2)}" height="${cfg.baseline}" fill="${colors.grid}" fill-opacity="0.16" />`
|
||||
: ''
|
||||
|
||||
const projectionLabel = projected
|
||||
? `<text x="${cfg.right - 8}" y="${cfg.top - 11}" text-anchor="end" class="triple-projection-label" font-size="${cfg.axisSize}">projeção ${htmlEscape(series[0].rows[lastIndex].label)}</text>`
|
||||
: ''
|
||||
|
||||
const gridLines = `<line class="triple-grid-line" x1="${cfg.left}" y1="${cfg.top}" x2="${cfg.right}" y2="${cfg.top}" stroke="${colors.grid}" stroke-opacity="0.32" stroke-width="2" />
|
||||
<line class="triple-grid-line" x1="${cfg.left}" y1="${Math.round((cfg.top + cfg.baseline) / 2)}" x2="${cfg.right}" y2="${Math.round((cfg.top + cfg.baseline) / 2)}" stroke="${colors.grid}" stroke-opacity="0.32" stroke-width="2" />
|
||||
<line class="triple-zero-line" x1="${cfg.left}" y1="${cfg.baseline}" x2="${cfg.right}" y2="${cfg.baseline}" stroke="${colors.grid}" stroke-width="2" />
|
||||
|
|
@ -615,14 +660,28 @@ export const tripleLineChart = ({ series, size = 'feed', colors, valence, format
|
|||
.map((serie) => {
|
||||
const color = colors[serie.tone] ?? colors.neutral
|
||||
const points = serie.rows.map((row, index) => ({ x: xAt(index), y: yAt(row.value) }))
|
||||
return `<polyline class="triple-series triple-series-${serie.tone}" points="${points.map((point) => `${point.x},${point.y}`).join(' ')}" fill="none" stroke="${color}" stroke-width="${cfg.stroke}" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<g class="triple-markers-${serie.tone}" fill="${color}">${points.map((point) => marker(serie.tone, point.x, point.y)).join('')}</g>`
|
||||
const solid = projected ? points.slice(0, -1) : points
|
||||
const polyline = `<polyline class="triple-series triple-series-${serie.tone}" points="${solid.map((point) => `${point.x},${point.y}`).join(' ')}" fill="none" stroke="${color}" stroke-width="${cfg.stroke}" stroke-linecap="round" stroke-linejoin="round" />`
|
||||
const dashed = projected
|
||||
? `<line class="triple-series-projection triple-series-${serie.tone}" x1="${points[count - 2].x}" y1="${points[count - 2].y}" x2="${points[count - 1].x}" y2="${points[count - 1].y}" stroke="${color}" stroke-width="${cfg.stroke}" stroke-linecap="round" stroke-dasharray="18 12" />`
|
||||
: ''
|
||||
const markers = `<g class="triple-markers-${serie.tone}" fill="${color}">${solid.map((point) => marker(serie.tone, point.x, point.y)).join('')}</g>`
|
||||
return `${polyline}${dashed}${markers}`
|
||||
})
|
||||
.join('')
|
||||
|
||||
const projectedMarkers = projected
|
||||
? series
|
||||
.map((serie) => {
|
||||
const point = { x: xAt(lastIndex), y: yAt(serie.rows[lastIndex].value) }
|
||||
return projectedMarker(serie.tone, point.x, point.y)
|
||||
})
|
||||
.join('')
|
||||
: ''
|
||||
|
||||
const changeOf = (serie) => {
|
||||
const firstValue = serie.rows[0].value
|
||||
const observed = serie.rows[lastIndex].value
|
||||
const observed = serie.rows[lastIndex]?.value
|
||||
if (!(firstValue > 0) || !Number.isFinite(observed)) return null
|
||||
const percent = Math.round(((observed - firstValue) / firstValue) * 100)
|
||||
const sign = percent > 0 ? '+' : percent < 0 ? '−' : ''
|
||||
|
|
@ -645,22 +704,36 @@ export const tripleLineChart = ({ series, size = 'feed', colors, valence, format
|
|||
const endBlocks = blocks
|
||||
.map((block) => {
|
||||
const { baselines, point } = block
|
||||
const leader = projected ? cfg.projectedLeader : cfg.leader
|
||||
const metricSize = projected ? cfg.projectedMetricSize : cfg.metricSize
|
||||
const metric = `${format(block.serie.rows[lastIndex])}${
|
||||
block.change ? ` · ${block.change.percentLabel}` : ''
|
||||
}`
|
||||
const text = (className, size, fill, value, index) =>
|
||||
`<text x="${cfg.textX}" y="${baselines[index]}" class="${className}" font-size="${size}"${fill ? ` fill="${fill}"` : ''}>${htmlEscape(value)}</text>`
|
||||
const text = (className, size, fill, value, index, options = {}) =>
|
||||
`<text x="${options.x ?? cfg.textX}" y="${baselines[index]}"${options.anchor ? ` text-anchor="${options.anchor}"` : ''} class="${className}" font-size="${size}"${fill ? ` fill="${fill}"` : ''}>${htmlEscape(value)}</text>`
|
||||
const metricLine = projected
|
||||
? [
|
||||
text('triple-end-metric', metricSize, null, format(block.serie.rows[lastIndex]), 1),
|
||||
block.change
|
||||
? text('triple-end-metric', metricSize, null, `· ${block.change.percentLabel}`, 1, {
|
||||
x: cfg.changeX,
|
||||
anchor: 'end',
|
||||
})
|
||||
: '',
|
||||
].join('')
|
||||
: text('triple-end-metric', metricSize, null, metric, 1)
|
||||
const valenceLine = text(
|
||||
'triple-end-valence',
|
||||
cfg.valenceSize,
|
||||
block.color,
|
||||
valence[block.serie.tone] ?? '',
|
||||
2,
|
||||
)
|
||||
return [
|
||||
`<polyline class="triple-end-leader" points="${cfg.leader.start},${point.y} ${cfg.leader.rail},${point.y} ${cfg.leader.bend},${baselines[1]} ${cfg.leader.end},${baselines[1]}" fill="none" stroke="${block.color}" stroke-width="3" />`,
|
||||
`<polyline class="triple-end-leader" points="${leader.start},${point.y} ${leader.rail},${point.y} ${leader.bend},${baselines[1]} ${leader.end},${baselines[1]}" fill="none" stroke="${block.color}" stroke-width="3" />`,
|
||||
text('triple-end-name', cfg.nameSize, null, block.serie.name, 0),
|
||||
text('triple-end-metric', cfg.metricSize, null, metric, 1),
|
||||
text(
|
||||
'triple-end-valence',
|
||||
cfg.valenceSize,
|
||||
block.color,
|
||||
valence[block.serie.tone] ?? '',
|
||||
2,
|
||||
),
|
||||
metricLine,
|
||||
valenceLine,
|
||||
].join('')
|
||||
})
|
||||
.join('')
|
||||
|
|
@ -682,14 +755,28 @@ export const tripleLineChart = ({ series, size = 'feed', colors, valence, format
|
|||
: change.percent < 0
|
||||
? 'diminuição'
|
||||
: 'sem variação'
|
||||
const trend = change ? `, ${direction} de ${Math.abs(change.percent)} por cento` : ''
|
||||
return `${serie.name}: de ${format(serie.rows[0])} em ${serie.rows[0].label} para ${format(serie.rows[lastIndex])} em ${serie.rows[lastIndex].label}${trend}`
|
||||
const destination = projected
|
||||
? ` para projeção de ${format(serie.rows[lastIndex])} em ${serie.rows[lastIndex].label}`
|
||||
: ` para ${format(serie.rows[lastIndex])} em ${serie.rows[lastIndex].label}`
|
||||
const trend = !change
|
||||
? ''
|
||||
: projected
|
||||
? `, variação de ${change.percent > 0 ? 'mais ' : change.percent < 0 ? 'menos ' : ''}${Math.abs(change.percent)} por cento até ${serie.rows[lastIndex].label} (projeção)`
|
||||
: `, ${direction} de ${Math.abs(change.percent)} por cento`
|
||||
return `${serie.name}: de ${format(serie.rows[0])} em ${serie.rows[0].label}${destination}${trend}`
|
||||
})
|
||||
.join('. ')}. Todos os pontos de cada série são observados.`
|
||||
.join('. ')}. ${
|
||||
projected
|
||||
? `O último segmento e os três marcadores de ${series[0].rows[lastIndex].label} são projetados.`
|
||||
: 'Todos os pontos de cada série são observados.'
|
||||
}`
|
||||
|
||||
return `<svg class="chart triple-chart" viewBox="0 0 ${cfg.width} ${cfg.height}" role="img" aria-label="${htmlEscape(ariaLabel)}" preserveAspectRatio="xMidYMid meet">
|
||||
${band}
|
||||
${gridLines}
|
||||
${projectionLabel}
|
||||
${lines}
|
||||
${projectedMarkers}
|
||||
${endBlocks}
|
||||
${axis}
|
||||
</svg>`
|
||||
|
|
|
|||
|
|
@ -187,16 +187,18 @@ const dualBody = (spec) => {
|
|||
|
||||
/**
|
||||
* Three-series time line (approved C205 extension of the two-series variant,
|
||||
* feed only): melhor = red + circle + "↑ amplia"; neutro = gray + diamond +
|
||||
* "↗ cresce"; neutro escuro = ink + triangle + "↘ diminui" (factual direction,
|
||||
* never the bad valence "↓ recua"). Every tone travels by word + arrow + shape
|
||||
* + color, and the brand blue leaves the data entirely.
|
||||
* projected final period certified by C207; feed only): melhor = red + circle +
|
||||
* "↑ amplia"; neutro = gray + diamond + "↗ cresce"; neutro escuro = ink +
|
||||
* triangle + "↘ diminui" (factual direction, never the bad valence "↓ recua").
|
||||
* Every tone travels by word + arrow + shape + color, and the brand blue leaves
|
||||
* the data entirely.
|
||||
*/
|
||||
const TRIPLE_COLORS = {
|
||||
good: SOLLA_PALETTE.highlight,
|
||||
neutral: SOLLA_PALETTE.axis,
|
||||
'neutral-dark': SOLLA_PALETTE.ink,
|
||||
grid: SOLLA_PALETTE.barStrong,
|
||||
paper: SOLLA_PALETTE.paper,
|
||||
ink: SOLLA_PALETTE.ink,
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +208,7 @@ const tripleBody = (spec) =>
|
|||
`<div class="triple-line-plot">${tripleLineChart({
|
||||
series: spec.series,
|
||||
size: 'feed',
|
||||
projected: Boolean(spec.projectedLabel),
|
||||
colors: TRIPLE_COLORS,
|
||||
valence: TRIPLE_VALENCE,
|
||||
format: (row) => formatValue(row.value),
|
||||
|
|
@ -358,6 +361,12 @@ html, body { margin: 0; background: ${SOLLA_PALETTE.paper}; }
|
|||
.triple-end-metric { fill: ${SOLLA_PALETTE.ink}; font-weight: 850; font-variant-numeric: tabular-nums; }
|
||||
.triple-end-valence { font-weight: 850; }
|
||||
.triple-end-leader { fill: none; stroke-width: 3; stroke-linecap: square; stroke-linejoin: round; }
|
||||
.triple-projection-band { fill: ${SOLLA_PALETTE.barStrong}; fill-opacity: 0.16; }
|
||||
.triple-projection-label { fill: ${SOLLA_PALETTE.axis}; font-weight: 700; }
|
||||
.triple-series-projection { stroke-dasharray: 18 12; }
|
||||
.triple-marker-projected { fill: ${SOLLA_PALETTE.paper}; stroke-width: 5; stroke-linejoin: round; }
|
||||
.triple-projected .triple-end-metric { font-size: 30px; }
|
||||
.triple-projected .source { max-width: 620px; }
|
||||
.anchor { padding-top: 20px; }
|
||||
.anchor-number { color: ${SOLLA_PALETTE.highlight}; font-size: 300px; line-height: 0.86; letter-spacing: -0.065em; font-weight: 900; font-variant-numeric: tabular-nums; }
|
||||
.anchor-copy { max-width: 760px; margin: 40px 0 0; font-size: 52px; line-height: 1.12; font-weight: 750; }
|
||||
|
|
@ -426,6 +435,7 @@ export const renderChartHtml = (spec, { brandLogo: brandLogoDataUri } = {}) => {
|
|||
const seriesCount = Array.isArray(spec.series) ? spec.series.length : 0
|
||||
const dual = seriesCount === 2
|
||||
const triple = seriesCount === 3
|
||||
const tripleProjected = triple && Boolean(spec.projectedLabel)
|
||||
const dualPositive = Boolean(spec.dualPositive)
|
||||
const dualPositiveStyles =
|
||||
!dualPositive || sizeKey !== 'feed'
|
||||
|
|
@ -480,7 +490,7 @@ export const renderChartHtml = (spec, { brandLogo: brandLogoDataUri } = {}) => {
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="canvas${dual ? ' dual-series' : ''}${triple ? ' triple-series' : ''}" role="img" aria-label="${htmlEscape(spec.headline)}">
|
||||
<article class="canvas${dual ? ' dual-series' : ''}${triple ? ' triple-series' : ''}${tripleProjected ? ' triple-projected' : ''}" role="img" aria-label="${htmlEscape(spec.headline)}">
|
||||
<div class="inner">
|
||||
<div class="top-rule"></div>
|
||||
<p class="context">${htmlEscape(kicker)}</p>
|
||||
|
|
|
|||
|
|
@ -419,6 +419,49 @@ describe('main — spec assembly, replay and the per-size log', () => {
|
|||
expect(stdout.join('')).toContain('3 séries × 3')
|
||||
})
|
||||
|
||||
it('assembles the projected three-series spec (C207)', async () => {
|
||||
const input = await writeInput(
|
||||
dir,
|
||||
'Ano,Estadual,Privada,Municipal\n2024,300,100,200\n2025,400,120,150\n2026,500,121,100\n',
|
||||
)
|
||||
const { launchBrowser } = launchTracker()
|
||||
const screenshot = screenshotStub()
|
||||
const outPath = join(dir, 'triple-projected.png')
|
||||
|
||||
await main({
|
||||
argv: [
|
||||
`--in=${input}`,
|
||||
'--type=line',
|
||||
'--headline=Produção estadual cresce e municipal cai',
|
||||
'--source=Ministério da Saúde — SIA/SUS',
|
||||
'--good=Estadual',
|
||||
'--neutral=Privada',
|
||||
'--neutral-dark=Municipal',
|
||||
'--projected=2026',
|
||||
`--out=${outPath}`,
|
||||
],
|
||||
repoRoot: dir,
|
||||
launchBrowser,
|
||||
screenshot,
|
||||
})
|
||||
|
||||
expect(screenshot).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ width: 1080, height: 1350, outPath }),
|
||||
)
|
||||
const spec = JSON.parse(
|
||||
await readFile(
|
||||
join(
|
||||
dir,
|
||||
'data/graficos-instagram/producao-estadual-cresce-e-municipal-cai.chart-spec.json',
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
)
|
||||
expect(spec).toMatchObject({ chartType: 'line', size: 'feed', projectedLabel: '2026' })
|
||||
expect(stdout.join('')).toContain('3 séries × 3')
|
||||
})
|
||||
|
||||
it('refuses an incomplete triad and a 2-series tone on the three-series line', async () => {
|
||||
const input = await writeInput(dir, 'Ano,Estadual,Municipal,Privada\n2015,1,2,3\n2016,2,2,2\n')
|
||||
const { launchBrowser } = launchTracker()
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ describe('validateSpec — the two-series time line', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('validateSpec — the three-series time line (C205 extension)', () => {
|
||||
describe('validateSpec — the three-series time line (C205 extension, C207 projection)', () => {
|
||||
const annual = (values: number[]) =>
|
||||
values.map((value, index) => ({ label: `${2015 + index}`, value }))
|
||||
const tripleSpec = (overrides = {}) => ({
|
||||
|
|
@ -530,8 +530,9 @@ describe('validateSpec — the three-series time line (C205 extension)', () => {
|
|||
).toThrow(/tom inválido/)
|
||||
})
|
||||
|
||||
it('refuses projection and crossing on the three-series extension', () => {
|
||||
expect(() => validateSpec(tripleSpec({ projectedLabel: '2026' }))).toThrow(/projeção/)
|
||||
it('certifies the projected last period (C207) and still refuses crossing', () => {
|
||||
expect(validateSpec(tripleSpec({ projectedLabel: '2026' }))).toBeTruthy()
|
||||
expect(() => validateSpec(tripleSpec({ projectedLabel: '2017' }))).toThrow(/último período/)
|
||||
expect(() => validateSpec(tripleSpec({ crossingLabel: '2026' }))).toThrow(/cruzamento/)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -403,3 +403,107 @@ describe('tripleLineChart — the approved three-series extension (C205, feed)',
|
|||
expect(evil).toContain('<b>x</b>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tripleLineChart — the projected final period (C207, feed)', () => {
|
||||
const annual = (values: number[]) =>
|
||||
values.map((value, index) => ({ label: `${2015 + index}`, value }))
|
||||
const series = [
|
||||
{
|
||||
name: 'Estadual',
|
||||
tone: 'good',
|
||||
rows: annual([
|
||||
522656, 545643, 804451, 1482193, 2733904, 3165119, 3646160, 3718176, 3669352, 3486832,
|
||||
3774183, 4213052,
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: 'Rede privada',
|
||||
tone: 'neutral',
|
||||
rows: annual([
|
||||
833679, 708303, 688405, 767470, 715170, 512628, 583789, 700002, 888211, 880871, 908345,
|
||||
855684,
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: 'Municipal',
|
||||
tone: 'neutral-dark',
|
||||
rows: annual([
|
||||
2904003, 2331052, 2133853, 2010696, 1968664, 1223339, 1682379, 1861214, 2014870, 2057296,
|
||||
1937385, 1492020,
|
||||
]),
|
||||
},
|
||||
]
|
||||
const colors = {
|
||||
good: '#e4102f',
|
||||
neutral: '#78716c',
|
||||
'neutral-dark': '#1c1917',
|
||||
grid: '#a8a29e',
|
||||
paper: '#faf9f7',
|
||||
ink: '#1c1917',
|
||||
}
|
||||
const valence = { good: '↑ amplia', neutral: '↗ cresce', 'neutral-dark': '↘ diminui' }
|
||||
const format = (row: { value: number }) =>
|
||||
new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 1 }).format(row.value)
|
||||
const svg = tripleLineChart({ series, colors, valence, format, projected: true })
|
||||
|
||||
it('covers exactly the last step with the band and names the projected period on top', () => {
|
||||
expect(svg).toContain('class="triple-projection-band" x="514" y="0" width="46" height="544"')
|
||||
expect(svg).toContain(
|
||||
'x="552" y="45" text-anchor="end" class="triple-projection-label" font-size="30">projeção 2026</text>',
|
||||
)
|
||||
expect(svg.match(/>projeção 2026</g)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('dashes only the last segment of each series', () => {
|
||||
expect(svg.match(/class="triple-series-projection/g)).toHaveLength(3)
|
||||
expect(svg.match(/stroke-dasharray="18 12"/g)).toHaveLength(3)
|
||||
expect(svg).toContain(
|
||||
'points="52,493 98,491 144,465 191,399 237,277 283,235 329,188 375,181 421,186 468,204 514,176"',
|
||||
)
|
||||
expect(svg).toContain(
|
||||
'class="triple-series-projection triple-series-good" x1="514" y1="176" x2="560" y2="133"',
|
||||
)
|
||||
})
|
||||
|
||||
it('hollows the three projected markers keeping the tone shape', () => {
|
||||
expect(svg).toContain(
|
||||
'class="triple-marker-projected triple-marker-projected-good" cx="560" cy="133" r="11" fill="#faf9f7" stroke="#e4102f" stroke-width="5"',
|
||||
)
|
||||
expect(svg).toContain(
|
||||
'class="triple-marker-projected triple-marker-projected-neutral-dark" d="M545 387 L575 387 L560 413 Z" fill="#faf9f7" stroke="#1c1917"',
|
||||
)
|
||||
expect(svg).toContain(
|
||||
'class="triple-marker-projected triple-marker-projected-neutral" d="M560 446 L574 460 L560 474 L546 460 Z" fill="#faf9f7" stroke="#78716c"',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the projected value with the variation that closes on the projected period', () => {
|
||||
expect(svg).toContain('points="578,133 584,133 590,108 594,108"')
|
||||
expect(svg).toContain('>4.213.052<')
|
||||
expect(svg).toContain('>· +706%<')
|
||||
expect(svg).toContain('>↑ amplia<')
|
||||
expect(svg).toContain('>1.492.020<')
|
||||
expect(svg).toContain('>· −49%<')
|
||||
expect(svg).toContain('>↘ diminui<')
|
||||
expect(svg).toContain('>855.684<')
|
||||
expect(svg).toContain('>· +3%<')
|
||||
expect(svg).toContain('>↗ cresce<')
|
||||
expect(svg).not.toContain('até 2025')
|
||||
expect(svg.indexOf('>Estadual<')).toBeLessThan(svg.indexOf('>Municipal<'))
|
||||
expect(svg.indexOf('>Municipal<')).toBeLessThan(svg.indexOf('>Rede privada<'))
|
||||
})
|
||||
|
||||
it('announces the projection and the observed variation in the aria label', () => {
|
||||
expect(svg).toContain(
|
||||
'Estadual: de 522.656 em 2015 para projeção de 4.213.052 em 2026, variação de mais 706 por cento até 2026 (projeção)',
|
||||
)
|
||||
expect(svg).toContain(
|
||||
'Municipal: de 2.904.003 em 2015 para projeção de 1.492.020 em 2026, variação de menos 49 por cento até 2026 (projeção)',
|
||||
)
|
||||
expect(svg).toContain(
|
||||
'Rede privada: de 833.679 em 2015 para projeção de 855.684 em 2026, variação de mais 3 por cento até 2026 (projeção)',
|
||||
)
|
||||
expect(svg).toContain('O último segmento e os três marcadores de 2026 são projetados')
|
||||
expect(svg).not.toContain('Todos os pontos de cada série são observados')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -411,6 +411,34 @@ describe('renderChartHtml — the three-series time line (C205)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// C207 approved extension: the projected final period rides the same triad
|
||||
// (feed only) with the neutral band, the dashed last segment and the three
|
||||
// hollow markers, while the gutter variation still closes on the last observed
|
||||
// period.
|
||||
describe('renderChartHtml — the projected three-series line (C207)', () => {
|
||||
const html = render(tripleSpec({ projectedLabel: '2026' }))
|
||||
const svg = html.slice(html.indexOf('<svg'), html.indexOf('</svg>'))
|
||||
|
||||
it('marks the canvas and brings band, label, dash and hollow markers', () => {
|
||||
expect(html).toContain('canvas triple-series triple-projected')
|
||||
expect(svg).toContain('class="triple-projection-band"')
|
||||
expect(svg).toContain('projeção')
|
||||
expect(svg.match(/class="triple-series-projection/g)).toHaveLength(3)
|
||||
expect(svg).toContain('class="triple-marker-projected triple-marker-projected-good"')
|
||||
expect(svg).toContain('class="triple-marker-projected triple-marker-projected-neutral"')
|
||||
expect(svg).toContain('class="triple-marker-projected triple-marker-projected-neutral-dark"')
|
||||
expect(svg).not.toContain('Todos os pontos de cada série são observados')
|
||||
})
|
||||
|
||||
it('ports the C207 classes and the reduced projected metric size', () => {
|
||||
expect(html).toContain('.triple-projection-band { fill:')
|
||||
expect(html).toContain('.triple-projection-label { fill:')
|
||||
expect(html).toContain('.triple-marker-projected { fill:')
|
||||
expect(html).toContain('.triple-projected .triple-end-metric { font-size: 30px; }')
|
||||
expect(html).toContain('.triple-projected .source { max-width: 620px; }')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderChartHtml — the shared footer (C206 brand mark)', () => {
|
||||
it('hugs the official asset ratio on the feed canvas', () => {
|
||||
const html = render(spec())
|
||||
|
|
|
|||
Loading…
Reference in a new issue