Files
ESPDocs/components/ComparacaoGraficos.jsx
T
Lucas RayanandClaude Opus 5.5 df39436dcb
CI / Lint e auditorias (push) Successful in 1m26s
CI / Trivy (dependências e segredos) (push) Successful in 18s
CI / Imagem Docker (linux/arm64) (push) Successful in 4m26s
Release / Plano (reaproveitar a imagem?) (push) Successful in 7s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 19s
Release / Deploy no Coolify (push) Skipped
Release / Cria a release no Gitea (push) Successful in 7s
fix(comparacao): eixo do gráfico de consumo em text-xs para passar na auditoria de design
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:28:18 -03:00

299 lines
18 KiB
React
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { Check, Minus, CircleDashed, Clock } from "lucide-react";
import { EIXOS_RADAR, METRICAS } from "@/data/comparacao-metricas";
const CORES = ["var(--serie-1)", "var(--serie-2)", "var(--serie-3)", "var(--serie-4)"];
const T = {
pt: {
visao: "Visão geral", visaoLede: "Notas de 0 a 10 em cada área, relativas às outras séries da Espressif. Quanto maior a área, mais completo o chip, mas repare em onde cada um se destaca.",
eixos: { desempenho: "Desempenho", memoria: "Memória", conectividade: "Conectividade", consumo: "Baixo consumo", ia: "IA / TinyML", perifericos: "Periféricos", seguranca: "Segurança" },
notaEditorial: "Notas editoriais do ESPDocs, baseadas nos datasheets.", verNotas: "Ver as notas em tabela", area: "Área",
consumo: "Consumo de energia", consumoLede: "Corrente típica do chip dormindo, em escala logarítmica: cada linha da grade é dez vezes a anterior. Barra menor dura mais na bateria.",
deep: "Deep sleep", light: "Light sleep", nd: "não divulgado",
bateria: `Duração teórica de uma bateria de 2000 mAh só em deep sleep. Na prática, a autodescarga e o resto da placa encurtam muito:`, anos: "anos", meses: "meses",
tinyml: "Suporte a TinyML", tinymlLede: "O que roda de aprendizado de máquina embarcado em cada chip, pelo suporte oficial da Espressif.",
recurso: "Recurso",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL (modelos otimizados)", espNn: "ESP-NN com kernels acelerados", vetorial: "Instruções vetoriais / IA", espSr: "ESP-SR (palavra de ativação, comandos de voz)" },
estado: { sim: "Sim", parcial: "Parcial", nao: "Não", previsto: "Previsto" },
tinymlNota: "Parcial: roda com limitações (pouca RAM no C2, versões antigas da ESP-DL no ESP32, kernels genéricos do ESP-NN). Previsto: chip anunciado, suporte ainda em desenvolvimento.",
},
en: {
visao: "Overview", visaoLede: "Scores from 0 to 10 in each area, relative to the other Espressif series. A larger area means a more complete chip, but look at where each one stands out.",
eixos: { desempenho: "Performance", memoria: "Memory", conectividade: "Connectivity", consumo: "Low power", ia: "AI / TinyML", perifericos: "Peripherals", seguranca: "Security" },
notaEditorial: "ESPDocs editorial scores, based on the datasheets.", verNotas: "Show scores as a table", area: "Area",
consumo: "Power consumption", consumoLede: "Typical current while the chip sleeps, on a logarithmic scale: each grid line is ten times the previous one. A shorter bar lasts longer on battery.",
deep: "Deep sleep", light: "Light sleep", nd: "not disclosed",
bateria: `Theoretical life of a 2000 mAh battery in deep sleep alone. In practice, self-discharge and the rest of the board cut it a lot:`, anos: "years", meses: "months",
tinyml: "TinyML support", tinymlLede: "What embedded machine learning runs on each chip, according to Espressif's official support.",
recurso: "Feature",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL (optimized models)", espNn: "ESP-NN with accelerated kernels", vetorial: "Vector / AI instructions", espSr: "ESP-SR (wake word, voice commands)" },
estado: { sim: "Yes", parcial: "Partial", nao: "No", previsto: "Planned" },
tinymlNota: "Partial: runs with limitations (little RAM on the C2, older ESP-DL releases on the ESP32, generic ESP-NN kernels). Planned: chip announced, support still in development.",
},
es: {
visao: "Visión general", visaoLede: "Notas de 0 a 10 en cada área, relativas a las demás series de Espressif. Cuanto mayor el área, más completo el chip, pero fíjese en dónde destaca cada uno.",
eixos: { desempenho: "Rendimiento", memoria: "Memoria", conectividade: "Conectividad", consumo: "Bajo consumo", ia: "IA / TinyML", perifericos: "Periféricos", seguranca: "Seguridad" },
notaEditorial: "Notas editoriales de ESPDocs, basadas en las hojas de datos.", verNotas: "Ver las notas en tabla", area: "Área",
consumo: "Consumo de energía", consumoLede: "Corriente típica del chip en reposo, en escala logarítmica: cada línea de la cuadrícula es diez veces la anterior. Una barra menor dura más con batería.",
deep: "Deep sleep", light: "Light sleep", nd: "no divulgado",
bateria: `Duración teórica de una batería de 2000 mAh solo en deep sleep. En la práctica, la autodescarga y el resto de la placa la reducen mucho:`, anos: "años", meses: "meses",
tinyml: "Soporte de TinyML", tinymlLede: "Qué aprendizaje automático embebido corre en cada chip, según el soporte oficial de Espressif.",
recurso: "Recurso",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL (modelos optimizados)", espNn: "ESP-NN con kernels acelerados", vetorial: "Instrucciones vectoriales / IA", espSr: "ESP-SR (palabra de activación, comandos de voz)" },
estado: { sim: "Sí", parcial: "Parcial", nao: "No", previsto: "Previsto" },
tinymlNota: "Parcial: funciona con limitaciones (poca RAM en el C2, versiones antiguas de ESP-DL en el ESP32, kernels genéricos de ESP-NN). Previsto: chip anunciado, soporte aún en desarrollo.",
},
fr: {
visao: "Vue d'ensemble", visaoLede: "Notes de 0 à 10 dans chaque domaine, relatives aux autres séries Espressif. Plus la surface est grande, plus la puce est complète, mais regardez où chacune se distingue.",
eixos: { desempenho: "Performances", memoria: "Mémoire", conectividade: "Connectivité", consumo: "Basse conso.", ia: "IA / TinyML", perifericos: "Périphériques", seguranca: "Sécurité" },
notaEditorial: "Notes éditoriales d'ESPDocs, d'après les fiches techniques.", verNotas: "Voir les notes en tableau", area: "Domaine",
consumo: "Consommation d'énergie", consumoLede: "Courant typique de la puce en veille, en échelle logarithmique : chaque ligne de la grille vaut dix fois la précédente. Une barre plus courte tient plus longtemps sur batterie.",
deep: "Deep sleep", light: "Light sleep", nd: "non publié",
bateria: `Durée théorique d'une batterie de 2000 mAh en deep sleep seul. En pratique, l'autodécharge et le reste de la carte la réduisent beaucoup :`, anos: "ans", meses: "mois",
tinyml: "Prise en charge de TinyML", tinymlLede: "Ce qui tourne en apprentissage automatique embarqué sur chaque puce, selon le support officiel d'Espressif.",
recurso: "Fonction",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL (modèles optimisés)", espNn: "ESP-NN avec noyaux accélérés", vetorial: "Instructions vectorielles / IA", espSr: "ESP-SR (mot d'éveil, commandes vocales)" },
estado: { sim: "Oui", parcial: "Partiel", nao: "Non", previsto: "Prévu" },
tinymlNota: "Partiel : fonctionne avec des limites (peu de RAM sur le C2, anciennes versions d'ESP-DL sur l'ESP32, noyaux ESP-NN génériques). Prévu : puce annoncée, support en cours de développement.",
},
de: {
visao: "Überblick", visaoLede: "Noten von 0 bis 10 je Bereich, relativ zu den anderen Espressif-Serien. Je größer die Fläche, desto vollständiger der Chip, aber achten Sie darauf, wo jeder glänzt.",
eixos: { desempenho: "Leistung", memoria: "Speicher", conectividade: "Konnektivität", consumo: "Geringer Verbrauch", ia: "KI / TinyML", perifericos: "Peripherie", seguranca: "Sicherheit" },
notaEditorial: "Redaktionelle Noten von ESPDocs, basierend auf den Datenblättern.", verNotas: "Noten als Tabelle anzeigen", area: "Bereich",
consumo: "Energieverbrauch", consumoLede: "Typischer Strom des schlafenden Chips, logarithmisch: jede Gitterlinie ist das Zehnfache der vorigen. Ein kürzerer Balken hält länger mit Batterie.",
deep: "Deep Sleep", light: "Light Sleep", nd: "nicht veröffentlicht",
bateria: `Theoretische Laufzeit eines 2000-mAh-Akkus allein im Deep Sleep. In der Praxis verkürzen Selbstentladung und der Rest der Platine sie stark:`, anos: "Jahre", meses: "Monate",
tinyml: "TinyML-Unterstützung", tinymlLede: "Welches eingebettete maschinelle Lernen auf jedem Chip läuft, laut offiziellem Espressif-Support.",
recurso: "Funktion",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL (optimierte Modelle)", espNn: "ESP-NN mit beschleunigten Kernels", vetorial: "Vektor- / KI-Befehle", espSr: "ESP-SR (Aktivierungswort, Sprachbefehle)" },
estado: { sim: "Ja", parcial: "Teilweise", nao: "Nein", previsto: "Geplant" },
tinymlNota: "Teilweise: läuft mit Einschränkungen (wenig RAM beim C2, ältere ESP-DL-Versionen beim ESP32, generische ESP-NN-Kernels). Geplant: Chip angekündigt, Unterstützung in Entwicklung.",
},
"zh-CN": {
visao: "总览", visaoLede: "各领域 0 到 10 分,相对于乐鑫其他系列。面积越大芯片越全面,但请留意每款芯片的强项。",
eixos: { desempenho: "性能", memoria: "内存", conectividade: "连接", consumo: "低功耗", ia: "AI / TinyML", perifericos: "外设", seguranca: "安全" },
notaEditorial: "ESPDocs 根据数据手册给出的编辑评分。", verNotas: "以表格查看评分", area: "领域",
consumo: "功耗", consumoLede: "芯片休眠时的典型电流,对数刻度:每条网格线是上一条的十倍。条越短,电池续航越长。",
deep: "Deep sleep", light: "Light sleep", nd: "未公布",
bateria: `2000 mAh 电池仅在 deep sleep 下的理论续航。实际上自放电和板上其他元件会大幅缩短:`, anos: "年", meses: "个月",
tinyml: "TinyML 支持", tinymlLede: "根据乐鑫官方支持,每款芯片能运行哪些嵌入式机器学习。",
recurso: "功能",
recursos: { tflm: "TensorFlow Lite Micro", espDl: "ESP-DL(优化模型)", espNn: "ESP-NN 加速内核", vetorial: "向量 / AI 指令", espSr: "ESP-SR(唤醒词、语音命令)" },
estado: { sim: "是", parcial: "部分", nao: "否", previsto: "计划中" },
tinymlNota: "部分:可运行但有限制(C2 内存小、ESP32 仅旧版 ESP-DL、ESP-NN 通用内核)。计划中:芯片已发布,支持仍在开发。",
},
};
/* Deep sleep puro com 2000 mAh, só para dar escala humana à corrente. */
function duracaoBateria(uA, t) {
const horas = 2000 / (uA / 1000);
const anos = horas / 8760;
return anos >= 1 ? `${Math.round(anos)} ${t.anos}` : `${Math.round(anos * 12)} ${t.meses}`;
}
function Radar({ chips, t }) {
const [foco, setFoco] = useState(null);
const C = 170, R = 112;
const n = EIXOS_RADAR.length;
const ponto = (i, v) => {
const a = -Math.PI / 2 + (i * 2 * Math.PI) / n;
return [C + Math.cos(a) * R * (v / 10), C + Math.sin(a) * R * (v / 10)];
};
return (
<svg viewBox="-70 0 480 340" className="mx-auto w-full max-w-xl" role="img" aria-label={`${t.visao}: ${chips.join(", ")}`}>
{[2, 4, 6, 8, 10].map((v) => (
<polygon key={v} points={EIXOS_RADAR.map((_, i) => ponto(i, v).join(",")).join(" ")} fill="none" stroke="var(--line)" strokeWidth={1} />
))}
{EIXOS_RADAR.map((eixo, i) => {
const [x, y] = ponto(i, 10);
const [lx, ly] = ponto(i, 12.4);
const anchor = Math.abs(lx - C) < 8 ? "middle" : lx > C ? "start" : "end";
return (
<g key={eixo}>
<line x1={C} y1={C} x2={x} y2={y} stroke="var(--line)" strokeWidth={1} />
<text x={lx} y={ly} textAnchor={anchor} dominantBaseline="middle" className="fill-[var(--muted)] text-[13px] font-medium">
{t.eixos[eixo]}
</text>
</g>
);
})}
{chips.map((chip, ci) => {
const notas = METRICAS[chip]?.notas;
if (!notas) return null;
const pts = EIXOS_RADAR.map((e, i) => ponto(i, notas[e]));
const apagado = foco !== null && foco !== chip;
return (
<g key={chip} onMouseEnter={() => setFoco(chip)} onMouseLeave={() => setFoco(null)} style={{ opacity: apagado ? 0.25 : 1, transition: "opacity .15s" }}>
<polygon points={pts.map((p) => p.join(",")).join(" ")} fill={CORES[ci]} fillOpacity={0.12} stroke={CORES[ci]} strokeWidth={2} strokeLinejoin="round" />
{pts.map(([x, y], i) => (
<circle key={i} cx={x} cy={y} r={4} fill={CORES[ci]} stroke="var(--surface)" strokeWidth={2}>
<title>{`${chip} · ${t.eixos[EIXOS_RADAR[i]]}: ${notas[EIXOS_RADAR[i]]}/10`}</title>
</circle>
))}
</g>
);
})}
</svg>
);
}
function Legenda({ chips }) {
return (
<ul className="flex flex-wrap gap-x-5 gap-y-2 text-sm font-medium text-ink">
{chips.map((chip, i) => (
<li key={chip} className="inline-flex items-center gap-2">
<span className="h-3 w-3 rounded-full" style={{ background: CORES[i] }} aria-hidden="true" />
{chip}
</li>
))}
</ul>
);
}
/* Escala log de 1 µA a 1000 µA. */
function BarrasConsumo({ titulo, campo, chips, t, mostrarBateria }) {
const pct = (v) => Math.max(2, (Math.log10(Math.max(v, 1)) / 3) * 100);
return (
<div>
<h4 className="text-sm font-semibold text-ink">{titulo}</h4>
<div className="relative mt-3">
<div className="pointer-events-none absolute inset-y-0 left-24 right-20 sm:left-28" aria-hidden="true">
{[0, 1, 2, 3].map((d) => (
<div key={d} className="absolute inset-y-0 border-l border-line" style={{ left: `${(d / 3) * 100}%` }} />
))}
</div>
<ul className="relative space-y-2.5">
{chips.map((chip, i) => {
const v = METRICAS[chip]?.[campo];
return (
<li key={chip} className="flex items-center gap-3 text-sm" title={v == null ? `${chip}: ${t.nd}` : `${chip}: ${v} µA`}>
<span className="w-21 shrink-0 font-medium text-ink sm:w-25">{chip}</span>
<span className="relative h-5 flex-1">
{v != null && <span className="absolute inset-y-0 left-0 rounded-r" style={{ width: `${pct(v)}%`, background: CORES[i] }} />}
</span>
<span className="w-17 shrink-0 text-right tabular-nums text-ink-soft">{v == null ? <span className="text-muted">{t.nd}</span> : `${v} µA`}</span>
</li>
);
})}
</ul>
<div className="relative mt-1.5 h-4 text-xs text-muted" aria-hidden="true">
<div className="absolute inset-y-0 left-24 right-20 sm:left-28">
{["1", "10", "100", "1000 µA"].map((r, d) => (
<span key={r} className="absolute -translate-x-1/2 whitespace-nowrap" style={{ left: `${(d / 3) * 100}%` }}>{r}</span>
))}
</div>
</div>
</div>
{mostrarBateria && (
<div className="mt-4 text-sm text-muted">
<p>{t.bateria}</p>
<ul className="mt-2 flex flex-wrap gap-x-5 gap-y-1">
{chips.filter((c) => METRICAS[c]?.deepSleep != null).map((c) => (
<li key={c}><span className="font-medium text-ink">{c}</span> ≈ {duracaoBateria(METRICAS[c].deepSleep, t)}</li>
))}
</ul>
</div>
)}
</div>
);
}
function Estado({ valor, t }) {
const map = {
sim: { Icon: Check, cls: "text-ok font-medium" },
parcial: { Icon: CircleDashed, cls: "text-gold font-medium" },
previsto: { Icon: Clock, cls: "text-muted" },
nao: { Icon: Minus, cls: "text-muted" },
};
const { Icon, cls } = map[valor] ?? map.nao;
return (
<span className={`inline-flex items-center gap-1.5 whitespace-nowrap ${cls}`}>
<Icon className="h-4 w-4" aria-hidden="true" />
{t.estado[valor] ?? t.estado.nao}
</span>
);
}
export default function ComparacaoGraficos({ chips, locale = "pt" }) {
const t = T[locale] ?? T.pt;
return (
<div className="space-y-8">
<section className="panel p-5 sm:p-7" aria-labelledby="radar-titulo">
<h2 id="radar-titulo" className="text-base font-semibold text-ink">{t.visao}</h2>
<p className="mt-1 max-w-2xl text-sm text-muted">{t.visaoLede}</p>
<div className="mt-5 grid items-center gap-6 lg:grid-cols-[1fr_auto]">
<Radar chips={chips} t={t} />
<div className="space-y-4 lg:w-64">
<Legenda chips={chips} />
<p className="text-xs text-muted">{t.notaEditorial}</p>
<details className="text-sm">
<summary className="min-h-11 cursor-pointer content-center font-medium text-ink-soft">{t.verNotas}</summary>
<table className="mt-2 w-full text-left text-xs">
<thead>
<tr className="border-b border-line text-muted">
<th scope="col" className="py-1.5 pr-2 font-medium">{t.area}</th>
{chips.map((c) => <th key={c} scope="col" className="py-1.5 pr-2 font-medium">{c.replace("ESP32-", "")}</th>)}
</tr>
</thead>
<tbody>
{EIXOS_RADAR.map((e) => (
<tr key={e} className="border-b border-line last:border-b-0">
<th scope="row" className="py-1.5 pr-2 font-medium text-muted">{t.eixos[e]}</th>
{chips.map((c) => <td key={c} className="py-1.5 pr-2 tabular-nums text-ink">{METRICAS[c]?.notas[e] ?? "–"}</td>)}
</tr>
))}
</tbody>
</table>
</details>
</div>
</div>
</section>
<div className="grid gap-8 lg:grid-cols-2">
<section className="panel p-5 sm:p-7" aria-labelledby="consumo-titulo">
<h2 id="consumo-titulo" className="text-base font-semibold text-ink">{t.consumo}</h2>
<p className="mt-1 text-sm text-muted">{t.consumoLede}</p>
<div className="mt-5 space-y-7">
<BarrasConsumo titulo={t.deep} campo="deepSleep" chips={chips} t={t} mostrarBateria />
<BarrasConsumo titulo={t.light} campo="lightSleep" chips={chips} t={t} />
</div>
</section>
<section className="panel p-5 sm:p-7" aria-labelledby="tinyml-titulo">
<h2 id="tinyml-titulo" className="text-base font-semibold text-ink">{t.tinyml}</h2>
<p className="mt-1 text-sm text-muted">{t.tinymlLede}</p>
<div className="mt-5 overflow-x-auto">
<table className="w-full min-w-[26rem] border-collapse text-left text-sm">
<thead>
<tr className="border-b border-line">
<th scope="col" className="py-2.5 pr-3 font-medium text-muted">{t.recurso}</th>
{chips.map((c, i) => (
<th key={c} scope="col" className="py-2.5 pr-3 font-semibold text-ink">
<span className="inline-flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full" style={{ background: CORES[i] }} aria-hidden="true" />
{c.replace("ESP32-", "")}
</span>
</th>
))}
</tr>
</thead>
<tbody>
{Object.keys(t.recursos).map((r) => (
<tr key={r} className="border-b border-line last:border-b-0">
<th scope="row" className="py-3 pr-3 align-top font-medium text-ink-soft">{t.recursos[r]}</th>
{chips.map((c) => <td key={c} className="py-3 pr-3 align-top"><Estado valor={METRICAS[c]?.tinyml[r]} t={t} /></td>)}
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-4 text-xs text-muted">{t.tinymlNota}</p>
</section>
</div>
</div>
);
}