Skip to content
  1. Examples

Geo mode with layouts new

This example shows how to use node grouping to group nodes in geo mode. In addition, it uses a reprojection trick to display the nodes that have no coordinates in a topology layout around the nodes that have coordinates. You can check using the scale control on the right bottom corner that the nodes are roughly 300 meters apart from each other.

ts
import Ogma, {
  LayoutOptions,
  Node,
  NodeList,
  RawGraph
} from '@linkurious/ogma';
import { GUI } from 'dat.gui';
const ogma = new Ogma({
  container: 'graph-container'
});

const hexToRgba = (hex: string, alpha: number) => {
  // convert hex color to rgba
  const r = parseInt(hex.substring(1, 3), 16);
  const g = parseInt(hex.substring(3, 5), 16);
  const b = parseInt(hex.substring(5, 7), 16);

  return `rgba(${r},${g},${b},${alpha})`;
};
interface NodeData {
  type: 'Central Office' | 'Department' | 'District Head Office';
  district: number;
}

interface Feature<Properties> {
  type: 'Feature';
  id: number;
  geometry: {
    type: 'Point';
    coordinates: [number, number];
  };
  properties: Properties;
}

const districtColor: Record<number, string> = {
  11: '#f0a202',
  17: '#f18805',
  13: '#d95d39',
  19: '#202c59'
};
ogma.styles.addNodeRule(node => node.isVirtual(), {
  color: node => {
    const district = node.getData('district') as number | undefined;
    if (district !== undefined) {
      const color = districtColor[district];
      return hexToRgba(color, 0.5);
    }
  }
});

ogma.styles.addRule({
  edgeAttributes: {
    color: '#333',
    width: 2
  }
});

ogma.styles.addEdgeRule(edge => edge.isVirtual(), {
  color: '#fff',
  stroke: {
    color: '#202c59',
    width: 2
  }
});

const graph: RawGraph<Feature<NodeData>, unknown> = await fetch(
  './stores.json'
).then(response => response.json());

await ogma.setGraph(graph);
// for the purpose of the example, remove the
// geo coordinates on all the leaf nodes
ogma
  .getNodes()
  .filter(node => node.getDegree() === 1)
  .setData('geometry.coordinates', () => undefined);

// bould the groups map
const groups = ogma.getNodes().reduce((acc: Map<number, NodeList>, node) => {
  const district = node.getData('properties.district');
  let group = acc.get(district);
  // central office
  if (district === undefined) return acc;
  // create new group if it doesn't exist
  if (!group) acc.set(district, node.toList());
  // add to existing group
  else acc.set(district, group.concat(node.toList()));
  return acc;
}, new Map<number, NodeList>());

// group cores, the node with geo coordinates
const centers = Array.from(groups.entries()).reduce(
  (acc, [district, nodes]) => {
    const center = nodes
      .filter(node => node.getData('geometry.coordinates'))
      .get(0);
    return acc.set(district, center);
  },
  new Map<number, Node>()
);

groups.forEach(async (nodes, district) => {
  const center = centers.get(district)?.getData('geometry.coordinates');
  await runLayout('grid', center, nodes, 300);
});
// enable geo mode
await ogma.geo.enable({
  latitudePath: 'geometry.coordinates.1',
  longitudePath: 'geometry.coordinates.0'
});
// add scale control to be able to compare the distances
ogma.geo.getMap()?.addControl(
  // @ts-expect-error Leaflet types need to be installed separately
  L.control.scale({
    maxWidth: 250,
    imperial: false,
    position: 'bottomright'
  })
);

// add grouping for readability
ogma.transformations.addNodeGrouping({
  groupIdFunction: node => {
    return `group-${node.getData('properties.district')}`;
  },
  nodeGenerator: (nodes, id) => {
    const district = nodes.get(0).getData('properties.district');
    return {
      id,
      data: {
        type: 'group',
        district
      }
    };
  },
  padding: 10,
  showContents: true,
  enabled: true
});

const gui = new GUI({ width: 300 });
type LayoutType = 'hierarchical' | 'force' | 'grid';
const settings = {
  layout: 'grid' as LayoutType
};

gui
  .add(settings, 'layout', ['hierarchical', 'force', 'grid'])
  .onChange(async (type: LayoutType) => {
    const distance = 300;
    ogma
      .getNodes()
      .filter(node => node.isVirtual())
      .forEach(group => {
        runLayout(
          type,
          group.getData('geometry.coordinates'),
          group.getSubNodes()!,
          distance
        );
      });
  });

