Powered by AppSignal & Oban Pro

KinoSigmajs Examples

guides/examples.livemd

KinoSigmajs Examples

Section

A collection of examples showing how to render interactive graphs with Kino.Sigma.

Setup

Mix.install([
  {:kino_sigmajs, path: Path.expand("..", __DIR__)},
  {:yog_ex, "~> 0.98"}
])

Example 1: A simple graph from a keyword list

Yog.Generator.Classic.binary_tree(11)
|> Yog.node_count()

Example 2: Using the builder API

alias Kino.Sigma.Graph

graph =
  Graph.new()
  |> Graph.add_node("elixir", label: "Elixir", color: "#6e28d9", size: 12)
  |> Graph.add_node("erlang", label: "Erlang", color: "#a90533", size: 10)
  |> Graph.add_node("beam", label: "BEAM", color: "#f59e0b", size: 8)
  |> Graph.add_node("livebook", label: "Livebook", color: "#3b82f6", size: 8)
  |> Graph.add_edge("elixir", "erlang", label: "runs on")
  |> Graph.add_edge("elixir", "beam", label: "compiles to")
  |> Graph.add_edge("livebook", "elixir", label: "built with")

Kino.Sigma.render(graph,
  layout: [type: :force_atlas_2, iterations: 120],
  height: "500px"
)

Example 3: Pre-positioned nodes (no layout)

When nodes already have x and y, you can skip the force-directed layout entirely.

Kino.Sigma.render(
  nodes: [
    %{id: "origin", label: "Origin", x: 0, y: 0, color: "#3b82f6", size: 10},
    %{id: "right", label: "Right", x: 1, y: 0, color: "#f43f5e", size: 8},
    %{id: "top", label: "Top", x: 0, y: 1, color: "#10b981", size: 8},
    %{id: "diagonal", label: "Diagonal", x: 1, y: 1, color: "#f59e0b", size: 8}
  ],
  edges: [
    %{source: "origin", target: "right"},
    %{source: "origin", target: "top"},
    %{source: "right", target: "diagonal"},
    %{source: "top", target: "diagonal"}
  ],
  layout: [type: :none],
  height: "400px"
)

Example 4: A larger random graph

This is where Sigma.js shines compared to static SVG renderers.

node_count = 2000
edge_count = 4000

nodes =
  for i <- 1..node_count do
    %{
      id: "node-#{i}",
      label: "#{i}",
      color: "#3b82f6",
      size: 4
    }
  end

edges =
  Stream.repeatedly(fn ->
    source = :rand.uniform(node_count)
    target = :rand.uniform(node_count)
    {source, target}
  end)
  |> Stream.filter(fn {source, target} -> source != target end)
  |> Stream.uniq()
  |> Enum.take(edge_count)
  |> Enum.map(fn {source, target} ->
    %{source: "node-#{source}", target: "node-#{target}", size: 1}
  end)

Kino.Sigma.render(
  nodes: nodes,
  edges: edges,
  layout: [type: :force_atlas_2, iterations: 100],
  height: "600px"
)

Example 5: Custom styling

nodes =
  for i <- 1..8 do
    %{
      id: "n#{i}",
      label: "Node #{i}",
      color: (if rem(i, 2) == 0, do: "#f43f5e", else: "#3b82f6"),
      size: 6 + rem(i, 4)
    }
  end

edges =
  for i <- 1..7 do
    %{
      source: "n#{i}",
      target: "n#{i + 1}",
      color: "#94a3b8",
      size: 2
    }
  end

Kino.Sigma.render(
  nodes: nodes,
  edges: edges,
  layout: [type: :force_atlas_2, iterations: 100],
  height: "400px"
)

Example 6: Rendering a Yog graph

If you build graphs with Yog, you can pass the graph directly to Kino.Sigma.render/1. Node data can be a string/atom label or a map with :label, :color, :size, :x, and :y.

graph =
  Yog.directed()
  |> Yog.add_node("elixir", %{label: "Elixir", color: "#6e28d9", size: 12})
  |> Yog.add_node("erlang", %{label: "Erlang", color: "#a90533", size: 10})
  |> Yog.add_node("beam", %{label: "BEAM", color: "#f59e0b", size: 8})
  |> Yog.add_edge_ensure(from: "elixir", to: "erlang", with: 1)
  |> Yog.add_edge_ensure(from: "elixir", to: "beam", with: 1)

Kino.Sigma.render(graph,
  layout: [type: :force_atlas_2, iterations: 120],
  height: "500px"
)

Example 7: Performance mode for large graphs

For graphs with thousands of nodes, labels and edge labels can slow down the renderer. Enable :performance mode and reduce ForceAtlas2 iterations to keep interactions smooth:

graph = Yog.Generator.Classic.binary_tree(10)

Kino.Sigma.render(graph,
  layout: [
    type: :force_atlas_2,
    iterations: 50,
    settings: %{barnesHutOptimize: true, slowDown: 5}
  ],
  performance: true,
  height: "800px"
)

Example 8: Downsampling and clustering

For graphs that are too large even in performance mode, you can downsample to a fixed number of nodes/edges or collapse nodes into clusters.

graph = Yog.Generator.Classic.barbell(100, 100)

# Render a random sample of 1,000 nodes and 2,000 edges
Kino.Sigma.render(graph,
  performance: true,
  height: "600px"
)

You can also collapse nodes by a shared attribute. Here we treat each node’s :color as its cluster:

nodes =
  for i <- 1..1000 do
    %{
      id: "n#{i}",
      label: "Node #{i}",
      color: (if rem(i, 2) == 0, do: "#f43f5e", else: "#3b82f6"),
      size: 4
    }
  end

edges =
  for _ <- 1..2000 do
    source = :rand.uniform(1000)
    target = :rand.uniform(1000)
    %{source: "node-#{source}", target: "node-#{target}", size: 1}
  end

Kino.Sigma.render(
  nodes: nodes,
  edges: edges,
  cluster_by: :color,
  performance: true,
  height: "600px"
)

Notes

  • The widget loads Sigma.js, Graphology, and ForceAtlas2 from CDN using dynamic ES module imports. Internet access is required on first render.
  • If you need offline use or bundled dependencies, open an issue on the repository.
  • :performance mode disables edge labels and hides labels/edges while panning or zooming. Pass :sigma_settings to override any Sigma.js renderer setting.
  • :max_nodes and :max_edges randomly sample the graph. :cluster_by collapses nodes sharing the same value into a single meta-node and aggregates inter-cluster edges.