// Componentes de visualização: Cartograma do Brasil, AreaChart, Sparkline, Funnel etc.
const { useState, useEffect, useRef, useMemo } = React;
// ── Cartograma de estados ─────────────────────────────────────
function BrazilCartogram({ data, metric, selected, onSelect }) {
const max = Math.max(0, ...data.map((d) => d[metric] || 0));
const hasData = max > 0;
const fmtVal = (s) => {
if (metric === "ruptureRate") return Math.round(s[metric] * 100) + "%";
if (metric === "patients") {
const v = s.patients;
return v >= 1000 ? (v / 1000).toFixed(v >= 10000 ? 0 : 1).replace(".", ",") + "k" : v;
}
const v = s.dispensed;
if (v >= 1_000_000) return (v / 1_000_000).toFixed(1).replace(".", ",") + "M";
return (v / 1000).toFixed(0) + "k";
};
const stepBgs = [0.0, 0.25, 0.5, 0.75, 1.0].map((intensity) => {
return `oklch(${94 - intensity * 38}% ${0.018 + intensity * 0.07} var(--accent-h, 165))`;
});
const cells = [];
for (let r = 0; r < 10; r++) {
for (let c = 0; c < 11; c++) {
const s = data.find((d) => d.col === c && d.row === r);
if (!s) {
cells.push(
);
continue;
}
const v = s[metric] || 0;
const intensity = hasData ? Math.pow(v / max, 0.55) : 0;
const bg = hasData
? `oklch(${94 - intensity * 38}% ${0.018 + intensity * 0.075} 165)`
: "oklch(91% 0.005 240)";
const fg = intensity > 0.45 ? "oklch(99% 0 0)" : "oklch(28% 0.02 240)";
const isWarn = s.ruptureRate > 0.18;
const showVal = metric !== "ruptureRate" ? intensity > 0.18 : true;
cells.push(
);
}
}
const metricLabel =
metric === "dispensed" ? "Cápsulas dispensadas" :
metric === "patients" ? "Pacientes ativos" :
"Taxa de ruptura";
return (
{cells}
{metricLabel}
menos
{stepBgs.map((bg, i) =>
)}
mais
ruptura ≥ 18%
);
}
const tipEl = (() => {
if (typeof document === "undefined") return null;
const el = document.createElement("div");
el.className = "tooltip";
document.body.appendChild(el);
return el;
})();
function showTip(e, s, metric) {
if (!tipEl) return;
const num = (n) => n.toLocaleString("pt-BR");
tipEl.innerHTML = `
${s.uf} · ${s.name}
Pacientes ativos${num(s.patients)}
Cápsulas / mês${num(s.dispensed)}
Caixas / mês${num(s.caixas)}
Ruptura${(s.ruptureRate * 100).toFixed(0)}% (${s.ruptureEstabs}/${s.estabs})
`;
tipEl.classList.add("is-on");
moveTip(e);
}
function moveTip(e) {
if (!tipEl) return;
tipEl.style.left = e.clientX + "px";
tipEl.style.top = e.clientY + "px";
}
function hideTip() { if (tipEl) tipEl.classList.remove("is-on"); }
// ── Sparkline ─────────────────────────────────────────────────
function Sparkline({ data, width = 110, height = 36, stroke = "var(--accent)", fill = true, dot = true }) {
if (!data || data.length === 0) return null;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const step = width / (data.length - 1);
const pts = data.map((v, i) => [i * step, height - (v - min) / range * (height - 4) - 2]);
const d = pts.map((p, i) => i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`).join(" ");
const dFill = d + ` L${width},${height} L0,${height} Z`;
return (
);
}
// ── Dual-axis chart: dispensação × ruptura ────────────────────
function DispensacaoRupturaChart({ months, height = 240 }) {
const ref = useRef(null);
const [w, setW] = useState(800);
const [hover, setHover] = useState(null);
useEffect(() => {
const obs = new ResizeObserver(([e]) => setW(e.contentRect.width));
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
const startIdx = months.findIndex((m) => m.caixas >= 200);
const data = startIdx >= 0 ? months.slice(Math.max(0, startIdx - 1)) : months;
const pad = { l: 48, r: 56, t: 18, b: 30 };
const innerW = Math.max(100, w - pad.l - pad.r);
const innerH = height - pad.t - pad.b;
const maxC = Math.max(...data.map((m) => m.caixas)) * 1.08;
const maxR = 0.35;
const xStep = data.length > 1 ? innerW / (data.length - 1) : innerW;
const yC = (v) => pad.t + innerH - v / maxC * innerH;
const yR = (v) => pad.t + innerH - v / maxR * innerH;
const ptsC = data.map((m, i) => [pad.l + i * xStep, yC(m.caixas)]);
const ptsR = data.map((m, i) => [pad.l + i * xStep, yR(m.ruptura)]);
const dC = ptsC.map((p, i) => i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`).join(" ");
const dCArea = dC + ` L${ptsC[ptsC.length - 1][0]},${pad.t + innerH} L${ptsC[0][0]},${pad.t + innerH} Z`;
const dR = ptsR.map((p, i) => i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`).join(" ");
const yTicks = 4;
const yArr = Array.from({ length: yTicks + 1 }, (_, i) => maxC / yTicks * i);
const yRArr = Array.from({ length: yTicks + 1 }, (_, i) => maxR / yTicks * i);
return (
{hover !== null &&
{fmtMonth(data[hover].c)}
Caixas
{data[hover].caixas.toLocaleString("pt-BR")}
Ruptura
{(data[hover].ruptura * 100).toFixed(1).replace(".", ",")}% · {data[hover].estabsRuptura} estab.
}
);
}
function PatientsChart({ months, height = 280 }) {
const ref = useRef(null);
const [w, setW] = useState(800);
useEffect(() => {
const obs = new ResizeObserver(([e]) => setW(e.contentRect.width));
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
const pad = { l: 44, r: 18, t: 14, b: 28 };
const innerW = Math.max(100, w - pad.l - pad.r);
const innerH = height - pad.t - pad.b;
const max = Math.max(...months.map((m) => m.ativos)) * 1.08;
const min = 0;
const xStep = innerW / (months.length - 1);
const yScale = (v) => pad.t + innerH - (v - min) / (max - min) * innerH;
const points = months.map((m, i) => [pad.l + i * xStep, yScale(m.ativos)]);
const dLine = points.map((p, i) => i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`).join(" ");
const dArea = dLine + ` L${pad.l + (months.length - 1) * xStep},${pad.t + innerH} L${pad.l},${pad.t + innerH} Z`;
const ticks = 4;
const yTicks = Array.from({ length: ticks + 1 }, (_, i) => max / ticks * i);
const ann = months.findIndex((m) => m.c === "202507");
const [hover, setHover] = useState(null);
return (
{hover !== null &&
{fmtMonth(months[hover].c)}
Ativos{months[hover].ativos.toLocaleString("pt-BR")}
Novos+{months[hover].novos.toLocaleString("pt-BR")}
Desistência−{months[hover].dropout}
}
);
}
function fmtMonth(c, short = false) {
const y = c.slice(0, 4), m = parseInt(c.slice(4), 10);
const names = ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"];
return short ? `${names[m - 1]}/${y.slice(2)}` : `${names[m - 1]} / ${y}`;
}
function Funnel({ rows }) {
const max = Math.max(...rows.map((r) => r.value));
return (
{rows.map((r, i) =>
{r.label}
{r.value.toLocaleString("pt-BR")}{r.suffix || ""}
)}
);
}
function Cohort({ data }) {
return (
{data.map((c, i) =>
{Math.round(c.pct)}
{c.m}
)}
);
}
function ReasonsList({ rows }) {
return (
{rows.map((r, i) =>
{Math.round(r.value * 100)}%
)}
);
}
function IndicatorBar({ indicator }) {
const v = Math.max(0, Math.min(2, indicator));
const pct = v / 2 * 100;
const mid = 50;
let tone = "ok";
if (indicator < 0.6) tone = "danger";
else if (indicator < 0.85) tone = "warn";
return (
{indicator === 0 ? "0,00× ruptura" : `${indicator.toFixed(2).replace(".", ",")}× média`}
);
}
Object.assign(window, {
BrazilCartogram, Sparkline, PatientsChart, DispensacaoRupturaChart,
Funnel, Cohort, ReasonsList, IndicatorBar, fmtMonth
});