Анализ данных в hippotable: графики и shareable URLs

Сегодня хотел рассказать вам про новые фичи hippotable — моего open-source проекта для анализа данных в браузере. Это построение графиков и шаринг дашбордов по ссылке. Я уже анонсировал проект на хабре, в двух словах:

  1. Импорт довольно больших CSV-файлов — тестировал до 100 Мб.

  2. Классное табличное представление — весь датасет можно проскроллить даже на телефоне.

  3. Базовые no-code операции для анализа данных: сортировка, фильтры, агрегация.

  4. Все работает прямо в браузере, так что чувствительные данные не покидают ваш компьютер.

Последние полгода мы обкатывали hippotable для ad-hoc аналитики на работе: смотрели логи и выгрузки данных по конверсиям и активности пользователей. В целом инструмент всем понравился, но вылезли некоторые проблемы — не то чтобы неожиданные, но я изначально планировал не улетать в космос, а запускать быстро и по чуть-чуть, а потом смотреть чего сильнее не хватает. Не хватало вот чего:

  1. Некоторые тренды сложно понять глазами по таблице без графиков. Можно переложить обработанные данные в эксель и построить графики там, но решить вопрос на месте гораздо круче.

  2. Аналитический пайплайн нельзя сохранить — при перезагрузке страницы его приходится набивать заново, а поделиться результатом можно только через экспорт или скриншотом.

Сегодняшний выпуск — в формате DevLog. Расскажу в деталях, как мы решали эти проблемы:

  1. Как выбирали библиотеку для графиков, и какие маленькие нюансы мы упустили.

  2. Как прикопать состояние в URL и не облажаться.

Для примеров сегодня работаем с интересным датасетом World Developmnent Indicators — числовыми данными (население, экономика, образование и прочая веселуха) по всем странам мира с 1960 года. Будет интересно (если вы такой же зануда, как и я), поехали!

Выбираем библиотеку визуализации

Цель номер один — научиться строить графики по таблицам. Мы не готовы тратить на это много времени, так что нам нужна библиотека, которая из коробки построит базовые графики — scatter plot, line / bar / pie chart. Библиотека должна быстро отрисовать довольно большие датасеты (~10K точек). Нам не очень важно хорошее API, потому что мы все равно оборачиваем конфигуратор в UI. Большая вариативность настроек и возможность собрать любую визуализацию — тоже не обязательно, потому что вынести всю эту мощь в no-code UI все равно не получится.

Теперь к выбору самой библиотеки: открываем релевантные топики на гитхабе (charts, visualization) и осматриваем все библиотеки с >1000 звездочек (или насколько вы готовы упороться; помните, мы ищем что-то популярное). Нужно как-то сокращать выбор, так что сразу поделим библиотеки по технологии рендеринга:

  • SVG / HTML. Очень просто рендерить графики и добавлять интерактивность, это же просто DOM. Но есть ограничения по количеству данных — на 10K DOM-узлов страницу начинает корёжить. Раз мы смотрим высокоуровневую библиотеку, нам все равно, насколько просто она устроена внутри, а вот жестких ограничений на размер датасета нам не надо. Сюда попадают Apexcharts, Plotly.js, Х6 и большая часть d3-оберток (billboard, visx). Отказ.

  • canvas. Вместо того чтобы делать из данных DOM-элементы и заставлять браузер их рендерить, можно самим покрасить пиксели на canvas — будет быстрее. Но под капотом всё сложнее: например, для тултипов в SVG достаточно прикрепить листенер на элемент, а на канвасе надо самим ковырять координаты курсора и искать коллизии. Славно, что это уже сделали за нас! В эту категорию попадают ECharts, G2, Chart.js

  • WebGL. Технически мы все равно используем canvas, но часть преобразований данных и растеризация переехали с JS в главном треде на GPU — данных влезет ещё больше, но под капотом все ещё сложнее. WebGL тащит в геоинформационной визуализации: MapboxGL, L7. А вот простых библиотек чтобы нашлепать графиков, не нашлось. Ну, в другой раз.

