В рамках выпускной квалификационной работы мне предложили две интересные темы: интерактивную карту или тренажёр для SQL-запросов. Я хотел посложнее, чтоб получить побольше навыков и поднабраться опыта к окончанию бакалавриата, поэтому выбрал первое. Получилась небольшая ГИС, полностью написанная на JavaScript при помощи D3.js и Charts.js.

Сразу скажу, что сейчас появилась небольшая проблема с тем, что фреймворк работает черезCloudflare, который блокируетсяРКН.
Из-за чего js-кодне прогружается, потому чтонет доступа к фреймворку. Что поможет в таких случаях?Внутреннее принятие неизбежного.Сейчас все работает вроде хорошо, но если не работает, то читаем зачеркнутый текст.
Забыл представиться, меня зовут Артём, я закончил бакалавриат по направлению «Математика и компьютерные науки» в СГУ им. Питирима Сорокина.
Этот проект, как сказано выше - моя выпускная квалификационная работа, и по совместительству - первое серьёзное веб-приложение, в котором я в одиночку соединил открытые данные, картографию, JavaScript и страдания упорство.
Полный проект на гитхабе.
Чуть расскажу о результате и перейду к тому как реализовывалось.
Функционал
Приложение позволяет интерактивно выбирать районы, просматривать динамику социально-экономических показателей, фильтровать данные и получать справки об объектах. Интерфейс поддерживает тёмную и светлую темы, переключаемые слои и полностью функционирует в браузере без установки.
Интерактивная карта в действии
Ниже — как выглядит карта в светлой и темной теме. На скриншотах ниже отображены административные районы Республики Коми. Это основной уровень, с которого пользователь начинает взаимодействие.


А вот так, после включения слоя (об этом будет позже), показываются интерактивные города/деревни/сёла, что позволяет спуститься на уровень муниципальных поселений.
Можно анализировать показатели по республике в целом, по районам или по конкретным населённым пунктам.уровень муниципальных поселений


Карта динамически подгружает объекты в целях оптимизации, поэтому на скрине выше нет рек, но при приближении они появляются. Как и появляются объекты инфраструктуры, дороги и здания.
Также появляются подписи улиц и рек, хотя реализованы они не очень хорошо, мне просто не хватило времени на всё, что я хотел.


Реализация
Итак, для визуализации карты я выбрал D3.js.
Конечно, есть более привычные решения вроде Leaflet или Mapbox. Но они работают с тайлами, а мне хотелось чего-то "настоящего" - полного контроля над отрисовкой и максимального погружения.
Да и честно говоря, хотелось немного усложнить себе жизнь - всё-таки диплом.
Данные
Честно сказать, на момент написания диплома, JS я знал только на уровне одного пройденного курса на интернет-платформе, ведь в вузе мы его не касались, но мне помогли упорство и нейросети. Начал заниматься этим в феврале, чтоб точно успеть сделать к июню, и не зря так рано.
Первым делом я начал искать готовые .svg-карты, потому что вообще не понимал, с чего начинается работа с геоданными. Все, что я находил - платные карты, а из бесплатных за первый час нашёл только карту России с делением по регионам.
Позже наткнулся на ресурс Geofabrik, там уже я нашел .shp карты, был очень этому рад. Но за этой радостью стоял один нюанс - карта не республики, а Северо-Западного Федерального Округа.
Понял что придется избавляться от лишнего.
Хотя сперва подумал, что буду работать со всеми регионами и просто поменяю название темы, но рад что отказался от этой идеи, потому что времени бы не хватило.
Позже выяснилось, что с .shp форматом JS не любит работать. Тогда мне посоветовали скачать QGIS.
Вот так выглядел исходный набор данных в QGIS, перед тем как я вырезал всё лишнее:

Набор разбит на карты рек, местностей, болот и т.д.
Я понял, что нужно вырезать все лишнее и оставить только все, что касается республики Коми. Сначала я пытался вырезать всё вручную, но в процессе выделения карты самой республики понял, что это слишком долго.
Пришла идея оптимизировать: я не случайно выше оставил часть про бесплатную карту РФ, потому что при помощи поиска по атрибутам выделил оттуда РК. С её помощью я выделил контур Республики Коми через таблицу атрибутов. Затем в QGIS применил фильтр «Пересечение» к каждому из слоёв СЗФО — оставляя только те объекты, которые попадали внутрь границ Коми.
Так получилось автоматически отфильтровать и собрать отдельный набор геоданных только по нужному региону.

