OnlyMapJS Latest release
GitHub Docs

← Gallery / Layers

Draw spatial index cells

S2Layer, GeohashLayer, QuadkeyLayer and A5Layer draw spatial index cells from a column of ids. The same power stations, binned four ways.

Open full screen ↗ View source ↗
<!DOCTYPE html>
<!--
  A SPATIAL INDEX cuts the Earth into numbered cells, so a location becomes a
  short text id. Bin your rows by that id upstream and the map needs no
  geometry at all: one accessor names the id column, and the layer works out
  where each cell sits and what shape it is.

  Which index you use is usually decided for you — by the warehouse, the vendor
  feed, or the team that binned the data. deck.gl ships a layer for each of the
  four common ones, and the shapes differ enough to matter, so this page puts
  the same 21,337 power stations through all four.

  The map: pick an index in the panel on the left. Quadkey squares stretch
  badly toward the poles, being Mercator tiles. Geohash rectangles narrow as
  they climb. S2 cells stay square-ish, being faces of a cube pushed onto a
  sphere. A5 pentagons hold near-equal area everywhere, which is what makes
  counts comparable between cells.
-->
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>OnlyMapJS — Draw spatial index cells</title>
  <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css" />
  <script type="module" src="https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js"></script>
  <style>
    :root { color-scheme: dark; }
    html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
    om-map {
      display: block; height: 100vh;
      --om-widget-bg: #11131b;
      --om-widget-fg: #f3f4f8;
      --om-widget-muted: #8b90a3;
      --om-widget-border: #262a36;
      --om-widget-hover-bg: #1c202a;
    }
  </style>
