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)fotos_csv = FileAttachment("data/fotos.csv").csv()
FOTO = new Map(fotos_csv.filter(d => d.foto_url).map(d => [d.especie, d]))
UFS = ["AC","AL","AP","AM","BA","CE","DF","ES","GO","MA","MT","MS","MG","PA","PB","PR","PE","PI","RJ","RN","RS","RO","RR","SC","SP","SE","TO"]
sem_dna = cobertura.filter(d => d.situacao !== "com DNA")Espécies sem DNA
espécies esperando a primeira sequência
Ordenadas por prioridade: ameaçadas (CR, EN, VU na Portaria MMA 1.783/2026), depois endêmicas e as descritas há mais tempo. “Sem DNA” quer dizer que nada foi encontrado sob o nome da lista e os sinônimos conhecidos; “a verificar”, que o DNA está só sob um sinônimo de circunscrição mais ampla. Só dados brutos marca as que já têm leituras de sequenciamento no SRA que ninguém montou: talvez baste montar. Não é prova de ausência: confira a observação de cada espécie.
filtradas = {
const q = busca.trim().toLowerCase();
return filtra_grupo(cobertura, grupo)
.filter(d => situacoes.includes(d.situacao))
.filter(d => uf === "Todas" || d.ufs_lista.includes(uf))
.filter(d => dominio === "Todos" || d.biomas_lista.includes(dominio))
.filter(d => forma === "Todas" || d.formas_lista.includes(forma) || d.substratos_lista.includes(forma))
.filter(d => familia === "Todas" || d.familia === familia)
.filter(d => !extras.includes("endêmicas do Brasil") || d.endemica)
.filter(d => !extras.includes("ameaçadas (CR, EN, VU)") || d.ameacada)
.filter(d => !extras.includes("com dados brutos no SRA") || d.brutos)
.filter(d => !q || `${d.especie} ${d.familia} ${d.ordem} ${d.autoria ?? ""}`.toLowerCase().includes(q))
.sort((a, b) => d3.ascending(a.prioridade, b.prioridade) ||
d3.ascending(a.ano_descricao ?? 9999, b.ano_descricao ?? 9999) ||
d3.ascending(a.especie, b.especie));
}link_bold = e => `https://portal.boldsystems.org/result?query=${encodeURIComponent(e)}[tax]`
link_genbank = e => `https://www.ncbi.nlm.nih.gov/nuccore/?term=${encodeURIComponent('"' + e + '"[Organism]')}`
ufs_curto = l => !l.length ? "sem UF" : l.length > 6 ? `${l.length} estados` : l.join(" · ")
cartao = d => {
const f = FOTO.get(d.especie);
const capa = f
? htl.html`<figure class="cc-foto"><img src="${f.foto_url}" alt="${d.especie}" loading="lazy">
<figcaption title="${f.foto_credito}">${f.foto_credito.replace(/^\(c\)\s*/, "© ").replace(/, some rights reserved.*/, "")} · <a href="https://www.inaturalist.org/taxa/${f.inat_id}" target="_blank">iNaturalist</a></figcaption></figure>`
: htl.html`<figure class="cc-foto cc-sem-foto"><div class="cc-silhueta" style="mask-image:url(assets/silhuetas/${d.grupo}.svg);-webkit-mask-image:url(assets/silhuetas/${d.grupo}.svg)"></div>
<figcaption>sem foto livre no iNaturalist</figcaption></figure>`;
const ameaca = selo_ameaca(d.categoria_ameaca);
const acao = d.adocao
? htl.html`<span class="cc-chip cc-adotada">${d.adocao}${d.adotada_por ? ` · ${d.adotada_por}` : ""}</span>`
: d.situacao !== "com DNA"
? htl.html`<a class="cc-cta cc-pequeno" href="${link_adote(d.especie)}" target="_blank">Adotar</a>` : "";
return htl.html`<article class="cc-especie">
${capa}
<div class="cc-corpo">
<div class="cc-topo">${selo_situacao(d.situacao)}${selo_brutos(d)}${ameaca}${d.endemica ? htl.html`<span class="cc-chip">endêmica</span>` : ""}</div>
<h3><em>${d.especie}</em></h3>
<p class="cc-autoria">${d.autoria ?? (d.ano_descricao ?? "")}</p>
<p class="cc-onde">${d.grupo_rotulo} · ${d.familia}${d.forma_vida ? ` · ${d.formas_lista.join(", ").toLowerCase()}` : ""}<br>${ufs_curto(d.ufs_lista)}${d.biomas ? ` · ${d.biomas_lista.join(", ")}` : ""}</p>
<p class="cc-bases"><a href="${link_bold(d.especie)}" target="_blank">BOLD <b>${fmtn(d.n_bold)}</b></a>
<a href="${link_genbank(d.especie)}" target="_blank">GenBank <b>${d.n_genbank == null ? "?" : fmtn(d.n_genbank)}</b></a>
${d.n_sra > 0 ? htl.html`<a href="${link_sra(d.especie)}" target="_blank">SRA <b>${fmtn(d.n_sra)}</b></a>` : ""}
${d.id_ffb ? htl.html`<a href="${link_ffb(d.id_ffb)}" target="_blank" title="Ficha na Flora e Funga do Brasil">Flora e Funga</a>` : ""}</p>
${d.situacao !== "com DNA" ? htl.html`<p class="cc-obs" title="${observacao(d)}">${observacao(d)}</p>` : ""}
<div class="cc-acao">${acao}</div>
</div>
</article>`;
}{
if (vista === "Tabela") {
return Inputs.table(filtradas, {
columns: ["especie", "adocao", "situacao", "grupo_rotulo", "familia", "ano_descricao",
"categoria_ameaca", "endemica", "forma_vida", "n_bold", "n_genbank", "n_sra", "ufs", "biomas", "observacao"],
header: {especie: "Espécie", situacao: "Situação", grupo_rotulo: "Grupo", familia: "Família",
ano_descricao: "Descrita em", categoria_ameaca: "Ameaça", endemica: "Endêmica",
forma_vida: "Forma de vida", biomas: "Domínios",
n_bold: "BOLD", n_genbank: "GenBank", n_sra: "SRA", ufs: "UFs", observacao: "Observação",
adocao: ""},
format: {
especie: d => htl.html`<em>${d}</em>`,
situacao: selo_situacao,
ano_descricao: d => d ? String(d) : "",
endemica: d => d ? "sim" : "",
n_bold: (d, i, dados) => htl.html`<a href="${link_bold(dados[i].especie)}" target="_blank" title="Abrir no BOLD">${fmtn(d)}</a>`,
n_genbank: (d, i, dados) => d == null ? "pendente" :
htl.html`<a href="${link_genbank(dados[i].especie)}" target="_blank" title="Abrir no GenBank">${fmtn(d)}</a>`,
n_sra: (d, i, dados) => d > 0
? htl.html`<a href="${link_sra(dados[i].especie)}" target="_blank" title="Leituras brutas no SRA">${fmtn(d)}</a>` : "",
ufs: d => d ? String(d).replaceAll(";", " ") : "sem dado",
biomas: d => d ? String(d).replaceAll(";", ", ") : "",
forma_vida: d => d ? String(d).replaceAll(";", ", ") : "",
observacao: (d, i, dados) => htl.html`<span class="nota-pequena">${observacao(dados[i])}</span>`,
adocao: (d, i, dados) => d
? htl.html`<span class="selo">${d}</span>`
: htl.html`<a class="btn-adote" href="${link_adote(dados[i].especie)}" target="_blank">adotar</a>`
},
width: {especie: 210, observacao: 380, ufs: 160, biomas: 160, forma_vida: 120},
rows: 30, layout: "fixed", select: false
});
}
// Cartões em páginas de 24, com "mostrar mais" (recomeça quando um filtro muda).
const passo = 24;
const grade = htl.html`<div class="cc-especies"></div>`;
const botao = htl.html`<button class="cc-cta cc-mais" type="button"></button>`;
let k = 0;
const mais = () => {
grade.append(...filtradas.slice(k, k + passo).map(cartao));
k += passo;
const resta = filtradas.length - k;
botao.hidden = resta <= 0;
botao.textContent = `Mostrar mais ${Math.min(passo, resta)} de ${fmtn(resta)}`;
};
botao.onclick = mais;
mais();
if (!filtradas.length) return md`Nenhuma espécie com estes filtros.`;
return htl.html`<div>${grade}<p class="cc-centro">${botao}</p></div>`;
}