Skip to main content

antv-l7

基于 WebGL 的大规模地理空间数据可视化引擎。适用于: (1) 创建交互式 WebGL 地图应用 (2) 可视化地理空间数据(点、线、面、热力图) (3) 构建位置数据驾驶舱 (4) 添加地图图层、交互和动画效果 (5) 处理并展示 GeoJSON、CSV 等空间数据

Source facts

Repository
antvis/L7
Last source activity
May 11, 2026 at 06:15
Detected SKILL.md language
Chinese
Stars
4,070
Forks
655

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
antv-l7
title
AntV L7 地理空间可视化
description
基于 WebGL 的大规模地理空间数据可视化引擎。适用于: (1) 创建交互式 WebGL 地图应用 (2) 可视化地理空间数据(点、线、面、热力图) (3) 构建位置数据驾驶舱 (4) 添加地图图层、交互和动画效果 (5) 处理并展示 GeoJSON、CSV 等空间数据
license
MIT
# AntV L7 地理空间可视化 AntV L7 是基于 WebGL 的大规模地理空间数据可视化引擎,支持多种地图底图和丰富的可视化图层类型。 ## ⚠️ 重要约束 **作用域声明:以下约束仅适用于 WeaveFox(本仓库)环境。** **在 WeaveFox 环境下,高德地图配置禁止传入 token 参数。** 在 WeaveFox 项目中,L7 已配置默认 token。在该环境下,`GaodeMap` 配置中**不要**设置 `token` 字段,否则会导致地图加载失败。 ```javascript // ✅ 正确 - 不传 token new GaodeMap({ center: [120.19, 30.26], zoom: 10, style: 'light', }); // ❌ 错误 - 禁止传入 token new GaodeMap({ center: [120.19, 30.26], zoom: 10, style: 'light', token: 'xxx', // 不要这样做! }); ``` ## 选型指南 ### 简单点位展示(< 100 个点) **优先使用 Marker**,而非 PointLayer: ```javascript import { Marker, Popup } from '@antv/l7'; let currentPopup = null; // ✅ 推荐 - 少量点位使用 Marker data.forEach((point) => { const marker = new Marker({ color: '#5B8FF9', draggable: false, }).setLnglat([point.lng, point.lat]); marker.on('click', () => { // 统一单实例 popup:先移除旧 popup,再创建新 popup if (currentPopup) scene.removePopup(currentPopup); currentPopup = new Popup({ anchor: 'bottom', className: 'outing-popup', lngLat: { lng: point.lng, lat: point.lat }, html: `<div>${point.name}</div>`, }); scene.addPopup(currentPopup); }); // 必须使用 scene.addMarker,否则地图缩放/平移时 Marker 不会更新位置 scene.addMarker(marker); }); ``` **Marker vs PointLayer 对比:** | 特性 | Marker | PointLayer | | -------- | ------------------- | ----------------------- | | 适用场景 | < 100 个点位 | > 100 个点位/大规模数据 | | 性能 | DOM 渲染,轻量 | WebGL 渲染,高性能 | | 交互 | 原生 DOM 事件,灵活 | 需绑定图层事件 | | 自定义 | 支持任意 HTML/CSS | 受限于 L7 样式配置 | | Popup | 内置支持 | 需额外配置 | ### 大规模数据可视化(> 100 个点) 使用 PointLayer 进行 WebGL 渲染: ```javascript import { PointLayer } from '@antv/l7'; // ✅ 推荐 - 大量点位使用 PointLayer const pointLayer = new PointLayer() .source(data, { parser: { type: 'json', x: 'lng', y: 'lat' }, }) .shape('circle') .size(10) .color('#5B8FF9'); scene.addLayer(pointLayer); ``` ## React 最佳实践 ### 核心原则 1. **单次初始化**:地图场景只初始化一次 2. **图层分离**:数据更新时只更新图层,不重建地图 3. **完整清理**:组件卸载时清理所有资源 4. **错误处理**:捕获初始化异常 ### 硬性规则(防止重复初始化) 以下规则为强约束,生成 React + L7 代码时必须遵守: 1. **初始化 effect 必须空依赖**:创建 `new Scene()` 的 `useEffect` 依赖必须是 `[]`。 2. **禁止在初始化 effect 依赖中放业务状态**:例如 `selectedId`、筛选条件、分页参数。 3. **禁止在初始化 effect 依赖中放易变回调**:例如来自父组件的 `onSelect`、`onClick`、`onChange`。 4. **状态更新走独立 effect**:选中态、数据源、样式变化只能更新图层/Marker,不得 `destroy + new Scene`。 5. **最新值使用 ref**:`loaded` 回调里若需要最新 props,使用 `latestXxxRef.current`,不要依赖闭包旧值。 6. **清理只在卸载时执行**:`scene.destroy()` 只能放在初始化 effect 的 cleanup 中,不能由状态变化触发。 7. **严格区分初始化与更新职责**:初始化负责 `Scene/Layer` 首建,更新负责 `setData`、更新 Marker 或样式。 8. **Popup 锚点默认规则**:点位/Marker 场景默认使用 `anchor: 'bottom'`,让弹窗显示在点位上方。 9. **Popup 方案必须单一**:统一使用 `scene.addPopup(...)`,禁止与 `marker.setPopup(...)`、`popup.addTo(scene)` 混用。 10. **Popup 必须单实例**:创建新 popup 前先 `scene.removePopup(oldPopup)`,避免页面出现多个弹窗。 11. **Marker 形态优先尖角样式**:优先使用“气泡主体 + 尖角(pointer)”图标,尖角必须指向真实经纬度点。 12. **Marker 锚点必须与尖角一致**:尖角在元素底部时,`anchor` 必须使用 `'bottom'`,禁止使用 `'center'` 导致点位偏移。 13. **Marker 文案对齐规则**:文字描述必须在 Marker 主体内水平居中,主体中心与实际点位保持视觉对称。 14. **多行文案稳定规则**:文案允许换行时,主体必须设置 `max-width` 与 `word-break`,且尖角保持 `margin: 0 auto` 居中,防止尖角偏离点位。 15. **应用 UI 层级规则**:地图应用的信息面板、Sidebar、模态框等非地图组件必须设置 `z-index >= 1000`,确保显示在地图 logo 和其他地图固定元素之上。 16. **Popup 关闭按钮规则**:创建 Popup 时必须启用 `closeButton: true`,允许用户点击关闭按钮关闭弹窗,提升交互体验。 17. **Popup 相对位置规则**:Popup 必须显示在 Marker/点位上方,需使用 `offset: [0, -40]` 向上移动,避免与 Marker 气泡重叠遮挡。 反例(禁止): ```typescript // 会导致 selectedId 变化时反复销毁并重建 Scene useEffect(() => { const scene = new Scene(...); return () => scene.destroy(); }, [selectedId, onSelectLocation]); ``` 正例(推荐): ```typescript // 只初始化一次 useEffect(() => { const scene = new Scene(...); return () => scene.destroy(); }, []); // 状态变化只更新图层,不重建 Scene useEffect(() => { pointLayerRef.current?.setData(nextData); }, [nextData]); ``` 说明:React 18 开发环境 `StrictMode` 会对 mount 做额外检查,可能看到两次初始化日志。只要初始化 effect 为空依赖且职责正确,生产环境不会因此重复重建。 ### Marker 样式约束(尖角指向点位) 当使用自定义 Marker DOM 时,默认采用“主体 + 尖角”结构: ```typescript function createMarkerElement(point: { name: string; color: string }) { const el = document.createElement('div'); el.style.cssText = 'position: relative; transform: translateY(-8px);'; el.innerHTML = ` <div style=" min-width: 88px; padding: 6px 10px; border-radius: 999px; background: ${point.color}; color: #fff; font-size: 12px; font-weight: 600; text-align: center; line-height: 1.2; box-shadow: 0 4px 12px rgba(0,0,0,0.2); position: relative; max-width: 160px; white-space: normal; word-break: break-word; ">${point.name}</div> <div style=" width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid ${point.color}; margin: 0 auto; "></div> `; return el; } const marker = new Marker({ element: createMarkerElement(point), anchor: 'bottom', }).setLnglat([point.lng, point.lat]); ``` 约束说明: - 尖角是点位指示器,必须位于元素底部中心。 - 文案容器使用 `text-align: center`,并保持左右内边距对称。 - 多行文案时必须限制最大宽度并允许换行,避免单行过长导致主体偏斜。 - 为避免遮挡点位,元素整体可上移少量像素(例如 `translateY(-8px)`)。 ### 使用 Marker(< 100 个点) ```typescript 'use client'; import { useEffect, useRef, useCallback } from 'react'; import { Scene, Marker, Popup } from '@antv/l7'; import { GaodeMap } from '@antv/l7-maps'; interface Location { id: string; lng: number; lat: number; name: string; } export function MarkerMap({ locations }: { locations: Location[] }) { const mapContainerRef = useRef<HTMLDivElement>(null); const sceneRef = useRef<Scene | null>(null); const markersRef = useRef<Marker[]>([]); const popupRef = useRef<Popup | null>(null); const isInitializedRef = useRef(false); // 创建/更新 Markers const updateMarkers = useCallback((scene: Scene, data: Location[]) => { // 1. 清理旧 marker(避免重复渲染和内存泄漏) markersRef.current.forEach(marker => { marker.remove(); }); markersRef.current = []; // 2. 创建新 marker data.forEach(point => { const marker = new Marker({ color: '#5B8FF9' }) .setLnglat([point.lng, point.lat]); marker.on('click', () => { if (popupRef.current) { scene.removePopup(popupRef.current); } popupRef.current = new Popup({ anchor: 'bottom', closeButton: true, offset: [0, -40], lngLat: { lng: point.lng, lat: point.lat }, html: `<div>${point.name}</div>`, }); scene.addPopup(popupRef.current); }); // 必须使用 scene.addMarker,否则地图缩放/平移时 Marker 不会联动 scene.addMarker(marker); markersRef.current.push(marker); }); }, []); useEffect(() => { if (!mapContainerRef.current || isInitializedRef.current) return; const scene = new Scene({ id: mapContainerRef.current, map: new GaodeMap({ center: [105, 35], zoom: 4, style: 'light', }), }); sceneRef.current = scene; isInitializedRef.current = true; scene.on('loaded', () => { updateMarkers(scene, locations); }); return () => { if (sceneRef.current) { if (popupRef.current) { sceneRef.current.removePopup(popupRef.current); popupRef.current = null; } markersRef.current.forEach(marker => marker.remove()); markersRef.current = []; sceneRef.current.destroy(); sceneRef.current = null; isInitializedRef.current = false; } }; }, []); // 数据更新时重建 markers useEffect(() => { if (isInitializedRef.current && sceneRef.current) { updateMarkers(sceneRef.current, locations); } }, [locations]); return ( <div ref={mapContainerRef} style={{ width: '100%', height: 'min(70vh, 600px)', minHeight: '360px' }} /> ); } ``` **Marker 关键注意点:** - `markersRef` 保存实例数组,用于数据更新时清理旧 marker - `scene.addMarker(marker)` 必须用它,`.addTo(scene)` 不会注册相机事件,导致 marker 与地图不同步 - Popup 统一使用 `scene.addPopup(...)`,不要和 `marker.setPopup(...)` 混用 - 点位弹窗默认 `anchor: 'bottom'`,确保弹窗显示在 marker 上方 - 组件卸载时遍历 `markersRef.current` 逐一 `remove()` ### 使用 PointLayer(> 100 个点) ```typescript 'use client'; import { useEffect, useRef, useCallback } from 'react'; import { Scene, PointLayer, Popup } from '@antv/l7'; import { GaodeMap } from '@antv/l7-maps'; interface Location { id: string; lng: number; lat: number; name: string; color: string; } interface MapProps { locations: Location[]; onLocationClick?: (location: Location) => void; } export function TravelMap({ locations, onLocationClick }: MapProps) { const mapContainerRef = useRef<HTMLDivElement>(null); const sceneRef = useRef<Scene | null>(null); const pointLayerRef = useRef<PointLayer | null>(null); const popupRef = useRef<Popup | null>(null); const isInitializedRef = useRef(false); const latestLocationsRef = useRef<Location[]>(locations); // 保持最新数据,避免 scene loaded 回调拿到旧闭包数据 useEffect(() => { latestLocationsRef.current = locations; }, [locations]); // 初始化地图(只执行一次) const initMap = useCallback(() => { if (!mapContainerRef.current || isInitializedRef.current) return; try { const scene = new Scene({ id: mapContainerRef.current, map: new GaodeMap({ center: [105, 35], zoom: 4, style: 'light', }), }); sceneRef.current = scene; isInitializedRef.current = true; return scene; } catch (error) { console.error('地图初始化失败:', error); throw error; } }, []); // 创建图层 const createLayer = useCallback((scene: Scene, data: Location[]) => { const pointData = data.map(loc => ({ lng: loc.lng, lat: loc.lat, name: loc.name, color: loc.color, id: loc.id, })); const pointLayer = new PointLayer() .source(pointData, { parser: { type: 'json', x: 'lng', y: 'lat' }, }) .shape('circle') .size(20) .color('color', (color: string) => color) .style({ opacity: 0.9, strokeWidth: 2, stroke: '#ffffff', });
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub