Skip to content
  1. Examples

Geo mode + marker clustering new

ogma-playground

Open this example online

Ogma documentation

How to run

Before running this example, please replace YOUR_ACCESS_KEY with your API key (which you can get from here) in package.json. Then :

sh
npm install
npm start

License (c) 2025 Linkurious SAS

ts
import Ogma, { GeoCoordinate } from '@linkurious/ogma';
import L from 'leaflet';
import supercluster from 'supercluster';
import { clusterMarker } from './custom_marker';

import 'leaflet/dist/leaflet.css';

Ogma.libraries['leaflet'] = L;

const ogma = new Ogma({
  container: 'graph-container'
});
ogma.styles.addRule({
  nodeAttributes: {
    color: '#0093ff',
    innerStroke: {
      width: 0
    },
    text: {
      content: node => node.getId(),
      color: '#fff',
      backgroundColor: '#333'
    }
  },
  edgeAttributes: {
    color: 'black',
    width: 1
  }
});
ogma.styles.setHoveredNodeAttributes({
  text: { backgroundColor: '#000' }
});
const markers = L.geoJSON(null, {
  pointToLayer: clusterMarker
});

const graph = await Ogma.parse.jsonFromUrl('./graph.json');
await ogma.setGraph(graph);

const center = ogma
  .getNodes()
  .get(0)
  .getGeoCoordinates() as Required<GeoCoordinate>;

const randomGeoJSON = randomPoints(1450, 15, center);

// create cluster index
const index = new supercluster({
  log: false,
  // pixels
  radius: 50,
  extent: 512,
  maxZoom: 20
}).load(randomGeoJSON.features);

await toggleGeo();

// update clusters
function update() {
  const bounds = getBounds(ogma.geo.getMap()!);
  const clusters = index.getClusters(bounds, ogma.geo.getZoom());

  // quick and dirty update method, you can implement a viewport cache
  markers.clearLayers();
  markers.addData(clusters);
}

async function toggleGeo() {
  await ogma.geo.toggle({
    disableNodeDragging: false,
    duration: 0
  });
  if (ogma.geo.enabled()) {
    markers.addTo(ogma.geo.getMap()!);
    ogma.geo.setView(48.06706753191901, 26.630859375, 7);
    ogma.events.on('viewChanged', update);
  }
}

function getBounds(map: L.Map) {
  const bounds = map.getBounds();
  return [
    bounds.getWest(),
    bounds.getSouth(),
    bounds.getEast(),
    bounds.getNorth()
  ];
}

function randomPoints(amount: number, size: number, center: GeoCoordinate) {
  return {
    features: Array.from({ length: amount }, () => {
      const lat = center.latitude! + (size / 2) * (Math.random() - 0.5);
      const lon = center.longitude! + size * (Math.random() - 0.5);
      return {
        type: 'Feature',
        properties: {},
        geometry: {
          type: 'Point',
          coordinates: [lon, lat]
        }
      };
    })
  };
}

document.getElementById('download')!.addEventListener('click', () => {
  ogma.export.png({ clip: true, width: 1280, height: 720 });
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@linkurious/ogma-annotations@1.1.16/dist/style.css"
    />
    <link type="text/css" rel="stylesheet" href="styles.css" />
    <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"
    />
  </head>
  <body>
    <div id="graph-container"></div>
    <div id="controls">
      <button id="download" title="Download annotations"></button>
    </div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
html,
body {
  font-family: 'IBM Plex Sans', Arial, Helvetica, sans-serif;
}

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

#controls {
  position: absolute;
  top: 2em;
  right: 2em;
  display: flex;
  z-index: 1000;
}

#controls button {
  width: 3em;
  height: 2.5em;
  margin: 0;
  border: 1px solid rgba(0, 0, 0, 0.2);
  background-color: #fff;
  color: #444;
  cursor: pointer;
  background-size: 50%;
  background-repeat: no-repeat;
  background-position: center center;
  margin-left: -1px;
  border-radius: 3px;
}

#controls button:hover,
#controls button:active,
#controls button.active {
  color: #222;
  background-color: #f0f0f0;
  border-color: rgba(0, 0, 0, 0.4);
}

#controls button:disabled {
  cursor: not-allowed;
  color: #999;
  background-color: #f9f9f9;
  border-color: rgba(0, 0, 0, 0.2);
  opacity: 0.5;
}

#draw-arrow {
  background-image: url('./img/icon-arrow.svg');
}

#draw-text {
  background-image: url('./img/icon-textbox.svg');
}

#download {
  background-image: url('./img/icon-download.svg');
}

#snapshot {
  background-image: url('./img/icon-photo.svg');
}

.marker-cluster-small {
  background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
  background-color: rgba(110, 204, 57, 0.6);
}

.marker-cluster-medium {
  background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
  background-color: rgba(240, 194, 12, 0.6);
}

.marker-cluster-large {
  background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
  background-color: rgba(241, 128, 23, 0.6);
}

