тепер На Яндекс.Картах можна створювати теплові карти

У Яндекс.Карт давно просили зробити засіб для візуалізації даних за допомогою теплових карт — і ми нарешті його зробили. Якщо ви — один з тих, кому це було дуже потрібно, можете переходити відразу до читання документації на гітхабі. Якщо ж ви ще не знаєте, чи потрібно це вам, — можете витратити трохи часу, щоб почитати статтю про те, що таке теплові карти, як вони генеруються на клієнта, і як ми поєднали генерацію теплової карти з географічною картою.


Відображення географічних точок з Вікіпедії

Що таке теплові карти, і навіщо вони потрібні

Отже, про все по порядку. Для початку давайте визначимося, що таке теплові карти і з чим їх їдять? Як підказує мені капітан очевидність вікіпедія, теплові карти (вони ж теплокарты, вони ж heatmap) — це графічне представлення даних, де додаткові змінні відображаються за допомогою кольору. Такий вид відображення буває дуже зручним. Наприклад, їм часто користуються веб-аналітики, щоб побачити найбільш активні частини сторінок сайту.

Ось такі карти кліків дозволяє будувати Яндекс:


Іноді буває корисним нанесення якихось кількісних показників на географічну карту, як у випадку відображення зон покриття мобільного зв'язку/інтернету у МТС:


Саме такі кейси і покликаний вирішувати модуль теплових карт, який ми створили. Перед тим, як я перейду до опису самого процесу створення модуля, я хотів би сказати ще пару слів про те, що з себе представляє наша модульна система, і як ви можете нею скористатися.

Модульна система

У версії 2.1 ми відкрили доступ користувачам до нашої модульній системі, яка написана на основі YModules, розробленою нашим колегою dfilatov. Ця модульна система має багато різних приємних особливостей, таких як асинхронний resolve залежностей, перевизначення модулів, etc. Вона була вже досить докладно описана автором на Хабре, так що якщо цікаво, можете почитати.

Відкриття модульної системи принесло нам приємний бонус — можливість для зовнішніх розробників створювати власні модулі. Начебто і нічого архіважливого, але завдяки цьому наші користувачі тепер можуть:
  • самостійно писати нову функціональність для API Яндекс.Карт і ділитися ним у зручному вигляді з іншими розробниками;
  • використовувати нашу модульну систему як основну, якщо програма цілком і повністю зав'язано на картах.
В якості першого прикладу ми і створили теплові карти.

Оскільки написати свої теплові карти не було самоціллю цієї затії (головним завданням було зробити готове рішення для API Яндекс.Карт), перед тим як почати писати код і думати над алгоритмом роботи, природно, я поліз на github шукати якісь готові рішення. Цілком очікувано було те, що різних реалізацій теплових карт було там трохи більше, ніж достатньо (майже дві з половиною сотні).

Трохи вивчивши вихідні коди різних проектів, я зупинив свою увагу на бібліотеці simpleheat авторства Mourner. У неї було два ключових переваги:
  • код всього проекту займав близько сотні рядків;
  • теплова карта добре тримала 10к точок без напрягу при отрисовках (при більшій кількості даних тестувати вже якось безглуздо, оскільки віддавати такі обсяги даних тільки для показу картинки на клієнт вкрай нерозумно).
Зрештою мені, звичайно, довелось переписати значну її частину, але все одно, мені здається, що це був кращий вибір. Всі інші рішення були куди більш громіздкими, але особливих булочок не надавали.

Алгоритм відтворення теплових карт

Поспілкувавшись з колегами, я зрозумів, що всі бачили теплові карти, всі знають, навіщо і що це. Але майже ніхто не знав, як вони відмальовує. Саме тому я постараюся описати цю частину більш детально.

API Яндекс.Карт надає можливість для відображення власної підкладки для карти, реалізується це за допомогою спеціального класу Layer. На вхід йому необхідно передати функцію, яка за номером тайла і рівня масштабування поверне url для завантаження тайла. Хто ще не знайомий з тайлами і тайловой графікою можете трохи почитати про них у вікіпедії і у нас документації.

Написання функції генератора url'ов для отримання тайлів — це фактично і є вся завдання створення теплової карти для нашого API.

Коли ми визначилися з тим, що від нас потрібно, ми почали думати над тим, як це зробити. Є два принципово різних методу для визначення теплової карти:
  • за допомогою двомірного скалярного (плоского) поля (фактично це функція двох змінних);
  • за допомогою набору простих або зважених точок (кожній точці у відповідність ставиться якесь позитивне число — її вага).
Перший метод є більш універсальним і включає в себе другий, але в теж час він дуже незручний для використання на практиці (як часто вам надають дані у вигляді функцій кількох змінних?), та й виглядає дивно і незрозуміло для непідготовлених користувачів. Тому без зайвих докорів сумління ми прийняли рішення, що будемо використовувати саме другий метод.

