Skip to content

Show Temperature, Rain and Wind in MapLibre GL JS

On this page

The Weather API delivers temperature, rain and wind as vector tiles. This example shows all three on a MapLibre GL JS map, below the place names, with buttons to switch between them and a legend in the colors of the Maptoolkit weather map.

import * as maplibregl from "maplibre-gl";
    import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";

    const API_KEY = "YOUR_API_KEY";

    // Colors and opacities of the Maptoolkit weather map at maptoolkit.com/weather.
    const LAYERS = {
      temperature: {
        field: "degree", unit: "°C", opacity: 0.4, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.2)", type: "interpolate",
        stops: [[-30, "rgb(150, 0, 104)"], [-20, "rgb(0, 0, 120)"], [-10, "rgb(0, 138, 196)"], [0, "rgb(20, 165, 145)"],
          [10, "rgb(140, 200, 30)"], [20, "rgb(255, 219, 0)"], [30, "rgb(210, 100, 10)"], [40, "rgb(135, 0, 15)"]],
      },
      precipitation: {
        field: "mm", unit: "mm", opacity: 0.4, legendOpacity: 0.45, outline: "rgba(0, 0, 0, 0.225)", type: "interpolate",
        stops: [[0.2, "rgb(47, 75, 190)"], [1, "rgb(21, 122, 151)"], [2, "rgb(10, 165, 77)"], [5, "rgb(0, 210, 0)"],
          [12, "rgb(255, 255, 0)"], [20, "rgb(230, 100, 0)"], [25, "rgb(229, 0, 0)"], [30, "rgb(170, 0, 29)"]],
      },
      wind: {
        // The tiles carry m/s; the legend shows km/h like the Maptoolkit weather map.
        field: "ms", unit: "km/h", opacity: 0.35, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.175)", type: "step", toLabel: (ms) => Math.round(ms * 3.6),
        base: "rgb(100, 0, 254)",
        stops: [[4, "rgb(0, 50, 254)"], [6, "rgb(0, 150, 254)"], [8, "rgb(0, 230, 240)"], [10, "rgb(17, 212, 17)"],
          [12, "rgb(0, 250, 0)"], [14, "rgb(254, 254, 0)"], [16, "rgb(254, 200, 0)"], [18, "rgb(254, 150, 0)"],
          [20, "rgb(230, 100, 0)"], [22, "rgb(200, 50, 29)"], [24, "rgb(170, 0, 29)"], [26, "rgb(200, 0, 100)"], [28, "rgb(254, 0, 150)"]],
      },
    };

    function fillColor({ type, field, stops, base }) {
      return type === "step"
        ? ["step", ["get", field], base, ...stops.flat()]
        : ["interpolate", ["linear"], ["get", field], ...stops.flat()];
    }

    // One legend row per stop, highest first, tinted with the layer's own color and opacity.
    function legendRows({ stops, base, legendOpacity, toLabel = (v) => v }) {
      const rows = stops.map(([value, color]) => [toLabel(value), color]);
      if (base) rows.unshift([toLabel(stops[0][0] / 2), base]);
      return rows.reverse().map(([label, color]) =>
        `<div style="background:${color.replace("rgb(", "rgba(").replace(")", `, ${legendOpacity})`)}">${label}</div>`).join("");
    }

    const map = new maplibregl.Map({
      container: "map",
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [11, 48.5],
      zoom: 5,
      // The weather tiles start at zoom 3; below that there is nothing to show.
      minZoom: 3,
      attributionControl: { compact: false },
    });
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    function show(key) {
      for (const name of Object.keys(LAYERS)) {
        map.setLayoutProperty(`weather-${name}`, "visibility", name === key ? "visible" : "none");
      }
      document.querySelectorAll("#weather button").forEach((button) =>
        button.classList.toggle("active", button.dataset.key === key));
      document.getElementById("legend").innerHTML = `<div class="unit">${LAYERS[key].unit}</div>` + legendRows(LAYERS[key]);
    }

    // "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
    map.once("style.load", () => {
      map.addSource("weather", { type: "vector", url: `https://weather.maptoolkit.net/vector/icon.0.json?api_key=${API_KEY}` });
      // Below the first label layer, so place names stay readable on top of the weather.
      const firstSymbolId = map.getStyle().layers.find((layer) => layer.type === "symbol")?.id;

      for (const [name, layer] of Object.entries(LAYERS)) {
        map.addLayer({
          id: `weather-${name}`, type: "fill", source: "weather", "source-layer": name,
          layout: { visibility: "none" },
          paint: { "fill-color": fillColor(layer), "fill-opacity": layer.opacity, "fill-outline-color": layer.outline },
        }, firstSymbolId);
      }
      show("temperature");
    });

    document.querySelectorAll("#weather button").forEach((button) =>
      button.addEventListener("click", () => show(button.dataset.key)));

    map.addControl(new LogoControl({ variant: "com" }));
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.css" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.css" />
  <!-- MapLibre GL JS 6 is an ES module. The import map tells the browser
       where to load "maplibre-gl" from. -->
  <script type="importmap">
    {
      "imports": {
        "maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.mjs"
      }
    }
  </script>
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
    #weather { position: absolute; top: 10px; left: 10px; z-index: 1; width: 36px; font: 11px/1 system-ui, sans-serif; }
    #weather .buttons, #weather .legend { background: #fff; border-radius: 6px; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); overflow: hidden; }
    #weather button {
      display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: none; background: none;
      cursor: pointer; color: #9aa0b4;
    }
    #weather button + button { border-top: 1px solid #eee; }
    #weather button.active { color: #303f7e; background: #eef0f6; }
    #weather button svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
    #weather .legend { margin-top: 10px; text-align: center; }
    #weather .unit { padding: 5px 0; font-weight: 600; }
    #weather .legend div:not(.unit) { padding: 5px 0; color: #1f2430; }
  </style>