function runLayout(
  type: 'hierarchical' | 'force' | 'grid',
  center: [number, number] | undefined,
  nodes: NodeList,
  distance: number
) {
  const options = {
    nodes,
    latitudePath: 'geometry.coordinates.1',
    longitudePath: 'geometry.coordinates.0',
    center: {
      latitude: center![1],
      longitude: center![0]
    }
  };
  switch (type) {
    case 'hierarchical':
      return ogma.geo.runLayout({
        ...options,
        type: 'hierarchical',
        options: {
          nodeDistance: distance,
          levelDistance: distance,
          gridDistance: distance,
          arrangeComponents: 'grid'
        }
      });
    case 'force':
      return ogma.geo.runLayout({
        ...options,
        type: 'force',
        options: {
          nodes,
          charge: 50,
          edgeLength: distance
        }
      });
    case 'grid':
      return ogma.geo.runLayout({
        ...options,
        type: 'grid',
        options: {
          nodes,
          colDistance: distance,
          rowDistance: distance
        }
      });
  }
}
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>
    <script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="graph-container"></div>
    <form class="toolbar" id="ui">
    <script type="module" src="./index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.dg.ac {
  z-index: 999 !important;
}
json
{
  "nodes": [
    {
      "id": "0",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 17
        },
        "geometry": {
          "coordinates": [
            2.302915632687615,
            48.89113528883203
          ],
          "type": "Point"
        },
        "id": 0
      }
    },
    {
      "id": "1",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "District Head Office",
          "district": 17
        },
        "geometry": {
          "coordinates": [
            2.3154087597822297,
            48.891032935711166
          ],
          "type": "Point"
        },
        "id": 1
      }
    },
    {
      "id": "2",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 17
        },
        "geometry": {
          "coordinates": [
            2.2997031955045486,
            48.887555624507996
          ],
          "type": "Point"
        },
        "id": 2
      }
    },
    {
      "id": "3",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 17
        },
        "geometry": {
          "coordinates": [
            2.3097045180901716,
            48.88542513348682
          ],
          "type": "Point"
        },
        "id": 3
      }
    },
    {
      "id": "4",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 17
        },
        "geometry": {
          "coordinates": [
            2.310695272037208,
            48.892007923790544
          ],
          "type": "Point"
        },
        "id": 4
      }
    },
    {
      "id": "5",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Central Office"
        },
        "geometry": {
          "coordinates": [
            2.3406616033647083,
            48.87162697244372
          ],
          "type": "Point"
        },
        "id": 5
      }
    },
    {
      "id": "6",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "District Head Office",
          "district": 19
        },
        "geometry": {
          "coordinates": [
            2.377639836901892,
            48.89497890039473
          ],
          "type": "Point"
        },
        "id": 6
      }
    },
    {
      "id": "7",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 19
        },
        "geometry": {
          "coordinates": [
            2.391277381644727,
            48.89082669920518
          ],
          "type": "Point"
        },
        "id": 7
      }
    },
    {
      "id": "8",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 19
        },
        "geometry": {
          "coordinates": [
            2.382738046821686,
            48.88579998124541
          ],
          "type": "Point"
        },
        "id": 8
      }
    },
    {
      "id": "9",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 11
        },
        "geometry": {
          "coordinates": [
            2.383380793843571,
            48.85530666732532
          ],
          "type": "Point"
        },
        "id": 9
      }
    },
    {
      "id": "10",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 11
        },
        "geometry": {
          "coordinates": [
            2.3781030418253124,
            48.856849903008055
          ],
          "type": "Point"
        },
        "id": 10
      }
    },
    {
      "id": "11",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "District Head Office",
          "district": 11
        },
        "geometry": {
          "coordinates": [
            2.3874811860540035,
            48.85096366936003
          ],
          "type": "Point"
        },
        "id": 11
      }
    },
    {
      "id": "12",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 11
        },
        "geometry": {
          "coordinates": [
            2.386708625877816,
            48.861881748592765
          ],
          "type": "Point"
        },
        "id": 12
      }
    },
    {
      "id": "13",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "Department",
          "district": 13
        },
        "geometry": {
          "coordinates": [
            2.3491292671401425,
            48.8305793466796
          ],
          "type": "Point"
        },
        "id": 13
      }
    },
    {
      "id": "14",
      "data": {
        "type": "Feature",
        "properties": {
          "type": "District Head Office",
          "district": 13
        },
        "geometry": {
 

...