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 estado
Onde estão as espécies sem DNA
Uma espécie entra em todos os estados onde ocorre. Ocorrência por UF: Flora e Funga do Brasil. espécies do recorte não têm UF em nenhuma fonte e ficam fora do mapa.
NOME_UF = Object.fromEntries("AC:Acre|AL:Alagoas|AP:Amapá|AM:Amazonas|BA:Bahia|CE:Ceará|DF:Distrito Federal|ES:Espírito Santo|GO:Goiás|MA:Maranhão|MT:Mato Grosso|MS:Mato Grosso do Sul|MG:Minas Gerais|PA:Pará|PB:Paraíba|PR:Paraná|PE:Pernambuco|PI:Piauí|RJ:Rio de Janeiro|RN:Rio Grande do Norte|RS:Rio Grande do Sul|RO:Rondônia|RR:Roraima|SC:Santa Catarina|SP:São Paulo|SE:Sergipe|TO:Tocantins".split("|").map(x => x.split(":")))por_uf = Object.keys(NOME_UF).map(uf => {
const sp = sel.filter(d => d.ufs_lista.includes(uf));
const sem = sp.filter(eh_sem).length;
return {uf, nome: NOME_UF[uf], total: sp.length, com: sp.filter(d => d.situacao === "com DNA").length,
verificar: sp.filter(d => d.situacao === "a verificar").length,
brutos: sp.filter(d => eh_sem(d) && d.brutos).length,
sem, pct: sp.length ? sem / sp.length : 0};
})geo = {
// A malha do IBGE segue a regra da mão direita (RFC 7946); o d3-geo espera o
// sentido oposto. Inverte os anéis de cada polígono que "cobre o mundo".
const g = await FileAttachment("data-raw/uf_ibge.geojson").json();
for (const f of g.features) {
if (d3.geoArea(f) > 2 * Math.PI) {
const polys = f.geometry.type === "Polygon" ? [f.geometry.coordinates] : f.geometry.coordinates;
for (const p of polys) for (const r of p) r.reverse();
}
}
return g;
}Passe o mouse sobre um estado para ver os números. Clique nele (ou no nome, no ranking) para ver a lista de espécies.
{
const idx = new Map(por_uf.map(d => [d.uf, d]));
const feats = geo.features.map(f => ({...f, properties: {...f.properties, ...idx.get(COD_UF[f.properties.codarea])}}));
const fc = {type: "FeatureCollection", features: feats};
const w = Math.min(width, 680);
const mapa = Plot.plot({
width: w, height: w * 0.97, style: {fontFamily: "Inter, sans-serif", fontSize: "12px", background: "transparent"},
projection: {type: "mercator", domain: fc},
color: {type: "linear", range: ["#eef3e4", "#9a4322"], legend: true, label: metrica,
tickFormat: metrica === "% sem DNA" ? fmtpct : fmtn},
marks: [
Plot.geo(fc, {fill: f => valor(f.properties), stroke: "var(--cc-papel)", strokeWidth: 1,
tip: {fontFamily: "Inter, sans-serif"},
title: f => `${f.properties.nome}
${fmtn(f.properties.total)} espécies
${fmtn(f.properties.sem)} sem DNA (${fmtpct(f.properties.pct)})
${fmtn(f.properties.verificar)} a verificar
${fmtn(f.properties.brutos)} das sem DNA só com dados brutos
clique para ver a lista`}),
Plot.geo(feats.filter(f => f.properties.uf === uf_sel), {stroke: "var(--cc-tinta)", strokeWidth: 2.5}),
Plot.text(fc.features, Plot.centroid({text: f => f.properties.uf, fill: "#1b2a20", stroke: "#f1f6ec",
strokeWidth: 3, fontSize: 10, fontWeight: 600, pointerEvents: "none"}))
]
});
// Clique no estado: um path por feição, na mesma ordem de feats.
mapa.querySelector('g[aria-label="geo"]').querySelectorAll("path").forEach((p, i) => {
p.addEventListener("click", () => escolhe_uf(feats[i].properties.uf));
});
return mapa;
}{
if (uf_sel) {
const d = por_uf.find(x => x.uf === uf_sel);
const lista = sel.filter(x => x.ufs_lista.includes(uf_sel) && eh_sem(x)).sort(ordena_prioridade);
const voltar = htl.html`<button class="cc-cta cc-pequeno" type="button">← Ranking</button>`;
voltar.onclick = () => escolhe_uf(null);
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>${voltar}</div>
<h3>${d.nome}</h3>
<p class="cc-uf-resumo"><b>${fmtn(lista.length)}</b> de ${fmtn(d.total)} espécies registradas no estado${
com_verificar.length ? "" : htl.html`; outras ${fmtn(d.verificar)} estão a verificar (marque "somar a verificar" para listá-las)`}.</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.</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>`;
}
const linhas = por_uf.slice().sort((a, b) => d3.descending(valor(a), valor(b)));
const max = d3.max(linhas, valor) || 1;
const fmtv = metrica === "% sem DNA" ? fmtpct : fmtn;
return htl.html`<p class="cc-chapeu">Ranking · ${metrica}${com_verificar.length && metrica !== "espécies na lista" ? " (com a verificar)" : ""}</p><ol class="cc-ufs">${linhas.map(d => {
const li = htl.html`<li class="cc-clicavel" tabindex="0" role="button" title="Ver as espécies de ${d.nome}">
<span class="cc-uf-nome">${d.nome}</span>
<span class="cc-uf-barra"><i style="width:${100 * valor(d) / max}%"></i></span>
<span class="cc-uf-valor">${fmtv(valor(d))}</span>
<span class="cc-uf-detalhe">${fmtn(d.total)} espécies · ${fmtn(d.verificar)} a verificar</span>
</li>`;
li.onclick = () => escolhe_uf(d.uf);
li.onkeydown = e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); escolhe_uf(d.uf); } };
return li;
})}</ol>`;
}