</head>
<body>
  <div id="map"></div>
  <div id="weather">
    <div class="buttons">
      <button data-key="temperature" title="Temperature"><svg viewBox="0 0 24 24"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></button>
      <button data-key="precipitation" title="Precipitation"><svg viewBox="0 0 24 24"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg></button>
      <button data-key="wind" title="Wind"><svg viewBox="0 0 24 24"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg></button>
    </div>
    <div class="legend" id="legend"></div>
  </div>
  <script type="module">
    import * as maplibregl from "maplibre-gl";
    import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";

    const API_KEY = "YOUR_API_KEY";

    // Colors and opacities of the Maptoolkit weather map at maptoolkit.com/weather.
    const LAYERS = {
      temperature: {
        field: "degree", unit: "°C", opacity: 0.4, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.2)", type: "interpolate",
        stops: [[-30, "rgb(150, 0, 104)"], [-20, "rgb(0, 0, 120)"], [-10, "rgb(0, 138, 196)"], [0, "rgb(20, 165, 145)"],
          [10, "rgb(140, 200, 30)"], [20, "rgb(255, 219, 0)"], [30, "rgb(210, 100, 10)"], [40, "rgb(135, 0, 15)"]],
      },
      precipitation: {
        field: "mm", unit: "mm", opacity: 0.4, legendOpacity: 0.45, outline: "rgba(0, 0, 0, 0.225)", type: "interpolate",
        stops: [[0.2, "rgb(47, 75, 190)"], [1, "rgb(21, 122, 151)"], [2, "rgb(10, 165, 77)"], [5, "rgb(0, 210, 0)"],
          [12, "rgb(255, 255, 0)"], [20, "rgb(230, 100, 0)"], [25, "rgb(229, 0, 0)"], [30, "rgb(170, 0, 29)"]],
      },
      wind: {
        // The tiles carry m/s; the legend shows km/h like the Maptoolkit weather map.
        field: "ms", unit: "km/h", opacity: 0.35, legendOpacity: 0.4, outline: "rgba(0, 0, 0, 0.175)", type: "step", toLabel: (ms) => Math.round(ms * 3.6),
        base: "rgb(100, 0, 254)",
        stops: [[4, "rgb(0, 50, 254)"], [6, "rgb(0, 150, 254)"], [8, "rgb(0, 230, 240)"], [10, "rgb(17, 212, 17)"],
          [12, "rgb(0, 250, 0)"], [14, "rgb(254, 254, 0)"], [16, "rgb(254, 200, 0)"], [18, "rgb(254, 150, 0)"],
          [20, "rgb(230, 100, 0)"], [22, "rgb(200, 50, 29)"], [24, "rgb(170, 0, 29)"], [26, "rgb(200, 0, 100)"], [28, "rgb(254, 0, 150)"]],
      },
    };

    function fillColor({ type, field, stops, base }) {
      return type === "step"
        ? ["step", ["get", field], base, ...stops.flat()]
        : ["interpolate", ["linear"], ["get", field], ...stops.flat()];
    }

    // One legend row per stop, highest first, tinted with the layer's own color and opacity.
    function legendRows({ stops, base, legendOpacity, toLabel = (v) => v }) {
      const rows = stops.map(([value, color]) => [toLabel(value), color]);
      if (base) rows.unshift([toLabel(stops[0][0] / 2), base]);
      return rows.reverse().map(([label, color]) =>
        `<div style="background:${color.replace("rgb(", "rgba(").replace(")", `, ${legendOpacity})`)}">${label}</div>`).join("");
    }

    const map = new maplibregl.Map({
      container: "map",
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [11, 48.5],
      zoom: 5,
      // The weather tiles start at zoom 3; below that there is nothing to show.
      minZoom: 3,
      attributionControl: { compact: false },
    });
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    function show(key) {
      for (const name of Object.keys(LAYERS)) {
        map.setLayoutProperty(`weather-${name}`, "visibility", name === key ? "visible" : "none");
      }
      document.querySelectorAll("#weather button").forEach((button) =>
        button.classList.toggle("active", button.dataset.key === key));
      document.getElementById("legend").innerHTML = `<div class="unit">${LAYERS[key].unit}</div>` + legendRows(LAYERS[key]);
    }

    // "style.load" fires as soon as the style is parsed; "load" would also wait for every tile.
    map.once("style.load", () => {
      map.addSource("weather", { type: "vector", url: `https://weather.maptoolkit.net/vector/icon.0.json?api_key=${API_KEY}` });
      // Below the first label layer, so place names stay readable on top of the weather.
      const firstSymbolId = map.getStyle().layers.find((layer) => layer.type === "symbol")?.id;

      for (const [name, layer] of Object.entries(LAYERS)) {
        map.addLayer({
          id: `weather-${name}`, type: "fill", source: "weather", "source-layer": name,
          layout: { visibility: "none" },
          paint: { "fill-color": fillColor(layer), "fill-opacity": layer.opacity, "fill-outline-color": layer.outline },
        }, firstSymbolId);
      }
      show("temperature");
    });

    document.querySelectorAll("#weather button").forEach((button) =>
      button.addEventListener("click", () => show(button.dataset.key)));

    map.addControl(new LogoControl({ variant: "com" }));
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create a MapLibre GL JS map of Central Europe with the Maptoolkit Weather API vector tiles, one fill layer each for temperature, precipitation and wind below the labels, buttons that switch between them, and a vertical legend, using the colors of the Maptoolkit weather map.

