Initial commit: interactive Kazakhstan internet speed map with lazy-loaded providers

This commit is contained in:
Iliyas 2026-05-25 12:58:01 +05:00
commit 48e63b12da
9 changed files with 640365 additions and 0 deletions

313
PRD.md Normal file
View File

@ -0,0 +1,313 @@
# PRD: Интерактивная карта скорости интернета по провайдерам Казахстана
**Версия:** 1.3
**Дата:** 25 мая 2026
**Статус:** Draft — уточнён после Q&A
---
## 1. Обзор продукта
### 1.1 Цель
Создать интерактивную веб-карту Казахстана, которая визуализирует данные Speedtest Intelligence по средним скоростям download/upload фиксированной сети в разрезе интернет-провайдеров. Карта должна позволять аналитикам и руководству быстро сравнивать качество покрытия и скорости разных провайдеров на уровне страны, области и города.
### 1.2 Контекст данных
Исходные данные — агрегация из Speedtest Intelligence, **формат: CSV файл**. Загружается в браузере через PapaParse без серверной части.
Структура CSV:
| Поле | Тип | Описание |
|------|-----|----------|
| `provider` | string | Название провайдера фиксированной сети |
| `latitude` | float | Широта точки измерения |
| `longitude` | float | Долгота точки измерения |
| `avg_download_mbps` | float | Средняя скорость скачивания, Мбит/с |
| `avg_upload_mbps` | float | Средняя скорость загрузки, Мбит/с |
Одна строка = один уникальный кластер `(provider, lat, lon)` с агрегированными метриками.
---
## 2. Целевая аудитория
**Продукт предназначен для внутреннего использования командой.** Публичный доступ и авторизация не требуются в рамках V1.
| Пользователь | Сценарий использования |
|---|---|
| Аналитик команды (основной) | Исследование качества покрытия, подготовка отчётов |
| Руководство | Быстрый обзор конкурентной картины на встречах |
| Смежные команды | Ситуативный доступ к карте для контекста |
---
## 3. Пользовательские сценарии (User Stories)
**US-01.** Как аналитик, я хочу видеть зоны покрытия каждого провайдера на карте Казахстана, чтобы сразу понимать географию присутствия.
**US-02.** Как аналитик, я хочу приблизить карту до уровня города и увидеть отдельные точки измерений, чтобы изучить детализированную картину внутри региона.
**US-03.** Как пользователь, я хочу навести мышь на зону или точку и увидеть всплывающую плашку с именем провайдера и средними скоростями, чтобы не переключаться между экранами.
**US-04.** Как аналитик, я хочу включать и отключать отдельных провайдеров, чтобы сравнивать двух-трёх игроков без визуального шума.
**US-05.** Как пользователь, я хочу видеть границы областей и городов Казахстана, чтобы соотносить данные с административным делением страны.
**US-06.** Как аналитик, я хочу кликнуть на область/город и увидеть сводку по всем провайдерам внутри этой административной единицы, чтобы делать региональные сравнения.
---
## 4. Функциональные требования
### 4.1 Карта (Map Layer)
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-01 | Базовая карта Казахстана на основе тайлов OpenStreetMap (или Mapbox/MapTiler) | Must |
| F-02 | Административные границы: области (14 ед.) и города республиканского значения | Must |
| F-03 | GeoJSON границ загружается из публичного источника (GADM, Natural Earth) или хранится локально | Must |
| F-04 | Границы отображаются полупрозрачным контуром, не перекрывая данные провайдеров | Must |
| F-05 | При клике на область — боковая панель со сводкой провайдеров внутри | Should |
### 4.2 Зоны покрытия (Cluster Layer — низкий zoom)
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-10 | При zoom ≤ 8 данные отображаются как зоны покрытия провайдера | Must |
| F-11 | Перед построением hull точки провайдера **группируются в географические кластеры** по расстоянию: две точки попадают в один кластер, если расстояние между ними ≤ 20 км (настраиваемый порог) | Must |
| F-12 | Hull строится **отдельно для каждого кластера** одного провайдера, а не по всем точкам сразу. Это предотвращает ситуацию, когда крупный провайдер (Казахтелеком) закрывает всю карту одним полигоном | Must |
| F-13 | Алгоритм кластеризации точек для hull: **DBSCAN** или жадный radius-grouping с eps = 20 км | Must |
| F-14 | Провайдеры с числом точек < 3 в кластере **исключаются** из отображения hull (hull по 1–2 точкам геометрически некорректен). Провайдеры с единственной точкой в датасете исключены полностью | Must |
| F-15 | Каждый провайдер имеет свой уникальный цвет; цвет консистентен между всеми слоями | Must |
| F-16 | Полигоны hull отображаются с прозрачностью заливки ~30–40%, чтобы перекрытия соседних зон были видимы | Must |
| F-17 | Heatmap по плотности точек — опция V2 | Could |
### 4.3 Точки измерений (Point Layer — высокий zoom)
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-20 | При zoom ≥ 9 hull-полигоны скрываются, вместо них отображаются кружки по координатам каждой точки | Must |
| F-21 | Размер кружка кодирует avg_download_mbps (больше скорость — больше кружок), нормализация по всему датасету | Should |
| F-22 | Цвет кружка — цвет провайдера (совпадает с цветом hull-зоны) | Must |
| F-23 | Плавный переход (opacity fade) между слоями при пересечении порога zoom | Should |
| F-24 | Провайдеры с единственной точкой в датасете **не отображаются** ни на одном zoom-уровне | Must |
### 4.4 Tooltip (всплывающая плашка)
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-30 | Tooltip появляется при наведении (hover) на зону или точку | Must |
| F-31 | Содержимое tooltip: название провайдера, avg download Мбит/с, avg upload Мбит/с | Must |
| F-32 | Для зон — в tooltip агрегированные значения по всем точкам провайдера в видимой зоне | Must |
| F-33 | Tooltip не перекрывает элементы управления картой | Should |
| F-34 | При клике на точку — зафиксированный popup с расширенными данными (кол-во измерений, если есть) | Could |
### 4.5 Фильтрация и легенда
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-40 | Легенда со списком провайдеров и их цветами — фиксированная панель | Must |
| F-41 | Чекбокс рядом с каждым провайдером — включить/выключить слой | Must |
| F-42 | Кнопка «выбрать все» / «снять все» | Should |
| F-43 | Поиск по названию провайдера в легенде (если провайдеров > 10) | Could |
### 4.6 Экспорт данных региона
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-60 | Кнопка «Экспорт CSV» доступна после клика на область/регион на карте | Must |
| F-61 | Экспортируется CSV со всеми точками провайдеров, попадающими в выбранную административную единицу (область или город) | Must |
| F-62 | Состав экспортируемых полей: `provider`, `latitude`, `longitude`, `avg_download_mbps`, `avg_upload_mbps` | Must |
| F-63 | Имя файла включает название региона и дату экспорта: `kz_{region}_{YYYY-MM-DD}.csv` | Should |
| F-64 | Если активны фильтры по провайдерам (чекбоксы), экспортируются только видимые провайдеры | Should |
| F-65 | Экспорт реализуется на стороне браузера (Blob API) — без серверной части | Must |
### 4.7 Цветовая шкала скоростей
| ID | Требование | Приоритет |
|----|-----------|-----------|
| F-70 | Диапазон значений: **0 — 1000 Мбит/с** для download и upload | Must |
| F-71 | Шкала **логарифмическая** (log10): большинство точек сосредоточено в нижнем диапазоне (1–100 Мбит/с), линейная шкала сделает их неразличимыми. Логарифм выравнивает визуальный вес | Must |
| F-72 | Цветовой градиент шкалы download: красный (≈0) → жёлтый (≈50 Мбит/с) → зелёный (≥300 Мбит/с) | Should |
| F-73 | В режиме «по скорости» в легенде отображается градиентная шкала с отметками: 1 / 10 / 50 / 100 / 300 / 1000 Мбит/с | Must |
| F-74 | Размер кружков на zoom ≥ 9 нормализуется по той же log-шкале: min-radius = 4px, max-radius = 18px | Should |
---
## 5. Нефункциональные требования
| Категория | Требование |
|-----------|-----------|
| **Производительность** | Первоначальная загрузка карты ≤ 3 с при типичном наборе данных (< 50 000 точек) |
| **Масштабируемость** | При > 50 000 точек — включение кластеризации (MarkerCluster или supercluster) |
| **Браузерная поддержка** | Chrome, Firefox, Edge — последние 2 версии |
| **Адаптивность** | Desktop-first; базовая работоспособность на планшете |
| **Доступность данных** | Данные загружаются из CSV/JSON файла или REST API-эндпоинта |
| **Автономность** | Карта работает без внешних платных API-ключей (OSM тайлы бесплатны) |
---
## 6. Технический стек (рекомендуемый)
| Компонент | Вариант | Обоснование |
|-----------|---------|-------------|
| Картографическая библиотека | **Leaflet.js** | Легковесный, бесплатный, широкая экосистема плагинов |
| Базовые тайлы | OpenStreetMap / CartoDB Positron | Без платного API-ключа |
| GeoJSON границ КЗ | GADM v4.1 (`gadm41_KAZ_1.json`) | Публичный, точные границы областей |
| Загрузка CSV | **PapaParse** | Парсинг CSV прямо в браузере, без бэкенда |
| Кластеризация для hull | **Turf.js** `@turf/clusters-dbscan` | DBSCAN с настраиваемым radius (eps = 20 км) прямо в браузере |
| Построение hull | **Turf.js** `@turf/convex` | Convex hull по точкам каждого кластера |
| Цветовые шкалы | D3.js (`d3-scale`, `d3-scale-chromatic`) | Лёгкий импорт, нет нужды в полном D3 |
| Деплой | **Локально в браузере** — статический HTML+JS файл, открывается напрямую (`file://`) или через `npx serve` | Без сервера, без авторизации |
| Авторизация | **Не требуется** | Внутренний инструмент |
| Обновление данных | **Вручную** — пользователь заменяет CSV файл и перезагружает страницу | Нет scheduled refresh |
---
## 7. UX-описание интерфейса
```
┌─────────────────────────────────────────────────────────────────┐
│ [Заголовок: Скорость интернета по провайдерам КЗ] [Режим ▾] │
├──────────────┬──────────────────────────────────────────────────┤
│ ЛЕГЕНДА │ │
│ │ │
│ ☑ Казахтелеком │ [ К А Р Т А ] │
│ ☑ Beeline │ │
│ ☑ Tele2 │ Зоны покрытия (zoom ≤ 8) │
│ ☑ Kcell │ → точки (zoom ≥ 9) │
│ ☑ Altel │ │
│ ... │ [Tooltip при hover] │
│ │ ┌─────────────────┐ │
│ [Выбрать все] │ Казахтелеком │ │
│ [Снять все] │ │ ↓ 87.4 Мбит/с │ │
│ │ │ ↑ 42.1 Мбит/с │ │
│ ─────────── │ └─────────────────┘ │
│ Окраска: │ │
│ ○ Провайдер │ │
│ ○ Скорость │ │
└──────────────┴──────────────────────────────────────────────────┘
```
---
## 8. Логика переключения слоёв
```
zoom ≤ 8 → показать: Hull-полигоны (один hull на географический кластер провайдера)
скрыть: точки
tooltip при hover: название провайдера + агрегированные avg по кластеру
zoom 8–9 → плавный crossfade между слоями (opacity transition)
zoom ≥ 9 → скрыть: hull-полигоны
показать: кружки по (lat, lon)
размер кружка = f(avg_download_mbps), нормализация по датасету
tooltip: данные конкретной точки
```
### Логика построения hull-кластеров
```
Для каждого провайдера:
1. Взять все точки провайдера
2. Исключить провайдера, если точек < 3 (hull невозможен)
3. Запустить DBSCAN с eps = 20 км, minPoints = 3
4. Для каждого кластера (не шум):
→ построить convex hull по точкам кластера
→ назначить цвет провайдера
→ добавить полигон на карту
5. Точки, отнесённые к «шуму» (изолированные):
→ не отображаются на zoom ≤ 8
→ отображаются как кружки на zoom ≥ 9
```
> **Параметр eps = 20 км** — настраиваемый. При необходимости аналитик может изменить порог
> через конфиг-переменную в начале JS-файла без правки основного кода.
---
## 9. Административное деление Казахстана
Источник GeoJSON для границ — **GADM v4.1**:
- Уровень 1 (`KAZ_1`): 17 областей + города Алматы, Астана, Шымкент
- Уровень 2 (`KAZ_2`): районы (используется опционально для детализации)
URL для загрузки: `https://gadm.org/download_country.html` → выбрать Kazakhstan, GeoJSON.
Альтернатива — **Natural Earth** (упрощённые границы, меньше детализации, но легче по весу).
---
## 10. Tooltip: спецификация содержимого
### При наведении на зону (zoom ≤ 8)
```
┌─────────────────────────────┐
│ 🌐 Казахтелеком │
│ Область: Алматинская │
│ ────────────────────────── │
│ ↓ Скачивание: 87.4 Мбит/с │
│ ↑ Загрузка: 42.1 Мбит/с │
│ Точек данных: 142 │
└─────────────────────────────┘
```
### При наведении на точку (zoom ≥ 9)
```
┌─────────────────────────────┐
│ 🌐 Казахтелеком │
│ 43.2561°N, 76.9285°E │
│ ────────────────────────── │
│ ↓ Скачивание: 91.2 Мбит/с │
│ ↑ Загрузка: 44.7 Мбит/с │
└─────────────────────────────┘
```
---
## 11. Открытые вопросы
Все вопросы закрыты. PRD готов к передаче в разработку.
| # | Вопрос | Статус | Решение |
|---|--------|--------|---------|
| Q1 | Формат входных данных | ✅ Закрыт | CSV, загрузка через PapaParse |
| Q2 | Нужна ли авторизация? | ✅ Закрыт | Нет — внутренний инструмент |
| Q3 | Обновление данных | ✅ Закрыт | Вручную — замена CSV файла + перезагрузка страницы |
| Q4 | Провайдеры с одной точкой | ✅ Закрыт | Исключены полностью из отображения |
| Q5 | Экспорт | ✅ Закрыт | CSV региона через Blob API в браузере (см. F-60–F-65) |
| Q6 | Тип зон покрытия | ✅ Закрыт | Convex hull с DBSCAN-кластеризацией (eps = 20 км) |
| Q7 | Деплой | ✅ Закрыт | Локально в браузере, статический HTML файл |
| Q8 | Диапазон скоростей | ✅ Закрыт | 0–1000 Мбит/с, логарифмическая шкала (см. F-70–F-74) |
---
## 12. Роадмап
### V1 (MVP)
- Базовая карта + OSM тайлы
- Загрузка CSV данных через PapaParse (drag & drop или file picker)
- DBSCAN-кластеризация точек провайдера (eps = 20 км) + convex hull на каждый кластер
- Точки при высоком zoom (≥ 9), размер по log-шкале download
- Hover tooltip: провайдер, download, upload
- Легенда с чекбоксами по провайдерам
- GeoJSON границы областей КЗ (GADM v4.1)
- Клик на область → выбор региона
- Экспорт CSV точек выбранного региона (с учётом активных фильтров)
### V2
- Режим окраски по скорости (градиент красный→зелёный) с переключателем
- Тепловая карта плотности (heatmap) как альтернатива hull
- Поиск провайдера в легенде
- Кластеризация маркеров при большом кол-ве точек (Leaflet.markercluster)
- Сводная боковая панель при клике на область: таблица провайдеров с их avg скоростями
### V3
- Сравнительный вид двух провайдеров (split или overlay)
- Исторические тренды по периодам (если данные будут содержать временну́ю метку)
- Настройка eps порога прямо из UI

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# Интерактивная карта скорости интернета по провайдерам Казахстана
Визуализация данных Speedtest Intelligence по средним скоростям download/upload фиксированной сети в разрезе интернет-провайдеров Казахстана.
## Быстрый старт
```bash
# 1. Клонировать / перейти в директорию проекта
cd speedtestMap
# 2. Поместить CSV с данными
# Скопируйте ваш speedtest_aggregated.csv в data/speedtest.csv
# 3. Скачать GeoJSON границ областей Казахстана
# https://gadm.org/download_country.html → Kazakhstan → GeoJSON (Level 1)
# Сохранить как data/kaz_regions.geojson
# 4. Запустить локальный сервер (решает CORS для fetch)
npx serve .
# 5. Открыть в браузере: http://localhost:3000
```
## Структура проекта
```
speedtestMap/
├── index.html # Единственный файл приложения (весь JS и CSS внутри)
├── data/
│ ├── speedtest.csv # Данные провайдеров (заменяется вручную)
│ └── kaz_regions.geojson # Границы областей КЗ (GADM KAZ_1)
├── plans/
│ └── implementation-plan.md # План реализации
└── README.md # Этот файл
```
## Формат CSV
| Поле | Тип | Описание |
|------|-----|----------|
| `provider` | string | Название провайдера |
| `latitude` | float | Широта |
| `longitude` | float | Долгота |
| `avg_download_mbps` | float | Средняя скорость скачивания (Мбит/с) |
| `avg_upload_mbps` | float | Средняя скорость загрузки (Мбит/с) |
## Функциональность
### V1 (MVP)
- Базовая карта Казахстана (CartoDB Positron тайлы)
- Загрузка CSV данных через PapaParse
- DBSCAN-кластеризация точек провайдера (eps = 20 км) + convex hull
- Точки при высоком zoom (≥ 9), размер по log-шкале download
- Hover tooltip: провайдер, download, upload
- Легенда с чекбоксами по провайдерам
- GeoJSON границы областей КЗ (GADM v4.1)
- Клик на область → выбор региона
- Экспорт CSV точек выбранного региона
### Переключение слоёв
- **zoom ≤ 8**: полигоны зон покрытия (hull)
- **zoom 8–9**: плавный crossfade
- **zoom ≥ 9**: точки измерений (WebGL)
## Технический стек
| Компонент | Библиотека |
|-----------|-----------|
| Карта | Leaflet.js 1.9.x |
| Тайлы | CartoDB Positron (бесплатно) |
| Рендеринг точек | Leaflet.glify (WebGL) |
| Парсинг CSV | PapaParse 5.x |
| Кластеризация | DBSCAN (собственная реализация в Web Worker) |
| Пространственный индекс | rbush 3.x |
| Цветовые шкалы | chroma.js 2.x |
## Обновление данных
1. Замените `data/speedtest.csv` новым файлом
2. Перезагрузите страницу (F5)
## Развёртывание
### Вариант A — Локальный сервер (рекомендуется)
```bash
npx serve .
```
Откройте `http://localhost:3000`
### Вариант B — Прямое открытие
Откройте `index.html` в браузере через `file://`
> **Внимание:** Некоторые браузеры блокируют `fetch()` для локальных файлов (CORS).
## Требования к браузеру
- Chrome, Firefox, Edge — последние 2 версии
- WebGL (для отображения > 10 000 точек)

