OnlyMapJS Latest release
GitHub Docs

← Gallery / Layers

Aggregate by H3 cells

H3HexagonLayer draws pre-binned data from a column of H3 cell ids: 45 years of cyclone tracks. H3ClusterLayer merges cells into outlines.

Open full screen ↗ View source ↗
<!DOCTYPE html>
<!--
  H3 is a global grid of hexagons, and every cell in it has an id. A row that
  carries that id needs no coordinates at all — H3HexagonLayer resolves the
  shape from the id. Reach for it when the binning already happened upstream,
  in a warehouse or a notebook, and the rows arrive counted.

  HexagonLayer is the other end of the same idea: hand it raw points and it
  bins them in the browser. Choose H3 when the bins have to line up with the
  ones the rest of your stack already uses.

  The map: every tropical cyclone track since 1980, from NOAA's IBTrACS
  archive, counted into hexagons. Colour is how many separate storms crossed
  the cell. The frame runs from Mongolia down to Australia, so both cyclone
  seasons are on screen at once — the northern one over the Philippines and the
  South China Sea, the southern one along the Australian coast. Hover a hexagon
  for its tally.
-->
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>OnlyMapJS — Aggregate by H3 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: #0d1220;
      --om-widget-fg: #eef2fb;
      --om-widget-muted: #8791ab;
      --om-widget-border: #232c42;
      --om-widget-hover-bg: #182036;
    }
  </style>
</head>
<body>

  <om-map center="[124, 13]" zoom="3.2" basemap="dark-matter">

    <!-- get-hexagon is the whole binding: point it at a column of H3 ids and
         the layer draws a cell for each one. The id carries its own
         resolution, so nothing here sets a cell size — these are resolution 3,
         roughly 60 km across. To change it, re-bin the data upstream:
         `latLngToCell(lat, lng, res)` in h3-js or `latlng_to_cell` in h3-py,
         then group by the id and count.

         A threshold scale suits a lopsided count. Most cells saw one or two
         storms and a few saw eighty, so a smooth ramp would leave almost the
         whole map in its first colour. The domain lists the BREAKS, so five
         colours take four numbers, and the legend turns them into labelled
         classes.

         coverage shrinks each hexagon inside its own cell, which is what makes
         the grid read as cells rather than as one sheet of colour.

         Add `extruded` with get-elevation and the same cells stand up as 3D
         bars. H3ClusterLayer is the sibling for outlines: give it get-hexagons
         — a LIST of ids per row — and it merges them into one shape, for a
         service area or a catchment rather than a grid. -->
    <om-layer id="storm-cells" type="H3HexagonLayer"
              data="../../data/cyclone-tracks-h3.csv"
              label="Storms crossing the cell, since 1980"
              get-hexagon="$h3"
              get-fill-color="scale($storms, threshold,
                                    ['#1e3a8a', '#0ea5e9', '#67e8f9', '#fde047', '#f97316'],
                                    domain=[3, 10, 25, 55])"
              coverage="0.94"
              opacity="0.85"
              stroked="false"
              pickable></om-layer>

    <om-widget type="legend" title="Storms since 1980" position="top-right"></om-widget>
    <om-widget type="zoom-controls" position="bottom-right"></om-widget>

    <om-widget position="top-left">
      <style>
        .panel { background: rgba(13,18,32,.88); color: #e3e9f6; padding: 10px 14px;
                 border-radius: 8px; font-size: 13px; max-width: 260px; line-height: 1.45; }
        .panel b { color: #fde047; }
      </style>
      <div class="panel">
        <b>Where hurricanes go.</b> 4,949 storms, 307,638 six-hourly fixes,
        counted into hexagons. The gap along the equator is real — a cyclone
        needs the Coriolis force to spin up.
      </div>
    </om-widget>

    <om-behavior on="hover" layer="storm-cells" action="show-tooltip" template="#cell-tip"></om-behavior>

    <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:#0d1220; color:#eef2fb;
                border:1px solid #232c42; box-shadow:0 3px 12px rgba(0,0,0,.55);
                font:12px/1.45 system-ui, sans-serif;">
      <b>{{storms}} storms</b> · peak {{max_wind}} kt<br />
      <span style="color:#8791ab">cell {{h3}}</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.