Небольшое отступление: забавно, что самая известная библиотека визуализации, d3, библиотекой визуализации на самом деле не является. Это ядро, которое связывает данные с элементами (абстракция уровня между jQuery и React / Vue) и всякие полезные хелперы для данных типа d3-scale. А вот чего в d3 нет, так это графиков из коробки, поэтому сверху наросло N оберток с разными комбинациями базовых графиков без цирка и удобных API для UI-фреймворков (observable plot, recharts, visx, nivo, victory). Вообще в d3 можно подпихнуть и canvas (официально заявлено: Bring data to life with SVG, Canvas and HTML.), но получается один большой костыль.

В итоге выбираем из canvas-библиотек: ECharts, G2, Chart.js. API G2 больше завязан на использование из кода (другие 2 работают с объектами-конфигами, которые проще собирать из конфигуратора). На первый взгляд Chart.js и ECharts держат паритет, так что я случайным образом (на самом деле по менее монструозной документации) выбрал Charts.js — сделаем первый подход на нём, получим больше информации об ограничениях, и может с новыми вводными попробуем подпихнуть другие библиотеки.

Строим графики

Теперь покажу, какие графики можно строить.

Сначала — линейные графики. Посмотрим, как менялось население мира по годам: сгруппируем по году, просуммируем население всех стран, построим графики: демо. Fun facts: последние 60 лет население мира растет примерно линейно; рост сельского населения резко замедлился в районе 2000 года. Графики определенно помогают.

6368214b9b762275e5d5619c635feece.png

Конечно, есть и другие виды графиков (ха-ха, любые, которые я могу нашлепать за полчаса перебором опций charts.js) — например, scatterplot для неупорядоченных данных. Посмотрим на импорт и экспорт стран в 2014 году. Fun facts:

  • 3 страны (Гонконг, хотя не уверен, что это страна, Люксембург и Сингапур) в принципе не занимаются ничем кроме перевалки импорта в экспорт

  • Кирибати вообще не занимается ничем, кроме того что все импортирует.

  • В целом мерещится корреляция, но конкретно мы это посчитаем в одной из следующих серий.

2179096da4dc476f0ade0dce0879bfdc.png

Не обошлось и без зашкварных в современной школе визуализации данных видов графиков — вот вам pie chart с населением по странам. Тут он используется по назначению, чтобы обратить внимание на долю нескольких категорий в сумме показателей. Я, конечно, знал что в Индии и Китае живет много людей, но не думал что прямо треть всех людей в мире, и что все остальные страны — такие карлики по сравнению с ними.

da9cb8976d5e0577310fbd94d0233b5d.png

Я скажу честно, графики получились довольно отстойными. С ними точно лучше, чем без них, но текущий вариант скорее годится чтобы пощупать архитектуру и получить больше информации о слабых местах. Выявили три больших проблемы:

  1. Графикам нужны заголовки, подписи осей и точек, правила для определения текста в тултипе.

  2. Для большей части графиков данные нужно еще немного предобработать — например, для pie chart нужно предварительно просуммировать данные по категориям, иначе на одно значение категории нагенерится несклько сегметнов, а это тупо.

  3. Хорошо бы совмещать на одном графике несколько показателей (сельское и городское население; 1 линия на 1 значение категориального столбца, чтобы построить семейство графиков с населением каждой страны в одно действие).

Главное ограничение тут — моя UX-фантазия и время.

Sharable URLs

Вторая большая проблема, которую хотелось решить — возможность сохранять анализ (сортировку, фильтры, агрегацию): и чтобы не потерять состояние локально при перезапуске браузера, и чтобы дать коллегам тоже поиграться с преднастроенным пайплайном. Архитектурно мы сразу заложили разделение данных и операция и сериализуемые описания пайпланов, так что сделать это будет несложно. Сверху можно будет накрутить другие фичи, из горячего — замена датасета с сохранением пайплайна: например, можем подготовить флоу, который строит pie chart с разбиением ошибок из логов по типу девайса и подпихивать туда разные логи.

Индустриальный вариант с сохранением пайплайна в БД на бекенде сразу отметаем: основная дизайн-цель проекта — простая развертка на своей инфраструктуре, а наличие бекенда тут точно не помогает. К тому же мне не очень хотелось бы самому разворачивать бекенд для hosted-версии и закрывать его авторизацией или рисковать получить 1К RPS на создание пайплайнов от каких-нибудь шалунов.

Варианты с локальным сохранением (localStorage / IndexedDB) тоже отбиваем, потому что они решают только половину задачи (переслать localStorage не выйдет). Остается сохранение состояния в URL.

Конфиг аналитического пайплайна — развесистая структура вида

[
  {
    "mode": "aggregate",
    "key": [
      "Year"
    ],
    "columns": [
      {
        "name": "pop_urban",
        "sourceCol": "population_urban",
        "fn": "sum"
      }
    ]
  },
  {
    "mode": "filter",
    "filters": [
      {
        "name": "pop_urban",
        "value": 1000000,
        "condition": "gt"
      }
    ]
  }
]

Так что хранить его мы будем в виде JSON в searchParams. Но не будем забывать, что максимальная длина URL ограничена — в нашем случае GitHub pages отбивает URL из более чем 8221 символов ошибкой 414 URI too long. В 8 Кб влезет большой пайплайн, но хотелось бы хорошо распорядиться этим местом и потюнить формат сериализации. Большая часть JSON-синтаксиса не являются URL-safe, так что наивный JSON вызывает много percent-encoding-а и сильно раздувается:

JSON.stringify(state)
// '[{"mode":"filter","filters":[{"name":"Year","value":2014,"condition":"eq"}]},{"mode":"order","col":"population","dir":"desc"}]'
JSON.stringify(state).length
// 126
encodeURIComponent(JSON.stringify(state))
// "%5B%7B%22mode%22%3A%22filter%22%2C%22filters%22%3A%5B%7B%22name%22%3A%22Year%22%2C%22value%22%3A2014%2C%22condition%22%3A%22eq%22%7D%5D%7D%2C%7B%22mode%22%3A%22order%22%2C%22col%22%3A%22population%22%2C%22dir%22%3A%22desc%22%7D%5D"
encodeURIComponent(JSON.stringify(state)).length
// 230 - раздутие x2

На помощь спешит bse64! Да, он тоже раздувает строку. Да, он тоже не полностью URL-safe. Но результат гораздо лучше!

encodeURIComponent(btoa(JSON.stringify(state)))
// "W3sibW9kZSI6ImZpbHRlciIsImZpbHRlcnMiOlt7Im5hbWUiOiJZZWFyIiwidmFsdWUiOjIwMTQsImNvbmRpdGlvbiI6ImVxIn1dfSx7Im1vZGUiOiJvcmRlciIsImNvbCI6InBvcHVsYXRpb24iLCJkaXIiOiJkZXNjIn1d"
encodeURIComponent(btoa(JSON.stringify(state))).length
// 168 

На пальцах, с base64 в URL влезет почти в 1.5 раза больше пайплайна. Прекрасно. Да, мы глазами URL теперь выглядит как гора мусора, и его нельзя редактровать руками —, но будем честны, работать с %%2F%5%%% тоже не супер удобно, и не стоит считать это базовым сценарием.

Оборачиваем всю эту сериализацию в очень красивый сигнал (как хук, только для solidjs), чтобы синхронизировать состояние приложения с URL, готово! Все примеры из секции про графики работали через sharable URL, но вот вам еще один — вся история покаазателей по России. С таким URL можно обновлять страницу, перезапускать браузер, отправлять его друзьям — бесплатно и без смс!

Сегодня рассказал, почему выбрал Charts.js для визуализации, какие графики получается строить, и поделился увлекательной историей про URL-сериализацию, JSON и base64. Надеюсь, вам было интересно! Лучшая помощь проекту — проголосовать за эту статью, поставить звездочку на гитхабе и, главное, пользоваться hippotable на работе или дома и делиться фидбеком.

© Habrahabr.ru