459
TRD.md Normal file
View File

@ -0,0 +1,459 @@
# TRD: Интерактивная карта скорости интернета по провайдерам Казахстана
**Версия:** 1.0
**Дата:** 25 мая 2026
**Связанный PRD:** PRD v1.3
**Статус:** Draft
---
## 1. Цели и ограничения
### 1.1 Технические цели
- Обработать и отобразить **300 000+ точек** без зависания браузера
- Работать как **единый статический HTML-файл** — без сервера, без сборщиков, без npm install
- Минимальный стек: только то, что необходимо для задачи
- Первый рендер карты — не позднее **2 секунд** после загрузки CSV
### 1.2 Ключевое ограничение
Leaflet с обычными `L.circleMarker` не справится с 300к точками — каждый маркер создаёт отдельный DOM-элемент. При таком масштабе единственный работающий подход — **рендеринг через Canvas**, где все точки рисуются как пиксели на одном элементе, а не как 300 000 DOM-узлов.
---
## 2. Технический стек
### 2.1 Итоговый стек
| Компонент | Библиотека | Версия | Подключение | Обоснование |
|-----------|-----------|--------|-------------|-------------|
| Карта | **Leaflet.js** | 1.9.x | CDN | Стандарт для статических карт, нет lock-in |
| Тайлы | **CartoDB Positron** | — | CDN (бесплатно) | Нейтральный фон, не перебивает данные провайдеров |
| Рендеринг точек | **Leaflet.glify** | 2.x | CDN | WebGL-рендеринг: 300к+ точек при 60 fps |
| GeoJSON границ | **GADM v4.1** | KAZ\_1 | Локальный файл | Точные границы областей КЗ |
| Парсинг CSV | **PapaParse** | 5.x | CDN | Потоковый парсинг, не блокирует UI |
| Геопространственные операции | **Turf.js** (только нужные модули) | 6.x | CDN (esm.sh) | DBSCAN + convex hull |
| Пространственный индекс | **rbush** | 3.x | CDN | R-tree для point-in-polygon при экспорте |
| Цветовые шкалы | **chroma.js** | 2.x | CDN | Лёгче D3-scale, достаточно для log-шкалы |
> **Итого внешних зависимостей: 7.** Все подключаются через CDN-теги в HTML.
> Фреймворк (React, Vue) — отсутствует. Vanilla JS.
### 2.2 Почему не Leaflet.markercluster
MarkerCluster агрегирует маркеры визуально, но по-прежнему создаёт DOM-элементы для каждого видимого кластера и каждой развёрнутой точки. При 300к точках и высоком zoom он всё равно зависает. Leaflet.glify рисует через WebGL — все 300к точек занимают один `<canvas>`, браузер рендерит их за один draw call.
### 2.3 Почему не Mapbox GL JS / Deck.gl
Mapbox требует API-ключ и аккаунт. Deck.gl — избыточен для одного слоя точек и требует сборщика. Цель — минимальный стек без внешних зависимостей от платных сервисов.
---
## 3. Архитектура
### 3.1 Структура файлов
```
project/
├── index.html # Единственный файл приложения (весь JS и CSS внутри)
├── data/
│ ├── speedtest.csv # Данные провайдеров (заменяется вручную при обновлении)
│ └── kaz_regions.geojson # Границы областей КЗ (GADM KAZ_1, однократная загрузка)
└── README.md # Инструкция: как обновить CSV и открыть карту
```
Всё приложение живёт в `index.html`. Пользователь открывает файл в браузере — карта работает.
### 3.2 Модули внутри index.html
```
index.html
├── <head> CDN-теги: Leaflet, glify, PapaParse, Turf, rbush, chroma
├── <style> CSS: layout, легенда, tooltip, панель экспорта
└── <script>
├── CONFIG Константы: EPS_KM, ZOOM_THRESHOLD, MIN_CLUSTER_POINTS, цвета провайдеров
├── DataLoader Загрузка и потоковый парсинг CSV → массив объектов
├── Preprocessor DBSCAN-кластеризация + построение hull-полигонов (Web Worker)
├── MapController Инициализация Leaflet, управление zoom, переключение слоёв
├── GlifyLayer Управление Leaflet.glify (точечный WebGL-слой)
├── HullLayer Управление Leaflet GeoJSON-слоями для hull-полигонов
├── AdminLayer Загрузка и отображение GeoJSON границ областей
├── TooltipCtrl Hover-логика для hull и точек
├── LegendCtrl Список провайдеров, чекбоксы, фильтрация
└── ExportCtrl Point-in-polygon (rbush) + генерация CSV через Blob API
```
---
## 4. Производительность: стратегия работы с 300к+ точками
Это центральная техническая проблема. Решается на трёх уровнях.
### 4.1 Уровень 1 — Парсинг CSV не блокирует UI
PapaParse запускается в режиме **streaming** с `worker: true`. CSV читается чанками по 10 000 строк в фоновом потоке. Главный поток остаётся отзывчивым, прогресс-бар обновляется через callback `step`.
```javascript
Papa.parse('data/speedtest.csv', {
worker: true, // парсинг в Web Worker
header: true,
dynamicTyping: true, // latitude/longitude сразу как float
chunk: (results) => {
appendToBuffer(results.data); // накапливаем чанки
updateProgressBar();
},
complete: () => onDataReady()
});
```
### 4.2 Уровень 2 — Предобработка в Web Worker
DBSCAN и построение hull — вычислительно тяжёлые операции. Они запускаются в отдельном **Web Worker** сразу после окончания парсинга, не блокируя карту.
```
Main thread Worker thread
───────────────────── ─────────────────────────────
CSV готов → postMessage ──────→ Получает массив точек
DBSCAN по каждому провайдеру
Строит hull-полигоны
Получает hulls ←────────────── postMessage(hulls GeoJSON)
Рендерит hull-слой
```
Пока Worker считает, карта уже отображена с тайлами и границами областей. Hull появляется поверх через 1–3 секунды в зависимости от объёма данных.
### 4.3 Уровень 3 — WebGL-рендеринг точек через Leaflet.glify
На zoom ≥ 9 точки рендерятся через `L.glify.points()`. Весь датасет (300к+ записей) передаётся в GPU как Float32Array один раз при инициализации. При последующем зуме/пане браузер перерисовывает canvas без обращения к JS.
```javascript
// Подготовка данных: один раз, при загрузке
const latLngs = points.map(p => [p.latitude, p.longitude]);
const colors = points.map(p => providerColor(p.provider)); // [r, g, b] 0..1
const sizes = points.map(p => logScale(p.avg_download_mbps, 4, 18));
L.glify.points({
map,
data: latLngs,
color: (i) => colors[i],
size: (i) => sizes[i],
opacity: 0.8,
click: onPointClick,
hover: onPointHover,
});
```
**Результат:** 300к точек = один WebGL draw call. Браузер рендерит за ~16 мс (60 fps).
### 4.4 Видимость слоёв по zoom
Никакого удаления/добавления DOM-элементов при зуме — только `show/hide` через opacity и `map.on('zoomend')`.
```
zoom < 8 → hull visible, glify opacity = 0 (точки скрыты)
zoom 8–9 → hull opacity анимируется 1→0, glify 0→0.8 (crossfade)
zoom ≥ 9 → hull hidden, glify opacity = 0.8 (точки видны)
```
Crossfade реализован через CSS-transition на canvas-элементе glify.
### 4.5 Viewport culling для hull
Hull-полигоны (GeoJSON) рендерит Leaflet SVG-слой. При 300к точках и десятках провайдеров полигонов будет не более нескольких сотен — SVG справится без дополнительных оптимизаций. Leaflet автоматически не рендерит полигоны вне viewport.
---
## 5. Алгоритм DBSCAN для hull-кластеров
### 5.1 Параметры
| Параметр | Значение | Где задаётся |
|----------|----------|--------------|
| `EPS_KM` | 20 км | `CONFIG` в начале JS |
| `MIN_POINTS` | 3 | `CONFIG` |
| Метрика расстояния | Haversine | Turf.js встроено |
### 5.2 Псевдокод (выполняется в Web Worker)
```
function buildHulls(points):
result = []
byProvider = groupBy(points, 'provider')
for each provider, providerPoints in byProvider:
if providerPoints.length < MIN_POINTS:
continue // полностью исключаем провайдера
clusters = dbscan(providerPoints, eps=EPS_KM, minPts=MIN_POINTS)
// clusters = { '0': [pt, pt, ...], '1': [...], '-1': [noise] }
for each clusterId, clusterPoints in clusters:
if clusterId == '-1': continue // шум — hull не строим
fc = turf.featureCollection(clusterPoints.map(toTurfPoint))
hull = turf.convex(fc)
if hull == null: continue // коллинеарные точки — пропускаем
hull.properties = {
provider: provider,
count: clusterPoints.length,
avg_download: mean(clusterPoints, 'avg_download_mbps'),
avg_upload: mean(clusterPoints, 'avg_upload_mbps'),
}
result.push(hull)
return turf.featureCollection(result)
```
### 5.3 Шумовые точки (DBSCAN label = -1)
Точки, не вошедшие ни в один кластер (изолированные), не отображаются на zoom ≤ 8. На zoom ≥ 9 они отображаются как обычные кружки в glify-слое — они уже включены в общий массив `latLngs`.
---
## 6. Пространственный индекс для экспорта
При клике на область нужно быстро найти все точки внутри её полигона из 300к записей. Наивный перебор (point-in-polygon для каждой точки) займёт ~500 мс. Решение — **R-tree индекс** через rbush.
### 6.1 Инициализация (однократно, после парсинга CSV)
```javascript
const tree = new RBush();
const items = points.map((p, i) => ({
minX: p.longitude, minY: p.latitude,
maxX: p.longitude, maxY: p.latitude,
index: i
}));
tree.load(items); // bulk insert O(n log n)
```
### 6.2 Поиск точек при клике на область
```javascript
function getPointsInRegion(regionPolygon, activeProviders) {
const bbox = turf.bbox(regionPolygon);
// 1. Быстрый bbox-фильтр через R-tree: O(log n + k)
const candidates = tree.search({
minX: bbox[0], minY: bbox[1],
maxX: bbox[2], maxY: bbox[3]
});
// 2. Точная проверка point-in-polygon только для кандидатов
return candidates
.map(c => points[c.index])
.filter(p => activeProviders.has(p.provider))
.filter(p => turf.booleanPointInPolygon(
turf.point([p.longitude, p.latitude]),
regionPolygon
));
}
```
Типичный bbox Алматинской области охватит ~5–15% всех точек как кандидатов. Точная проверка только по ним — вместо 300к итераций получаем ~15–45к, результат за < 50 мс.
---
## 7. Экспорт CSV
```javascript
function exportRegionCSV(regionName, filteredPoints) {
const header = 'provider,latitude,longitude,avg_download_mbps,avg_upload_mbps\n';
const rows = filteredPoints.map(p =>
`"${p.provider}",${p.latitude},${p.longitude},${p.avg_download_mbps},${p.avg_upload_mbps}`
).join('\n');
const blob = new Blob([header + rows], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const date = new Date().toISOString().slice(0, 10);
const name = `kz_${regionName.toLowerCase().replace(/\s+/g, '_')}_${date}.csv`;
const a = document.createElement('a');
a.href = url; a.download = name; a.click();
URL.revokeObjectURL(url);
}
```
---
## 8. Цветовая шкала
### 8.1 Цвета провайдеров
Цвета назначаются автоматически из фиксированной палитры при первом появлении провайдера в данных. Палитра из 20 различимых цветов достаточна для типичного числа провайдеров в КЗ.
```javascript
const PALETTE = [
'#e6194b','#3cb44b','#4363d8','#f58231','#911eb4',
'#42d4f4','#f032e6','#bfef45','#fabed4','#469990',
'#dcbeff','#9A6324','#fffac8','#800000','#aaffc3',
'#808000','#ffd8b1','#000075','#a9a9a9','#ffffff'
];
const providerColorMap = new Map();
let colorIndex = 0;
function providerColor(name) {
if (!providerColorMap.has(name)) {
providerColorMap.set(name, PALETTE[colorIndex++ % PALETTE.length]);
}
return providerColorMap.get(name);
}
```
### 8.2 Логарифмическая шкала для размера кружков
```javascript
// chroma.js используется только для градиента легенды
// Для размера — ручной log10
const LOG_MIN = Math.log10(1); // = 0
const LOG_MAX = Math.log10(1000); // = 3
function logScale(mbps, minPx, maxPx) {
const clamped = Math.max(1, Math.min(1000, mbps));
const t = (Math.log10(clamped) - LOG_MIN) / (LOG_MAX - LOG_MIN); // 0..1
return minPx + t * (maxPx - minPx);
}
```
Отметки на шкале легенды: `[1, 5, 10, 50, 100, 300, 1000]` Мбит/с — равномерно распределены в log-пространстве.
---
## 9. Инициализация и порядок загрузки
```
1. DOMContentLoaded
└── Инициализация Leaflet карты (центр: 48.0°N 68.0°E, zoom: 5)
└── Загрузка тайлов CartoDB Positron
2. Параллельно:
├── fetch('data/kaz_regions.geojson') → AdminLayer.init()
│ └── Рендер границ областей (L.geoJSON, clickable)
└── PapaParse.parse('data/speedtest.csv', {worker: true})
└── По завершении: onDataReady(points)
3. onDataReady(points):
├── Строим rbush-индекс (main thread, ~200 мс для 300к)
├── Инициализируем GlifyLayer (передаём все точки в GPU)
│ └── Слой скрыт (opacity=0) до zoom ≥ 9
├── Строим LegendCtrl (список провайдеров из данных)
└── postMessage → Web Worker: запуск DBSCAN + hull
4. Worker завершил работу → onHullsReady(hullsGeoJSON):
└── Рендер HullLayer (L.geoJSON полигоны)
└── Слой виден при zoom ≤ 8
5. Карта полностью готова к работе
```
**Ожидаемые тайминги (300к точек, современный ноутбук):**
| Этап | Время |
|------|-------|
| Карта + тайлы | ~0.5 с |
| Парсинг CSV (Worker) | ~1.5–2 с |
| rbush bulk insert | ~0.2 с |
| GlifyLayer init (GPU upload) | ~0.3 с |
| DBSCAN + hull (Worker) | ~2–4 с |
| **Карта интерактивна (без hull)** | **~2 с** |
| **Hull появляется** | **~4–6 с** |
---
## 10. Конфигурационный блок
В начале `<script>` выделен явный блок CONFIG — аналитик меняет параметры здесь, не трогая логику:
```javascript
const CONFIG = {
// Кластеризация для hull
EPS_KM: 20, // радиус DBSCAN в километрах
MIN_CLUSTER_POINTS: 3, // минимум точек для кластера
// Zoom-порог переключения слоёв
ZOOM_THRESHOLD: 9, // ≤8 = hull, ≥9 = точки
// Шкала размеров кружков (px)
POINT_MIN_PX: 4,
POINT_MAX_PX: 18,
// Шкала скоростей (Мбит/с)
SPEED_MIN: 1,
SPEED_MAX: 1000,
// Данные
CSV_PATH: 'data/speedtest.csv',
GEOJSON_PATH: 'data/kaz_regions.geojson',
// Начальный вид карты
MAP_CENTER: [48.0, 68.0],
MAP_ZOOM: 5,
};
```
---
## 11. Обработка граничных случаев
| Случай | Поведение |
|--------|-----------|
| Провайдер с < 3 точками в любом кластере | Полностью исключён из обоих слоёв |
| Коллинеарные точки (hull = null) | Кластер пропускается без ошибки |
| avg_download_mbps = 0 или null | `logScale` зажимает до min=1, кружок min-размера |
| CSV с отсутствующими полями | PapaParse `skipEmptyLines: true`, строки с null lat/lon фильтруются |
| Браузер без WebGL | Leaflet.glify выводит предупреждение; fallback — `L.circleMarker` с лимитом 10к точек |
| GeoJSON границ недоступен | AdminLayer не рендерится, остальное работает |
---
## 12. Нефункциональные требования (технические детали)
| Требование из PRD | Техническое решение |
|---|---|
| 300к+ точек без зависания | WebGL через Leaflet.glify, один draw call |
| Дальние масштабы не нагружают систему | Hull-слой: SVG-полигоны (их сотни, не тысячи). Glify opacity=0, но данные уже в GPU |
| Первый рендер ≤ 2 с | Карта и границы рендерятся до окончания парсинга CSV |
| Без сервера | Все операции в браузере: Worker, rbush, Blob API |
| Обновление данных вручную | Замена `data/speedtest.csv`, перезагрузка страницы |
| Chrome, Firefox, Edge | WebGL поддерживается везде. Протестировать Worker + glify в Firefox (known issues с some glify versions) |
---
## 13. Риски и митигация
| Риск | Вероятность | Митигация |
|------|-------------|-----------|
| Leaflet.glify не поддерживает hover на 300к точек с приемлемой задержкой | Средняя | Протестировать на реальных данных; fallback — hover только при zoom ≥ 11 (меньше точек в viewport) |
| DBSCAN на 300к точках в Worker занимает > 10 с | Низкая (работает на subset по провайдерам) | Если медленно — заменить Turf DBSCAN на собственный grid-based radius grouping (O(n) вместо O(n²)) |
| GADM GeoJSON слишком тяжёлый (> 5 МБ) | Средняя | Упростить через mapshaper.org до tolerance=0.01°, целевой размер < 500 КБ |
| WebGL недоступен на корпоративных машинах (старые драйверы) | Низкая | Добавить детект `L.glify.supported()` и fallback-предупреждение |
---
## 14. Инструкция по развёртыванию
```bash
# 1. Структура готова — скачать GADM GeoJSON
# https://gadm.org/download_country.html → Kazakhstan → GeoJSON (Level 1)
# Сохранить как data/kaz_regions.geojson
# 2. Поместить CSV с данными
cp speedtest_aggregated.csv data/speedtest.csv
# 3. Открыть карту
# Вариант A — прямо в браузере (если браузер разрешает file://)
open index.html
# Вариант B — через локальный сервер (рекомендуется, решает CORS для fetch)
npx serve .
# → открыть http://localhost:3000
# 4. Обновление данных
cp new_speedtest_aggregated.csv data/speedtest.csv
# Перезагрузить страницу в браузере — F5
```
> **Примечание:** Браузеры блокируют `fetch()` для локальных файлов по соображениям безопасности (`file://` CORS). Вариант B через `npx serve` решает это одной командой без установки зависимостей.

