// Plant Analytics — bespoke widgets (hardened copy of the kit's Widgets.jsx).
// Differences vs the kit: every `Math.max(...)` / total denominator is guarded
// against 0 / empty (real Firebase data is often all-zeros early on, which would
// otherwise produce NaN widths/colors).
(function () {
const NS = window.HyperChartsDesignSystem_e51a95;
const { TrendChip, Badge } = NS;
const I = window.HyperIcons;
function smooth(points, t = 0.5) {
if (points.length < 2) return '';
const d = [`M ${points[0][0]} ${points[0][1]}`];
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[i - 1] || points[i], p1 = points[i], p2 = points[i + 1], p3 = points[i + 2] || p2;
const c1x = p1[0] + ((p2[0] - p0[0]) / 6) * t * 2, c1y = p1[1] + ((p2[1] - p0[1]) / 6) * t * 2;
const c2x = p2[0] - ((p3[0] - p1[0]) / 6) * t * 2, c2y = p2[1] - ((p3[1] - p1[1]) / 6) * t * 2;
d.push(`C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2[0]} ${p2[1]}`);
}
return d.join(' ');
}
function ChartCard({ title, eyebrow, action, children, span, pad = 22, style }) {
return (
{(title || action) && (
{eyebrow &&
{eyebrow}
}
{title &&
{title}
}
{action}
)}
{children}
);
}
function StatTile({ icon, label, value, delta, sub, accent = 'var(--app-accent)', span }) {
return (
{label}
{icon && {icon}}
{value}
{delta != null && }
{sub &&
{sub}}
);
}
function Funnel({ steps, height = 296, stepW = 64, accent = '#8338EC', mode = 'percent' }) {
const max = (steps[0] && steps[0].value) || 1;
const W = stepW * steps.length;
const pillH = 40, funTop = 62;
const funH = height - funTop - 6;
const cy = funTop + funH / 2;
const maxH = funH - 8;
const pillW = Math.min(stepW - 8, 62);
const xOf = (i) => i * stepW + stepW / 2;
const topPts = steps.map((s, i) => [xOf(i), cy - (s.value / max) * maxH / 2]);
const botPts = steps.map((s, i) => [xOf(i), cy + (s.value / max) * maxH / 2]);
topPts.unshift([0, topPts[0][1]]); botPts.unshift([0, botPts[0][1]]);
topPts.push([W, topPts[topPts.length - 1][1]]); botPts.push([W, botPts[botPts.length - 1][1]]);
const topD = smooth(topPts);
const botRev = [...botPts].reverse();
const botD = smooth(botRev);
const area = `${topD} L ${botRev[0][0]} ${botRev[0][1]} ${botD.replace(/^M[^C]*/, '')} Z`;
const uid = 'fn' + Math.random().toString(36).slice(2, 7);
const haloScale = 1.32;
const haloTop = topPts.map(([x, y]) => [x, cy - (cy - y) * haloScale]);
const haloBotRev = botRev.map(([x, y]) => [x, cy + (y - cy) * haloScale]);
const haloTopD = smooth(haloTop);
const haloBotD = smooth(haloBotRev);
const haloArea = `${haloTopD} L ${haloBotRev[0][0]} ${haloBotRev[0][1]} ${haloBotD.replace(/^M[^C]*/, '')} Z`;
return (
{steps.map((s, i) => {
const lost = i === 0 ? 0 : steps[i - 1].value - s.value;
return (
{String(i + 1).padStart(2, '0')}
{s.name}
0 ? 'var(--negative)' : 'var(--text-muted)' }}>{lost > 0 ? '-' + lost.toLocaleString() : '—'}
{lost > 0 ? 'users left' : 'entry'}
);
})}
);
}
function MiniFunnel({ stages }) {
const max = (stages[0] && stages[0].value) || 1;
return (
{stages.map((s, i) => {
const pct = (s.value / max) * 100;
const conv = i === 0 ? 100 : (s.value / (stages[i - 1].value || 1)) * 100;
return (
{s.name}
{s.value.toLocaleString()}{conv.toFixed(1)}%
);
})}
);
}
function HeatGrid({ data, color = '#2ACF56', rows = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }) {
const hours = Array.from({ length: 12 }, (_, i) => i * 2);
const max = Math.max(...data.flat(), 1);
return (
{hours.map((h) =>
{String(h).padStart(2, '0')}
)}
{rows.map((r, ri) => (
{r}
{hours.map((h, ci) => {
const v = (data[ri] && data[ri][ci]) || 0;
const a = 0.08 + (v / max) * 0.92;
return ;
})}
))}
);
}
function CohortGrid({ cohorts, weeks = 8 }) {
return (
Cohort
{Array.from({ length: weeks }).map((_, w) =>
W{w}
)}
{cohorts.map((c, ri) => (
{c.label}
{c.size.toLocaleString()}
{c.values.map((v, ci) => {
const a = v == null ? 0 : 0.12 + (v / 100) * 0.88;
return
{v != null && 0.55 ? '#fff' : 'var(--text-secondary)' }}>{v}%}
;
})}
))}
);
}
function ComparisonCard({ title, periodA, periodB, rangeA, rangeB, metrics, span = 6 }) {
const Sel = ({ label, range }) => (
{label}
);
return (
VS
{periodA}
{periodB}
{metrics.map((m, i) => {
const delta = m.a ? ((m.b - m.a) / m.a) * 100 : 0;
return (
{m.label}
{m.fmt ? m.fmt(m.a) : m.a.toLocaleString()}
{m.fmt ? m.fmt(m.b) : m.b.toLocaleString()}
);
})}
);
}
function DropList({ items, accent = '#FF5C7A' }) {
const max = Math.max(...items.map((i) => i.value), 1);
const total = items.reduce((s, i) => s + i.value, 0) || 1;
const uid = 'dl' + Math.random().toString(36).slice(2, 7);
return (
{items.map((it, i) => {
const pct = (it.value / total) * 100;
return (
{i + 1}
{it.label}
{it.value.toLocaleString()}
{pct.toFixed(0)}%
);
})}
);
}
function DistBars({ items, color = 'var(--app-accent)', unit = '' }) {
const max = Math.max(...items.map((i) => i.value), 1);
return (
{items.map((it, i) => (
{it.label}
{it.value.toLocaleString()}{unit}
))}
);
}
function DonutStat({ data, centerValue, centerLabel, size = 188 }) {
const { PieChart } = NS;
const palette = ['var(--series-violet)', 'var(--series-orange)', 'var(--series-green)', 'var(--series-pink)', 'var(--series-blue)', 'var(--series-amber)'];
const glow = (data[0] && data[0].color) || 'var(--series-violet)';
const thickness = 15;
return (
{data.map((dd, i) => {
const c = dd.color || palette[i % 6];
return (
);
})}
);
}
function WorldMap({ values, top }) {
const ref = React.useRef(null);
const mapRef = React.useRef(null);
React.useEffect(() => {
if (!window.jsVectorMap || !ref.current) return;
ref.current.innerHTML = '';
try {
mapRef.current = new window.jsVectorMap({
selector: ref.current, map: 'world',
// Zoom (buttons + scroll wheel) + drag-to-pan when zoomed in.
zoomButtons: true, zoomOnScroll: true, zoomMax: 12, zoomMin: 1, zoomAnimate: true,
backgroundColor: 'transparent',
regionStyle: { initial: { fill: '#EDEDF3', stroke: '#ffffff', 'stroke-width': 0.4 }, hover: { fill: '#8676FF' } },
});
} catch (e) { return; }
const vals = Object.values(values);
const maxV = vals.length ? Math.max(...vals) : 1;
const c0 = [237, 233, 251], c1 = [131, 56, 236];
const lerp = (a, b, t) => Math.round(a + (b - a) * t);
const paint = () => {
Object.entries(values).forEach(([code, v]) => {
const t = Math.pow(v / (maxV || 1), 0.7);
const col = `rgb(${lerp(c0[0], c1[0], t)},${lerp(c0[1], c1[1], t)},${lerp(c0[2], c1[2], t)})`;
const p = ref.current && ref.current.querySelector(`path[data-code="${code}"]`);
if (p) { p.setAttribute('fill', col); p.style.fill = col; }
});
};
paint(); setTimeout(paint, 80);
}, [JSON.stringify(values)]);
return (
Top 5 countries
{top.map((c, i) => (
{c.flag} {c.name}
{c.pct}%
))}
);
}
// ── Nice axis ticks + compact number formatting ──
function niceTicks(min, max, count) {
if (max <= min) max = min + 1; // avoid a degenerate [v, v] axis
let range = max - min;
if (range <= 0) range = Math.abs(max) || 1;
const rawStep = range / Math.max(count, 1);
const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));
const norm = rawStep / mag;
const step = (norm >= 5 ? 5 : norm >= 2 ? 2 : 1) * mag;
const niceMin = Math.floor(min / step) * step;
const niceMax = Math.ceil(max / step) * step;
const ticks = [];
for (let v = niceMin; v <= niceMax + step * 0.5; v += step) ticks.push(Math.round(v * 1e6) / 1e6);
return ticks.length >= 2 ? ticks : [min, max];
}
function compactNum(n) {
const a = Math.abs(n);
if (a >= 1e9) return (n / 1e9).toFixed(a >= 1e10 ? 0 : 1) + 'B';
if (a >= 1e6) return (n / 1e6).toFixed(a >= 1e7 ? 0 : 1) + 'M';
if (a >= 1e3) return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1) + 'K';
return String(Math.round(n * 100) / 100);
}
// ── Interactive line chart: x/y axes, gridlines, hover crosshair + tooltip ──
function LineChart({ series, labels, height = 210, area = true, valueFmt }) {
const wrapRef = React.useRef(null);
const [w, setW] = React.useState(760);
const [hi, setHi] = React.useState(null);
React.useEffect(() => {
if (!wrapRef.current || typeof ResizeObserver === 'undefined') return;
const ro = new ResizeObserver((es) => { const cw = es[0] && es[0].contentRect.width; if (cw) setW(Math.max(220, Math.round(cw))); });
ro.observe(wrapRef.current);
return () => ro.disconnect();
}, []);
const data0 = (series && series[0] && series[0].data) || [];
const n = (labels && labels.length) || data0.length;
const W = w, H = height;
const padL = 48, padR = 16, padT = 14, padB = 30;
const plotW = W - padL - padR, plotH = H - padT - padB;
const fmtAxis = valueFmt || compactNum;
if (!n) return No data
;
const allVals = (series || []).flatMap((s) => (s.data || []).filter((v) => typeof v === 'number'));
const rawMax = allVals.length ? Math.max(...allVals) : 1;
const rawMin = Math.min(0, allVals.length ? Math.min(...allVals) : 0);
const ticks = niceTicks(rawMin, rawMax, 4);
const yMin = ticks[0], yMax = ticks[ticks.length - 1];
const span = (yMax - yMin) || 1;
const X = (i) => padL + (n <= 1 ? plotW / 2 : (i / (n - 1)) * plotW);
const Y = (v) => padT + plotH - ((v - yMin) / span) * plotH;
const baseY = Y(Math.max(yMin, 0));
const labStep = Math.max(1, Math.ceil(n / 7));
const showDots = n <= 31;
const uid = 'lc' + Math.random().toString(36).slice(2, 7);
const onMove = (e) => {
const r = wrapRef.current && wrapRef.current.getBoundingClientRect();
if (!r || !r.width) return;
const fx = e.clientX - r.left;
let best = 0, bd = Infinity;
for (let i = 0; i < n; i++) { const dd = Math.abs(X(i) - fx); if (dd < bd) { bd = dd; best = i; } }
setHi(best);
};
return (
setHi(null)}>
{hi != null && (
W * 0.62 ? 'calc(-100% - 10px)' : '10px'})`,
background: 'var(--surface-card)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-card)', padding: '8px 11px', minWidth: 96,
}}>
{labels ? labels[hi] : '#' + (hi + 1)}
{(series || []).map((s, si) => (
{s.name && {s.name}}
{typeof (s.data && s.data[hi]) === 'number' ? Math.round(s.data[hi]).toLocaleString() : '—'}
))}
)}
);
}
window.PA = { ChartCard, StatTile, Funnel, MiniFunnel, HeatGrid, CohortGrid, ComparisonCard, DistBars, DropList, DonutStat, WorldMap, LineChart, smooth };
})();