</head>
<body>

  <om-map center="[15, 30]" zoom="1.5" basemap="dark-matter">

    <!-- One accessor per index is the whole difference between these four
         layers: get-s2-token, get-geohash, get-quadkey, get-pentagon. Each
         reads a column of ids and needs nothing else to place a cell.

         An id carries its own level, so none of these layers takes a cell
         size — the file decides how coarse the map is. These are S2 level 4,
         geohash precision 2, quadkey zoom 5 and A5 resolution 3, binned to
         land in the same size range.

         All four share one threshold scale, so a colour means the same thing
         from index to index and the legend holds still when you switch.

         Load cell ids from a format that carries its own column types — Arrow
         here, or JSON. A quadkey like 02301 is TEXT, and a CSV reader types a
         column of digits as numbers: the leading zero goes, and the cell goes
         with it.

         All four extrude. Add `extruded` with get-elevation and
         elevation-scale and the cells stand up as 3D bars — `gigawatts` is in
         these files and is the natural height. `wireframe` outlines their
         sides. Give the map some pitch first, and expect a tall cell to hide
         the ones behind it. -->
    <om-layer id="s2" type="S2Layer"
              data="../../data/plants-cells-s2.arrow"
              label="S2 — level 4"
              get-s2-token="$s2"
              get-fill-color="scale($plants, threshold,
                                    ['#172554', '#1d4ed8', '#22d3ee', '#facc15', '#f97316'],
                                    domain=[5, 25, 90, 300])"
              stroked get-line-color="[15, 23, 42, 160]" line-width-min-pixels="0.6"
              opacity="0.85" pickable></om-layer>

    <om-layer id="geohash" type="GeohashLayer"
              data="../../data/plants-cells-geohash.arrow"
              label="Geohash — precision 2" visible="false"
              get-geohash="$geohash"
              get-fill-color="scale($plants, threshold,
                                    ['#172554', '#1d4ed8', '#22d3ee', '#facc15', '#f97316'],
                                    domain=[5, 25, 90, 300])"
              stroked get-line-color="[15, 23, 42, 160]" line-width-min-pixels="0.6"
              opacity="0.85" pickable></om-layer>

    <om-layer id="quadkey" type="QuadkeyLayer"
              data="../../data/plants-cells-quadkey.arrow"
              label="Quadkey — zoom 5" visible="false"
              get-quadkey="$quadkey"
              get-fill-color="scale($plants, threshold,
                                    ['#172554', '#1d4ed8', '#22d3ee', '#facc15', '#f97316'],
                                    domain=[5, 25, 90, 300])"
              stroked get-line-color="[15, 23, 42, 160]" line-width-min-pixels="0.6"
              opacity="0.85" pickable></om-layer>

    <!-- A5 ids are hex strings. Pass the string straight through — the layer
         widens it to the 64-bit cell id itself. -->
    <om-layer id="a5" type="A5Layer"
              data="../../data/plants-cells-a5.arrow"
              label="A5 — resolution 3" visible="false"
              get-pentagon="$a5"
              get-fill-color="scale($plants, threshold,
                                    ['#172554', '#1d4ed8', '#22d3ee', '#facc15', '#f97316'],
                                    domain=[5, 25, 90, 300])"
              stroked get-line-color="[15, 23, 42, 160]" line-width-min-pixels="0.6"
              opacity="0.85" pickable></om-layer>

    <!-- The four indexes are alternatives, not layers to stack, so the switch
         is a radio group rather than the legend's checkboxes. `toggle-layer`
         FLIPS a layer's visibility rather than setting it, so one pick emits
         the action twice: once for the index going out, once for the one
         coming in. `this.state` is the per-instance stash that remembers
         which one is on. -->
    <om-widget position="top-left">
      <style>
        .panel { background: rgba(17,19,27,.9); color: #e6e8ef; padding: 10px 14px;
                 border-radius: 8px; font-size: 13px; max-width: 265px; line-height: 1.45; }
        .panel b { color: #facc15; }
        .schemes { display: grid; gap: 5px; margin: 10px 0 0; padding: 10px 0 0;
                   border: 0; border-top: 1px solid #262a36; }
        .schemes label { display: flex; gap: 7px; align-items: center; cursor: pointer; }
        .schemes input { accent-color: #facc15; cursor: pointer; }
      </style>
      <div class="panel">
        <b>One dataset, four grids.</b> Colour is how many power stations fall
        in the cell. Switch index and watch the same data change shape. Hover a
        cell for its tally.
        <fieldset class="schemes">
          <label><input type="radio" name="scheme" value="s2" checked> S2 — level 4</label>
          <label><input type="radio" name="scheme" value="geohash"> Geohash — precision 2</label>
          <label><input type="radio" name="scheme" value="quadkey"> Quadkey — zoom 5</label>
          <label><input type="radio" name="scheme" value="a5"> A5 — resolution 3</label>
        </fieldset>
      </div>
      <script type="om/widget">
        this.watch = [];
        this.render = (ctx) => {
          this.state.shown = "s2";
          for (const radio of this.$$('input[name="scheme"]')) {
            radio.onchange = () => {
              ctx.emit("toggle-layer", { layer: this.state.shown });
              ctx.emit("toggle-layer", { layer: radio.value });
              this.state.shown = radio.value;
            };
          }
        };
      </script>
    </om-widget>

    <!-- The legend reads its classes off get-fill-color, so the five colours
         and their breaks are never written twice. -->
    <om-widget type="legend" title="Power stations per cell" position="top-right"></om-widget>
    <om-widget type="zoom-controls" position="bottom-right"></om-widget>

    <om-behavior on="hover" layer="s2" action="show-tooltip" template="#cell-tip"></om-behavior>
    <om-behavior on="hover" layer="geohash" action="show-tooltip" template="#cell-tip"></om-behavior>
    <om-behavior on="hover" layer="quadkey" action="show-tooltip" template="#cell-tip"></om-behavior>
    <om-behavior on="hover" layer="a5" action="show-tooltip" template="#cell-tip"></om-behavior>

    <om-widget type="attribution"
               text="Power stations © World Resources Institute, Global Power Plant Database v1.3, CC BY 4.0"
               position="bottom-start"></om-widget>

    <om-fallback>
      <p style="font: 15px system-ui, sans-serif; padding: 24px; max-width: 42ch">
        <strong>This map needs JavaScript.</strong><br />
        Open this file in a web browser such as Chrome, Safari, or Firefox.
      </p>
    </om-fallback>

  </om-map>

  <template id="cell-tip">
    <div style="padding:7px 10px; border-radius:7px; background:#11131b; color:#f3f4f8;
                border:1px solid #262a36; box-shadow:0 3px 12px rgba(0,0,0,.55);
                font:12px/1.45 system-ui, sans-serif;">
      <b>{{plants}} power stations</b><br />
      <span style="color:#8b90a3">{{gigawatts}} GW installed</span>
    </div>
  </template>

</body>
</html>

The snippet above loads the latest release. The demo above it is pinned to v0.5.3, so it keeps working when a new version ships.