// ════════════════════════════════════════════════════════════════ // TrendsSection — кластеры идей с momentum + scatter plot // ════════════════════════════════════════════════════════════════ const { useState: useStateTrends } = React; function TrendsSection({ onOpenCluster }) { const D = window.SDH_DATA; const [filter, setFilter] = useStateTrends('all'); const filtered = filter === 'all' ? D.TREND_CLUSTERS : D.TREND_CLUSTERS.filter(t => t.momentum === filter); const [active, setActive] = useStateTrends(null); const [scatterFocus, setScatterFocus] = useStateTrends(null); // Click on scatter dot → focus + scroll to card function handleScatterSelect(id) { setScatterFocus(id); setActive(id); setTimeout(() => { const el = document.querySelector(`[data-trend-card="${id}"]`); if (el) { const rect = el.getBoundingClientRect(); const targetY = window.scrollY + rect.top - 120; window.scrollTo({ top: targetY, behavior: 'smooth' }); } }, 50); setTimeout(() => setScatterFocus(null), 2200); } return (
{/* HERO */}
тренды · semantic clusters

Что растёт в корпусе на этой неделе

Семантическая кластеризация {D.corpus.length.toLocaleString('ru-RU')} стартапов даёт {D.TREND_CLUSTERS.length} активных направлений. Размер точки = объём кластера, цвет = направление импульса, положение по горизонтали = темп прироста к прошлой неделе.

{/* Scatter */}
{/* legend */}
горизонталь: импульс вертикаль: объём наведите на точку или кликните для перехода к кластеру
{/* FILTER */} импульс {['all', 'rising', 'steady', 'cooling'].map(m => ( ))} } right={обновлено: 23 мая 10:00} /> {/* GRID */}
{filtered.map(t => ( setActive(t.id === active ? null : t.id)} expanded={active === t.id} flash={scatterFocus === t.id} onOpen={() => onOpenCluster && onOpenCluster(t)} /> ))}
); } function Legend({ dot, label }) { return ( {label} ); } function TrendCard({ trend, onClick, expanded, flash, onOpen }) { const tintHue = trend.momentum === 'rising' ? 'emerald' : trend.momentum === 'cooling' ? 'rose' : 'info'; // figure out italic accent in title (if title contains 'AI') const renderTitle = (title) => { if (/AI/.test(title)) { const parts = title.split(/(AI)/); return parts.map((p, i) => p === 'AI' ? AI : {p}); } return title; }; return (

{renderTitle(trend.title)}

{trend.count} стартапов

{trend.recommendation || 'Сила сигнала растёт. Следующий шаг: прототип на 3 пользовательских сценариях.'}

{expanded && (
горизонт активности (30 дней)
)}
); } // Recommendations come from data — but if not, fall back TrendsSection.recommendations = { rising: 'Сила сигнала растёт. Следующий шаг: прототип на трёх пользовательских сценариях.', steady: 'Сигнал устойчивый. Готовы к найму первого специалиста и пилоту с одним клиентом.', cooling: 'Снижение интереса. Рекомендуем пересобрать гипотезу или приостановить направление.', }; window.TrendsSection = TrendsSection;