Для зручності роботи користувачів ми вирішили, що будемо підтримувати всі самі популярні формати вхідних даних, які використовуються в API (Number[][], IGeoObject, IGeoObject[], ICollection, ICollection[], GeoQueryResult, JSON), з-за цього нам довелося накласти не дуже приємне обмеження на програмний інтерфейс теплокарт. Теплокарте можна задавати лише набір даних не можна видаляти або додавати точки з цього набору. Таким чином, для роботи з даними ми надаємо всього лише два методу: getData() і setData().

Після того, як ми отримали дані, ми наводимо їх до єдиного формату і переводимо в глобальні піксельні координати. З такими даними вже відносно просто працювати, оскільки для кожного тайла можна легко сказати, які точки в нього потрапляють, а які ні.

Після того, як дані були предподготовлены можна почати їх перетворювати. Як намалювати питання, начебто, не варто (Canvas — наше все, тим більше у нього є чудова функціональність getDataURL, особливо необхідний в нашому випадку, оскільки саме url тайла ми повинні надати API).

Для відтворення кожної окремої точки будемо використовувати кисть (малюнок зліва), яка представляє з себе чорно-білий градієнт і малюється на canvas'е досить просто:

var brush = document.createElement('canvas'),
context = brush.getContext('2d'),
radius = 20,
gradient = context.createRadialGradient(radius, radius, 0, radius, radius, radius);

gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');

context.fillStyle = gradient;
context.fillRect(0, 0, 2 * radius, 2 * radius);

Вага точки буде визначати, з якою прозорістю кисть буде «малювати» точку на тайлі. Після того, як ми отрисуем всі точки тайла, у нас вийде такий собі негатив нашого тайла теплової карти.

var canvas = document.createElement('canvas'),
context = canvas.getContext('2d'),
maxOfWeights = 1,
radius = 20;

context.clearRect(0, 0, 256, 256);

for (var i = 0, length = points.length; i < length; i++) {
context.globalAlpha = Math.min(points[i].weight / maxOfWeights, 1);
context.drawImage(
brush, points[i].coords[0] - radius, points[i].coords[1] - radius
);
}

Після чого тайл буде розфарбований встановленням кольору кожного пікселя з градієнта (options.gradient) у відповідності зі значенням його прозорості. Прозорість кожного пікселя тайла буде дорівнює загальній прозорості теплової карти (options.opacity).

// Створюємо градієнт.
var canvas = document.createElement('canvas'),
context = canvas.getContext('2d'),
gradient = context.createLinearGradient(0, 0, 0, 256),
gradientOption = {
0.1: 'rgba(128, 255, 0, 0.7)',
0.2: 'rgba(255, 255, 0, 0.8)',
0.7: 'rgba(234, 72, 58, 0.9)',
1.0: 'rgba(162, 36, 25, 1)'
};
canvas.width = 1;
canvas.height = 256;

for (var i in gradientOption) {
if (gradientOption.hasOwnProperty(i)) {
gradient.addColorStop(i, gradientOption[i]);
}
}

context.fillStyle = gradient;
context.fillRect(0, 0, 1, 256);

// Розфарбовуємо пікселі тайла.
var gradientData = context.getImageData(0, 0, 1, 256).data;
var opacity = 0.5

for (var i = 3, length = pixels.length, j; i < length; i += 4) {
if (pixels[i]) {
j = 4 * pixels[i];
pixels[i - 3] = gradientData[j];
pixels[i - 2] = gradientData[j + 1];
pixels[i - 1] = gradientData[j + 2];

pixels[i] = opacity * (gradientData[j + 3] || 255);
}
}

Начебто і все, але немає. Завжди знайдуться якісь виняткові ситуації, які доведеться обробити додатково. І в нашому випадку можливість завдання необмеженого зверху ваги точки може призвести до того, що одна точка «погасить» всі інші. Так, наприклад, якщо додати на карту кілька сотень точок з вагою один і одну точку з вагою тисяча, то видно буде тільки остання (малюнок зліва).

Тому щоб уникнути таких ситуацій ми ввели додаткову опцію intensityOfMidpoint — це параметр, який визначає, яка прозорість (фактично визначає який колір) повинна бути у медіанної за вагою точки. Таким чином, нам вдасться згладити екстремуми для звичайних користувачів (малюнок праворуч), а інші зможуть підлаштувати опцію до потрібних значень.

Як цим користуватися

Детальна інструкція по завантаженню модуля є документації на github'e. Після чого для використання досить просто підключити його через модульну систему.

ymaps.modules.require(['Heatmap'], function (Heatmap) {
var data = [[37.782551, -122.445368], [37.782745, -122.444586]],
heatmap = new Heatmap(data);
heatmap.setMap(myMap);
});

Також ми підготували невелике демо, яке дозволить наочно побачити роботу більшості опцій.


Всі свої питання/побажання/обурення або подяки можете писати issues на github'е, у нас в клубі або ж безпосередньо мне на пошту alt-j@yandex-team.ru.

Замість висновку

Як ви, напевно, зрозуміли писати свої модулі для API Яндекс.Карт весело і просто. Пробуйте, експериментуйте, діліться з нами вашими результатами. Ще раз наведу список важливих посилань:


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.