.marker-cluster {
  background-clip: padding-box;
  border-radius: 20px;
}
.marker-cluster div {
  width: 30px;
  height: 30px;
  margin-left: 5px;
  margin-top: 5px;

  text-align: center;
  border-radius: 15px;
}
.marker-cluster span {
  line-height: 30px;
}
json
{
  "name": "ogma-embed-ogma-playground",
  "dependencies": {
    "@linkurious/ogma-annotations": "1.1.23",
    "d3": "7.9.0",
    "@linkurious/ogma": "https://get.linkurio.us/api/get/npm/ogma/undefined/?secret=YOUR_API_KEY"
  }
}
ts
import {
  Marker,
  MarkerOptions,
  LatLng,
  LatLngTuple,
  divIcon,
  circleMarker,
  point
} from 'leaflet';
import { Feature, Point } from 'geojson';
const MARKER_STYLES = {
  small: {
    border: `rgba(181, 226, 140, 0.6)`,
    fill: `rgba(110, 204, 57, 0.6)`
  },

  medium: {
    border: `rgba(241, 211, 87, 0.6)`,
    fill: `rgba(240, 194, 12, 0.6)`
  },

  large: {
    border: `rgba(253, 156, 115, 0.6)`,
    fill: `rgba(241, 128, 23, 0.6)`
  }
};

interface CustomMarkerClass extends Marker {
  export: (ctx: CanvasRenderingContext2D) => Promise<void>;
  feature: Feature<
    Point,
    {
      point_count: number;
      point_count_abbreviated: string;
      cluster: boolean;
    }
  >;
}

export const CustomMarker: {
  new (latlng: LatLng | LatLngTuple, options: MarkerOptions): CustomMarkerClass;
} = Marker.extend({
  export: function (ctx: CanvasRenderingContext2D) {
    const latlng = this.getLatLng();
    const { x, y } = ogma.geo.getMap().latLngToContainerPoint(latlng);
    const count = this.feature.properties.point_count!;
    const size: keyof typeof MARKER_STYLES =
      count < 100 ? 'small' : count < 1000 ? 'medium' : 'large';

    const { fill, border } = MARKER_STYLES[size];
    ctx.save();
    ctx.translate(x, y);
    ctx.fillStyle = fill;
    ctx.strokeStyle = border;
    ctx.lineWidth = 2 * devicePixelRatio;
    ctx.beginPath();
    ctx.arc(0, 0, 10 * devicePixelRatio, 0, Math.PI * 2);

    ctx.fill();
    ctx.stroke();

    ctx.fillStyle = MARKER_STYLES[size].fill;
    ctx.fill();

    // Draw the text
    ctx.font = `${devicePixelRatio * 12}px Arial`;
    ctx.fillStyle = '#222';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(this.feature.properties.point_count_abbreviated, 0, 2);

    ctx.stroke();
    ctx.restore();
    return Promise.resolve();
  }
});

export const customMarker = (
  latlng: LatLng | LatLngTuple,
  options: MarkerOptions
) => new CustomMarker(latlng, options);

export function clusterMarker(
  feature: Feature<
    Point,
    {
      cluster?: boolean;
      point_count?: number;
      point_count_abbreviated?: string;
    }
  >
) {
  if (feature.properties!.cluster) {
    const count = feature.properties.point_count!;
    const size = count < 100 ? 'small' : count < 1000 ? 'medium' : 'large';
    const icon = divIcon({
      html: `<div><span>${feature.properties.point_count_abbreviated}</span></div>`,
      className: `marker-cluster marker-cluster-${size}`,
      iconSize: point(40, 40)
    });
    return customMarker(
      feature.geometry.coordinates.slice().reverse() as LatLngTuple,
      {
        icon
      }
    );
  }
  return circleMarker(
    feature.geometry.coordinates.slice().reverse() as LatLngTuple,
    {
      color: feature.properties.cluster ? 'red' : '#0093ff',
      radius: feature.properties.cluster ? 10 : 5
    }
  );
}
json
{
  "nodes": [
    {
      "id": "A",
      "data": {
        "latitude": 47.887458688596475,
        "longitude": 25.46383666992188
      },
      "attributes": {
        "radius": 10,
        "x": 0,
        "y": 0,
        "color": "darkred"
      }
    },
    {
      "id": "B",
      "data": {
        "latitude": 43.905630088673846,
        "longitude": 15.47723007202148
      },
      "attributes": {
        "radius": 10,
        "x": 100,
        "y": 0,
        "color": "darkred"
      }
    },
    {
      "id": "C",
      "data": {
        "latitude": 39.905630088673846,
        "longitude": 17.47723007202148
      },
      "attributes": {
        "radius": 10,
        "x": 100,
        "y": 0,
        "color": "darkred"
      }
    }
  ],
  "edges": [
    {
      "source": "A",
      "target": "B",
      "attributes": { "width": 5 }
    },
    {
      "source": "C",
      "target": "A",
      "attributes": { "width": 5 }
    }
  ]
}