Skip to content

Loading data

A layer draws whatever its source points at. Most maps load a GeoJSON file that sits on the same site as the page: one you drew on geojson.io, exported from QGIS, or downloaded from an open-data portal. This guide starts there, then covers files on other sites, points written in the YAML, and sources shared between layers.

  1. Put the GeoJSON file in your site: in a data/ folder next to your HTML page, or in public/data/ in an Astro or Vite project. To follow along, download cities.geojson (five US cities, each with a name and a population).

  2. Point a source’s url: at it:

    map.yaml
    type: map
    id: cities
    config:
    center: [-97, 38]
    zoom: 2.4
    mapStyle: "https://tiles.openfreemap.org/styles/liberty"
    layers:
    - id: cities
    type: circle
    source:
    type: geojson
    url: "/data/cities.geojson"
    paint:
    circle-radius: 8
    circle-color: "#e63946"
    circle-stroke-width: 2
    circle-stroke-color: "#ffffff"
  3. Open the page through a web server (npx serve, astro dev, or your host). You should see five red dots across the United States:

Five cities loaded from /data/cities.geojson

Edit this map in the playground

The file can hold a FeatureCollection, a single Feature, or a bare geometry ({"type": "Polygon", …}); a single feature or geometry is read as a one-feature collection.

  • /data/cities.geojson starts at the site root. In Astro, public/ is the root, so public/data/cities.geojson is /data/cities.geojson.
  • data/cities.geojson (no leading slash) starts at the folder of the web page, not of the YAML file. A map at maps/city.yaml shown on index.html loads data/cities.geojson from beside index.html.
  • On a site published under a subpath, such as a GitHub Pages project site at https://<user>.github.io/<repo>/, give <ml-map> a base="/<repo>/" and keep the paths as they are. The Astro components set it for you. See Publishing & subpaths.

When a file doesn’t load, the map still draws and a notice lists the problem, for example “Data file /data/citys.geojson didn’t load (404 Not Found), so layer cities has no data”, followed by the address the browser tried.

A full URL works the same way. The USGS earthquake feeds are a good test, because their server allows other sites to read them:

type: map
id: earthquakes
config:
center: [-120, 37]
zoom: 4
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
layers:
- id: earthquakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"
paint:
circle-radius: 5
circle-color: "#ef4444"
circle-opacity: 0.7
This week's earthquakes, from the USGS feed

Edit this map in the playground

When the other site blocks the request (CORS)

Section titled “When the other site blocks the request (CORS)”

A browser only reads a file from another site if that site allows it (CORS). When it doesn’t, the console shows a CORS error and the layer stays empty. You can’t fix that from your page. Download the file and serve it from your own site instead, or ask the data provider whether they offer a CORS-enabled address. For data that changes, a small proxy on your own server is the other option.

For a handful of places you type by hand, skip the file. locations: takes a list of points, each with coordinates (longitude first) and an optional name and description:

type: map
id: stops
config:
center: [-73.98, 40.75]
zoom: 12
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
layers:
- id: stops
type: circle
source:
type: geojson
locations:
- coordinates: [-74.00, 40.73]
name: "Stop A"
- coordinates: [-73.96, 40.77]
name: "Stop B"
paint:
circle-radius: 7
circle-color: "#2563eb"

The same shorthand exists for one point, one polygon and one line. Each takes the place of url: or data: on a geojson source, and its name and description become the feature’s properties:

KeyDrawscoordinates
location:one point[lon, lat]
locations:many pointsone coordinates per list item
region:a polygona list of rings: [[[lon, lat], …]], first point repeated last
route:a linea list of at least two points: [[lon, lat], …]

When the features need other properties (a population for a popup, a category to colour by), write the GeoJSON itself under data::

source:
type: geojson
data:
type: FeatureCollection
features:
- type: Feature
geometry: { type: Point, coordinates: [-74.006, 40.7128] }
properties: { name: "New York", population: 8336817 }

Past a few dozen features, a file is easier to edit and keeps the YAML readable.

CSV files (including a Google Sheet published as CSV) aren’t supported: a layer whose url points at one shows “CSV files aren’t supported” on the map. Convert the table to GeoJSON first, for example by opening it in geojson.io or QGIS and saving it as GeoJSON, then point url at the .geojson file.

When several layers draw the same data (a fill and its outline, points and their labels), define the source once under sources: and name it from each layer:

