Как стать автором
Поиск
Написать публикацию
Обновить

Интерактивная визуализация спортивных коэффициентов: что удалось, а что нет

Уровень сложностиСредний
Время на прочтение3 мин
Количество просмотров327

Потянул live-данные с mygameodds co, собрал real-time графики на D3.js, столкнулся с диким хаосом в структуре данных, решил через нормализацию, но провалился с адаптивом.

Цель

Построить интерактивный дашборд, визуализирующий изменение спортивных коэффициентов в реальном времени. Аналог систем мониторинга, только вместо метрик — лайв-кэфы с букмекерского API.


Архитектура

  • Источник данных: mygameodds.co

  • Стек:

  • D3.js (визуализация)

  • WebSocket (стриминг)

  • TypeScript (вся логика)

  • Vite + React (обвязка, рендер)


Работа с API

Документации к mygameodds.co не было — всё собиралось через инспекцию сети и reverse engineering.

📡 Подключение к WebSocket

const socket = new WebSocket("wss://stream.mygameodds.co/live");

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  handleIncomingEvent(data);
};

Сообщения приходят пачками, в формате:

{
  "match_id": 1234,
  "event": "odds_update",
  "markets": [
    {
      "type": "match_winner",
      "odds": {
        "home": 1.72,
        "draw": 3.1,
        "away": 4.5
      }
    }
  ],
  "timestamp": "2025-08-03T14:22:01Z"
}

Проблема — никакой стабильности. В других матчах:

  • odds = массив с ключами "name" / "value"

  • Время — только updated_at, иногда в формате Unix

  • Названия исходов ("team1", "x", "team2")


Нормализация данных

Чтобы унифицировать структуру для визуализации, написал модуль normalizeOdds(data: RawEvent): NormalizedOdds[].

🔄 Пример нормализатора

function normalizeOdds(event: RawEvent): NormalizedOdds[] {
  const ts = new Date(event.timestamp || event.updated_at || Date.now()).toISOString();
  
  return event.markets.map(m => {
    const odds = m.odds || {};
    
    const entries = Array.isArray(odds)
      ? Object.fromEntries(odds.map((o: any) => [o.name.toLowerCase(), o.value]))
      : odds;

    return {
      matchId: event.match_id,
      type: m.type,
      timestamp: ts,
      home: entries.home || entries.team1 || null,
      draw: entries.draw || entries.x || null,
      away: entries.away || entries.team2 || null,
    };
  });
}

Выход:

{
  matchId: 1234,
  type: 'match_winner',
  timestamp: '2025-08-03T14:22:01Z',
  home: 1.72,
  draw: 3.1,
  away: 4.5
}

Хранилище и поток данных

Сделал Map<matchId, MatchState> — храним историю коэффициентов по матчам.

При каждом odds_update пушим новые точки в массив и триггерим requestAnimationFrame на ререндер.

🧠 Простая структура:

interface MatchState {
  history: {
    timestamp: string
    home: number | null
    draw: number | null
    away: number | null
  }[]
}

Визуализация на D3.js

Задачи:

  • Нарисовать три линии (home, draw, away)

  • Обновлять данные в real-time

  • Добавить зум и pan (D3 Zoom Behavior)

📈 Отрисовка графика

const svg = d3.select('#chart')
  .attr('width', width)
  .attr('height', height);

const x = d3.scaleTime().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);

const line = d3.line<OddsPoint>()
  .x(d => x(new Date(d.timestamp)))
  .y(d => y(d.home));

svg.append("path")
  .datum(match.history)
  .attr("class", "line-home")
  .attr("d", line);

Каждая линия рисуется отдельно (три path по одному на исход).

Обновление делаю через join().attr("d", line) внутри RAF.


Проблемы с адаптивом

На десктопе всё круто. Но...

  • На мобилке зум ломается: пальцы срабатывают некорректно, события touchmove конфликтуют с pan

  • SVG не влезает по ширине, горизонтальный скролл не помогает

  • FPS падает при 200+ точках на графике

Рассматриваю переход на:

  • Canvas — ради производительности

  • WebGL (Pixi.js) — если графиков будет много


Что планирую дальше

  • Перевести визуализацию на Canvas

  • Сделать отложенную отрисовку (debounce + batch updates)

  • Добавить фильтры по маркету

  • Попробовать SSR для снижения TTI (если рендерить статику)


Итоги

Удалось:

✅ Протянуть live WebSocket-данные

✅ Привести хаотичные odds к единому формату

✅ Собрать интерактивный график на D3.js

✅ Показать реальные движения коэффициентов по матчам

Провалилось:

❌ Мобильный UX (зум/переходы)

❌ Нет поддержки сложных маркетов (азиатские форы, тоталы)

❌ Перформанс падает на больших выборках


P.S.

Если у кого-то был опыт переноса подобных графиков с SVG на Canvas или WebGL — поделитесь ссылками / демками / подходами. Готов open-source-ить часть решений, если будет интерес.

Теги:
Хабы:
-1
Комментарии0

Публикации

Ближайшие события