1
data/kaz_regions.geojson Normal file

File diff suppressed because one or more lines are too long

76
data/speedtest.csv Normal file
View File

@ -0,0 +1,76 @@
provider,latitude,longitude,avg_download_mbps,avg_upload_mbps
Казахтелеком,43.2561,76.9285,87.4,42.1
Казахтелеком,43.2350,76.9100,91.2,44.7
Казахтелеком,43.2700,76.9500,78.5,38.2
Казахтелеком,43.2200,76.8900,95.0,46.0
Казахтелеком,43.2900,76.9700,82.3,40.5
Казахтелеком,43.2100,76.8800,88.1,43.0
Казахтелеком,43.2450,76.9150,76.4,36.8
Казахтелеком,43.2600,76.9350,93.7,45.2
Казахтелеком,43.2750,76.9550,79.9,39.1
Казахтелеком,43.2250,76.8950,85.6,41.3
Казахтелеком,51.1800,71.4450,72.5,35.0
Казахтелеком,51.1600,71.4250,68.3,32.1
Казахтелеком,51.2000,71.4650,74.1,36.2
Казахтелеком,51.1400,71.4050,70.8,34.5
Казахтелеком,51.2200,71.4850,76.2,37.8
Казахтелеком,49.8000,73.1200,65.4,31.0
Казахтелеком,49.8200,73.1400,62.1,29.8
Казахтелеком,49.7800,73.1000,67.8,32.5
Казахтелеком,49.8400,73.1600,63.5,30.2
Казахтелеком,49.7600,73.0800,69.2,33.1
Beeline,43.2561,76.9285,120.5,58.3
Beeline,43.2350,76.9100,115.2,55.1
Beeline,43.2700,76.9500,130.8,62.4
Beeline,43.2200,76.8900,108.6,52.0
Beeline,43.2900,76.9700,125.3,60.1
Beeline,43.2100,76.8800,112.4,54.7
Beeline,43.2450,76.9150,118.9,57.2
Beeline,43.2600,76.9350,135.1,64.8
Beeline,43.2750,76.9550,122.7,59.3
Beeline,43.2250,76.8950,110.3,53.5
Beeline,51.1800,71.4450,95.2,45.0
Beeline,51.1600,71.4250,88.7,42.3
Beeline,51.2000,71.4650,92.4,44.1
Beeline,51.1400,71.4050,85.6,40.8
Beeline,51.2200,71.4850,98.1,46.5
Beeline,49.8000,73.1200,78.3,37.0
Beeline,49.8200,73.1400,82.5,39.2
Beeline,49.7800,73.1000,75.1,35.8
Beeline,49.8400,73.1600,80.4,38.5
Beeline,49.7600,73.0800,76.8,36.4
Tele2,43.2561,76.9285,45.2,21.0
Tele2,43.2350,76.9100,42.8,19.5
Tele2,43.2700,76.9500,48.1,22.3
Tele2,43.2200,76.8900,40.5,18.8
Tele2,43.2900,76.9700,46.7,21.8
Tele2,43.2100,76.8800,43.4,20.1
Tele2,43.2450,76.9150,44.9,20.8
Tele2,43.2600,76.9350,49.2,22.9
Tele2,43.2750,76.9550,41.6,19.2
Tele2,43.2250,76.8950,47.3,22.0
Tele2,51.1800,71.4450,38.5,17.2
Tele2,51.1600,71.4250,35.2,16.0
Tele2,51.2000,71.4650,40.1,18.5
Tele2,51.1400,71.4050,36.8,16.8
Tele2,51.2200,71.4850,39.4,18.1
Kcell,43.2561,76.9285,65.3,31.2
Kcell,43.2350,76.9100,62.1,29.8
Kcell,43.2700,76.9500,68.5,32.7
Kcell,43.2200,76.8900,60.4,28.5
Kcell,43.2900,76.9700,66.8,31.9
Kcell,43.2100,76.8800,63.2,30.1
Kcell,43.2450,76.9150,64.7,30.9
Kcell,43.2600,76.9350,70.1,33.5
Kcell,43.2750,76.9550,61.5,29.2
Kcell,43.2250,76.8950,67.2,32.0
Altel,43.2561,76.9285,55.8,26.4
Altel,43.2350,76.9100,52.3,24.8
Altel,43.2700,76.9500,58.1,27.5
Altel,43.2200,76.8900,50.6,23.9
Altel,43.2900,76.9700,56.4,26.8
Altel,43.2100,76.8800,53.7,25.2
Altel,43.2450,76.9150,54.9,26.0
Altel,43.2600,76.9350,59.2,28.1
Altel,43.2750,76.9550,51.8,24.5
Altel,43.2250,76.8950,57.1,27.0
1 provider latitude longitude avg_download_mbps avg_upload_mbps
2 Казахтелеком 43.2561 76.9285 87.4 42.1
3 Казахтелеком 43.2350 76.9100 91.2 44.7
4 Казахтелеком 43.2700 76.9500 78.5 38.2
5 Казахтелеком 43.2200 76.8900 95.0 46.0
6 Казахтелеком 43.2900 76.9700 82.3 40.5
7 Казахтелеком 43.2100 76.8800 88.1 43.0
8 Казахтелеком 43.2450 76.9150 76.4 36.8
9 Казахтелеком 43.2600 76.9350 93.7 45.2
10 Казахтелеком 43.2750 76.9550 79.9 39.1
11 Казахтелеком 43.2250 76.8950 85.6 41.3
12 Казахтелеком 51.1800 71.4450 72.5 35.0
13 Казахтелеком 51.1600 71.4250 68.3 32.1
14 Казахтелеком 51.2000 71.4650 74.1 36.2
15 Казахтелеком 51.1400 71.4050 70.8 34.5
16 Казахтелеком 51.2200 71.4850 76.2 37.8
17 Казахтелеком 49.8000 73.1200 65.4 31.0
18 Казахтелеком 49.8200 73.1400 62.1 29.8
19 Казахтелеком 49.7800 73.1000 67.8 32.5
20 Казахтелеком 49.8400 73.1600 63.5 30.2
21 Казахтелеком 49.7600 73.0800 69.2 33.1
22 Beeline 43.2561 76.9285 120.5 58.3
23 Beeline 43.2350 76.9100 115.2 55.1
24 Beeline 43.2700 76.9500 130.8 62.4
25 Beeline 43.2200 76.8900 108.6 52.0
26 Beeline 43.2900 76.9700 125.3 60.1
27 Beeline 43.2100 76.8800 112.4 54.7
28 Beeline 43.2450 76.9150 118.9 57.2
29 Beeline 43.2600 76.9350 135.1 64.8
30 Beeline 43.2750 76.9550 122.7 59.3
31 Beeline 43.2250 76.8950 110.3 53.5
32 Beeline 51.1800 71.4450 95.2 45.0
33 Beeline 51.1600 71.4250 88.7 42.3
34 Beeline 51.2000 71.4650 92.4 44.1
35 Beeline 51.1400 71.4050 85.6 40.8
36 Beeline 51.2200 71.4850 98.1 46.5
37 Beeline 49.8000 73.1200 78.3 37.0
38 Beeline 49.8200 73.1400 82.5 39.2
39 Beeline 49.7800 73.1000 75.1 35.8
40 Beeline 49.8400 73.1600 80.4 38.5
41 Beeline 49.7600 73.0800 76.8 36.4
42 Tele2 43.2561 76.9285 45.2 21.0
43 Tele2 43.2350 76.9100 42.8 19.5
44 Tele2 43.2700 76.9500 48.1 22.3
45 Tele2 43.2200 76.8900 40.5 18.8
46 Tele2 43.2900 76.9700 46.7 21.8
47 Tele2 43.2100 76.8800 43.4 20.1
48 Tele2 43.2450 76.9150 44.9 20.8
49 Tele2 43.2600 76.9350 49.2 22.9
50 Tele2 43.2750 76.9550 41.6 19.2
51 Tele2 43.2250 76.8950 47.3 22.0
52 Tele2 51.1800 71.4450 38.5 17.2
53 Tele2 51.1600 71.4250 35.2 16.0
54 Tele2 51.2000 71.4650 40.1 18.5
55 Tele2 51.1400 71.4050 36.8 16.8
56 Tele2 51.2200 71.4850 39.4 18.1
57 Kcell 43.2561 76.9285 65.3 31.2
58 Kcell 43.2350 76.9100 62.1 29.8
59 Kcell 43.2700 76.9500 68.5 32.7
60 Kcell 43.2200 76.8900 60.4 28.5
61 Kcell 43.2900 76.9700 66.8 31.9
62 Kcell 43.2100 76.8800 63.2 30.1
63 Kcell 43.2450 76.9150 64.7 30.9
64 Kcell 43.2600 76.9350 70.1 33.5
65 Kcell 43.2750 76.9550 61.5 29.2
66 Kcell 43.2250 76.8950 67.2 32.0
67 Altel 43.2561 76.9285 55.8 26.4
68 Altel 43.2350 76.9100 52.3 24.8
69 Altel 43.2700 76.9500 58.1 27.5
70 Altel 43.2200 76.8900 50.6 23.9
71 Altel 43.2900 76.9700 56.4 26.8
72 Altel 43.2100 76.8800 53.7 25.2
73 Altel 43.2450 76.9150 54.9 26.0
74 Altel 43.2600 76.9350 59.2 28.1
75 Altel 43.2750 76.9550 51.8 24.5
76 Altel 43.2250 76.8950 57.1 27.0

