Token导航 LogoToken导航TokenDH.com
研究检索需要联网github未标认证来源可访问clear审计通过

large-scale-map-visualization大比例尺地图可视化

Agent Skill

用于辅助数据整理、表格处理、CSV/Excel 分析、指标计算和图表准备。它适合让 Agent 清洗字段、汇总数据、发现异常、生成统计口径或把分析结果转成可读说明。使用时需要确认数据来源、字段含义和时间范围,避免把样本数据当全量事实;涉及敏感数据、导出文件或批量写回时,应先确认权限和脱敏边界。

总安装

2,595

周安装

106

GitHub Stars

98

下载量

840
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:large-scale-map-visualization(大比例尺地图可视化)
来源仓库:https://github.com/erichowens/some_claude_skills
仓库路径:skills/large-scale-map-visualization
安装命令:
npx skills add https://github.com/erichowens/some_claude_skills --skill large-scale-map-visualization
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/erichowens/some_claude_skills --skill large-scale-map-visualization

简介

large-scale-map-visualization 用于辅助数据整理、表格处理、CSV/Excel 分析、指标计算和图表准备。

  • 它适合让 Agent 清洗字段、汇总数据、发现异常、生成统计口径或把分析结果转成可读说明。
  • 使用时需要确认数据来源、字段含义和时间范围,避免把样本数据当全量事实;涉及敏感数据、导出文件或批量写回时,应先确认权限和脱敏边界。
  • 安装命令为 npx skills add https://github.com/erichowens/some_claude_skills --skill large-scale-map-visualization。
  • 适用宿主包括 Codex、Claude、Cursor、Gemini CLI,分类属于研究检索。

SKILL.md

Large-Scale Map Visualization Expert

Master of high-performance web map implementations handling 5,000-100,000+ geographic data points. Specializes in Leaflet.js optimization, spatial clustering algorithms, viewport-based loading, and progressive disclosure UX patterns for map-based applications.

Activation Triggers

Activate on: "map performance", "too many markers", "slow map", "clustering", "10k points", "marker clustering", "leaflet performance", "spatial visualization", "geospatial clustering", "viewport loading", "map data optimization", "real-time map", "Supercluster", "marker cluster"

NOT for: Static map images (use Mapbox/Google Static) | 3D visualizations (use Maplibre GL) | Non-geographic data visualization (use D3.js/Chart.js) | Simple maps with <100 markers (vanilla Leaflet is fine)

Core Expertise

Performance Architecture

┌─────────────────────────────────────────────────────────────┐
│              MAP PERFORMANCE TIERS                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  0-100 markers    → Vanilla Leaflet (no optimization)      │
│  100-1,000        → Basic clustering (react-leaflet-cluster)│
│  1,000-10,000     → Supercluster + viewport loading        │
│  10,000-50,000    → Supercluster + canvas + sampling       │
│  50,000-500,000   → Web Workers + server-side clustering   │
│  500,000+         → MVT tiles + backend pre-aggregation    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Technology Stack Decisions

Use CaseBest LibraryWhy
React + <5k pointsreact-leaflet-clusterSimple drop-in, wraps Leaflet.markercluster
React + 5-50k pointsuse-supercluster hook3-5x faster, viewport-aware, GeoJSON native
React + 50k+ pointssupercluster + Web WorkersOffload clustering to background thread
Static sitesServer-side clusteringPre-compute at build time
Real-time updatesCanvas renderer + samplingMinimize DOM manipulation

Key Techniques

1. Marker Clustering with Supercluster

Why Supercluster beats alternatives:

  • Performance: Handles 500k points in 1-2 seconds vs 8+ seconds for Leaflet.markercluster
  • Architecture: Index-based k-d tree clustering, can run server-side or in Workers
  • API: Simple GeoJSON input/output
  • Viewport-aware: Only clusters visible points

Implementation Pattern:

import useSupercluster from "use-supercluster";

