cobertura_bruta = FileAttachment("data/site.csv").csv({typed: true})
adocoes = FileAttachment("data/adocoes.csv").csv({typed: true})
status = FileAttachment("data/status.json").json()
REPO = "{{< meta website.repo-url >}}"
ROTULO_GRUPO = new Map([
["angiospermas", "Angiospermas"], ["gimnospermas", "Gimnospermas"],
["samambaias", "Samambaias e licófitas"]
])
OPCOES_GRUPO = ["Todos", ...ROTULO_GRUPO.values()]
GRUPO_POR_ROTULO = new Map([...ROTULO_GRUPO].map(([k, v]) => [v, k]))
// Domínios fitogeográficos da Flora e Funga, do maior para o menor número de espécies.
DOMINIOS = ["Mata Atlântica", "Amazônia", "Cerrado", "Caatinga", "Pampa", "Pantanal"]
FORMAS_VIDA = ["Árvore", "Arbusto", "Subarbusto", "Erva", "Liana/volúvel/trepadeira", "Palmeira",
"Bambu", "Suculenta", "Dracenóide"]
SUBSTRATOS = ["Terrícola", "Rupícola", "Epífita", "Hemiepífita", "Aquática", "Parasita",
"Hemiparasita", "Saprófita"]
// Cores de situação (as mesmas de tema-claro.scss; tons médios que funcionam
// no claro e no escuro): verde-folha / ocre / terracota. E uma cor por grupo.
SITUACOES = ["com DNA", "a verificar", "sem DNA"]
COR_SITUACAO = ["#8cc283", "#d6a23a", "#c4633a"]
COR_GRUPO = ["#3a8a55", "#b8532c", "#5b6f8a"]
// Portaria MMA 1.783/2026: CR, EN e VU (com "(infra)" quando só há infraespecíficos
// listados e "(PE)" para possivelmente extinta).
eh_ameacada = c => /^(CR|EN|VU)/.test(c ?? "")
AMEACADAS = new Set(["CR", "EN", "VU"])
lista_de = v => v ? String(v).split(";") : []
cobertura = {
const adot = new Map(adocoes.map(a => [a.especie, a]));
return cobertura_bruta.map(d => {
const a = adot.get(d.especie);
const cat = d.categoria_ameaca ?? "";
const endemica = d.endemica_br === true || d.endemica_br === "TRUE";
return {
...d,
grupo_rotulo: ROTULO_GRUPO.get(d.grupo),
ufs_lista: lista_de(d.ufs),
biomas_lista: lista_de(d.biomas),
formas_lista: lista_de(d.forma_vida),
substratos_lista: lista_de(d.substrato),
endemica,
ameacada: eh_ameacada(cat),
// leituras brutas no SRA, sem sequência montada (marca, não é situação)
brutos: d.dados_brutos === true || d.dados_brutos === "TRUE",
adocao: a ? a.situacao : null,
adotada_por: a ? a.instituicao : null,
// prioridade: ameaçada > endêmica > descrita há mais tempo (a portaria não traz DD)
prioridade: (eh_ameacada(cat) ? 0 : 1) * 10 + (endemica ? 0 : 1)
};
});
}
filtra_grupo = (dados, rotulo) =>
rotulo === "Todos" ? dados : dados.filter(d => d.grupo === GRUPO_POR_ROTULO.get(rotulo))
fmt = d3.format(",d")
fmtpct = d3.format(".0%")
fmtn = n => n == null ? "" : fmt(n).replace(/,/g, ".")
conta = dados => {
const c = d3.rollup(dados, v => v.length, d => d.situacao);
return {
total: dados.length,
com: c.get("com DNA") ?? 0,
verificar: c.get("a verificar") ?? 0,
sem: c.get("sem DNA") ?? 0,
brutos: dados.filter(d => d.brutos).length
};
}
// Observação completa: nas "sem DNA", site.csv omite a frase padrão.
observacao = d => d.situacao === "sem DNA"
? "Nada encontrado no BOLD ou no GenBank sob este nome e os sinônimos conhecidos" + (d.observacao ? `; ${d.observacao}` : "") + "."
: (d.observacao ?? "")
link_adote = especie =>
`${REPO}/issues/new?template=adote.yml&title=${encodeURIComponent("Adoção: " + especie)}&especie=${encodeURIComponent(especie)}`
link_sra = especie =>
`https://www.ncbi.nlm.nih.gov/sra/?term=${encodeURIComponent('"' + especie + '"[Organism]')}`
link_ffb = id => id ? `https://floradobrasil.jbrj.gov.br/FB${id}` : null
// Marca das espécies sem sequência montada que têm leituras brutas no SRA.
selo_brutos = d => d.brutos
? htl.html`<a class="cc-chip cc-brutos" href="${link_sra(d.especie)}" target="_blank" title="Há leituras de sequenciamento no SRA, mas nenhuma sequência montada no BOLD ou no GenBank">só dados brutos</a>`
: ""
selo_situacao = s => {
const cls = s === "com DNA" ? "com" : s === "sem DNA" ? "sem" : "verificar";
const ico = s === "com DNA" ? "✓" : s === "sem DNA" ? "✕" : "?";
return htl.html`<span class="situacao ${cls}">${ico} ${s}</span>`;
}
cor_ameaca = c => c?.startsWith("CR") ? "cr" : c?.startsWith("EN") ? "en" : c?.startsWith("VU") ? "vu" : "outra"
selo_ameaca = c => eh_ameacada(c)
? htl.html`<span class="cc-chip cc-${cor_ameaca(c)}" title="Portaria MMA 1.783/2026">${c}</span>` : ""
// Lista curta de espécies (painéis de estado e de domínio).
item_especie = (x, com_situacao) => htl.html`<li>
<a href="${link_adote(x.especie)}" target="_blank" title="Adotar esta espécie"><em>${x.especie}</em></a>
<span class="cc-uf-chips">${com_situacao ? selo_situacao(x.situacao) : ""}${selo_brutos(x)}${selo_ameaca(x.categoria_ameaca)}${x.endemica ? htl.html`<span class="cc-chip">endêmica</span>` : ""}</span>
<small>${x.grupo_rotulo} · ${x.familia}${x.ano_descricao ? ` · ${x.ano_descricao}` : ""}</small>
</li>`
ordena_prioridade = (a, b) =>
d3.ascending(a.situacao === "sem DNA" ? 0 : 1, b.situacao === "sem DNA" ? 0 : 1) ||
d3.ascending(a.prioridade, b.prioridade) ||
d3.ascending(a.ano_descricao ?? 9999, b.ano_descricao ?? 9999) ||
d3.ascending(a.especie, b.especie)Por domínio
A lacuna em cada domínio fitogeográfico
Amazônia, Caatinga, Cerrado, Mata Atlântica, Pampa e Pantanal, como na Flora e Funga do Brasil. Uma espécie entra em todos os domínios onde ocorre; exclusivas são as que só ocorrem em um. espécies do recorte não têm domínio informado e ficam fora.
sel = filtra_grupo(cobertura, grupo)
sem_dominio = sel.filter(d => d.biomas_lista.length === 0).length
eh_sem = d => d.situacao === "sem DNA" || (com_verificar.length > 0 && d.situacao === "a verificar")
no_dominio = (d, dom) => d.biomas_lista.includes(dom) && (!so_exclusivas.length || d.biomas_lista.length === 1)por_dominio = DOMINIOS.map(dom => {
const sp = sel.filter(d => no_dominio(d, dom));
const sem = sp.filter(eh_sem).length;
return {dom, total: sp.length, com: sp.filter(d => d.situacao === "com DNA").length,
verificar: sp.filter(d => d.situacao === "a verificar").length,
sem_dna: sp.filter(d => d.situacao === "sem DNA").length,
brutos: sp.filter(d => eh_sem(d) && d.brutos).length,
ameacadas: sp.filter(d => eh_sem(d) && d.ameacada).length,
sem, pct: sp.length ? sem / sp.length : 0};
})Clique em um domínio para ver as espécies e a lacuna por forma de vida.
htl.html`<div class="cc-grupos cc-dominios">${por_dominio.map(d => {
const barra = [d.com, d.verificar, d.sem_dna].map((v, i) =>
htl.html`<i style="width:${100 * v / (d.total || 1)}%;background:${COR_SITUACAO[i]}"></i>`);
const el = htl.html`<div class="cc-grupo cc-clicavel ${d.dom === dom_sel ? "cc-ativo" : ""}" tabindex="0" role="button" title="Ver as espécies de ${d.dom}">
<h3>${d.dom}</h3>
<span class="cc-total">${fmtn(d.total)} espécies${so_exclusivas.length ? " exclusivas" : ""}</span>
<div class="cc-barra">${barra}</div>
<span class="cc-linha cc-sem"><span>sem DNA${com_verificar.length ? " e a verificar" : ""}</span><b>${fmtn(d.sem)} · ${fmtpct(d.pct)}</b></span>
<span class="cc-linha"><span>ameaçadas sem DNA</span><b>${fmtn(d.ameacadas)}</b></span>
<span class="cc-linha cc-brutos-linha"><span>só dados brutos</span><b>${fmtn(d.brutos)}</b></span>
</div>`;
el.onclick = () => escolhe(d.dom === dom_sel ? null : d.dom);
el.onkeydown = e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); el.onclick(); } };
return el;
})}</div>`{
const no_recorte = d => dom_sel ? no_dominio(d, dom_sel)
: d.biomas_lista.length > 0 && (!so_exclusivas.length || d.biomas_lista.length === 1);
const lista = sel.filter(d => no_recorte(d) && eh_sem(d)).sort(ordena_prioridade);
const voltar = htl.html`<button class="cc-cta cc-pequeno" type="button">← Todos</button>`;
voltar.onclick = () => escolhe(null);
const d = por_dominio.find(x => x.dom === dom_sel);
const titulo = dom_sel ? dom_sel : "Todos os domínios";
const resumo = dom_sel
? htl.html`<b>${fmtn(lista.length)}</b> de ${fmtn(d.total)} espécies${so_exclusivas.length ? " exclusivas" : ""} do domínio${com_verificar.length ? "" : htl.html`; outras ${fmtn(d.verificar)} estão a verificar`}.`
: htl.html`<b>${fmtn(lista.length)}</b> espécies${so_exclusivas.length ? " exclusivas de um domínio" : " com domínio informado"} ${com_verificar.length ? "sem DNA ou a verificar" : "sem DNA"}. Escolha um domínio ao lado.`;
return htl.html`<div class="cc-uf-painel">
<div class="cc-uf-topo"><span class="cc-chapeu">${grupo === "Todos" ? "Plantas vasculares" : grupo} · ${com_verificar.length ? "sem DNA e a verificar" : "sem DNA"}</span>${dom_sel ? voltar : ""}</div>
<h3>${titulo}</h3>
<p class="cc-uf-resumo">${resumo}</p>
${lista.length ? htl.html`<ul class="cc-uf-especies">${lista.slice(0, 500).map(x => item_especie(x, com_verificar.length > 0))}</ul>
${lista.length > 500 ? htl.html`<p class="nota-pequena">Mostrando as 500 primeiras, por prioridade (ameaçadas, endêmicas, descritas há mais tempo).</p>` : ""}` : htl.html`<p>Nenhuma espécie com estes filtros.</p>`}
<p class="nota-pequena">Clique no nome para adotar. Para ver todas, filtrar por família ou buscar, use <a href="especies.html">Espécies sem DNA</a>.</p>
</div>`;
}Forma de vida
Fração das espécies de cada forma de vida e substrato sem DNA. Uma espécie pode ter mais de uma forma de vida (erva e subarbusto, por exemplo) e entra em cada uma.
{
const base = sel.filter(d => dom_sel ? no_dominio(d, dom_sel) : (!so_exclusivas.length || d.biomas_lista.length === 1));
const linha = (rotulo, f) => {
const sp = base.filter(f);
const sem = sp.filter(eh_sem).length;
return {rotulo, total: sp.length, sem, pct: sp.length ? sem / sp.length : 0};
};
const grafico = linhas => Plot.plot({
width: Math.min(width, 760), height: linhas.length * 26 + 30, marginLeft: 170, marginRight: 130,
style: {fontFamily: "Inter, sans-serif", fontSize: "13px", background: "transparent"},
x: {label: null, axis: "top", grid: true, domain: [0, 1], tickFormat: fmtpct},
y: {label: null, domain: linhas.map(d => d.rotulo), tickSize: 0},
marks: [
Plot.barX(linhas, {x: "pct", y: "rotulo", fill: COR_SITUACAO[2], inset: 2, rx: 2, tip: true,
title: d => `${d.rotulo}\n${fmtn(d.sem)} de ${fmtn(d.total)} espécies (${fmtpct(d.pct)})`}),
Plot.text(linhas, {x: "pct", y: "rotulo", dx: 6, textAnchor: "start",
text: d => `${fmtpct(d.pct)} · ${fmtn(d.sem)} de ${fmtn(d.total)}`, fill: "currentColor", fillOpacity: 0.7}),
Plot.ruleX([0])
]
});
const formas = FORMAS_VIDA.map(f => linha(f, d => d.formas_lista.includes(f))).filter(d => d.total >= 10);
const substratos = SUBSTRATOS.map(s => linha(s, d => d.substratos_lista.includes(s))).filter(d => d.total >= 10);
return htl.html`<h4 class="cc-chapeu">Forma de vida</h4>${grafico(formas)}<h4 class="cc-chapeu">Substrato</h4>${grafico(substratos)}`;
}Forma de vida e substrato como na Flora e Funga do Brasil. Formas com menos de 10 espécies no recorte não aparecem.