Skip to content
  1. Examples

Timeline

This is the example of custom timeline controls used in combination with Ogma filter transformation.

ts
import Ogma from '@linkurious/ogma';
import { Controller as TimelinePlugin } from '@linkurious/ogma-timeline-plugin';
const ogma = new Ogma<{
  type: string;
  label: string;
  start: number;
}>({
  container: 'graph-container'
});
const range = Date.now() - new Date('August 19, 1975 23:15:30').getTime();

ogma.setOptions({
  backgroundColor: '#FDFAF7'
});
// Create a <style> tag to hold the dynamically generated CSS
const styleTag = document.createElement('style');
document.head.appendChild(styleTag);
const typeToColor = {
  'Climate-change': {
    textColor: '#027C47',
    background: '#E7F3BA',
    nodeColor: '#00B064'
  },
  Geography: {
    textColor: '#067E9F',
    background: '#CFF2FC',
    nodeColor: '#067E9F'
  },
  Physics: {
    textColor: '#46037D',
    background: '#F6EDFE',
    nodeColor: '#9961C7'
  },
  Geology: {
    textColor: '#A64700',
    background: '#FFF7D6',
    nodeColor: '#F4AF44'
  },
  Ice: {
    textColor: '#037C7D',
    background: '#E5FFFF',
    nodeColor: '#00CCCC'
  },
  Economy: {
    textColor: '#990000',
    background: '#FFE8D7',
    nodeColor: '#990000'
  },
  Politics: {
    textColor: '#9B0032',
    background: '#FFECEA',
    nodeColor: '#EB5153'
  },
  Biology: {
    textColor: '#404A1F',
    background: '#F6F7DF',
    nodeColor: '#A0C32C'
  }
};
ogma.styles.setTheme({
  selectedNodeAttributes: {
    radius: 10,
    outerStroke: {
      width: 4,
      color: '#FF720D'
    }
  },
  hoveredNodeAttributes: {
    radius: 10,
    outerStroke: {
      width: 4,
      color: '#FF720D'
    },
    duration: 500,
    easing: 'quadraticInOut'
  }
});
const highlightedLight = ogma.styles.createClass({
  name: 'highlightedLight',
  edgeAttributes: {
    color: '#F9C198',
    width: 1.2
  },
  nodeAttributes: {
    outerStroke: {
      width: 4,
      color: '#F9C198'
    }
  }
});
const highlighted = ogma.styles.createClass({
  name: 'highlighted',
  edgeAttributes: {
    color: '#FF720D',
    width: 1.2
  },
  nodeAttributes: {
    outerStroke: {
      width: 4,
      color: '#FF720D'
    }
  }
});

ogma.events.on('mouseover', evt => {
  if (!evt.target || !evt.target.isNode) return;
  const node = evt.target;
  const neighbors = node.getAdjacentElements();
  neighbors.nodes.addClass('highlightedLight', {
    duration: 500,
    easing: 'quadraticInOut'
  });
  neighbors.edges
    .filter(e => !e.isSelected())
    .addClass('highlightedLight', {
      duration: 500,
      easing: 'quadraticInOut'
    });
});

ogma.events.on('mouseout', () => {
  highlightedLight.clearNodes();
  highlightedLight.clearEdges();
});

ogma.events.on(['nodesSelected', 'nodesUnselected'], evt => {
  const nodes = ogma.getSelectedNodes();
  const neighbors = nodes.getAdjacentElements();
  highlighted.clearNodes();
  highlighted.clearEdges();
  neighbors.nodes.addClass('highlighted', {
    duration: 500,
    easing: 'quadraticInOut'
  });
  neighbors.edges
    .filter(e => !e.isSelected())
    .addClass('highlighted', {
      duration: 500,
      easing: 'quadraticInOut'
    });
});

ogma.styles.addNodeRule({
  color: node => typeToColor[node.getData('type')].nodeColor,
  text: {
    content: node => ` ${node.getData('label')} `,
    // padding: 10,
    size: 14
  },
  radius: 8,
  opacity: 1
});

ogma.styles.setHoveredNodeAttributes({
  text: {
    color: '#fff',
    backgroundColor: '#272727'
  }
});

ogma.styles.setSelectedNodeAttributes({
  text: {
    color: '#fff',
    backgroundColor: '#272727'
  }
});

ogma.styles.addEdgeRule({
  width: 0.5
});

ogma.styles.setHoveredEdgeAttributes({
  width: 2,
  color: '#FF720D'
});