export function OptimizedMap({ locations }: { locations: Place[] }) {
  const mapRef = useRef<L.Map | null>(null);
  const [bounds, setBounds] = useState<BBox | null>(null);
  const [zoom, setZoom] = useState(10);

  // Convert to GeoJSON Feature collection
  const points = useMemo(() =>
    locations.map(place => ({
      type: "Feature" as const,
      properties: {
        cluster: false,
        placeId: place.id,
        place
      },
      geometry: {
        type: "Point" as const,
        coordinates: [place.longitude, place.latitude]
      }
    })),
    [locations]
  );

  // Cluster points based on viewport
  const { clusters, supercluster } = useSupercluster({
    points,
    bounds,
    zoom,
    options: {
      radius: 75,        // Cluster radius in pixels
      maxZoom: 16,       // Stop clustering at street level
      minPoints: 2       // Minimum points to form cluster
    }
  });

  // Update viewport on map move
  useEffect(() => {
    if (!mapRef.current) return;

    const handleMove = () => {
      const map = mapRef.current!;
      const b = map.getBounds();
      setBounds([b.getWest(), b.getSouth(), b.getEast(), b.getNorth()]);
      setZoom(map.getZoom());
    };

    mapRef.current.on("moveend", handleMove);
    handleMove(); // Initial load

    return () => mapRef.current?.off("moveend", handleMove);
  }, []);

  return (
    <MapContainer ref={mapRef} preferCanvas={true}>
      {clusters.map(cluster => {
        const [lng, lat] = cluster.geometry.coordinates;
        const { cluster: isCluster, point_count } = cluster.properties;

        if (isCluster) {
          return (
            <Marker
              key={`cluster-${cluster.id}`}
              position={[lat, lng]}
              icon={createClusterIcon(point_count, zoom)}
              eventHandlers={{
                click: () => {
                  const expansionZoom = Math.min(
                    supercluster!.getClusterExpansionZoom(cluster.id),
                    18
                  );
                  mapRef.current?.setView([lat, lng], expansionZoom, {
                    animate: true
                  });
                }
              }}
            />
          );
        }

        return (
          <PlaceMarker
            key={cluster.properties.placeId}
            place={cluster.properties.place}
          />
        );
      })}
    </MapContainer>
  );
}

2. Viewport-Based Loading (Supabase + PostGIS)

Database Function:

CREATE OR REPLACE FUNCTION find_in_viewport(
  min_lng DOUBLE PRECISION,
  min_lat DOUBLE PRECISION,
  max_lng DOUBLE PRECISION,
  max_lat DOUBLE PRECISION,
  zoom_level INTEGER DEFAULT 11,
  max_results INTEGER DEFAULT 10000
)
RETURNS TABLE (
  id UUID,
  name TEXT,
  latitude DOUBLE PRECISION,
  longitude DOUBLE PRECISION
  /* other fields */
) AS $$
BEGIN
  -- At low zoom levels, sample to reduce density
  IF zoom_level < 9 THEN
    RETURN QUERY
    SELECT
      p.id, p.name,
      ST_Y(p.geog::geometry) as latitude,
      ST_X(p.geog::geometry) as longitude
    FROM places p
    WHERE p.geog && ST_MakeEnvelope(min_lng, min_lat, max_lng, max_lat, 4326)::geography
    AND random() < 0.2  -- Show 20% for performance
    LIMIT max_results / 2;
  ELSE
    -- Full data at higher zoom
    RETURN QUERY
    SELECT
      p.id, p.name,
      ST_Y(p.geog::geometry) as latitude,
      ST_X(p.geog::geometry) as longitude
    FROM places p
    WHERE p.geog && ST_MakeEnvelope(min_lng, min_lat, max_lng, max_lat, 4326)::geography
    LIMIT max_results;
  END IF;
END;
$$ LANGUAGE plpgsql STABLE;

-- Ensure spatial index exists
CREATE INDEX IF NOT EXISTS idx_places_geog ON places USING GIST (geog);

React Query Hook:

import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/lib/supabase";

type BBox = [number, number, number, number]; // [west, south, east, north]

export function usePlacesInViewport(
  bounds: BBox | null,
  zoom: number,
  enabled = true
) {
  return useQuery({
    queryKey: ["places", "viewport", bounds?.join(","), zoom],
    queryFn: async () => {
      if (!bounds) return [];

      const [west, south, east, north] = bounds;

      const { data, error } = await supabase.rpc("find_in_viewport", {
        min_lng: west,
        min_lat: south,
        max_lng: east,
        max_lat: north,
        zoom_level: zoom
      });

      if (error) throw error;
      return data || [];
    },
    enabled: enabled && !!bounds,
    staleTime: 5 * 60 * 1000,    // 5 min (locations rarely change)
    gcTime: 30 * 60 * 1000,       // 30 min in cache
    refetchOnWindowFocus: false
  });
}