type: map
id: cities-labelled
config:
center: [-98.5, 39.8]
zoom: 3
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
sources:
cities:
type: geojson
url: "/data/cities.geojson"
layers:
- id: city-dots
type: circle
source: cities
paint:
circle-radius: 6
circle-color: "#e63946"
- id: city-names
type: symbol
source: cities
layout:
text-field: ["get", "name"]
text-offset: [0, 1.2]

The file is downloaded once, and a live named source is polled once for all the layers that use it. A source written on a layer (source: { type: geojson, … }) belongs to that layer alone; the two forms mix freely in one document. A layer whose source: names a source the document doesn’t define fails validation at that line, with a “did you mean”.

Thousands of points in one place hide each other. With cluster: true the source groups nearby points into one feature per group, and splits them again as the reader zooms in. A named source lets three layers draw the groups, their counts and the single points:

clusters.yaml
type: map
id: clustered-earthquakes
config:
center: [-120, 37]
zoom: 3
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
sources:
earthquakes:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson"
cluster: true
clusterRadius: 50 # pixels: points this close are grouped
clusterMaxZoom: 14 # from zoom 15, every point is drawn alone
layers:
- id: clusters
type: circle
source: earthquakes
filter: ["has", "point_count"]
paint:
circle-color: "#3b82f6"
circle-opacity: 0.8
circle-radius: ["step", ["get", "point_count"], 20, 100, 30, 750, 40]
- id: cluster-count
type: symbol
source: earthquakes
filter: ["has", "point_count"]
layout:
text-field: "{point_count_abbreviated}"
text-size: 12
- id: unclustered
type: circle
source: earthquakes
filter: ["!", ["has", "point_count"]]
paint:
circle-radius: 6
circle-color: "#ef4444"
This month's earthquakes, clustered. Zoom in to see single points.

Edit this map in the playground

  • A cluster is a feature with a point_count property (and point_count_abbreviated, such as “1.2k”). ["has", "point_count"] picks out the clusters; ["!", ["has", "point_count"]] the single points.
  • The label layer has no text-font, so it uses the basemap’s font. If you name a font, it must be one the basemap provides (Layers › Labels).
  • clusterProperties adds totals to each cluster, such as the largest magnitude in it. Every clustering key is in the sources reference.

To zoom into a cluster when it’s clicked, see the gallery’s Create and style clusters.

  • Vector tiles: type: vector with a TileJSON url: or a tiles: template (https://…/{z}/{x}/{y}.pbf). Each layer then names the source-layer: it draws. See Add a vector tile source.
  • Raster tiles and WMS: type: raster with a tiles: template and tileSize: 256. See Add a raster tile source and Add a WMS source.
  • Large files (tens of megabytes and up, imagery, elevation): PMTiles, FlatGeobuf, GeoParquet and Cloud Optimized GeoTIFF read only the part in view. See Large data.
  • Data that changes: see Live data.

Which source type fits which data, elevation and image overlays included, is in Sources & layers. Every source type and key is in the sources reference.

Each map keeps a file it downloaded in memory for five minutes, so layers that load the same URL fetch it once. The memory is cleared when the page reloads. cache: { ttl: 60000 } on a source changes the time (in milliseconds), and cache: { enabled: false } turns it off.

To do something when a layer’s data arrives or fails (update a count, show a message), listen for <ml-map>’s ml-map:layer-data-loaded and ml-map:layer-data-error events; see <ml-map> events. With the JavaScript API, the same moments are renderer.on("layer:data-loaded", …) and renderer.on("layer:data-error", …).

Troubleshooting covers maps that don’t show at all. For a layer that stays empty, read the map’s notice:

  • “didn’t load (404 Not Found)”: the path is wrong for the page. Check the address the notice says the browser tried, and the path rules above.
  • “it isn’t JSON — a wrong path often returns a web page instead”: the same problem on a host that answers a missing file with a page.
  • “it is JSON but not GeoJSON”: the file has no "type": "FeatureCollection" (or Feature, or a geometry). An API response often wraps the GeoJSON in another object; point url: at an address that returns the GeoJSON itself.
  • “pages opened from a file (file://) can’t load data files”: open the page through a web server instead of double-clicking it.
  • A CORS error in the console: see above.
  • “CSV files aren’t supported”: see CSV and spreadsheets.

The “Loading map…” text and aria-busy need no setup; how to restyle or replace them is in <ml-map> styling hooks.