Разработка карты
Дальше я уже начал работать с картой и JS. При помощи нейросеток понял как работать с d3, они помогли накидать макет, чтоб отображать саму карту, разделенную на районы.
Карта
Изначально я хотел, чтобы при большом масштабе отображались все подробные слои, а при отдалении — только карта Республики Коми. Казалось бы, логично. Но на практике такая карта требовала невероятное количество ресурсов: всё тормозило, даже при средней детализации.
Чтобы снизить нагрузку, я пошёл другим путём: стал подгружать только тот район, к которому приближается пользователь. Да, в ГИС-системах это делается автоматически — подгружается только то, что в пределах экрана (например можно было делать это через quadtree
в d3.js), но на тот момент у меня не было понимания как это реализовать, поэтому я выбрал этот вариант.
Теперь я вырезал карты для каждого района снова, используя тот же метод, при помощи таблицы атрибутов выделяя районы и нужные мне для них карты. Я оставил следующие карты : "Границы", "Населенные пункты", "Терминалы", "Болота", "Инфраструктура", "Пляжи", "Парковки", "Церкви", "Здания", "Железные дороги", "Дороги", "Реки".
Получилось всего 240 карт (по 12 карт для 20 районов). Занимаемое место на диске - 1.12гб. Ближе к защите я уменьшил объем данных примерно на 10%, сократив геоданные до 5 знаков после запятой. А после удалил нулевые атрибуты. Итоговый набор карт занимает 359 мб. Я добавил функцию подсветки регионов, а дальше пошла оптимизация отображения и работа над дизайном.
Дизайн
После реализации отображения я сразу приступил к подбору цветов объектов. Я старался не ориентироваться на другие решения а собрать свою личную цветовую гамму. И в этот же момент подумал «как круто будет сделать две темы». Взял за основные цвета зеленый и бежевый для светлой темы, а для темной синий и оранжевый.
Первая задача — мне нужно было сделать логику переключения режима отображения выбранного типа карт. И разработать интерфейс.

Итого: у меня есть кнопка смены темы,
кнопка вк/вык_лючающая все карты,
кнопки переключающие отображение кнопок (в правой части экрана) и иконок (в верхней части экрана), которыми можно вк/вык_лючать определенный тип карт.

Все элементы интерфейса я рисовал вручную в Inkscape, в формате .svg. Хотелось сделать все иконки в минималистичном стиле, но фантазия закончилась после основных. Я же все-таки по образованию математик :(
Переключатели-кнопки (справа) реализованы в виде обычных HTML-кнопок, в них нет чего-то интересного. Нажимаешь - меняют цвет и текст, вк/вык_лючают карты.
А есть переключатели-иконки (сверху), вот они красивые (ну, как минимум старательные), и функциональные: переключают режимы отображения карты, а при нажатии меняют цвет или подсветку, в зависимости от текущей темы. Я пытался сделать их интуитивно понятными для пользователя и некоторые получили интересные детали.
Например, иконка зданий оформлена в виде панельного дома - таких в Сыктывкаре, где я живу, довольно много.
Но есть одна интересная деталь: в верхней части дома я добавил коми орнамент. На иконке он почти незаметен. Но и в реальности такие орнаменты легко упустить из виду, хотя мне они кажутся очень атмосферными и самобытными.
Вот они — мои 48 шедевров SVG-графики:

Для того, чтоб нарисовать эти иконки мне понадобилось чуть больше 24 часов рабочего времени. А теперь можете их брать и вы, хотя я даже не знаю где они могут пригодиться.
А знаете на что у меня ушло столько же времени? А возможно даже больше.
Работа с данными
Я решил подключить Wikidata API, чтоб при нажатии на объект у меня отображались возможные данные с википедии, чтоб было поинтереснее и ограничиться этим. Но научный руководитель мне говорит:
Это намного больше чем я хотел, но все же не то. Нужно добавить социально-экономические показатели у районов.
После этой фразы я приступаю к поиску данных. Я не понимал откуда их брать, подсказать не могли, но потом приглянулся Росстат.
Сразу же - первая подножка: когда я решил загрузить где-то третий по счёту показатель, сайт начал возвращать 404. Я предположил, что либо запросов слишком много, либо таблицы слишком большие. Поэтому начал брать только те таблицы, которые нравились мне самому, а с ними проблем уже не было. Я скачивал .csv-файлы и надеялся, что дальше будет легко. Но иногда всплывало что-то вроде:
«Необходимо, чтобы в заголовке, в шапке и в боковике таблицы был выбран не менее одного признака».
Хотя показатели как-то же должны автоматически там распределять.
В общем к программистам Росстата у меня большой вопрос.
А уж работа с этими .csv-файлами оттуда — просто мука. Мне нужен был унифицированный вид, но его не было. Я писал python‑скрипты, чтоб перевести данные для каждой из таблиц, потому что вид у них был не поддающийся логике. Где‑то значение в той же строке, где‑то в следующей, где‑то через одну и прочие проблемы. Спустя где‑то 25 часов я перевел 17 таблиц в JSON формат в единый вид и приступил к оформлению поля информационных показателей. Конец страданиям. На защите меня спросили: «а почему не использовали парсинг данных с Росстата с какой‑нибудь периодичностью, данные же могут обновляться" - идея конечно крутая, но мне кажется это адом.
Для визуализации самих показателей я подключил Charts.js. Потому что быстро, красиво, удобно, и хотелось, чтоб d3 был для логики карты, а charts для графиков.
Техническая часть
Немного про код: просто скажу что делают функции. Продублирую, что можно (и нужно) посмотреть полный проект на гитхабе.
Основные функции отрисовки и логики карты
render() – отвечает за полную перерисовку карты (отрисовывает фон, районные слои, слои объектов и подсвеченные элементы в зависимости от масштаба и темы).
drawMap(geojsonFile, fillColor, strokeColor, opacity) – загружает главный GeoJSON и запускает
render()
.updateProjection() – подгоняет проекцию D3 под размер канваса, чтобы карта занимала весь экран.
clearMap() – очищает текущие данные районов.
Загрузка и кеширование геоданных
loadDistrictMaps(district, colorScheme) – загружает слои объектов для выбранного района (здания, дороги, леса и т.д.), используя кэш и фильтрацию по зуму.
updateDistrictCenters() – загружает геометрию границ районов, чтобы потом определять ближайший к центру экрана район.
getClosestDistrict() – определяет, какой район сейчас ближе всего к центру карты.
Наведение и взаимодействие
isPointInPolygon(point, polygon) – проверяет, лежит ли точка внутри полигона (используется в
getClosestDistrict()
).addRiverLabel(feature) – отрисовывает название реки и дороги по направлению потока.
capitalizeFirstLetter(str) – делает первую букву строки заглавной (используется для выделенного объекта).
Управление слоями карты и интерфейсом
createMapControls(mapType) – создаёт кнопки управления слоями (иконки и текстовые кнопки).
setButtonState(type, index, state) – переключает видимость слоя и обновляет текст кнопки.
updateMapMode() – проверяет, активен ли какой-либо слой и загружает соответствующие слои района.
updateIconButtonStyles() – обновляет иконки кнопок в зависимости от темы и состояния.
updateButtonStyles() – меняет стили текстовых кнопок в зависимости от темы и активности.
applyBoxShadows() – обновляет цвета подсказок в зависимости от темы.
changeIconColor() – меняет иконки главных кнопок управления (меню, темы и пр.) в зависимости от темы и состояния.
Переключение темы
themeButton.onclick – переключает тёмную/светлую тему, перерисовывает карту, включает нужную цветовую схему.
Интеграция с Wikidata и OSM
fetchWikiData(wikidataID) – загружает данные объекта по Wikidata ID и отображает их в infoBox.
fetchWikiIDfromOSM(osmID) – по OSM ID делает запрос к Overpass API, чтобы найти связанный Wikidata ID и вызвать
fetchWikiData()
.fetchEntityLabel(entityID) – получает название сущности Wikidata по ID (например, тип объекта, столицу и пр.).
updateInfoBoxWiki(entity, wikidataID) – собирает и отображает подробную информацию об объекте в infoBox, включая изображения, флаг, герб и кнопку социальных показателей.
Социальные показатели (данные)
getAvailableFiles() – возвращает список доступных JSON-файлов с социальными показателями.
findFilesContainingRegionName(regionName) – ищет файлы, в которых есть данные по данному региону.
showFileListInSocialTab() – отображает список файлов с показателями для выбранного региона.
loadRegionsData() – загружает JSON-файл с показателями и извлекает все доступные метрики.
findRegionData(regionName) – находит объект данных региона по его названию, учитывая исключения.
showSocialIndicators(regionName) – отображает таблицу и графики с показателями по региону (использует Chart.js).
getChartColors() – возвращает палитру цветов для графиков в зависимости от текущей темы.
Прочее
toggleSocialButton() – показывает/скрывает социальную вкладку с данными.
closeInfoBox() – скрывает окно информации.
Заключение
Чем дольше я сидел над проектом, тем сильнее боялся не успеть, так как приближалось начало сессии. Но в итоге я всё успел: закрыл сессию, сдал диплом за месяц до предзащиты. Формально мой диплом назывался "стартап", но стартап, который нельзя открыть (во всех смыслах) - звучит, мягко говоря, сомнительно. Потенциал коммерциализации проекта передает привет Роскомнадзору.
Собираюсь дальше поступать в магистратуру и искать работу.
В любом случае, я доволен результатом. Надеюсь, статья окажется полезной тем, кто занимается разработкой интерактивных карт.
Я не стал подробно расписывать реализацию — скорее дал "удочку" в виде репозитория на GitHub, а дальше, думаю, вы разберётесь.
Тем, кто включилу себя внутреннее принятие неизбежного : Вот сылочка на саму карту: карта.
На телефон я физически не успел адаптировать, а сейчас уже не вижу смысла в этом, но можно включить «версию для пк», там будет уже поприятнее.
Буду рад обратной связи в комментариях.
Комментарии (8)
rubbelg
10.07.2025 11:11почему решил использовать cdn, а не npm пакеты. тогда бы и не было проблем с
что фреймворк работает через Cloudflare, который блокируется РКН.
<script src="https://d3js.org/d3.v7.min.js"></script> <!-- Подключаем D3.js (Основной фреймворк)--> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
Монолитный файл на 1.5к+ строк... лучше уж разбить его на какие-то мини файлики
Название json файлов кириллицей...
button.style.backgroundColor = isActive ? (darkMode ? "rgba(33, 37, 90, 0.8)" : "#f0d3c0") : (darkMode ? '' : '');
еще интересны эти условия (darkMode ? '' : '')
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++)
лучше выносить polygon.length в отдельную переменную, чтобы она не считалась каждый раз, ведь элементов может быть очень много
Nadart Автор
10.07.2025 11:11Спасибо, что так подробно.
1) cdn просто проще и я не знал про npm. Оказалось, что проблема была не с charts, а с d3, поменял наsrc
="https://cdn.jsdelivr.net/npm/d3@7"
и все заработало с чистым интернетом. Почитаю про npm и поменяю как будет время.
2) Монолитный файл был просто привычнее для меня. Если честно, это мой первый крупный проект. Учту на будущее, что лучше разбивать на несколько файлов.
3) Кириллица. Думал что не будет проблем с этим, но какое-то внутреннее ощущение сказало, что ты прав и стоит поменять. Поменял названия, теперь все хорошо.
4) Условия - просто рудименты. Понемногу от них избавляюсь, но не все замечаю.
5) Тут тоже исправил, соглашусь, что надо было вынести.
filippov70
10.07.2025 11:11А почему не OpenLayers, если у вас web-ГИС?
Nadart Автор
10.07.2025 11:11Если кратко,то
Они работают с тайлами, а мне хотелось чего-то "настоящего" - полного контроля над отрисовкой и максимального погружения. Да и честно говоря, хотелось немного усложнить себе жизнь - всё-таки диплом.
Ales911
10.07.2025 11:11Респект. Гуд Джоб для диплома.
Хотя без Гугла скажу что в Коми три места более менее цивилизованы: Усинск, Ухта и столица. Родной Троицко-Печорский то ещё медвежье место
aborigen81
Плюсую.
ИМХО: для диплома просто отлично.
Опять же, личное мнение о недостатке выбранного построения карты при изменении масштаба.
Например, ткнул в Усть-Цилемский район, и фокус случайно выбрал так, что сразу попал в лес :) .Смотрю на карту и не очень понятно нужно куда-то "крутить" карту - влево-вправо-вверх-вниз, чтобы дойти до прорисовываемых объектов или менять масштаб.
Взял более знакомый Койгородский. Ну тут хотя бы знаю где искать сам Койгородок, поэтому получилось быстрее.
Nadart Автор
Может я не правильно понял, что вы имели ввиду, но я предполагал, что управление будет не кликами, а колесиком мышки. Если зажать ЛКМ и перемещать мышку, то перемещаемся по карте. Если попал в лес, то уменьшаешь зум колесиком мышки и зажимая ЛКМ перемещаешься куда интересует.
Я добавил в правый нижний угол уровень зума и при наведении показано с какого уровня отображаются объекты. И подписи объектов тоже добавил для лучшей ориентации.
aborigen81
Спасибо.
Попробую.