/* global React */ // ──────────────────────────────────────────────────────────── // DIGEST — экран «Сводка за период» (12h по умолчанию) // Открывается через deep-link ?screen=digest или из боттом-таба. // ──────────────────────────────────────────────────────────── window.DigestScreen = ({ T, dark, onBack, onOpenEvent, hours = 12 }) => { const [period, setPeriod] = React.useState(hours); // 12 | 24 const [events, setEvents] = React.useState(null); const [error, setError] = React.useState(null); // Pull the user's preferred magnitude filter from the reactive store so the // Сводка screen matches what's shown in Telegram's digest message. const data = window.DATA.useDataState(); const minMag = data.userMagFilter ?? 4.0; React.useEffect(() => { let cancelled = false; setEvents(null); setError(null); window.DATA.fetchEventsSince({ hours: period, minMag }) .then((rows) => { if (!cancelled) setEvents(rows); }) .catch((e) => { if (!cancelled) setError(String(e)); }); return () => { cancelled = true; }; }, [period, minMag]); const now = new Date(); const since = new Date(now.getTime() - period * 3600 * 1000); const fmtMsk = (d) => new Intl.DateTimeFormat("ru-RU", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Europe/Moscow", }).format(d); const fmtDate = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "Europe/Moscow", }).format(now); const periodMsk = `${fmtMsk(since)} — ${fmtMsk(now)} МСК`; if (error) { return (
{error}
); } if (events === null) { return (
ЗАГРУЗКА…
); } const total = events.length; const ru = events.filter(e => e.isRu).length; const world = total; // anything in BD is M≥4; "world" = total here const m5plus = events.filter(e => e.mag >= 5.0).length; const m6plus = events.filter(e => e.mag >= 6.0).length; const m7plus = events.filter(e => e.mag >= 7.0).length; const maxMag = total > 0 ? Math.max(...events.map(e => e.mag)) : 0; const maxEvent = events.find(e => e.mag === maxMag); // Top 5 by magnitude const top5 = [...events].sort((a, b) => b.mag - a.mag).slice(0, 5); // Hourly bins for the inline chart const bins = makeHourlyBins(events, since, now); const maxBin = Math.max(1, ...bins.map(b => b.count)); return (
{/* Период переключатель */}
{[12, 24].map(h => ( ))} {periodMsk}
{/* Сводка-карточки */}
{/* No-events placeholder */} {total === 0 && (
За {period} часов событий M≥{minMag.toFixed(1)} нет
{period === 12 ? "Попробуйте «24 ч» — обычно за сутки 3–10 событий." : "Сейсмическая активность ниже среднего."}
{period === 12 && ( )}
)} {/* Распределение M */} {total > 0 && (
ПО МАГНИТУДЕ
)} {/* График по часам */}
Активность · число событий за час
Россия/СНГ Мир
{/* Топ событий */} {top5.length > 0 && (
ТОП ПО МАГНИТУДЕ
{top5.map((e, i, arr) => ( onOpenEvent(e)} last={i === arr.length - 1} /> ))}
)} {/* Footer-инфо */}
ИСТОЧНИКИ: USGS · EMSC · мониторинг 24/7
); }; // ────────── helpers ────────── function makeHourlyBins(events, since, now) { const totalMs = now.getTime() - since.getTime(); const totalHours = Math.max(1, Math.round(totalMs / 3600000)); const bins = []; for (let i = 0; i < totalHours; i++) { const t0 = new Date(since.getTime() + i * 3600000); bins.push({ t: t0, count: 0, ru: 0 }); } for (const e of events) { const t = new Date(e.timeUtc); const idx = Math.min(totalHours - 1, Math.max(0, Math.floor((t.getTime() - since.getTime()) / 3600000))); if (bins[idx]) { bins[idx].count++; if (e.isRu) bins[idx].ru++; } } return bins; } // Responsive (viewBox-scaled) bar chart with proper axes. // Layout (in viewBox units): // left strip L_PAD = 28px → Y-axis tick labels (0, mid, max) // bottom strip B_PAD = 18px → X-axis labels (start, mid, end times in МСК) // plot area the rest of (W × H) const HourlyBars = ({ T, bins, maxBin }) => { const W = 360, H = 120; const L_PAD = 28, R_PAD = 6, T_PAD = 6, B_PAD = 18; const plotW = W - L_PAD - R_PAD; const plotH = H - T_PAD - B_PAD; const barW = bins.length > 0 ? plotW / bins.length : plotW; // Y-axis ticks: 0, half, max — keep them as round integers when possible. const yMax = Math.max(1, maxBin); const yMid = Math.ceil(yMax / 2); const tickFor = (v) => T_PAD + plotH - (v / yMax) * plotH; // X-axis labels: first, middle, last hour in МСК. const fmtTickHour = (d) => { if (!d) return ""; return new Intl.DateTimeFormat("ru-RU", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Europe/Moscow", }).format(d); }; const xLabels = bins.length > 0 ? [bins[0].t, bins[Math.floor(bins.length / 2)].t, bins[bins.length - 1].t] : []; return ( {/* Y gridlines + tick labels */} {[yMax, yMid, 0].map((v, i) => ( {v} ))} {/* Bars */} {bins.map((b, i) => { const x = L_PAD + i * barW; const totalH = (b.count / yMax) * plotH; const ruH = (b.ru / yMax) * plotH; const worldH = totalH - ruH; const baseY = T_PAD + plotH; return ( ); })} {/* X-axis labels */} {xLabels[0] && ( {fmtTickHour(xLabels[0])} )} {xLabels[1] && ( {fmtTickHour(xLabels[1])} )} {xLabels[2] && ( {fmtTickHour(xLabels[2])} МСК )} ); }; const Pill = ({ T, k, v, accent, danger }) => { const color = danger ? T.danger : accent ? T.accent : T.text; return (
{k}
{v}
); }; const DigestRow = ({ ev, T, onClick, last }) => (
{ev.place}
{ev.flag} {ev.timeMsk} МСК · h={ev.depth.toFixed(1)} км {ev.confirmed && ✓ 2 ист.}
); function truncate(s, n) { if (!s) return ""; return s.length <= n ? s : s.slice(0, n - 1) + "…"; }