How it works

Weather is a vector tile source, not a REST call. One source, added with a TileJSON URL, holds a layer per value: temperature with the field degree, precipitation with mm and wind with ms. The map streams the tiles as you pan.

One fill layer per value, switched with visibility. All three layers are added once, with their own source-layer, and the buttons on the left set visibility to show one of them. The source and its tiles are shared, so switching does not download anything again.

The colors are those of the Maptoolkit weather map. Temperature and rain interpolate between the same stops, at the same opacity, as maptoolkit.com/weather, and wind uses its step expression in 2 m/s steps. A thin dark outline separates the areas. The legend is built from the same stops, highest value on top, so a color on the map always has its row. The wind tiles store m/s, and toLabel converts the legend to km/h, as on the weather map.

Below the labels. Passing the first symbol layer’s id as the second argument to addLayer inserts the weather under it, so place names stay readable on top.

Zoom limits. The weather tiles cover zoom levels 3 to 8. minZoom: 3 stops the map there; zoomed out further, the layer would disappear. Beyond zoom 8 the zoom 8 data is stretched.

The layers are added on style.load, which fires as soon as the style is ready. The more common load event also waits for every initial tile of the basemap, which delays the weather without need.

Coverage is not global: the ICON models cover Germany and its neighbors at high resolution and Europe more broadly. Outside that area the tiles are empty.

Next steps

The tiles also carry a weather_label layer with temperature, humidity, wind speed and direction per place, which a symbol layer can print on the map.

To report the exact value under the cursor, query the visible layer on click with queryRenderedFeatures() and show its field in a popup.