All versions of this documentation
X

Styling API

Nodes and edges have multiple visual attributes: position, color, shape, size, image, text, etc (see the full list here). Ogma provides multiple ways to alter these attributes, depending on the need.

Most sections of this tutorial will be illustrated with examples using nodes, but it works exactly the same for edges.

Basics

Assigning attributes

The most simple way to change the attributes of an element (node or edge) is to use the setAttributes method:

const node = ogma.addNode({ id: 0 });

node.setAttributes({
  x: 10,
  y: 30,
  color: 'blue',
  text: { content: 'My node' }
});

It is also possible to specify attributes when adding a node/edge, by providing an attributes property. Attributes specified this way are called "original attributes".

ogma.addNode({
  id: 1,
  attributes: { x: 30, y: 30, color: 'red', text: { content: 'My other node' } }
});

Result:

Note: in case of nested attributes, you can often use a shortcut to specify the main "sub-attribute":

// The following two lines are equivalent:

node.setAttributes({ text: { content: 'Some text' } });

node.setAttributes({ text: 'Some text' });
// Here the value of the `text` property is not an object, thus is alias-ed to `text.content`

You can also set a single attribute with setAttribute (with no 's'):

node.setAttribute('shape', 'square');
node.setAttribute('text.color', 'blue');

// Notice how you specify a nested property name

Retrieving attributes

You can retrieve a single attribute with getAttribute (without the 's'):

const node = ogma.addNode({ id: 0 });
node.setAttributes({
  x: 10,
  y: 30,
  color: 'blue',
  text: { content: 'My node' }
});

console.log(node.getAttribute('color')); // "blue"

To retrieve some (or all) attributes of a node, use the getAttributes method:

const node = ogma.addNode({ id: 0 });
node.setAttributes({
  x: 10,
  y: 30,
  color: 'blue',
  text: { content: 'My node' }
});

console.log(node.getAttributes(['x', 'y', 'text.content']));
// {x: 10, y: 30, text: {content: 'My node'}}
  • The method takes the list of attributes to retrieve.
  • If no argument is specified, the method will return an object containing all attributes.
  • Nested attributes are specified with a dot-separated string.

Note: There is also a node.getPosition() method that is a shortcut for node.getAttributes(['x', 'y']).

Resetting attributes

You can reset attributes to their original value (the ones specified when the node/edge was added) by using the resetAttributes method:

const node = ogma.addNode({
  id: 2,
  attributes: {
    x: 20,
    y: 50,
    color: 'green',
    text: { content: 'Yet another node' }
  }
});

node.setAttributes({ color: 'darkblue', shape: 'square' });

console.log(node.getAttribute('color')); // "darkblue"
console.log(node.getAttribute('shape')); // "square"

node.resetAttributes();

console.log(node.getAttribute('color')); // "green"
console.log(node.getAttribute('shape')); // "circle"
// no original attribute was specified for the "shape" attribute, so the system default value is assigned

Before reset:

After reset:

By default, all attributes are reset but it is possible to specify a subset of attributes to reset:

// Reset the coordinates and the color back to their original value.
// All other attributes are left untouched
node.resetAttributes(['x', 'y', 'color']);

Working with NodeList/EdgeList

All these methods also work on the NodeList/EdgeList objects:

// Color all the nodes in the graph in gold
ogma.getNodes().setAttributes({ color: 'gold' });

When used with a NodeList/EdgeList, the setAttributes method can also take a list of attributes objects as parameter. In that case, the first object is assigned to the first element of the list, the second object to the second element, etc.

ogma.getNodes(['My node', 'Yet another node']).setAttributes([
  { x: 0, y: 40, radius: 3, color: 'lightgreen' },
  { x: 5, y: 55, radius: 2, color: 'pink' }
]);

console.log(ogma.getNode('My node').getAttribute('color')); // "lightgreen"
console.log(ogma.getNode('Yet another node').getAttribute('color')); // "pink"

Similarly, calling getAttributes on a NodeList or EdgeList retrieves a list of objects, and getAttribute retrieves a list of values:

const nodes = ogma.getNodes(['My node', 'Yet another node']);

nodes.setAttributes([
  { x: 0, y: 40, radius: 3, color: 'lightgreen' },
  { x: 5, y: 55, radius: 2, color: 'pink' }
]);

console.log(nodes.getAttributes(['x', 'y'])); // [{x: 0, y: 0}, {x: 10, y: 20}]
console.log(nodes.getAttribute('color')); // ['red', 'blue']

A very straightforward usage of this feature is the ability to save the state of a list of nodes, and restore it later.

The resetAttributes method also work with lists:

// Reset the graph to its original state
ogma.getNodes().resetAttributes();
ogma.getEdges().resetAttributes();

Animations

If desired, attribute transitions can be animated. Specify the duration (in milliseconds) and easing as the second parameter of setAttributes.

const node = ogma.addNode({ id: 3, attributes: { color: 'red' } });

// Animate the color change of the node from red to blue in 500 ms.
node.setAttributes(
  { color: 'blue' },
  { duration: 500, easing: 'quadraticOut' }
);

Global rules

Creating rules

Most often, graphs don't contain any attribute but contain node and edge data. When loaded in Ogma, they look raw because the default style is applied.

ogma.setGraph({
  nodes: [
    {
      id: 0,
      data: {
        type: 'country',
        name: 'USA',
        population: 325365189,
        currency: 'USD'
      },
      attributes: { x: 0.1414247453212738, y: 36.075706481933594 }
    },
    {
      id: 1,
      data: {
        type: 'country',
        name: 'France',
        population: 66991000,
        currency: 'EUR'
      },
      attributes: { x: -13.736449241638184, y: -31.714202880859375 }
    },
    {
      id: 2,
      data: {
        type: 'country',
        name: 'Germany',
        population: 82175700,
        currency: 'EUR'
      },
      attributes: { x: 26.934364318847656, y: -31.93191909790039 }
    },
    {
      id: 3,
      data: { type: 'person', name: 'Norwood Preston', age: 44 },
      attributes: { x: 6.068506240844727, y: 6.21354866027832 }
    },
    {
      id: 4,
      data: { type: 'person', name: 'Kurtis Levi', age: 24 },
      attributes: { x: 29.330284118652344, y: 22.172880172729492 }
    },
    {
      id: 5,
      data: { type: 'person', name: 'Amias Kev', age: 38 },
      attributes: { x: -24.19040298461914, y: 35.77732849121094 }
    },
    {
      id: 6,
      data: { type: 'person', name: 'Carver Derren', age: 16 },
      attributes: { x: -34.62548065185547, y: -22.868541717529297 }
    },
    {
      id: 7,
      data: { type: 'person', name: 'Bevis Tel', age: 73 },
      attributes: { x: -22.90767478942871, y: -4.446183681488037 }
    },
    {
      id: 8,
      data: { type: 'person', name: 'Loyd Garfield', age: 32 },
      attributes: { x: 32.98543167114258, y: -9.278616905212402 }
    }
  ],
  edges: [
    { id: 0, source: 3, target: 0, data: { type: 'lives_in' } },
    { id: 1, source: 4, target: 0, data: { type: 'lives_in' } },
    { id: 2, source: 5, target: 0, data: { type: 'lives_in' } },
    { id: 3, source: 6, target: 1, data: { type: 'lives_in' } },
    { id: 4, source: 7, target: 1, data: { type: 'lives_in' } },
    { id: 5, source: 8, target: 2, data: { type: 'lives_in' } },
    { id: 6, source: 3, target: 4, data: { type: 'knows' } },
    { id: 7, source: 3, target: 8, data: { type: 'knows' } },
    { id: 8, source: 3, target: 7, data: { type: 'knows' } },
    { id: 9, source: 4, target: 8, data: { type: 'knows' } },
    { id: 10, source: 6, target: 7, data: { type: 'knows' } }
  ]
});

Rather than looping through every node, we can define global rules to assign the attributes of the nodes and edges.

ogma.styles.addRule({
  nodeAttributes: {
    color: 'orange',
    text: node => node.getData('name')
  },
  edgeAttributes: edge => {
    return {
      opacity: Math.min(
        edge.getSource().getAttribute('opacity'),
        edge.getTarget().getAttribute('opacity')
      ),
      shape: {
        type: 'line',
        head: edge.getData('type') === 'owns' ? 'arrow' : null
      }
    };
  }
});

The nodeAttributes and edgeAttributes fields are similar to the NodeAttributes and EdgeAttributes structure, except there can be functions at any level. Functions at non-leaf level should return an object containing values for the nested attributes. As shown in the example, it's even possible to provide a single function that will return the whole attribute object.

Rules can be based on attributes and data. When an element's (node or edge) data or attribute changes, Ogma recomputes all rules for the node/edge and its adjacent elements. It is possible to specify exactly when a rule should be refreshed with additional options, this is the object of the next tutorial.

Quick note on data: the data property is an object whose content is up to the user. It is manipulable through methods getData and setData. See the Data API tutorial for a more in depth explanation.

Selectors

It is possible to specify a selector to indicate on which nodes/edges the rule should be applied:

ogma.styles.addRule({
  nodeAttributes: { color: 'red' },

  // The rule will only be applied to nodes for which the data `foo` is equal to `'bar'`
  nodeSelector: node => node.getData('foo') === 'bar'
});

Advantages of rules instead of setAttributes:

  • Rules are automatically applied to nodes/edges added to the graph.
ogma.styles.addRule({ nodeAttributes: { color: 'red' } });
const node = ogma.addNode({ id: 0 });

console.log(node.getAttribute('color')); // "red"
  • Rules are automatically re-applied whenever there is a relevant change in the graph
ogma.styles.addRule({
  nodeAttributes: {
    text: node => node.getData('name')
  }
});

const node = ogma.addNode({ id: 0, data: { name: 'John' } });

console.log(node.getAttribute('text')); // "John"

node.setData('name', 'James');

console.log(node.getAttribute('text')); // "James"
  • Rules are applied after original attributes, but before attributes set via setAttributes. This means that you can build a logic that uses setAttributes on top of the global rules:
const node = ogma.addNode({
  id: 0,
  attributes: { color: 'blue' },
  data: { name: 'Google', type: 'company' }
});

node.setAttributes({ text: 'Facebook' });

ogma.styles.addRule({
  nodeAttributes: {
    color: node => {
      if (node.getData('type') === 'company') {
        return 'red';
      }
      return 'black';
    },
    text: node => node.getData('name')
  }
});

// The text set by `setAttributes` takes precedence over the rule
console.log(node.getAttribute('text')); // "Facebook"

// The color assigned by the rule takes precedence over the original color
console.log(node.getAttribute('color')); // "red"

Note: addRule returns an object that can be used to manipulate the rule (removing, refreshing or re-ordering it)

Removing rules

Removing a rule is done with the destroy method:

const node = ogma.addNode({ id: 0, attributes: { color: 'blue' } });
const rule = ogma.styles.addRule({ nodeAttributes: { color: 'red' } });

console.log(node.getAttribute('color')); // "red"

rule.destroy();

console.log(node.getAttribute('color')); // "blue"

Re-ordering rules

Multiple rules can be applied at the same time. Therefore, conflicts may occur:

// Simple case: what color is assigned to the nodes?

ogma.styles.addRule({ nodeAttributes: { color: 'red' } });
ogma.styles.addRule({ nodeAttributes: { color: 'blue' } });

Rules are internally stored in an array: every time a rule is added, it is added at the end of the array. When attributes are computed, rules are applied one by one in the order they appear in the array (by default rules added last are applied last). In this example, the nodes will be blue.

Although not common, it may be necessary to re-order the rules within this array in order to modify the order in which they are applied. In such case, the following methods are useful:

  • rule.getIndex(): retrieves the index of the specified rule
  • rule.setIndex(index): changes the index of the specified rule

Here is an example that demonstrates swapping the priority of two rules:

function swapRulePriority(rule1, rule2) {
  const index1 = rule1.getIndex();
  const index2 = rule2.getIndex();

  rule1.setIndex(index2);
  rule2.setIndex(index1);
}

Refreshing rules

It can happen that a rule references an external variable. If this variable changes, the rule needs to be manually refreshed to take the change into account:

const myMapping = {
  person: 'red',
  country: 'blue',
  company: 'green'
};

const rule = ogma.styles.addRule({
  nodeAttributes: {
    color: node => myMapping[node.getData('type')]
  }
});

// Change a variable referenced by the rule
myMapping.company = 'purple';

// Manually refresh the rule
rule.refresh();

Retrieving all rules

You can retrieve all rules with styles.getRuleList():

// This example deletes all the rules

ogma.styles.getRuleList().forEach(rule => rule.destroy());

Classes

Classes are a way to highlight nodes/edges to show they are in a meaningful and temporary state.

Selection and hover

These two classes are managed by Ogma itself, and are not directly accessible by the user. To affect how the nodes look like when hovered/selected, Ogma provides the setHoveredNodeAttributes and setSelectedNodeAttributes methods:

const node = ogma.addNode({ id: 0 });

ogma.styles.setSelectedNodeAttributes({ outerStroke: { color: 'green' } });
node.setSelected(true);

console.log(node.getAttribute('outerStroke.color')); // "green"

The argument is the same as the nodeAttributes parameter for the rules, and thus can contain functions:

// In this example, we display the name of the nodes as their text, only when they are selected

const node = ogma.addNode({ id: 0, data: { name: 'John' } });

ogma.styles.setSelectedNodeAttributes({
  outerStroke: {
    color: 'green'
  },
  text: node => node.getData('name')
});

console.log(node.getAttribute('text')); // null
node.setSelected(true);
console.log(node.getAttribute('text')); // "John"

You can also pass null so the attributes of nodes are not changed when hovered/selected:

ogma.styles.setSelectedNodeAttributes(null);

Creating custom classes

  • Creating a class is done via the ogma.styles.createClass method.
  • Adding and removing a class from a node/edge is done via the addClass and removeClass methods.
  • It is possible to do operations on the class object directly (such as retrieving the list of nodes/edges that have the class)

In this example, when the user clicks on a node we highlight the shortest path between a pre-determined node and the clicked node:

// Define the class, and the attributes associated to it
const myClass = ogma.styles.createClass({
  name: 'shortestPathHighlight',
  nodeAttributes: { outerStroke: { color: 'black', width: 5 } },
  edgeAttributes: { color: 'black' }
});

// Remove the style applied to nodes when they are selected
ogma.styles.setSelectedNodeAttributes(null);

ogma.events.on('click', evt => {
  // This is the node that will be used to compute the shortest path
  const sourceNode = ogma.getNode(0);

  // When the user clicks, first clear the current highlighted shortest path, if any
  myClass.clearNodes();
  myClass.clearEdges();

  // If a node was right clicked, highlight the shortest path
  if (evt.target && evt.target.isNode) {
    const clickedNode = evt.target;

    ogma.algorithms
      .shortestPath({
        source: sourceNode,
        target: clickedNode
      })
      .then(shortestPath => {
        // `shortestPath` is null if no path exists
        if (shortestPath) {
          // Highlight all the nodes and edges in the shortest path
          shortestPath.nodes.addClass('shortestPathHighlight');
          shortestPath.edges.addClass('shortestPathHighlight');
        }
      });
  }
});

// Finally, load a graph on which to test the feature
ogma.generate
  .grid({ rows: 5, columns: 5 })
  .then(graph => ogma.setGraph(graph))
  .then(() => {
    // Remove a few edges to make things more interesting
    return ogma.removeEdges([0, 2, 3, 5, 7, 10, 13, 14, 18, 19, 22]);
  })
  .then(() => ogma.view.locateGraph());

A few things to note:

  • Classes are applied after individual attributes and rules.
  • Classes are applied using the same mechanism as rules: the class added last is applied last
  • Builtin classes (selection and hover) are always applied last, after user classes

Summary

Attributes are computed in the following order:

  • original attributes
  • rules (from first added to last added)
  • individual attributes (that are assigned with setAttributes)
  • custom classes (from first added to last added)
  • builtin classes (selection and hover)

It is especially important to keep this order in mind when defining rules/classes that are computed using other attributes.

Performance notes

When working with a large number or nodes/edges:

  • Avoid using functions in rules/classes; use constants as much as possible.
  • Prefer using one single method on a NodeList/EdgeList rather that the same method on each node/edge individually (e.g nodeList.setAttributes({color: 'red'}) rather than nodeList.forEach(node => node.setAttributes({color: 'red'}))).

The next tutorial is more advanced and shows how to optimize rules and classes by telling Ogma what attributes they depend on.

Themes

You can use theme presets for all styling at once. We have compiled several styling themes for Ogma in our public @linkurious/ogma-styles repository. These themes are applied before all the styling rules and are a good way of producing a global stylesheet that expresses the "look and feel" of your visualisation

import Ogma from '@linkurious/ogma';
import { midsummerNight as theme } from '@linkurious/ogma-styles'

const ogma = new Ogma({ ... });
ogma.styles.setTheme(theme);

Best Practices

Check out our best practices tutorial to learn more about how to make your styles fast and easy to maintain.