Add interactivity
In Part 3, you styled the graph declaratively and added a hover effect. In this final step, you will highlight a node’s neighbors on hover and add a search bar that focuses the camera on results.
The goal
When the user hovers a node, its neighbors stay colored while everything else fades to grey. A search bar lets the user type a node name, highlights matches, and animates the camera to the selected node.
This requires custom state: sigma’s built-in isHovered flag only applies to the single hovered node. To grey out
everything else, you need a way to mark which nodes and edges are “active” (part of the hovered neighborhood).
Define custom state
Custom states for nodes, edges and the graph itself, are typed with generics given to the Sigma constructor. The
easiest way to actually specify them is actually to simply give the initial custom state values we want to Sigma. This
will allow Sigma to infer its types:
const renderer = new Sigma(graph, document.getElementById("container")!, { customNodeState: { isActive: false, }, customEdgeState: { isActive: false, }, customGraphState: { hasActiveSubgraph: false, }, styles: { // ... },});Add conditional styles for the active subgraph
Update the styles to grey out nodes and edges that are not active when a subgraph is highlighted:
styles: { nodes: [ // ... { // When there is an active subgraph but the node is not active or hovered, it should be grey, without label: when: (_attrs, state, graphState) => graphState.hasActiveSubgraph && !state.isActive && !state.isHovered, then: { color: "#eee", label: "" }, }, ], edges: [ // ... { // When there is an active subgraph but edge doesn't connect two active nodes, it should be grey: when: (_attrs, state, graphState) => graphState.hasActiveSubgraph && !state.isActive, then: { color: "#eee" }, }, ],},Wire up the custom states
After creating the renderer, add event listeners that set the active subgraph on hover:
function setActiveSubgraph(activeNodes: Set<string> | null) { graph.forEachNode((node) => { renderer.setNodeState(node, { isActive: activeNodes?.has(node) }); });
graph.forEachEdge((edge) => { const [source, target] = graph.extremities(edge); renderer.setEdgeState(edge, { isActive: activeNodes?.has(source) && activeNodes?.has(target) }); });
renderer.setGraphState({ hasActiveSubgraph: !!activeNodes });
// Trigger a renderer update: renderer.refresh({ skipIndexation: true });}
renderer.on("enterNode", ({ node }) => { const neighbors = new Set(graph.neighbors(node)); neighbors.add(node); setActiveSubgraph(neighbors);});
renderer.on("leaveNode", () => { setActiveSubgraph(null);});setNodeState and setEdgeState update state flags one element at a time. The refresh({ skipIndexation: true }) call
tells sigma to re-evaluate styles, without re-computing the spatial index (since no positions changed).
Try hovering a node: its neighbors should stay colored while the rest fades out.
Put the active subgraph on top
Now, the highlighted subgraph is not always easy to see, as its edges can sometimes be hidden behind unfocused nodes. In sigma v4, all items are rendered in the same single WebGL context, and it’s now possible to render the active subgraph on top of the rest of the graph.
We want three different layers:
- On the very top, the highlighted node and its label
- Then, the active subgraph
- Then, the rest of the greyed out items
To do this, we first need to specify the depthLayers primitives. Primitives are the link between sigma engine and
the styles. Styles basically express how to bind data to the primitives, while the primitives express what exactly sigma
should render. Here, for instance, depthLayers declares the exhaustive ordered list of all the different depths we
need to render the graph as we want:
const renderer = new Sigma(graph, document.getElementById("container")!, { // ... primitives: { // Declare depth layers, from deepest to topest: depthLayers: [ // Base graph "edges", "nodes", // Active subgraph "activeEdges", "activeNodes", // Hovered node "topNodes", ], }, styles: { // ... },});From there, depth can refer to those values, directly in the styles (each depth layer paints both the item and
its label, in that order):
styles: { nodes: [ // ... { whenState: "isActive", then: { depth: "activeNodes" }, }, { whenState: "isHovered", then: { depth: "topNodes" }, }, ], edges: [ // ... { whenState: "isActive", then: { depth: "activeEdges" }, }, ],},Et voilà, we now have the active subgraph properly highlighted.
Full code and result
import Graph from "graphology";import Sigma from "sigma";import { DEFAULT_STYLES } from "sigma/types";
interface Dataset { nodes: { key: string; label: string; tag: string; cluster: string; x: number; y: number; score: number }[]; edges: [string, string][]; clusters: { key: string; color: string; clusterLabel: string }[];}
const response = await fetch("/data/wikipedia.json");const dataset: Dataset = await response.json();
const clusterColors = Object.fromEntries(dataset.clusters.map((c) => [c.key, c.color]));
const graph = new Graph();const scoreExtents = { min: Infinity, max: -Infinity };
for (const node of dataset.nodes) { scoreExtents.min = Math.min(scoreExtents.min, node.score); scoreExtents.max = Math.max(scoreExtents.max, node.score); graph.addNode(node.key, { label: node.label, x: node.x, y: node.y, cluster: node.cluster, score: node.score, });}
for (const [source, target] of dataset.edges) { if (graph.hasNode(source) && graph.hasNode(target) && !graph.hasEdge(source, target)) { graph.addEdge(source, target); }}
const renderer = new Sigma(graph, document.getElementById("sigma-container") as HTMLElement, { customNodeState: { isActive: false, }, customEdgeState: { isActive: false, }, customGraphState: { hasActiveSubgraph: false, }, primitives: { depthLayers: [ // Base graph "edges", "nodes", // Active subgraph "activeEdges", "activeNodes", // Hovered node "topNodes", ], }, styles: { nodes: [ DEFAULT_STYLES.nodes, { color: { attribute: "cluster", dict: clusterColors, defaultValue: "#999", }, size: { attribute: "score", min: 10, max: 50, minValue: scoreExtents.min, maxValue: scoreExtents.max }, label: { attribute: "label" }, }, { when: (_attrs, state, graphState) => graphState.hasActiveSubgraph && !state.isActive && !state.isHovered, then: { color: "#eee", label: "" }, }, { whenState: "isActive", then: { depth: "activeNodes" }, }, { whenState: "isHovered", then: { depth: "topNodes" }, }, ], edges: [ DEFAULT_STYLES.edges, { color: "#ccc", size: 5 }, { when: (_attrs, state, graphState) => graphState.hasActiveSubgraph && !state.isActive, then: { color: "#eee" }, }, { whenState: "isActive", then: { depth: "activeEdges" }, }, ], },});
function setActiveSubgraph(activeNodes: Set<string> | null) { graph.forEachNode((node) => { renderer.setNodeState(node, { isActive: activeNodes?.has(node) }); });
graph.forEachEdge((edge) => { const [source, target] = graph.extremities(edge); renderer.setEdgeState(edge, { isActive: activeNodes?.has(source) && activeNodes?.has(target) }); });
renderer.setGraphState({ hasActiveSubgraph: !!activeNodes });
// Trigger a renderer update: renderer.refresh({ skipIndexation: true });}
renderer.on("enterNode", ({ node }) => { const neighbors = new Set(graph.neighbors(node)); neighbors.add(node); setActiveSubgraph(neighbors);});
renderer.on("leaveNode", () => { setActiveSubgraph(null);});What you built
Starting from an empty page, you now have:
- A graph of 2,000+ Wikipedia articles rendered with WebGL
- Nodes colored by cluster and sized by relevance
- Hover highlighting that shows a node’s neighborhood
Where to go from here
Now that you understand the core concepts (graphology data, styles, state, events), explore the rest of the documentation:
- How-to guides: specific recipes for nodes, edges, labels, interactivity, and more
- Concepts: deeper understanding of styles, primitives, rendering, and coordinate systems
- Reference: complete settings, events, and API reference
- Examples: interactive examples with source code