3. Progressive Disclosure Strategy

Show appropriate detail levels based on zoom:

const getClusterOptions = (zoom: number) => ({
  radius: zoom < 10 ? 100 : zoom < 14 ? 75 : 50,
  maxZoom: 16,
  minPoints: zoom < 10 ? 5 : 2
});

const getMarkerSize = (zoom: number) =>
  zoom < 12 ? 24 : zoom < 15 ? 32 : 40;

const shouldShowLabel = (zoom: number) => zoom >= 14;

4. Canvas Rendering for Performance

import L from "leaflet";

// Enable canvas renderer globally
const canvasRenderer = L.canvas({
  tolerance: 10,      // Hit detection tolerance
  padding: 0.5        // Extra render area (0.5 = 50% of viewport)
});

const mapOptions = {
  preferCanvas: true,
  renderer: canvasRenderer,
  // Disable animations on mobile
  zoomAnimation: !isMobile(),
  fadeAnimation: !isMobile(),
  markerZoomAnimation: !isMobile()
};

Performance gain: 3-5x faster rendering with 1,000+ markers

5. Efficient Cluster Icons

import L from "leaflet";

// Use divIcon (faster than custom components)
function createClusterIcon(count: number, zoom: number) {
  const size = getMarkerSize(zoom);

  return L.divIcon({
    html: `
      <div style="
        width: ${size}px;
        height: ${size}px;
        background: linear-gradient(135deg, #d97706, #f59e0b);
        border-radius: 50%;
        border: 3px solid #1a1410;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-weight: bold;
        font-size: ${zoom < 12 ? '10px' : '14px'};
        box-shadow: 0 4px 12px rgba(0,0,0,0.4);
      ">
        ${count}
      </div>
    `,
    className: "cluster-icon",
    iconSize: [size, size],
    iconAnchor: [size / 2, size / 2]
  });
}

6. Debounced Map Events

import { useDebouncedCallback } from "use-debounce";

const handleMapMove = useDebouncedCallback(() => {
  const bounds = mapRef.current?.getBounds();
  const zoom = mapRef.current?.getZoom();
  if (bounds && zoom) {
    setBounds([
      bounds.getWest(),
      bounds.getSouth(),
      bounds.getEast(),
      bounds.getNorth()
    ]);
    setZoom(zoom);
  }
}, 300); // 300ms debounce

useEffect(() => {
  mapRef.current?.on("moveend", handleMapMove);
  return () => mapRef.current?.off("moveend", handleMapMove);
}, []);

Performance Benchmarks

Based on real-world testing and research (sources in references):

Strategy1k points5k points10k pointsMobile (4G)
No clustering800ms3.5s ❌8s ❌12s ❌
Basic clustering400ms1.8s ⚠️4s ⚠️6s ❌
Leaflet.markercluster200ms800ms ⚠️2s ⚠️3s ⚠️
Supercluster + viewport150ms ✅300ms ✅500ms ✅800ms ✅
Supercluster + canvas100ms ✅200ms ✅350ms ✅500ms ✅

Target Performance Goals:

  • Initial load: <500ms (perceived)
  • Pan/zoom: <200ms response
  • Marker click: <100ms
  • Mobile: 2x desktop times acceptable

UX Patterns

Cluster Interaction Patterns

  1. Click to Expand (Recommended)

- Click cluster → zoom to expansion zoom level - Shows "spider" view of underlying points

  1. Click to List

- Click cluster → show sidebar with all items - Good for dense areas (downtown cores)

  1. Hover Preview

- Hover cluster → show count + top 3 items - Good for discovery UX

Loading States

{isLoading && (
  <div className="absolute inset-0 bg-leather-900/50 backdrop-blur-sm z-[1000] flex items-center justify-center">
    <div className="text-sand-100">
      Loading {loadedCount} of {totalCount} locations...
    </div>
  </div>
)}

Empty States

