Хороплетная карта строится на принципе окрашивания географических объектов в зависимости от значения некоторой переменной. Геометрия территории задаётся через GeoJSON или TopoJSON, а данные статистики привязываются к этим объектам через ключевые идентификаторы. В D3.js этот процесс реализуется через связку географических проекций, генераторов пути и механизмов объединения данных.
Географическая основа хороплета представляется набором Feature-объектов:
{
"type": "Feature",
"properties": {
"id": "RU-KG",
"name": "Kyrgyzstan region"
},
"geometry": {
"type": "Polygon",
"coordinates": [...]
}
}
Ключевым элементом выступает properties.id, который
используется для связывания с внешними наборами данных. Статистические
значения обычно хранятся отдельно:
const data = [
{ id: "RU-KG", value: 42 },
{ id: "RU-MO", value: 87 }
];
Процесс привязки данных осуществляется через создание индексной
структуры. Наиболее эффективный подход — использование
Map:
const dataMap = new Map(data.map(d => [d.id, d.value]));
После этого географические объекты обогащаются значениями:
geojson.features.forEach(feature => {
feature.properties.value = dataMap.get(feature.properties.id) || 0;
});
Такой подход позволяет подготовить данные к визуальному отображению без избыточных поисковых операций во время отрисовки.
Отображение координат на плоскость выполняется через проекции D3:
const projection = d3.geoMercator()
.scale(120)
.translate([width / 2, height / 2]);
Проекция преобразует долготу и широту в координаты SVG. Для генерации
контуров используется geoPath:
const path = d3.geoPath().projection(projection);
geoPath принимает геометрию и возвращает SVG-пути,
готовые для отрисовки.
Создание хороплета начинается с контейнера:
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
Далее выполняется привязка географических объектов:
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path);
На этом этапе отображается базовая карта без окрашивания.
Цветовая шкала задаёт соответствие между значениями и визуальными оттенками. Для хороплетов чаще используется непрерывная или квантильная шкала:
const colorScale = d3.scaleSequential()
.domain([0, d3.max(data, d => d.value)])
.interpolator(d3.interpolateBlues);
Применение шкалы к геометрии:
svg.selectAll("path")
.attr("fill", d => colorScale(d.properties.value));
Цвет становится функцией значения, что создаёт визуальное распределение данных по территории.
При необходимости дискретизации используется квантильное разбиение:
const colorScale = d3.scaleQuantile()
.domain(data.map(d => d.value))
.range(["#deebf7", "#9ecae1", "#3182bd"]);
Такой подход снижает влияние выбросов и упрощает восприятие распределений.
Для категориальных данных применяется:
const colorScale = d3.scaleOrdinal()
.domain(["low", "medium", "high"])
.range(["#f0f0f0", "#bdbdbd", "#636363"]);
В реальных наборах данных идентификаторы могут не совпадать напрямую. Тогда используется нормализация ключей:
const normalize = id => id.trim().toUpperCase();
const dataMap = new Map(data.map(d => [normalize(d.id), d.value]));
geojson.features.forEach(f => {
f.properties.value = dataMap.get(normalize(f.properties.id)) || 0;
});
Такая схема снижает риск расхождения форматов данных.
Для оптимизации размера данных применяется TopoJSON. Он хранит общие границы между полигонами, уменьшая дублирование координат.
Конвертация в GeoJSON:
const geojson = topojson.feature(topoData, topoData.objects.regions);
После преобразования структура используется так же, как стандартный GeoJSON.
При изменении набора данных выполняется пересчёт цветовой шкалы и обновление атрибутов:
function upd ate(newData) {
const dataMap = new Map(newData.map(d => [d.id, d.value]));
svg.selectAll("path")
.transition()
.duration(600)
.attr("fill", d => colorScale(dataMap.get(d.properties.id) || 0));
}
Плавные переходы уменьшают визуальный шум при изменении состояния карты.
Размер карты зависит от контейнера, поэтому проекция часто пересчитывается:
projection
.scale(width / 6)
.translate([width / 2, height / 2]);
При изменении размеров окна выполняется пересчёт пути:
svg.selectAll("path").attr("d", path);
Отсутствие значений требует отдельной логики отображения:
.attr("fill", d => {
const v = d.properties.value;
return v == null ? "#ccc" : colorScale(v);
});
Такой подход позволяет явно отделить неизвестные значения от нулевых.
Для сильно асимметричных распределений используется логарифмическое преобразование:
const valueScale = d3.scaleLog()
.domain([1, d3.max(data, d => d.value)])
.range([0, 1]);
После нормализации значения подаются в цветовую шкалу.
Наведение и подсветка реализуются через события:
svg.selectAll("path")
.on("mouseover", function() {
d3.select(this).attr("opacity", 0.7);
})
.on("mouseout", function() {
d3.select(this).attr("opacity", 1);
});
Для отображения значений используется всплывающий слой:
.on("mousemove", (event, d) => {
tooltip
.style("left", event.pageX + "px")
.style("top", event.pageY + "px")
.text(d.properties.value);
});
При работе с иерархическими территориями выполняется агрегация значений:
const grouped = d3.rollups(
data,
v => d3.sum(v, d => d.value),
d => d.region
);
Результат используется для обновления геометрии на более высоком уровне детализации.
При больших наборах данных используется кэширование путей:
const cache = new Map();
svg.selectAll("path")
.attr("d", d => {
if (!cache.has(d)) cache.se t(d, path(d));
return cache.get(d);
});
Это снижает вычислительную нагрузку при повторных рендерах.
Хороплет часто дополняется точечными слоями:
svg.selectAll("circle")
.data(points)
.enter()
.append("circle")
.attr("cx", d => projection([d.lon, d.lat])[0])
.attr("cy", d => projection([d.lon, d.lat])[1])
.attr("r", 3);
Такая комбинация позволяет сопоставлять агрегированные и локальные данные на одной карте.