Almost every Looker project eventually gets the same request: "can we see this on a map?" Stores, warehouses, delivery routes, service territories, customers by postcode — the data is already in the warehouse, and Looker ships map visualisations out of the box. And yet the first attempt usually produces one of three results: a blank map, a map with a single dot in the Gulf of Guinea (hello, 0,0), or a map that takes ninety seconds and melts the warehouse.
Geospatial is one of the few areas of Looker where the visualisation layer depends almost entirely on getting the LookML types right. This tutorial walks through the modelling side properly: which dimension types produce which maps, how map_layer and custom TopoJSON work, how to model warehouse-native geography columns such as BigQuery GEOGRAPHY, and how to keep maps fast.
The four map visualisations, and what each one needs
Looker has four native map types, and each one requires a specific shape of data. Choosing the visualisation first and the model second is the usual cause of a blank map.
| Visualisation | Needs | Typical use |
|---|---|---|
| Map (points) | A dimension of type: location | Individual stores, assets, events |
| Map (regions / choropleth) | A dimension with a map_layer_name | Sales by state, country, custom territory |
| Google Maps | type: location, or a geocodable string | Point maps with Google base tiles and drilling |
| Static map (single value) | A single type: location row | A dashboard tile pinned to one site |
The interactive point map and the choropleth are the two you will build most, and they are driven by different LookML.
Point maps: type: location
A location dimension is not a single column. It is a pair of columns declared together:
dimension: store_location {
type: location
sql_latitude: ${TABLE}.latitude ;;
sql_longitude: ${TABLE}.longitude ;;
label: "Store Location"
}
Notes that save hours:
- There is no
sql:parameter on a location dimension. Usesql_latitudeandsql_longitude. A copiedsql:line is silently useless. - Latitude and longitude must be numeric. Strings from a CSV load will render nothing. Cast in the view, not in the visualisation:
sql_latitude: CAST(${TABLE}.lat AS FLOAT64) ;;. - Looker plots the dimension as a
lat,lonstring in the data table. If you need the parts separately for a tooltip or export, add plaintype: numberdimensions alongside it. - Guard against junk coordinates in the model rather than teaching every analyst to filter them:
dimension: store_location {
type: location
sql_latitude:
CASE WHEN ${TABLE}.latitude BETWEEN -90 AND 90
AND ${TABLE}.longitude BETWEEN -180 AND 180
AND NOT (${TABLE}.latitude = 0 AND ${TABLE}.longitude = 0)
THEN ${TABLE}.latitude END ;;
sql_longitude:
CASE WHEN ${TABLE}.latitude BETWEEN -90 AND 90
AND ${TABLE}.longitude BETWEEN -180 AND 180
AND NOT (${TABLE}.latitude = 0 AND ${TABLE}.longitude = 0)
THEN ${TABLE}.longitude END ;;
}
The 0,0 exclusion matters more than it looks: nulls and failed geocodes land there, and a cluster of them in the Atlantic is the single most common "the map is wrong" ticket.
Point maps are row-level maps
A point map plots one marker per row of the result set. Plot 200,000 rows and the browser, not the warehouse, is what falls over. Aggregate before you map:
- Group to a site, hex, or geohash dimension and plot one point per group with a sized measure.
- Or add an explore-level
limitand a required filter so nobody runs the unbounded version.
A cheap grid rollup in BigQuery:
dimension: geohash_5 {
type: string
sql: ST_GEOHASH(ST_GEOGPOINT(${TABLE}.longitude, ${TABLE}.latitude), 5) ;;
}
dimension: geohash_centroid {
type: location
sql_latitude: ST_Y(ST_GEOGPOINTFROMGEOHASH(${geohash_5})) ;;
sql_longitude: ST_X(ST_GEOGPOINTFROMGEOHASH(${geohash_5})) ;;
}
Now "events by area" is a few hundred points instead of a few hundred thousand.
Choropleths: built-in map layers
For region maps, Looker does not want coordinates. It wants a key that matches a feature in a map layer. The built-in types are the fast path:
dimension: country {
type: string
map_layer_name: countries
sql: ${TABLE}.country_iso ;;
}
dimension: state {
type: string
map_layer_name: us_states
sql: ${TABLE}.state_name ;;
}
dimension: postcode {
type: zipcode
sql: ${TABLE}.zip5 ;;
}
Looker ships layers including countries, uk_postcode_areas, us_states, us_counties, us_zipcode_tabulation_areas and us_congressional_districts, plus the shorthand dimension types type: zipcode and type: region.
The entire game here is key matching. Built-in layers match on specific property values — ISO codes for countries, full names or FIPS codes for US geographies — and a mismatch renders a grey, empty map with no error. Practical rules:
- Normalise in LookML, not in the warehouse export:
sql: UPPER(TRIM(${TABLE}.country_iso)) ;;. - Two-letter versus three-letter ISO codes are the classic failure. Pick one and match the layer.
- Keep a "was this mapped?" measure so blank regions are visible as data quality, not mystery:
measure: unmapped_rows {
type: count
filters: [country: "-NOT NULL"]
hidden: no
}
Custom map layers: your own TopoJSON
Business geography rarely matches government geography. Sales territories, delivery zones, franchise areas and service regions all need a custom layer, declared once in the model file and referenced from any dimension.
map_layer: sales_territories {
file: "/maps/sales_territories.topojson"
property_key: "territory_code"
property_label_key: "territory_name"
projection: "mercator"
extents_json_url: ""
}
dimension: territory {
type: string
map_layer_name: sales_territories
sql: ${TABLE}.territory_code ;;
}
What matters in practice:
- TopoJSON, not GeoJSON. Convert with
geo2topofrom thetopojsontoolchain, and simplify aggressively (toposimplify) — a 40 MB boundary file will make every dashboard load feel broken. Aim for well under a few megabytes. property_keyis the join key. Its values must match your dimension's SQL output exactly, including case and leading zeros. Zero-padded codes stored as integers in the warehouse are a frequent break:sql: LPAD(CAST(${TABLE}.zone AS STRING), 3, '0') ;;.- The file lives in the LookML project (commit it under a
maps/folder) or can be served from a URL withfile:replaced by a hosted reference. Committing it means territory boundary changes go through Git review like any other model change — which is exactly what you want when the boundaries decide who gets commission. - Set
projectiondeliberately.mercatoris the usual choice;equirectangularoralbersUsasuit specific extents.
Warehouse-native geography: BigQuery GEOGRAPHY and friends
Modern warehouses have real spatial types, and Looker can lean on them instead of reimplementing geometry in Liquid.
A GEOGRAPHY column cannot be shown directly, so expose the pieces Looker understands and push the spatial work down to SQL:
dimension: customer_point {
type: location
sql_latitude: ST_Y(${TABLE}.geo) ;;
sql_longitude: ST_X(${TABLE}.geo) ;;
}
dimension: distance_to_store_km {
type: number
sql: ST_DISTANCE(${TABLE}.geo, ${stores.geo}) / 1000 ;;
value_format_name: decimal_1
}
dimension: within_service_area {
type: yesno
sql: ST_CONTAINS(${territories.boundary}, ${TABLE}.geo) ;;
}
A spatial join in an explore is legitimate but expensive; it will not use ordinary indexes and in BigQuery it scans. Two mitigations that work:
- Materialise the expensive assignment (customer to territory, event to zone) in a persistent derived table refreshed on a datagroup, and join to the resulting key rather than recomputing
ST_CONTAINSon every query. - Pre-filter with a cheap bounding box before the exact predicate, so the expensive function runs on far fewer rows.
Snowflake (ST_DISTANCE, ST_WITHIN), Databricks (h3_* functions) and Postgres/PostGIS all map onto the same pattern: the geometry lives in SQL, LookML exposes typed, cheap-to-render outputs.
A parameterised radius filter
Users almost always want "within X km of Y". That is a filtered measure driven by templated filters:
filter: center_latitude {
type: number
suggest_dimension: store.latitude
}
filter: center_longitude {
type: number
}
dimension: distance_from_center_km {
type: number
sql: ST_DISTANCE(
${TABLE}.geo,
ST_GEOGPOINT({% parameter center_longitude %}, {% parameter center_latitude %})
) / 1000 ;;
}
Wrap the parameters in a required filter on the explore so an empty value never produces a full-table distance calculation.
Making maps useful, not just pretty
- Drills. Point maps honour
drill_fields; a marker click that returns the site record, last order date and owner turns a map into an operational tool instead of decoration. - Value formatting and labels. Choropleth tooltips use the dimension label and the measure's
value_format; set both, or users see raw codes and unrounded floats. - Region permissions. Maps expose geography, and geography is often the unit of access control. Apply the same
access_filter/ user-attribute pattern you use elsewhere so a regional manager's map shows only their territory — otherwise a map tile becomes the one place your row-level security leaks. - Caching. Map queries are ordinary queries: attach them to a datagroup so a dashboard of six map tiles hits cache rather than six fresh spatial scans.
- Mobile and embeds. Custom TopoJSON downloads on every render. In an embedded dashboard on a slow connection, layer size is the user experience.
A checklist before you ship a map
- Coordinates are numeric, bounds-checked, and
0,0is excluded. - Point maps are aggregated or hard-limited — no unbounded row-level plots.
- Choropleth keys are normalised and verified to match the layer's
property_key; unmatched rows are measurable. - Custom TopoJSON is simplified, committed to the project, and reviewed like code.
- Spatial joins are materialised in a PDT with a refresh policy, not recomputed per query.
- Drill fields, labels and value formats are set on every mapped dimension and measure.
- Access filters apply to the geographic dimension.
Get those seven right and maps become one of the highest-engagement things in a Looker instance — the tiles executives actually open. Skip them and you get the blank grey map that quietly convinces a business that Looker "cannot do maps".
If you are modelling territories, service areas, or warehouse-native geography and want the spatial layer designed once, properly, our Looker developers do exactly this kind of work — get in touch.