{!isLoading && clusters.length === 0 && (
  <div className="absolute inset-0 flex items-center justify-center z-[999]">
    <div className="text-center max-w-md p-6">
      <MapPin className="h-12 w-12 text-sand-400 mx-auto mb-4" />
      <h3 className="font-bitter text-xl text-sand-100 mb-2">
        No locations in this area
      </h3>
      <p className="text-sand-400 mb-4">
        Try zooming out or searching a different location.
      </p>
      <button onClick={resetView} className="btn-primary">
        Reset View
      </button>
    </div>
  </div>
)}

Common Pitfalls

❌ Anti-patterns to Avoid

  1. Loading all data upfront // BAD: Fetches 10k records on mount const {data} = useQuery(["all-places"], fetchAllPlaces);
  2. Re-rendering on every map move // BAD: Updates state on every pixel map.on("move", () => setBounds(map.getBounds()));
  3. Complex marker components // BAD: React component per marker <Marker icon={<ComplexSVGComponent />} />
  4. No zoom-level adaptation // BAD: Same clustering at all zoom levels const clusterOptions = {radius: 80, maxZoom: 20};

✅ Best Practices

  1. Viewport-based loading with debouncing
  2. Simple marker icons (divIcon with inline styles)
  3. Progressive disclosure (adapt to zoom level)
  4. Canvas rendering for large datasets
  5. Proper React Query cache configuration

Real-World Examples

Zillow Pattern

  • Low zoom: Neighborhood price clusters
  • Medium zoom: Individual properties with price
  • High zoom: Full property cards
  • Click: Expand cluster or open details

Airbnb Pattern

  • Server-side: Pre-cluster at 10 zoom levels
  • Client-side: Viewport API with 300ms debounce
  • Rendering: Canvas for price labels
  • Interaction: Hover for preview, click for details

OpenStreetMap Pattern

  • Tile-based: Pre-rendered raster tiles
  • Vector tiles: For 100k+ POIs
  • Simplification: Reduce detail at low zoom
  • Caching: Aggressive CDN + browser cache

Tech Stack Compatibility

Frameworks

  • ✅ Next.js 13+ (App Router + Server Components)
  • ✅ Next.js Pages Router
  • ✅ Vite + React
  • ✅ Remix
  • ✅ Astro (with client islands)

Databases

  • Supabase (PostGIS) - Recommended, built-in spatial indexing
  • ✅ PostgreSQL + PostGIS
  • ⚠️ MongoDB (geospatial queries slower than PostGIS)
  • ⚠️ Firebase (limited spatial query support)

Map Libraries

  • Leaflet.js - Best for static tiles + markers
  • ✅ Mapbox GL JS - Better for vector tiles
  • ✅ Maplibre GL JS - Open-source Mapbox alternative
  • ❌ Google Maps API - Expensive, less flexible

Migration Checklist

When optimizing an existing slow map:

  • Measure current performance (Chrome DevTools Performance tab)
  • Count total markers/points in dataset
  • Check if spatial index exists on database (EXPLAIN ANALYZE)
  • Install clustering library (npm install use-supercluster)
  • Implement viewport-based loading
  • Add canvas renderer option
  • Test on mobile device (4G throttling)
  • Add loading states
  • Implement progressive disclosure
  • Set up performance monitoring
  • Document zoom-level behaviors

Dependencies

{
  "dependencies": {
    "leaflet": "^1.9.4",
    "react-leaflet": "^4.2.1",
    "supercluster": "^8.0.1",
    "use-supercluster": "^1.2.0",
    "@tanstack/react-query": "^5.0.0",
    "use-debounce": "^10.0.0"
  }
}

References

Research Papers

Technical Guides

UX Research

Version History

  • 2026-01-09: Initial skill creation based on sobriety.tools places map optimization
  • Research synthesized from 8 authoritative sources
  • Tested with Next.js 15, Leaflet 1.9.4, Supabase PostGIS

Skill Author: Claude Code (Sonnet 4.5) Domain: Geospatial Data Visualization, Web Performance Complexity: Advanced (requires PostGIS, React, spatial algorithms knowledge)

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

windsurf

27.91%
按下载量换算234

Claude Code

22.75%
按下载量换算191

Codex

18.11%
按下载量换算152

Gemini CLI

12.09%
按下载量换算102

Antigravity

8.33%
按下载量换算70

OpenCode

3.5%
按下载量换算29

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills