Перейти до вмісту

Дизайн-система аналітичних інструментів

Спільна дизайн-система для внутрішніх аналітичних інструментів Abitly (дашборди, звіти, публічні data-viz-поверхні). Дистильована з live-дашборда «Вступ 2026» (референс-реалізація). Мета — щоб кожен наступний аналітичний інструмент виглядав як частина одного продукту й будувався за перевіреним патерном, без зайвих залежностей.

Джерело істини коду: .context/vstup-2026-live/assets/{style.css,app.js}.

  1. Темна, one-accent. Один акцент (тепла помаранчева --acc:#ff8f2e) на глибокому синьо-чорному тлі. Другий колір (--edbo синій) — лише для «порівняльної» серії. Semantic (ok/warn/bad) — точково. Мультисерійні графіки — окрема стримана палітра (нижче).
  2. 0 залежностей. Vanilla JS + CSS, графіки — інлайновий SVG (жодних Highcharts/D3/Chart.js). Чому: повний контроль над темною темою, self-contained (працює офлайн, один HTML), мікро-вага, нічого не «протікає» стилями. Ідіом графіка — <path>/<rect> у viewBox.
  3. snapshot-once, read-from-cache. Уся важка аналітика рахується рідко (cron) і матеріалізується в один незмінний snapshot.json; сторінка — статика, читає лише його з CDN → 0 запитів у БД на візит, тримає вірусний сплеск. Деталі патерну — ARCHITECTURE дашборда.
  4. Кожен блок self-contained для скріншота. Картка має бренд-підвал (лого + джерело + abitly.org), що «виживає» вирізаним у канал/медіа поза контекстом сторінки.
  5. Тільки реальні дані + чесність про прогалини. Нічого не вигадуємо. Якщо у даних немає щоденної історії — показуємо знімок, підписаний як знімок, а не малюємо фейкову криву (див. морф прогноз→реальність).
  6. Табличні цифри всюди. Клас .num (font-variant-numeric: tabular-nums lining-nums) на кожному числі — щоб лічильники не «стрибали» під час анімації.
  7. Повага до prefers-reduced-motion — усі анімації/переходи вимикаються.

Єдине джерело кольору/розмірів — CSS-змінні у :root. Копіюй блок як є:

:root{
/* поверхні */
--bg:#0a0f14; --bg-glow:#12202b;
--card:#111a21; --card2:#152029; --chip:#0e161c;
--line:#1e2a33; --line2:#17222a;
/* текст (4 рівні контрасту) */
--ink:#eef4f8; --ink2:#c2cfd8; --muted:#7e909d; --faint:#54636e;
/* акцент (один) */
--acc:#ff8f2e; --acc-ink:#ffab5c; --acc-soft:rgba(255,143,46,.13); --acc-line:rgba(255,143,46,.32);
/* порівняльна серія (напр. зовнішнє джерело) */
--edbo:#4a9eef; --edbo-ink:#8fc2f5;
/* semantic */
--ok:#42d07f; --warn:#f0c33c; --bad:#f2635a;
/* розмір/форма/шрифт */
--pad:clamp(18px,2.4vw,26px);
--radius:20px;
--maxw:1120px;
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
}

Мультисерійна палітра графіків (теплий→холодний рамп, стриманий, читається на тлі) + нейтральний «Інші»:

const STREAM_COLORS = ['#ff7a3d','#ff9e4f','#ffc15b','#ffdf7a','#d7cf5f','#9fce77',
'#5fc79a','#46b5b8','#49a0d4','#5b86e0','#8a7de6','#b56fd0'];
const STREAM_OTHER = '#3f4f5b'; // згорнутий хвіст «Інші»

Фон сторінки — тло + м’який радіальний глоу згори: background-image:radial-gradient(1200px 520px at 50% -180px, var(--bg-glow) 0%, rgba(18,32,43,0) 70%).

  • 12-колонкова сітка .grid (gap:clamp(13px,1.7vw,18px)); картки беруть .c4/.c5/.c6/.c7/.c8/.c12. На max-width:820px усе згортається в одну колонку.
  • Контейнер .wrap{max-width:var(--maxw);margin:0 auto}.
  • Картка — градієнтне тло + бордер + --radius, overflow:clip, display:flex;flex-direction:column (щоб бренд-підвал притискався донизу через margin-top:auto):
.card{ background:linear-gradient(180deg,var(--card) 0%,var(--card2) 140%);
border:1px solid var(--line); border-radius:var(--radius); padding:var(--pad);
min-width:0; overflow:clip; position:relative; display:flex; flex-direction:column; }
.card .head{ display:flex; justify-content:space-between; align-items:flex-start; gap:12px; margin-bottom:14px }
.card h3{ font-size:15px; font-weight:700; letter-spacing:-.01em; color:var(--ink) }
.card .sub{ font-size:12.5px; color:var(--muted); margin-top:3px }
КомпонентКлас(и)Призначення
Hero-метрика.hero .big + .kpis .kpiодна велика цифра (clamp(52px,12vw,108px)) + сітка 4 KPI
Тег-бейдж.tag.live / .tag.db / .tag.bench / .tag.planмаркер джерела/статусу блоку (pill, uppercase)
Бренд-підвал.brand (.logo/.src/.url)самодостатність скріншота: лого + джерело + abitly.org
Дво-смугний список.bars .brow .mbar .mfill.a/.bпорівняння двох серій на напрям/ЗВО (акцент vs --edbo)
Пігулки-фільтри.pill (.on)вибір предмета/зрізу
Перемикач.switch (.switch-track/.switch-knob/.switch-txt)режим блоку (напр. «прогноз ⇄ реальність»)
Крива-роадмап.road .step (.done/.active/.next)таймлайн дат/етапів
Бейдж свіжості.freshness (.stale)«станом на …» + пульсуюча крапка .dot

Пульс акценту (жива крапка) — @keyframes pulse на .dot; анімація лічильника — easing 1 - (1-t)³ у JS-хелпері countUp().

Спільні правила для будь-якого графіка:

  • Адаптивний SVG: <svg viewBox="0 0 1000 H" preserveAspectRatio="none"> + CSS svg{display:block;width:100%;height:auto;overflow:visible}. Уся геометрія рахується в просторі viewBox (ширина 1000), розтягується під контейнер.
  • Осі: класи .axis-lbl (fill:var(--faint)) і .grid-line (stroke:var(--line2)).
  • Hover-тултип .ctip — абсолютно позиційований, з edge-flip (панель стає з протилежного від курсора боку й повністю влазить у картку, без обрізання цифр) + вертикальна пунктирна лінія-приціл. #chart{position:relative} обов’язково.

Готові патерни (усі хендрольнуті, у renderXxx() у app.js):

  • Area-curve — кумулятивна крива (area-fill градієнт --acc → прозорий + лінія з glow), hover-крапка + tooltip за будь-який день.
  • Bar-histogram — розподіл (напр. балів) із підсвіченим бакетом + перцентиль-вердикт.
  • Streamgraph — «річка» напрямів у часі: центрована wiggle-база (baseline = −total/2), inside-out порядок шарів (найбільші в центрі, «Інші» — на край), Catmull-Rom→cubic-bezier згладжування країв, топ-N + згорнутий хвіст «Інші», hover показує склад за день.
  • Forecast→reality морф — розширення стримграфа: смуги продовжуються за пунктирну межу (seam) у знімок реальності (лінійний морф з останньої точки прогнозу в реальну кінцеву величину, підписана зона). Патерн для будь-якого «прогноз → факт», коли у факту немає щоденної історії.

Пайплайн даних (для нового інструмента)

Section titled “Пайплайн даних (для нового інструмента)”
  1. Write-path (рідко, cron): read-only-збір агрегатів з прод-БД (SSM-тунель, SET default_transaction_read_only=on; statement_timeout, команда-виключення, нуль ПД)
    • зовнішні джерела → build_snapshot.py зливає й рахує похідні → snapshot.json.
  2. Публікація: заливка snapshot.json на CDN/CF Pages + purge шляху (атомарно — заміна одного файлу; сайт ніколи не бачить напівстан).
  3. Read-path (кожен візит): fetch('data/snapshot.json') — один запит, повністю з edge. Опційно другий файл live.json для швидких лічильників (короткий TTL, клієнт поллить).
  4. Свіжість чесно: бейдж «станом на …» показує реальний generated_at, не імітує realtime.

Кешування (edge): snapshot.jsonmax-age=300, stale-while-revalidate=86400; іммутабельні ассети (app.js/style.css з хешем) — max-age=31536000, immutable.

Як застосувати до нового інструмента

Section titled “Як застосувати до нового інструмента”
  • Скопіюй блок токенів :root + .num + reduced-motion-guard.
  • Візьми .grid + .card (+ .head/.sub/.tag/.brand) як каркас блоків.
  • Для чисел — hero/KPI-патерн; для порівнянь — дво-смугний список; для розрізів — пігулки/перемикач.
  • Графік — обери один із inline-SVG-патернів; не тягни чарт-бібліотеку.
  • Дані — snapshot-once write-path (read-only, PII-free), сторінка читає лише snapshot.json з CDN.
  • Чесність: немає щоденної історії → знімок, підписаний як знімок; кожен блок має бренд-підвал.
  • Гейт: працює офлайн з одного HTML+JSON; перевір скріншот кожного блоку окремо.
  • Live-дашборд «Вступ 2026» — референс-реалізація цієї системи.
  • abitly-analytics — сусідня аналітична поверхня (abitly.org/analytics) тієї ж моделі «снапшот замість live-обчислень».
  • simulation — типове джерело продуктових даних для таких інструментів.