9
glify_check.js Normal file

File diff suppressed because one or more lines are too long

1267
index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
# Implementation Plan: Интерактивная карта скорости интернета по провайдерам Казахстана
## Overview
Single static HTML file application that visualizes Speedtest Intelligence data on an interactive map of Kazakhstan. No build tools, no server, no npm — everything runs in the browser from a single `index.html`.
## Architecture Diagram
```mermaid
flowchart TD
subgraph "Data Layer"
CSV["data/speedtest.csv"] --> PP["PapaParse streaming<br/>Web Worker"]
PP --> Points["Points Array<br/>{provider, lat, lon, dl, ul}"]
end
subgraph "Processing Layer"
Points --> RBUSH["rbush R-tree<br/>spatial index"]
Points --> WORKER["Web Worker: DBSCAN + convex hull"]
WORKER --> Hulls["Hulls GeoJSON<br/>FeatureCollection"]
end
subgraph "Rendering Layer"
MAP["Leaflet Map<br/>CartoDB Positron tiles"]
GEOJSON["data/kaz_regions.geojson"] --> ADMIN["AdminLayer<br/>L.geoJSON regions"]
Hulls --> HULL["HullLayer<br/>L.geoJSON polygons"]
Points --> GLIFY["GlifyLayer<br/>Leaflet.glify WebGL"]
end
subgraph "UI Controls"
LEGEND["LegendCtrl<br/>checkboxes, colors"]
TOOLTIP["TooltipCtrl<br/>hover info"]
EXPORT["ExportCtrl<br/>CSV via Blob API"]
end
MAP --> ADMIN
MAP --> HULL
MAP --> GLIFY
MAP --> LEGEND
MAP --> TOOLTIP
MAP --> EXPORT
```
## Zoom-based Layer Switching
```mermaid
flowchart LR
Z5["zoom ≤ 8"] --> HULL_VIEW["Hull polygons visible<br/>Glify points hidden"]
Z8["zoom 8-9"] --> FADE["Crossfade transition<br/>opacity 1→0 / 0→0.8"]
Z9["zoom ≥ 9"] --> POINT_VIEW["Glify points visible<br/>Hull polygons hidden"]
```
## File Structure
```
c:/Users/1/speedtestMap/
├── index.html # Single-file application (all JS + CSS inline)
├── data/
│ ├── speedtest.csv # Provider speed data (user replaces manually)
│ └── kaz_regions.geojson # Kazakhstan admin boundaries (GADM KAZ_1)
├── plans/
│ └── implementation-plan.md # This file
└── README.md # Deployment & usage instructions
```
## Implementation Steps (in order)
### Step 1: Project Scaffolding
- Create `data/` directory
- Create `index.html` with HTML5 boilerplate
- Add CDN links for all 7 dependencies:
- Leaflet 1.9.x CSS + JS
- Leaflet.glify 2.x
- PapaParse 5.x
- Turf.js 6.x (from esm.sh or bundle)
- rbush 3.x
- chroma.js 2.x
- Add inline `<style>` block for layout
- Add inline `<script>` block structure with all modules
### Step 2: CONFIG Block
Place at top of `<script>`:
- `EPS_KM: 20` — DBSCAN radius
- `MIN_CLUSTER_POINTS: 3` — min points for cluster
- `ZOOM_THRESHOLD: 9` — layer switch zoom level
- `POINT_MIN_PX: 4`, `POINT_MAX_PX: 18` — circle size range
- `SPEED_MIN: 1`, `SPEED_MAX: 1000` — speed range
- `CSV_PATH`, `GEOJSON_PATH` — data file paths
- `MAP_CENTER: [48.0, 68.0]`, `MAP_ZOOM: 5`
- `PALETTE` — 20 distinguishable colors
### Step 3: DataLoader Module
- `Papa.parse()` with `worker: true`, streaming mode
- Chunk callback: accumulate data, update progress bar
- Complete callback: call `onDataReady(points)`
- Filter out rows with null lat/lon or missing provider
- Filter out providers with < 3 total points (per PRD F-24)
### Step 4: Preprocessor (Web Worker)
- Inline Web Worker (blob URL) for DBSCAN + hull
- Receive points array via `postMessage`
- Group by provider
- For each provider with ≥ 3 points:
- Run `@turf/clusters-dbscan` with `eps=EPS_KM`, `minPoints=MIN_CLUSTER_POINTS`
- For each cluster (excluding noise/-1):
- Build `turf.convex()`
- Attach properties: provider, count, avg_download, avg_upload
- Return `turf.featureCollection(hulls)` via `postMessage`
### Step 5: MapController
- Initialize Leaflet map at `MAP_CENTER`, `MAP_ZOOM`
- Add CartoDB Positron tile layer (no API key needed)
- Handle `zoomend` event for layer switching
- Manage crossfade opacity between hull and glify layers
### Step 6: AdminLayer
- `fetch('data/kaz_regions.geojson')` → parse GeoJSON
- Render with `L.geoJSON()`, style with semi-transparent borders
- Make regions clickable → trigger region selection
- On click: highlight region, show export button, compute point-in-polygon
### Step 7: HullLayer
- Receive hulls GeoJSON from Worker
- Render with `L.geoJSON()`, each polygon colored by provider
- Fill opacity ~30-40%, stroke for visibility
- Show/hide based on zoom level
- Attach hover handlers for tooltip
### Step 8: GlifyLayer
- Prepare data arrays: `latLngs`, `colors`, `sizes`
- Initialize `L.glify.points()` once with all points
- Set initial opacity = 0 (hidden at low zoom)
- On zoom ≥ 9: opacity = 0.8
- Handle `hover` and `click` events from glify
### Step 9: TooltipCtrl
- Custom tooltip div (not Leaflet default)
- On hull hover: show provider name, region, avg speeds, point count
- On point hover: show provider name, coordinates, speeds
- Position tooltip near cursor, avoid overlapping map controls
- On point click: show fixed popup with extended data
### Step 10: LegendCtrl
- Fixed panel with provider list
- Each provider: colored circle + name + checkbox
- Checkbox toggles visibility of both hull and glify layers for that provider
- "Select all" / "Deselect all" buttons
- Built from unique providers in loaded data
### Step 11: ExportCtrl
- On region click: build rbush spatial index query
- Filter by active providers (from LegendCtrl checkboxes)
- Generate CSV string with header + rows
- Create Blob, trigger download via `<a>` click
- Filename: `kz_{region}_{YYYY-MM-DD}.csv`
### Step 12: Crossfade & Polish
- CSS transition on glify canvas opacity
- Smooth fade between hull and point layers at zoom 8-9
- Progress bar during CSV loading
- WebGL support detection with fallback message
### Step 13: README.md
- Project description
- How to obtain GADM GeoJSON
- How to prepare CSV data
- Deployment options (file:// vs npx serve)
- How to update data
## Key Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Point rendering | Leaflet.glify (WebGL) | 300k+ points at 60 fps, single draw call |
| Clustering algorithm | DBSCAN via Turf.js | Built-in Haversine distance, configurable eps |
| Spatial index | rbush | Fast bbox search for point-in-polygon |
| Color scale | chroma.js | Lighter than D3-scale, sufficient for log gradient |
| Deployment | Static file + npx serve | No build step, no API keys |
| Data loading | PapaParse streaming Worker | Non-blocking UI, handles large CSVs |
## Edge Cases Handled
- Providers with < 3 points: excluded entirely
- Collinear points (hull = null): cluster skipped gracefully
- Null/zero speeds: clamped to min=1 Mbps
- Missing CSV fields: filtered during parse
- No WebGL: detection + fallback message
- GeoJSON unavailable: map works without admin boundaries

File diff suppressed because it is too large Load Diff