This tutorial shows how to pull map shapes from an external connection, render them with d3-geo, and connect data from Lightdash inside a data app.
The ingredients:
- An external connection that serves GeoJSON country shapes.
- A prompt describing the globe and its interactions.
- A Lightdash query joined onto the shapes by country code.
1. Set up an external connection for geo data
The map needs country boundary shapes. Data apps fetch external data through external connections — a project admin registers the API once, and apps call it through a proxy that pins requests to the registered host.
A great free source is the Natural Earth dataset mirrored on jsDelivr. Create a connection under Project Settings → Data app connections with:
- Name -
world_geojson (the name becomes the alias the app calls — keep it short and descriptive)
- Base URL -
https://cdn.jsdelivr.net
- Auth - None (it’s a public CDN)
- Allowed methods -
GET
- Allowed paths -
/gh/nvkelso/natural-earth-vector/geojson/
- Content types -
application/json and application/geo+json
- Max response size - raise it to 8 MB. The world-countries file used below is ~0.8 MB, which brushes against the 1 MB default; higher-resolution files need more (see Troubleshooting).
In the connection’s Instructions field, tell the agent what lives there — this text is passed into the app-build sandbox:
Finally, use the wizard’s Test step with the path /gh/nvkelso/natural-earth-vector/geojson/ne_110m_admin_0_countries.geojson and Save as sample — the generator then knows the exact response shape when it builds your app.
Where else geo data can come from
Any HTTPS API an admin registers works the same way:
- Your own storage - host simplified GeoJSON for your sales regions, store catchment areas, or delivery zones on S3/GCS/your CDN, and register that origin.
- Open-data APIs - many government portals serve boundaries as GeoJSON (e.g. census, postcode, or administrative-area APIs).
- No connection at all - for small, stable shapes you can ask the agent to inline a simplified GeoJSON directly into the app source. No network, no connection to manage — best under a few hundred KB.
Prefer GeoJSON over TopoJSON sources. TopoJSON is smaller on the wire, but decoding it needs the topojson-client package, which isn’t available in the app sandbox. Natural Earth publishes plain GeoJSON, so no conversion is needed.
2. Prompt the globe
Create a new app with the Custom template. Describing the projection, the interactions, and the connection by name gets you most of the way in one prompt. This is the kind of prompt that produced the World Atlas app:
Prompting tips for maps:
- Name the projection. “Orthographic globe”, “Natural Earth projection”, “Mercator focused on Europe” — d3 supports them all, and naming one removes the guesswork.
- Name the connection and the file path. The agent sees the connection’s instructions and samples, but spelling out the exact file avoids a wrong guess.
- Describe the interactions — rotate, hover tooltips, click-to-select, metric switcher — as user-visible behavior.
- Point at inspiration. The d3 gallery is a good shared vocabulary for the look you’re after.
3. Link the connection
When you create or iterate on the app, open the resource picker in the builder and link world_geojson. The link is what authorizes the app to call the connection at runtime — the prompt text alone doesn’t grant access.
If the app calls a connection that isn’t linked, the fetch fails with:
You can review and change an app’s linked connections from the same picker when iterating, without regenerating the app.
Under the hood, the generated code fetches shapes like this:
and the globe itself is standard d3-geo — a projection, a path generator, and one SVG <path> per country:
You rarely write this by hand — but recognizing the shape helps you iterate with precise follow-up prompts (“slow the auto-rotation”, “thicken country borders on hover”).
A second connection: live data per location
The same mechanism works for any API. The World Atlas app links a second connection, weather (Base URL https://api.open-meteo.com, path /v1/forecast, no auth), and fetches a forecast when you click a country:
4. Add Lightdash query data
So far the colors come from properties baked into the GeoJSON. The real power move is coloring the map from your semantic layer — revenue by country, active users by region, on-time delivery by state — with the same permissions and access controls as everything else in your project.
Ask for it in an iteration prompt:
The generated code runs a Lightdash query and joins it onto the features by country code:
Mind your join keys. A map only lights up where a row in your data matches a shape in the geo file. Pick a key that exists on both sides — country code, region code, postcode, admin ID — and make sure the format matches (ISO alpha-2 vs alpha-3, upper vs lower case, leading zeros). If your warehouse uses a different code than the geo file does, either translate it in the app or expose a matching dimension in your model.
Because the query runs through Lightdash, viewers only ever see data they’re allowed to see — a map in a data app respects user attributes and project permissions like any chart.
5. Flat maps and regions
A globe is one projection swap away from a flat map. Useful iteration prompts:
- “Make it a flat world map instead of a globe” →
d3.geoNaturalEarth1()
- “Focus the map on Europe only” → filter features, then fit the projection
For sub-country maps, Natural Earth also publishes first-level admin areas (states and provinces) under the same connection path — ne_110m_admin_1_states_provinces.geojson, or the more detailed ne_50m_... variants (raise the connection’s max response size first).
Troubleshooting
This app is not linked to the requested connection - the app calls an alias that isn’t linked. Link the connection in the builder’s resource picker.
- Fetch fails or the response is cut off - the GeoJSON is bigger than the connection’s max response size. The 110m world file is ~0.8 MB (fine at 8 MB); 50m is ~5 MB; 10m files are larger still. Raise the limit, or step down a resolution — 110m is plenty for a world view.
- Countries missing or gray after a join - check the join key. Use
ADM0_A3 (see the tip above), and make sure your dimension returns ISO alpha-3 codes.
- The agent tried to
fetch() a URL directly - direct network calls are blocked in the app sandbox. Data must come through externalFetch on a linked connection. If this happens, iterate with “use the world_geojson connection instead of fetching directly”.
- No map tiles / basemap imagery - tile servers are blocked by the app’s content security policy, by design. d3’s SVG projections don’t need them. For a tile-based scatter or choropleth on a dashboard, use the core Map chart instead.
- Fonts look different than expected - external stylesheets (e.g. Google Fonts) are also blocked by the content security policy; the app falls back to system fonts. Ask for system fonts in your prompt to avoid the console noise.
Related pages