Skip to content
  1. Examples

Transport Network

This example shows how to use geo clustering to make a map with a large number of nodes more readable. It uses the geoClustering transformation to ungroup nodes on zoom and group them when zooming out.

ts
import Ogma from '@linkurious/ogma';
Ogma.libraries['leaflet'] = window.L;

const ogma = new Ogma<{ name: string; line: number }>({
  container: 'graph-container'
});

fetch('./france-train-stations.json')
  .then(response => response.json())
  .then(graph => ogma.setGraph(graph))
  .then(() =>
    ogma.geo.enable({
      tiles: { url: '//{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }
    })
  );

const colors = [
  '#FFFFFF',
  '#5E5348',
  '#56787D',
  '#A47E61',
  '#d99f4f',
  '#64909f',
  '#bbaca5',
  '#66a4be'
];
ogma.styles.addNodeRule({
  text: {
    content: node => node.getData('name'),
    size: node => (node.getData('name').startsWith('line') ? 10 : 12),
    font: 'IBM Plex Sans',
    minVisibleSize: 20
  },
  color: node => colors[node.getData('line')! % 8],
  innerStroke: { width: 0 },
  outerStroke: { color: '#222', width: 0.5 }
});
ogma.styles.addEdgeRule({
  color: edge => colors[edge.getData('line')! % 8]
});

ogma.styles.setHoveredNodeAttributes({
  outerStroke: { color: '#222', width: 1 },
  text: {
    backgroundColor: '#222',
    color: '#fff'
  }
});

const clustering = ogma.transformations.addGeoClustering({
  groupIdFunction: node => node.getData('line').toString(),
  nodeGenerator: (nodes, groupId) => ({
    id: groupId,
    data: {
      name:
        nodes.size > 1
          ? `line ${nodes.get(0).getData('line')}`
          : nodes.get(0).getData('name'),
      line: nodes.get(0).getData('line')
    }
  })
});

document
  .querySelector('#cluster')!
  .addEventListener('change', () => clustering.toggle());
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.3/leaflet.css"
    />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.3/leaflet.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
      rel="stylesheet"
    />
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <div class="control-bar" id="controls">
      <label>
        <span>Geo mode</span>
        <input type="checkbox" id="mode" checked />
      </label>
    </div>
    <form class="toolbar" id="ui">
      <div class="section mode">
        <label class="toggle-switch">
          <input
            id="cluster"
            type="checkbox"
            name="mode-switch"
            value="cluster"
            checked
            class="checkbox"
          />
          <div class="toggle-switch-background">
            <div class="toggle-switch-handle"></div>
          </div>
          <span>Clustering</span>
        </label>
      </div>
    </form>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
body {
  font-family: 'IBM Plex sans', sans-serif;
  margin: 0;
  padding: 0;
  background: #f5f5f5;
  color: #222222;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.toolbar {
  display: block;
  position: absolute;
  top: 20px;
  right: 20px;
  padding: 10px;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
  border-radius: 5px;
  font-weight: 300;
  z-index: 9999;
  background: white;
}

.toolbar .section {
  position: relative;
  display: block;
}

.toggle-switch {
  position: relative;
  display: inline-block;
  width: 80px;
  height: 40px;
  cursor: pointer;
}

.toggle-switch input[type='checkbox'] {
  display: none;
}

.toggle-switch-background {
  width: 100%;
  height: 100%;
  background-color: #ddd;
  border-radius: 20px;
  box-shadow: inset 0 0 0 2px #ccc;
  transition: background-color 0.3s ease-in-out;
}

.toggle-switch-handle {
  position: absolute;
  top: 5px;
  left: 5px;
  width: 30px;
  height: 30px;
  background-color: #fff;
  border-radius: 50%;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
  transition: transform 0.3s ease-in-out;
}

.toggle-switch::before {
  content: '';
  position: absolute;
  top: -25px;
  right: -35px;
  font-size: 12px;
  font-weight: bold;
  color: #aaa;
  text-shadow: 1px 1px #fff;
  transition: color 0.3s ease-in-out;
}

.toggle-switch input[type='checkbox']:checked + .toggle-switch-handle {
  transform: translateX(45px);
  box-shadow:
    0 2px 5px rgba(0, 0, 0, 0.2),
    0 0 0 3px #05c46b;
}

.toggle-switch input[type='checkbox']:checked + .toggle-switch-background {
  background-color: #05c46b;
  box-shadow: inset 0 0 0 2px #04b360;
}

.toggle-switch input[type='checkbox']:checked + .toggle-switch:before {
  content: 'On';
  color: #05c46b;
  right: -15px;
}

.toggle-switch
  input[type='checkbox']:checked
  + .toggle-switch-background
  .toggle-switch-handle {
  transform: translateX(40px);
}

.toggle-switch span {
  display: block;
  margin-top: 0.5em;
  text-align: center;
}
json
{
  "nodes": [
    {
      "id": 1,
      "data": {
        "name": "Bandol",
        "latitude": 43.14049104738302,
        "longitude": 5.749815526229215,
        "line": 0,
        "km": 50
      }
    },
    {
      "id": 2,
      "data": {
        "name": "Pouilly-sur-Loire",
        "latitude": 47.28252848176941,
        "longitude": 2.965051211539134,
        "line": 1,
        "km": 214
      }
    },
    {
      "id": 3,
      "data": {
        "name": "Arbois",
        "latitude": 46.91270132076251,
        "longitude": 5.763090017438219,
        "line": 2,
        "km": 401
      }
    },
    {
      "id": 4,
      "data": {
        "name": "Cases-de-Pène",
        "latitude": 42.77898338901533,
        "longitude": 2.7931166200241657,
        "line": 3,
        "km": 463
      }
    },
    {
      "id": 5,
      "data": {
        "name": "Nantes",
        "latitude": 47.22623886712025,
        "longitude": -1.5187659795664639,
        "line": 4,
        "km": 428
      }
    },
    {
      "id": 6,
      "data": {
        "name": "Culmont-Chalindrey",
        "latitude": 47.808466511639466,
        "longitude": 5.4459063706995465,
        "line": 5,
        "km": 390
      }
    },
    {
      "id": 7,
      "data": {
        "name": "La Penne-sur-Huveaune",
        "latitude": 43.28478914045096,
        "longitude": 5.5155538408980345,
        "line": 0,
        "km": 12
      }
    },
    {
      "id": 8,
      "data": {
        "name": "Gannat",
        "latitude": 46.097617862497984,
        "longitude": 3.2048652200449,
        "line": 6,
        "km": 378
      }
    },
    {
      "id": 9,
      "data": {
        "name": "Haluchère-Batignolles",
        "latitude": 47.24914745463753,
        "longitude": -1.5225453757032819,
        "line": 4,
        "km": 433
      }
    },
    {
      "id": 10,
      "data": {
        "name": "La Chartre-sur-le-Loir",
        "latitude": 47.740608359192024,
        "longitude": 0.581929079377478,
        "line": 7,
        "km": 201
      }
    },
    {
      "id": 11,
      "data": {
        "name": "Feuquières-Broquiers",
        "latitude": 49.6545131273755,
        "longitude": 1.83106831857752,
        "line": 8,
        "km": 119
      }
    },
    {
      "id": 12,
      "data": {
        "name": "Amiens",
        "latitude": 49.8899171570771,
        "longitude": 2.3092764940777903,
        "line": 9,
        "km": 130
      }
    },
    {
      "id": 13,
      "data": {
        "name": "Labège-Village",
        "latitude": 43.530185715400734,
        "longitude": 1.5332979503119948,
        "line": 10,
        "km": 267
      }
    },
    {
      "id": 14,
      "data": {
        "name": "Lardy",
        "latitude": 48.51996498537976,
        "longitude": 2.2535711520859407,
        "line": 11,
        "km": 42
      }
    },
    {
      "id": 15,
      "data": {
        "name": "Le Soler",
        "latitude": 42.678079974423696,
        "longitude": 2.7876024386942424,
        "line": 12,
        "km": 474
      }
    },
    {
      "id": 16,
      "data": {
        "name": "L'Estaque",
        "latitude": 43.36379521414173,
        "longitude": 5.321365695857461,
        "line": 13,
        "km": 870
      }
    },
    {
      "id": 17,
      "data": {
        "name": "Pont-à-Mousson",
        "latitude": 48.9002071898534,
        "longitude": 6.050608194237219,
        "line": 14,
        "km": 362
      }
    },
    {
      "id": 18,
      "data": {
        "name": "Les Échets",
        "latitude": 45.87449953551212,
        "longitude": 4.910745269670009,
        "line": 15,
        "km": 20
      }
    },
    {
      "id": 19,
      "data": {
        "name": "Villeneuve-le-Roi",
        "latitude": 48.73821835135393,
        "longitude": 2.4269289897862905,
        "line": 11,
        "km": 12
      }
    },
    {
      "id": 20,
      "data": {
        "name": "Uckange",
        "latitude": 49.303441043459934,
        "longitude": 6.156190363131995,
        "line": 16,
        "km": 181
      }
    },
    {
      "id": 21,
      "data": {
        "name": "Brunémont",
        "latitude": 50.27814083996918,
        "longitude": 3.146083415609878,
        "line": 17,
        "km": 210
      }
    },
    {
      "id": 22,
      "data": {
        "name": "Arches",
        "latitude": 48.11988792019504,
        "longitude": 6.528289800019224,
        "line": 18,
        "km": 11
      }
    },
    {
      "id": 23,
      "data": {
        "name": "Calais-Fréthun",
        "latitude": 50.90157078712958,
        "longitude": 1.8115554027850775,
        "line": 19,
        "km": 112
      }
    },
    {
      "id": 24,
      "data": {
        "name": "Beillant",
        "latitude": 45.701017141940206,
        "longitude": -0.5268853168032458,
        "line": 7,
        "km": 501
      }
    },
    {
      "id": 25,
      "data": {
        "name": "St-Denis-de-Pile",
        "latitude": 44.97850876287167,
        "longitude": -0.19795164441633,
        "line": 11,
        "km": 539
      }
    },
    {
      "id": 26,
      "data": {
        "name": "La Courneuve-

...