ogma.styles.setSelectedEdgeAttributes({
  width: 2,
  color: '#FF720D'
});
Ogma.parse
  .gexfFromUrl('files/arctic-small.gexf')
  .then(graph => {
    graph.nodes.forEach((node, i) => {
      // Add a random date to the node
      const start = Date.now() - Math.random() * range;
      node.data.start = start;
    });
    Object.entries(typeToColor).forEach(([type, { textColor, background }]) => {
      // Inject the CSS rule for the backgroundoup
      styleTag.sheet?.insertRule(
        `.${type} { background-color: ${background}; }`,
        styleTag.sheet.cssRules.length
      );
      styleTag.sheet?.insertRule(
        `.${type}.vis-item.timeline-item.vis-selected { background-color: ${textColor}; color: white;}`,
        styleTag.sheet.cssRules.length
      );
      styleTag.sheet?.insertRule(
        `.${type}.vis-item.timeline-item.vis-line.vis-selected,
         .${type}.vis-item.timeline-item.vis-dot.vis-selected
         { border-color: ${textColor};}
        `,
        styleTag.sheet.cssRules.length
      );
      styleTag.sheet?.insertRule(
        `.${type} { border-color: ${textColor}; color: ${textColor}; }`,
        styleTag.sheet.cssRules.length
      );
    });

    return ogma.setGraph(
      {
        nodes: graph.nodes.slice(50, 100),
        edges: graph.edges
      },
      { ignoreInvalid: true }
    );
  })
  .then(() => ogma.layouts.force({ locate: true, gpu: true }))
  .then(() => {
    const container = document.getElementById('timeline');
    const timelinePlugin = new TimelinePlugin(ogma, container, {
      minTime: new Date('January 1, 1975 00:00:00').getTime(),
      maxTime: new Date('January 1, 2022 00:00:00').getTime(),
      timeBars: [
        new Date('January 1, 1975 00:00:00'),
        new Date('January 1, 2025 00:00:00')
      ],
      barchart: {
        graph2dOptions: {
          drawPoints: false,
          barChart: { width: 30, align: 'center' }
        }
      },
      timeline: {
        timelineOptions: {},
        nodeItemGenerator: node => ({
          content: `${node.getData('label')}`
        }),
        getNodeClass: node => {
          return node.getData('type');
        }
      }
    });
    window.timelinePlugin = timelinePlugin;
    /* let isSelecting = false;
    timelinePlugin.on(
      'select',
      ({ nodes, edges }: { nodes: NodeList; edges: EdgeList }) => {
        isSelecting = true;
        ogma.getNodes().setSelected(false);
        ogma.getEdges().setSelected(false);

        if (nodes) nodes.setSelected(true);
        if (edges) edges.setSelected(true);
        if (!nodes.size && !edges.size) return;
        const bounds = nodes ? nodes.getBoundingBox() : edges.getBoundingBox();
        if (bounds.width < 256) {
          bounds.pad(256 - bounds.width);
        }
        ogma.view.moveToBounds(bounds, {
          easing: 'quadraticInOut',
          duration: 1000
        });
        isSelecting = false;
      }
    );
    ogma.events.on(
      ['nodesSelected', 'edgesSelected', 'nodesUnselected', 'edgesUnselected'],
      () => {
        if (isSelecting) return;
        timelinePlugin.setSelection({
          nodes: ogma.getSelectedNodes(),
          edges: ogma.getSelectedEdges()
        });
        const minMax = ogma
          .getSelectedNodes()
          .getData('start')
          .concat(ogma.getSelectedEdges().getData('start'))
          .reduce(
            (acc, time) => {
              if (time < acc.min) acc.min = time;
              if (time > acc.max) acc.max = time;
              return acc;
            },
            { min: Infinity, max: -Infinity }
          );
        const year = 1000 * 60 * 60 * 24 * 365;
        // add one year margin between min and max
        minMax.min -= year;
        minMax.max += year;
        timelinePlugin.setWindow(minMax.min, minMax.max);
      }
    );

    const filter = ogma.transformations.addNodeFilter({
      criteria: node => {
        return timelinePlugin.filteredNodes.has(node.getId());
      }
    });
    // Hook it to the timeline events
    timelinePlugin.on('timechange', () => {
      filter.refresh();
    });
    timelinePlugin.showBarchart();*/
  });
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      href="https://cdn.jsdelivr.net/npm/vis-timeline@latest/styles/vis-timeline-graph2d.min.css"
      rel="stylesheet"
      type="text/css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@linkurious/ogma-timeline-plugin@latest/dist/style.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=Roboto:wght@300&display=swap"
      rel="stylesheet"
    />
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="graph-container"></div>
    <div id="timeline"></div>
    <script src="index.ts"></script>
  </body>
</html>
css
body {
  display: grid;
  grid-template-rows: auto 33%;
  height: 100vh;
  margin: 0;
  padding: 0;
}
#timeline > div {
  height: 100%;
}
/* vis-item vis-box timeline-item nodes 95 node Economy vis-readonly */
.vis-item.vis-box {
  border-radius: 4px;
}
.vis-item {
  font-weight: 700;
  padding: 4px 4px;
  border-width: 1px;
}

.rect.vis-bar,
rect.vis-group.nodes.node.vis-bar {
  fill-opacity: 1 !important;
}
.vis-item.vis-box.timeline-item.vis-selected {
  border: 0;
}
.vis-timeline {
  background-color: #fdfaf7;
}
.vis-bar {
  fill: #177356;
  stroke: none;
}

.vis-bar.vis-selected {
  fill: #00b064;
  stroke: #10503c;
}
.vis-custom-time {
  cursor: ew-resize;
  cursor: url('files/arrow-left-right-circle-fill.svg'), auto;
}
.vis-filtered {
  opacity: 0.5;
}
json
{
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@linkurious/ogma": "^5.2.3",
    "vis-data": "7.1.16",
    "vis-timeline": "7.7.2",
    "leaflet": "1.9.4",
    "@linkurious/ogma-timeline-plugin": "0.2.15"
  }
}