--- name: d3-geo description: Use when creating maps, working with geographic projections, or processing GeoJSON data. Invoke for world maps, choropleth maps, projection types, geo path generators, spherical geometry, or geographic feature manipulation. allowed-tools: Read, Grep, Glob --- # D3 Geographic Visualization Expert ## Purpose Expert knowledge of D3's geographic projections and map rendering. Covers map projections, GeoJSON processing, geographic path generators, spherical geometry, and cartographic features. ## When to Use Invoke this skill when: - Creating world maps or regional maps - Working with GeoJSON or TopoJSON data - Choosing or configuring map projections - Building choropleth (colored region) maps - Computing geographic centroids or bounds - Measuring distances on a sphere - Creating globe visualizations - Rotating or clipping projections - Handling geographic coordinates - Debugging map rendering issues ## Documentation Available **Location**: `/Users/zach/Documents/cc-skills/docs/d3/` **Coverage** (266 files): - **Core Geography** (91 files): - Path generator for rendering GeoJSON - Spherical math (distance, area, length) - Geographic bounds and centroids - Great circle interpolation - Clipping and rotation - **Projections** (151 files): - 30+ projection types - Azimuthal: orthographic, stereographic, gnomonic - Cylindrical: Mercator, equirectangular - Conic: Albers, Lambert conformal - Composite projections - Projection configuration (scale, translate, rotate) - **Polygon Operations** (24 files): - Spherical polygon area - Polygon contains point - Polygon clipping - Polygon centroid ## Related Skills - **d3-core-data**: For color scales in choropleth maps - **d3-shapes-paths**: For combining geo with other shapes - **d3-interaction-animation**: For interactive maps - **d3-layouts-hierarchies**: For hierarchical geographic data ## Best Practices - **Use appropriate projections** - Mercator for web tiles, Albers for US maps - **Fit projections to bounds** - Use .fitSize() or .fitExtent() - **Simplify TopoJSON** - Reduce file size for web delivery - **Handle dateline wrapping** - Use antimeridian clipping - **Set precision carefully** - Balance quality vs. performance - **Use geographic centroids** - More accurate than planar centroids - **Cache path generator** - Reuse for multiple features - **Consider adaptive sampling** - For large datasets ## Examples ### Example 1: Basic World Map **Question**: "How do I create a simple world map?" **Response**: ```javascript import * as d3 from 'd3'; // Load GeoJSON data const worldData = await d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'); // Convert TopoJSON to GeoJSON const countries = topojson.feature(worldData, worldData.objects.countries); // Create projection const projection = d3.geoNaturalEarth1() .scale(200) .translate([400, 300]); // Center of SVG // Create path generator const path = d3.geoPath() .projection(projection); // Create SVG const svg = d3.create('svg') .attr('width', 800) .attr('height', 600); // Draw countries svg.selectAll('path') .data(countries.features) .join('path') .attr('d', path) .attr('fill', '#ccc') .attr('stroke', '#fff') .attr('stroke-width', 0.5); // Add graticule (grid lines) const graticule = d3.geoGraticule(); svg.append('path') .datum(graticule()) .attr('d', path) .attr('fill', 'none') .attr('stroke', '#ddd') .attr('stroke-width', 0.5); // Add sphere outline svg.append('path') .datum({ type: 'Sphere' }) .attr('d', path) .attr('fill', 'none') .attr('stroke', '#000') .attr('stroke-width', 1.5); ``` **Auto-fit projection to data**: ```javascript const projection = d3.geoNaturalEarth1() .fitSize([800, 600], countries); ``` **References**: - See: `docs/d3/geo/path.md` - See: `docs/d3/geo-projection/` ### Example 2: US State Choropleth Map **Question**: "How do I create a choropleth map with state-level data?" **Response**: ```javascript import * as d3 from 'd3'; // Load US states TopoJSON const us = await d3.json('https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json'); const states = topojson.feature(us, us.objects.states); // Sample data (state ID -> value) const data = new Map([ ['01', 45], // Alabama ['06', 82], // California ['36', 68], // New York // ... more states ]); // Create projection optimized for US const projection = d3.geoAlbersUsa() .scale(1000) .translate([400, 300]); const path = d3.geoPath() .projection(projection); // Create color scale const colorScale = d3.scaleSequential() .domain([0, 100]) .interpolator(d3.interpolateBlues); // Create SVG const svg = d3.create('svg') .attr('width', 800) .attr('height', 600); // Draw states svg.selectAll('path') .data(states.features) .join('path') .attr('d', path) .attr('fill', d => { const value = data.get(d.id); return value ? colorScale(value) : '#ccc'; }) .attr('stroke', '#fff') .attr('stroke-width', 0.5) .on('mouseenter', function(event, d) { d3.select(this) .attr('stroke', '#000') .attr('stroke-width', 2); // Show tooltip const value = data.get(d.id); console.log(`${d.properties.name}: ${value}`); }) .on('mouseleave', function() { d3.select(this) .attr('stroke', '#fff') .attr('stroke-width', 0.5); }); // Add legend const legendWidth = 300; const legendHeight = 10; const legend = svg.append('g') .attr('transform', 'translate(250, 550)'); // Create gradient for legend const defs = svg.append('defs'); const gradient = defs.append('linearGradient') .attr('id', 'legend-gradient'); gradient.selectAll('stop') .data(d3.range(0, 1.1, 0.1)) .join('stop') .attr('offset', d => `${d * 100}%`) .attr('stop-color', d => colorScale(d * 100)); legend.append('rect') .attr('width', legendWidth) .attr('height', legendHeight) .style('fill', 'url(#legend-gradient)'); // Add legend axis const legendScale = d3.scaleLinear() .domain([0, 100]) .range([0, legendWidth]); const legendAxis = d3.axisBottom(legendScale) .ticks(5); legend.append('g') .attr('transform', `translate(0, ${legendHeight})`) .call(legendAxis); ``` **Albers USA Projection**: - Optimized for US maps - Automatically positions Alaska and Hawaii - Better area representation than Mercator **References**: - See: `docs/d3/geo-projection/albers-usa.md` ### Example 3: Interactive Globe with Rotation **Question**: "How do I create a draggable globe?" **Response**: ```javascript import * as d3 from 'd3'; // Load world data const world = await d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'); const countries = topojson.feature(world, world.objects.countries); // Create orthographic projection (globe) const projection = d3.geoOrthographic() .scale(250) .translate([400, 300]) .rotate([0, 0]); // [longitude, latitude, roll] const path = d3.geoPath() .projection(projection); // Create SVG const svg = d3.create('svg') .attr('width', 800) .attr('height', 600); // Add ocean (sphere background) svg.append('path') .datum({ type: 'Sphere' }) .attr('class', 'ocean') .attr('d', path) .attr('fill', '#a8d5ff'); // Add countries const countriesGroup = svg.append('g') .selectAll('path') .data(countries.features) .join('path') .attr('d', path) .attr('fill', '#bbb') .attr('stroke', '#fff') .attr('stroke-width', 0.5); // Add graticule const graticule = d3.geoGraticule(); svg.append('path') .datum(graticule()) .attr('d', path) .attr('fill', 'none') .attr('stroke', '#ccc') .attr('stroke-width', 0.5) .attr('opacity', 0.5); // Add drag behavior const drag = d3.drag() .on('start', function(event) { // Store starting rotation this._rotation = projection.rotate(); this._coords = [event.x, event.y]; }) .on('drag', function(event) { const rotation = this._rotation; const coords = this._coords; // Calculate rotation delta const dx = event.x - coords[0]; const dy = event.y - coords[1]; // Update projection rotation projection.rotate([ rotation[0] + dx * 0.5, rotation[1] - dy * 0.5, ]); // Re-render svg.selectAll('path').attr('d', path); }); svg.call(drag); // Add zoom behavior const zoom = d3.zoom() .scaleExtent([0.5, 5]) .on('zoom', (event) => { projection.scale(250 * event.transform.k); svg.selectAll('path').attr('d', path); }); svg.call(zoom); ``` **Other 3D Projections**: - `geoOrthographic()` - Globe view - `geoStereographic()` - Perspective from point - `geoGnomonic()` - Great circles as straight lines **References**: - See: `docs/d3/geo-projection/orthographic.md` ### Example 4: Computing Geographic Measurements **Question**: "How do I calculate distances and areas on a sphere?" **Response**: ```javascript import * as d3 from 'd3'; // Define coordinates [longitude, latitude] const newYork = [-74.006, 40.7128]; const london = [-0.1278, 51.5074]; const tokyo = [139.6917, 35.6895]; // Calculate great circle distance (in radians) const distanceRadians = d3.geoDistance(newYork, london); console.log('Distance (radians):', distanceRadians); // Convert to kilometers (Earth radius = 6371 km) const distanceKm = distanceRadians * 6371; console.log('Distance (km):', distanceKm.toFixed(0)); // ~5,570 km // Calculate area of polygon (in steradians) const polygon = { type: 'Polygon', coordinates: [[ [-74, 40], [-73, 40], [-73, 41], [-74, 41], [-74, 40], ]], }; const areaSteradians = d3.geoArea(polygon); console.log('Area (steradians):', areaSteradians); // Convert to square kilometers (Earth surface area = 510M km²) const areaKm2 = areaSteradians * (6371 * 6371); console.log('Area (km²):', areaKm2.toFixed(0)); // Calculate path length const lineString = { type: 'LineString', coordinates: [newYork, london], }; const lengthRadians = d3.geoLength(lineString); const lengthKm = lengthRadians * 6371; console.log('Path length (km):', lengthKm.toFixed(0)); // Calculate centroid const centroid = d3.geoCentroid(polygon); console.log('Centroid [lon, lat]:', centroid); // Check if point is inside polygon const point = [-73.5, 40.5]; const contains = d3.geoContains(polygon, point); console.log('Contains point:', contains); // true or false // Interpolate along great circle const interpolate = d3.geoInterpolate(newYork, london); console.log('25% along:', interpolate(0.25)); console.log('50% along:', interpolate(0.5)); console.log('75% along:', interpolate(0.75)); // Calculate bounds const bounds = d3.geoBounds(polygon); console.log('Bounds [[west, south], [east, north]]:', bounds); ``` **Spherical vs. Planar**: - Use `d3.geo*` for geographic coordinates (lat/lon) - Use `d3.polygon*` for projected/screen coordinates **References**: - See: `docs/d3/geo/distance.md` - See: `docs/d3/geo/area.md` - See: `docs/d3/geo/centroid.md` ### Example 5: Custom Projection Configuration **Question**: "How do I configure projection for a specific region?" **Response**: ```javascript import * as d3 from 'd3'; // Load region data (e.g., Europe) const europe = await d3.json('europe.geojson'); // Method 1: Manual configuration const projection1 = d3.geoMercator() .center([10, 50]) // Center on [lon, lat] .scale(500) .translate([400, 300]); // SVG center // Method 2: Fit to bounds (recommended) const projection2 = d3.geoMercator() .fitSize([800, 600], europe); // Method 3: Fit with padding const projection3 = d3.geoMercator() .fitExtent([[20, 20], [780, 580]], europe); // [[x0, y0], [x1, y1]] // Method 4: Fit width, maintain aspect ratio const projection4 = d3.geoMercator() .fitWidth(800, europe); // Method 5: Fit height, maintain aspect ratio const projection5 = d3.geoMercator() .fitHeight(600, europe); // Projection with rotation const projection6 = d3.geoMercator() .rotate([-10, 0, 0]) // [yaw, pitch, roll] .fitSize([800, 600], europe); // Set clipping const projection7 = d3.geoMercator() .clipAngle(90) // Clip to hemisphere .fitSize([800, 600], europe); // Set precision (adaptive sampling) const projection8 = d3.geoMercator() .precision(0.1) // Lower = more points, higher quality .fitSize([800, 600], europe); // Get projection functions const path = d3.geoPath(projection2); // Project coordinate to screen const [x, y] = projection2([10, 50]); // [lon, lat] -> [x, y] // Invert screen to coordinate const [lon, lat] = projection2.invert([400, 300]); // [x, y] -> [lon, lat] // Check if coordinate is visible const visible = projection2([10, 50]); // null if clipped ``` **Common Projections**: - `geoMercator()` - Web maps (preserves angles) - `geoAlbers()` - US/Europe maps (preserves area) - `geoEquirectangular()` - Simple lat/lon mapping - `geoConicEqualArea()` - Regional maps - `geoNaturalEarth1()` - World maps (compromise) **References**: - See: `docs/d3/geo/projection.md` - See: `docs/d3/geo-projection/` ## Common Patterns ### Zoom to Feature ```javascript function zoomToFeature(feature) { const [[x0, y0], [x1, y1]] = path.bounds(feature); svg.transition() .duration(750) .call(zoom.transform, d3.zoomIdentity .translate(width / 2, height / 2) .scale(Math.min(8, 0.9 / Math.max( (x1 - x0) / width, (y1 - y0) / height ))) .translate(-(x0 + x1) / 2, -(y0 + y1) / 2) ); } ``` ### Highlight Neighbors ```javascript const neighbors = topojson.neighbors(us.objects.states.geometries); svg.selectAll('path') .on('mouseenter', function(event, d, i) { svg.selectAll('path') .filter((_, j) => neighbors[i].includes(j)) .attr('fill', 'orange'); }); ``` ### Add Labels to Centroids ```javascript svg.selectAll('text') .data(countries.features) .join('text') .attr('transform', d => { const [x, y] = path.centroid(d); return `translate(${x}, ${y})`; }) .attr('text-anchor', 'middle') .text(d => d.properties.name); ``` ## Search Helpers ```bash # Find projection docs grep -r "projection\|geoMercator\|geoAlbers" /Users/zach/Documents/cc-skills/docs/d3/geo-projection/ # Find path generator docs grep -r "geoPath\|path.area\|path.bounds" /Users/zach/Documents/cc-skills/docs/d3/geo/ # Find spherical math docs grep -r "geoDistance\|geoArea\|geoCentroid" /Users/zach/Documents/cc-skills/docs/d3/geo/ # Find clipping docs grep -r "clip\|rotate" /Users/zach/Documents/cc-skills/docs/d3/geo/ # List projection types ls /Users/zach/Documents/cc-skills/docs/d3/geo-projection/ ``` ## Common Errors - **Map not centered**: Use .fitSize() instead of manual scale/translate - Solution: `projection.fitSize([width, height], geoJSON)` - **Countries upside down**: Y coordinates in wrong direction - Solution: Ensure projection range is correct (already handled by D3) - **Dateline artifacts**: Features crossing 180° longitude - Solution: Use antimeridian clipping or split features - **Performance issues**: Too many vertices - Solution: Simplify TopoJSON with mapshaper or use .precision() - **Projection returns null**: Coordinate outside clip bounds - Solution: Check .clipAngle() or handle null values ## Performance Tips 1. **Use TopoJSON** - Smaller file size, shared boundaries 2. **Simplify geometries** - Use mapshaper or topojson-simplify 3. **Set appropriate precision** - Balance quality vs. speed 4. **Cache path generator** - Reuse for all features 5. **Use canvas rendering** - For large/complex maps 6. **Limit feature count** - Filter or aggregate for interactive maps ## Notes - Documentation covers D3 v7 (latest version) - All projections support .fitSize(), .fitExtent(), .fitWidth(), .fitHeight() - Orthographic projection shows one hemisphere at a time - AlbersUsa is composite projection (mainland + Alaska + Hawaii) - GeoJSON uses [longitude, latitude] order (not lat/lon!) - Spherical calculations use radians (multiply by Earth radius for km) - TopoJSON is more efficient than GeoJSON for maps - File paths reference local documentation cache - For latest updates, check https://d3js.org/d3-geo