Skip to content
  1. Examples

Parallel view

This example shows how to synchronize Ogma with and Excel view. Select nodes on the graph to see it highlighted in the tabular view, and vice versa.

ts
import Ogma from '@linkurious/ogma';
import { stox, toRawGraph } from './utils';

type NodeData = {
  label: string;
};

type EdgeData = {
  weight: number;
};

const ogma = new Ogma<NodeData, EdgeData>({
  container: 'graph-container'
});
const layout = () =>
  ogma.layouts.force({
    locate: { padding: 80 }
  });

// Add some edge styling rules
ogma.styles.addEdgeRule({
  color: e => '#80ce87',
  shape: 'arrow',
  width: e => Math.log(e.getData('weight'))
});

// Add some node styling rules
ogma.styles.addNodeRule({
  text: {
    content: n => n.getData('label'),
    minVisibleSize: 2
  },
  radius: n => n.getDegree(),
  icon: {
    content: n => '\uf007',
    font: 'Font Awesome 5 Free',
    color: 'rgb(61,139,223)',
    minVisibleSize: 0
  },
  outerStroke: {
    color: 'rgb(61,139,223)',
    width: 2
  },
  color: 'white'
});

const grid = new x_spreadsheet(document.getElementById('xlsx'), {
  mode: 'read',
  row: {
    len: 18,
    height: 25
  },
  col: {
    len: 5,
    width: 100
  },
  view: {
    height: () => document.getElementById('xlsx').clientHeight,
    width: () => document.getElementById('xlsx').clientWidth
  }
});
// load the xlsx from the URL and parse it returning a JSON
fetch('files/xlsx-small.xlsx')
  .then(res => {
    if (!res.ok) throw new Error('fetch failed');
    return res.arrayBuffer();
  })
  .then(ab => {
    const data = new Uint8Array(ab);
    const workbook = XLSX.read(data, { type: 'array' });
    grid.loadData(stox(workbook));
    const { nodes, edges } = toRawGraph(workbook);
    return ogma.setGraph({ nodes, edges }).then(layout);
  });

let fromSpreadsheet = false;
let fromOgma = false;

grid.on('cell-selected', (cell, ri, ci) => {
  if (fromOgma) {
    fromOgma = false;
    return;
  }
  // check if we clicked on the nodes sheet or edges sheet.
  const type = grid.sheet.data.name;
  ogma.getNodes().setSelected(false);
  ogma.getEdges().setSelected(false);
  fromSpreadsheet = true;
  if (type === 'nodes') {
    ogma
      .getNodes()
      .get(ri - 1)
      .setSelected(true);
  } else {
    ogma
      .getEdges()
      .get(ri - 1)
      .setSelected(true);
  }
});

grid.on('cells-selected', (cell, { sri, sci, eri, eci }) => {
  if (fromOgma) {
    fromOgma = false;
    return;
  }
  const type = grid.sheet.data.name;
  ogma.getNodes().setSelected(false);
  ogma.getEdges().setSelected(false);
  fromSpreadsheet = true;
  if (type === 'nodes') {
    ogma
      .getNodes()
      .slice(sri - 1, eri - 1)
      .setSelected(true);
  } else {
    ogma
      .getEdges()
      .slice(sri - 1, eri - 1)
      .setSelected(true);
  }
});

ogma.events.on('nodesSelected', ({ nodes }) => {
  if (fromSpreadsheet) {
    fromSpreadsheet = false;
    return;
  }
  fromOgma = true;
  grid.bottombar.clickSwap2(grid.bottombar.items[0]);
  const firstId = nodes.get(0).getId();
  const firstNodeIndex = ogma
    .getNodes()
    .getId()
    .map((id, i) => (id === firstId ? i : undefined))
    .find(e => e !== undefined);

  grid.sheet.selector.set(firstNodeIndex + 1, 0);
  grid.sheet.selector.setEnd(firstNodeIndex + 1, 3);
});

ogma.events.on('edgesSelected', ({ edges }) => {
  if (fromSpreadsheet) {
    fromSpreadsheet = false;
    return;
  }
  fromOgma = true;
  grid.bottombar.clickSwap2(grid.bottombar.items[1]);
  const firstId = edges.get(0).getId();
  const firstEdgeIndex = ogma
    .getEdges()
    .getId()
    .map((id, i) => (id === firstId ? i : undefined))
    .find(e => e !== undefined);
  console.log({ firstEdgeIndex, firstId });
  grid.sheet.selector.set(firstEdgeIndex + 1, 0);
  grid.sheet.selector.setEnd(firstEdgeIndex + 1, 3);
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <link
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css"
      rel="stylesheet"
    />
    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.11.12/dist/xlsx.core.min.js"></script>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/x-data-spreadsheet@1.1.5/dist/xspreadsheet.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/x-data-spreadsheet@1.1.5/dist/xspreadsheet.js"></script>
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <div id="xlsx"></div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 40%;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
}
html{
  overflow: hidden;
}

#xlsx{
  top: 40%;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  overflow: hidden;
  margin: 0;
}
ts
export function stox(wb) {
  var out = [];
  wb.SheetNames.forEach(function (name) {
    var o = { name: name, rows: {} };
    var ws = wb.Sheets[name];
    var range = XLSX.utils.decode_range(ws['!ref']);
    // sheet_to_json will lost empty row and col at begin as default
    range.s = { r: 0, c: 0 };
    var aoa = XLSX.utils.sheet_to_json(ws, {
      raw: false,
      header: 1,
      range: range
    });

    aoa.forEach(function (r, i) {
      var cells = {};
      r.forEach(function (c, j) {
        cells[j] = { text: c };

        var cellRef = XLSX.utils.encode_cell({ r: i, c: j });

        if (ws[cellRef] != null && ws[cellRef].f != null) {
          cells[j].text = '=' + ws[cellRef].f;
        }
      });
      o.rows[i] = { cells: cells };
    });

    o.merges = [];
    (ws['!merges'] || []).forEach(function (merge, i) {
      //Needed to support merged cells with empty content
      if (o.rows[merge.s.r] == null) {
        o.rows[merge.s.r] = { cells: {} };
      }
      if (o.rows[merge.s.r].cells[merge.s.c] == null) {
        o.rows[merge.s.r].cells[merge.s.c] = {};
      }

      o.rows[merge.s.r].cells[merge.s.c].merge = [
        merge.e.r - merge.s.r,
        merge.e.c - merge.s.c
      ];

      o.merges[i] = XLSX.utils.encode_range(merge);
    });

    out.push(o);
  });
  console.log(out);
  return out;
}

export function toRawGraph(workbook) {
  const nodes = XLSX.utils.sheet_to_json(workbook.Sheets.nodes).map(record => {
    return { id: record.id, data: { label: record.label } };
  });
  const edges = XLSX.utils
    .sheet_to_json(workbook.Sheets.edges)
    .map((record: { source: string; target: string }, row: number) => {
      return {
        id: record.source + '-' + record.target + '-' + row,
        source: record.source,
        target: record.target,
        data: { weight: record.weight }
      };
    });
  return { nodes, edges };
}