Skip to content
  1. Tutorials

Styling optimization

It's common to have rules (or classes) that depend on attributes assigned by other rules. For example:

ts
const typeToHighlight = 'country';

const rule1 = ogma.styles.addRule({
  nodeAttributes: {
    opacity: node => {
      if (node.getData('type') === typeToHighlight) return 1;
      return 0.2;
    }
  }
});

const rule2 = ogma.styles.addRule({
  edgeAttributes: {
    opacity: edge => {
      const opacities = edge.getExtremities().getAttribute('opacity');
      return Math.min(opacities[0], opacities[1]);
    }
  }
});

The first rule highlights a type of nodes by fading all other nodes, and the second rule makes so edges that have at least one faded extremity are also faded.

Notice that while the two rules could have been merged in one, they are declared separately because the second one depends on the first one. Had they been merged, they would have been applied at the same time and thus the second one would not have been able to use the result of the first one.

Now say we refresh the first rule with a different type of node to highlight:

js
typeToHighlight = 'person';
rule1.refresh();

This will update the first rule, and also the second rule that depends on it. The second rule is also updated because by default, Ogma assumes that functions used in rules are based on all of the node's adjacent nodes, adjacent edges, attributes, data, and whether they are selected or not. In practice that means that when a node's attribute, data or selection state changes, all rules and classes that contain a function are re-evaluated for the node, its adjacent nodes and its adjacent edges.

This has the advantage that by default, all rules are properly updated when a dependency changes. The obvious drawback is that refreshing a rule can refresh some non-relevant rules which don't need to be refreshed. For example:

js
const ruleColor = 'blue';

const rule1 = ogma.styles.addRule({
  nodeAttributes: {
    color: () => ruleColor
  }
});

const rule2 = ogma.styles.addRule({
  nodeSelector: node => node.getData('type') === 'company',
  nodeAttributes: {
    radius: node => {
      // Returns the number of employees
      return node
        .getAdjacentNodes()
        .filter(node => node.getData('type') === 'employee')
        .reduce((acc, value) => acc + value, 0);
    }
  }
});

function changeColor(color) {
  ruleColor = color;
  rule1.refresh();
}

Here, calling changeColor will refresh the first rule, and also the second rule because Ogma has no way to know that the second rule does not depend on the result of the first one.

Specifying dependencies

On small graphs or with simple rules this problem is usually not noticeable, but with large graphs or complex rules it can be. To avoid this problem, Ogma provides a nodeDependencies and edgeDependencies parameters that you can be used to specify what the functions in the rule depend on. For example, here is how you would specify the dependencies for the above snippets:

js
const typeToHighlight = 'country';

ogma.styles.addRule({
  nodeAttributes: {
    opacity: node => {
      if (node.getData('type') === typeToHighlight) return 1;
      return 0.2;
    }
  },
  nodeDependencies: {
    // Indicates that the function refers to the data of the node on which the rule is being applied
    // That means that the rule should be automatically refreshed for a node when its data changes
    self: {
      data: true
    }
  }
});

ogma.styles.addRule({
  edgeAttributes: {
    opacity: edge => {
      const opacities = edge.getExtremities().getAttribute('opacity');
      return Math.min(opacities[0], opacities[1]);
    }
  },
  edgeDependencies: {
    // Indicates that the function depends on the opacity of the edge extremities, meaning that the rule should be
    // automatically refreshed for an edge when one of its extremities' opacity changes
    extremities: {
      attributes: ['opacity']
    }
  }
});

const ruleColor = 'blue';

ogma.styles.addRule({
  nodeAttributes: {
    color: () => ruleColor
  },
  // The rule does not depend on anything (the `ruleColor` variable is managed by the user)
  nodeDependencies: null
});

ogma.styles.addRule({
  nodeSelector: node => node.getData('type') === 'company',
  nodeAttributes: {
    radius: node => {
      // Returns the number of employees
      return node
        .getAdjacentNodes()
        .filter(node => node.getData('type') === 'employee')
        .reduce((acc, value) => acc + value, 0);
    }
  },
  nodeDependencies: {
    // The rule depends on the data of the node itself (because of the selector)
    self: { data: true },
    // The rule depends on the data of the adjacent nodes (because of the radius function)
    adjacentNodes: { data: true }
  }
});

The structure of NodeDependencies and EdgeDependencies can be found in the documentation.

A Dependency object has the following fields:

  • attributes: an array of attribute names (dot-separated for nested attributes) indicating which attribute the rule/class depends on. Attributes names can be prefixes; for example, "text" refers to all nested text attributes ("text.content", "text.size", "text.font", etc). It's also possible to specify "all" instead of an array to indicates that the rule/class depends on all attributes. Notice that a rule refresh cannot trigger the refresh of a rule that is applied before.
  • data: a boolean indicating if the rule/class depends on the element data. Contrary to attributes, it's not possible to specify exactly on which data fields the rule/class depends on.
  • selection: a boolean indicating if the rule depends on whether the element is selected or not.
  • hover: a boolean indicating if the rule depends on whether the element is hovered or not.

For example:

js
ogma.styles.addRule({
  nodeAttributes: node => {
    /* ... */
  },
  edgeAttributes: edge => {
    /* ... */
  },
  nodeDependencies: {
    self: {
      data: true,
      selection: true
    },
    adjacentNodes: {
      attributes: ['color', 'text.content', 'halo'],
      data: true
    },
    adjacentEdges: {
      attributes: ['opacity']
    }
  },
  edgeDependencies: {
    self: {
      attributes: ['color']
    },
    extremities: {
      data: true
    }
  }
});

This indicates that the rule should be re-computed for a node when:

  • the node's data changes
  • the node becomes selected or unselected
  • the color, text content, or any halo attribute of one of its adjacent node changes
  • the data of one of its adjacent node changes
  • the opacity attribute of one of its adjacent edge changes

And this indicates that the rule should be re-computed for an edge when:

  • the color attribute of the edge changes
  • the data of one of its extremities changes

Finally, consider the following rule:

js
ogma.style.addRule({
  nodeAttributes: {
    icon: node => node.getAdjacentNodes().size
  }
});

This rule makes so the icon of each node indicates the number of adjacent nodes it has. This rule depends on the adjacent nodes, but does not depend on the attributes, data or selection state of the adjacent nodes. To indicates that the rule only depends on the existence of adjacent nodes, you can pass true to the adjacentNodes dependency instead of passing an object (also works for adjacentEdges):

js
ogma.style.addRule({
  nodeAttributes: {
    icon: node => node.getAdjacentNodes().size
  },
  nodeDependencies: {
    adjacentNodes: true
  }
});

This makes so the rule is only re-computed for a node when its number of adjacent nodes changes.

Specifying output attributes

Consider this rule:

js
ogma.styles.addRule({
  nodeAttributes: {
    color: 'blue',
    radius: node => node.getData('foo'),
    badges: node => {
      const result = {};
      const nbNeighbors = node.getAdjacentNodes().size;
      const badgeKey =
        node.getData('badgePosition') === 'left' ? 'topLeft' : 'topRight';

      result[badgeKey].text.content = nbNeighbors;

      return result;
    }
  }
});

Ogma knows that this rule assigns the color and radius attributes, because they are "leaf" attributes. It means that it knows that if this rule is refreshed for some nodes, it must also refresh the rules/classes that have the color or radius as dependency.

But when a function is specified at a non-leaf level (like badges here), Ogma has no way to know which nested attributes are assigned by this function, especially given that it can change at each run. In this case, Ogma assumes that the function can potentially assign all nested attributes. It means that when the rule is refreshed, it will also refresh the rules/classes that have any of the badges attributes as dependency, even if they don't match what is actually returned by the function.

This creates the same problem as with dependencies: that refreshing a rule could trigger a refresh of some non-relevant rules/classes that don't need to be refreshed. This problem is especially obvious when defining a rule that is a single function.

To avoid this problem, you can specify which attributes can be returned by the rule with the nodeOutput and edgeOutput parameters:

js
ogma.styles.addRule({
  nodeAttributes: {
    color: 'blue',
    radius: node => node.getData('foo'),
    badges: node => {
      const result = {};
      const nbNeighbors = node.getAdjacentNodes().size;
      const badgeKey =
        node.getData('badgePosition') === 'left' ? 'topLeft' : 'topRight';

      result[badgeKey].text.content = nbNeighbors;

      return result;
    }
  },
  nodeOutput: {
    attributes: [
      'color',
      'radius',
      'badges.topLeft.text.content',
      'badges.topRight.text.content'
    ]
  }
});

This allows Ogma to be much more efficient in knowing which rules/classes to refresh when this rule is refreshed. Notice that if you do specify a nodeOutput parameter, you must specify all output attributes, even the ones that Ogma could infer. Here color and radius could have been inferred by Ogma, but you need to specify them anyway.

Lastly, output attribute names work the same as for dependencies: you can specify only a prefix, and Ogma will treat it as the list of all nested attributes for that prefix. That would sort of defeat the purpose of specifying output attributes manually, but can sometimes be useful.

Summary

  • When defining complex rules or classes, you can specify their dependencies in the nodeDependencies and edgeDependencies parameters to avoid them being refreshed unnecessary often.
  • When defining rules or classes with functions at non-leaf levels, you can specify the attributes that can be returned by the functions in the nodeOutput and edgeOutput parameters, to avoid triggering the refresh of other rules/classes unnecessarily.
  • A rule/class refresh cannot trigger the refresh of another rule/class that is applied before.