
Maintenance fork (2026). The original aysylu/loom
is no longer maintained. This fork supports current Clojure / ClojureScript and
fixes correctness bugs (see CHANGELOG). It is published to Clojars as
net.clojars.savya/loom.
Watch the Loom talk at Clojure/West 2014. View the slides. Watch the talk at LispNYC. View the slides.
deps.edn:
net.clojars.savya/loom {:mvn/version "1.4.0"}
Leiningen:
[net.clojars.savya/loom "1.4.0"]
Or use the maintained fork directly as a git dependency (deps.edn):
io.github.jsavyasachi/loom {:git/tag "1.3.0" :git/sha "c666221b9c3ad9e600a48fe4853d9cfbf17c87e5"}
loom.graph - records & constructors
loom.alg - algorithms (see also loom.alg-generic)
loom.gen - graph generators
loom.attr - graph attributes
loom.label - graph labels
loom.io - read, write, and view graphs in external formats
loom.derived - derive graphs from existing graphs using maps and filters
Graph I/O
loom.io supports GraphML, GEXF, EDN-encoded edge lists, adjacency JSON, and
DOT import. Use write-graphml, write-gexf, write-edge-list, and
write-adjacency-json with their corresponding read-* functions;
dot-str/dot remain available for DOT export and read-dot imports DOT.
Weighted graphs, directedness, node values, and Loom attributes are preserved
by the matching writer/reader pairs.
loom.gen is portable across Clojure and ClojureScript. The seeded generator
arities of gen-newman-watts, gen-barabasi-albert, gen-rand, and gen-rand-p
use the same deterministic PRNG on both platforms, so the same seed produces
the same graph. Omit :seed (or the positional seed) for a time-based seed.
Graph algorithms throw ExceptionInfo/ex-info with structured data for invalid
inputs. Traversal and path functions report :type :loom.alg/missing-node;
Dijkstra and A* report :type :loom.alg/negative-weight because they require
non-negative edge weights. Bellman-Ford and Johnson continue to accept negative
weights (subject to their negative-cycle rules). Maximum flow reports
:loom.flow/missing-node, :loom.flow/negative-capacity, or
:loom.flow/malformed-constraint for invalid source/sink or capacity inputs.
Join the Loom mailing list to ask questions.
Create a graph:
;; Initialize with any of: edges, adacency lists, nodes, other graphs
(def g (graph [1 2] [2 3] {3 [4] 5 [6 7]} 7 8 9))
(def dg (digraph g))
(def wg (weighted-graph {:a {:b 10 :c 20} :c {:d 30} :e {:b 5 :d 5}}))
(def wdg (weighted-digraph [:a :b 10] [:a :c 20] [:c :d 30] [:d :b 10]))
(def rwg (gen-rand (weighted-graph) 10 20 :max-weight 100))
(def fg (fly-graph :successors range :weight (constantly 77)))
For parallel edges, use multigraph or multidigraph. Each edge receives a
stable key; use edges-with-ids (or out-edges-with-ids) to address a specific
edge and pass it to weight or loom.attr:
(def mg (multigraph [1 2 :rail 10] [1 2 :road 20]))
(map edge-key (edges-with-ids mg))
;; => (:rail :road :rail :road) ; undirected edges are exposed in both directions
(weight mg (first (filter #(= :rail (edge-key %))
(out-edges-with-ids mg 1))))
;; => 10
For large bulk edge lists, graph-from-edges, digraph-from-edges,
weighted-graph-from-edges, and weighted-digraph-from-edges build adjacency
maps with transients and persist them once at the end.
If you have GraphViz installed, and its binaries are in the path, you can view graphs with loom.io/view:
(view wdg) ;opens image in default image viewer
Inspect:
(nodes g)
=> #{1 2 3 4 5 6 7 8 9}
(edges wdg)
=> ([:a :c] [:a :b] [:c :d] [:d :b])
(successors g 3)
=> #{2 4}
(predecessors wdg :b)
=> #{:a :d}
(out-degree g 3)
=> 2
(in-degree wdg :b)
=> 2
(weight wg :a :c)
=> 20
(map (juxt graph? directed? weighted?) [g wdg])
=> ([true false false] [true true true])
Add or remove items. Graphs are immutable, so these functions return new graphs:
(add-nodes g "foobar" {:name "baz"} [1 2 3])
(add-edges g [10 11] ["foobar" {:name "baz"}])
(add-edges wg [:e :f 40] [:f :g 50]) ;weighted edges
(remove-nodes g 1 2 3)
(remove-edges g [1 2] [2 3])
(subgraph g [5 6 7])
Traverse a graph:
(bf-traverse g) ;lazy
=> (9 8 5 6 7 1 2 3 4)
(bf-traverse g 1)
=> (1 2 3 4)
(pre-traverse wdg) ;lazy
=> (:a :b :c :d)
(post-traverse wdg) ;not lazy
=> (:b :d :c :a)
(topsort wdg)
=> (:a :c :d :b)
Pathfinding:
(bf-path g 1 4)
=> (1 2 3 4)
(bf-path-bi g 1 4) ;bidirectional, parallel
=> (1 2 3 4)
(dijkstra-path wg :a :d)
=> (:a :b :e :d)
(dijkstra-path-dist wg :a :d)
=> [(:a :b :e :d) 20]
Flow:
;; max-flow uses weighted edges as capacities and returns [flow-map value]
(max-flow wdg :a :d)
;; min-cost-flow reads :demand from nodes and :capacity/:cost from edges.
;; Negative demand supplies flow; positive demand consumes it. It returns
;; [flow-map total-cost], matching max-flow's result shape.
(min-cost-flow g)
;; Attribute names can be customized with an optional map.
(min-cost-flow g {:capacity :cap :cost :unit-cost :demand :balance})
Other stuff:
(connected-components g)
=> [[1 2 3 4] [5 6 7] [8] [9]]
(bf-span wg :a)
=> {:c [:d], :b [:e], :a [:b :c]}
(pre-span wg :a)
=> {:a [:b], :b [:e], :e [:d], :d [:c]}
(dijkstra-span wg :a)
=> {:a {:b 10, :c 20}, :b {:e 15}, :e {:d 20}}
Graph analysis functions include pagerank, degree-centrality,
closeness-centrality, betweenness-centrality, eigenvector-centrality,
hits, articulation-points, bridges, biconnected-components, k-core,
eccentricity, radius, and diameter. pagerank accepts :damping,
:iterations, and :tol; iterative centrality algorithms accept :iterations
and :tol. Structural decomposition functions operate on undirected graphs.
Attributes on nodes and edges:
(def attr-graph (-> g
(add-attr 1 :label "node 1")
(add-attr 4 :label "node 4")
(add-attr-to-nodes :parity "even" [2 4])
(add-attr-to-edges :label "edge from node 5" [[5 6] [5 7]])))
; Return attribute value on node 1 with key :label
(attr attr-graph 1 :label)
=> "node 1"
; Return attribute value on node 2 with key :parity
(attr attr-graph 2 :parity)
=> "even"
; Getting an attribute that doesn't exist returns nil
(attr attr-graph 3 :label)
=> nil
; Return all attributes for node 4
; Two attributes found
(attrs attr-graph 4)
=> {:parity "even", :label "node 4"}
; Return attribute value for edge between nodes 5 and 6 with key :label
(attr attr-graph 5 6 :label)
=> "edge from node 5"
; Return all attributes for edge between nodes 5 and 7
(attrs attr-graph 5 7)
=> {:label "edge from node 5"}
; Getting an attribute that doesn't exist returns nil
(attrs attr-graph 3 4)
=> nil
; Remove the attribute of node 4 with key :label
(def attr-graph (remove-attr attr-graph 4 :label))
; Return all attributes for node 4
; One attribute found because the other has been removed
(attrs attr-graph 4)
=> {:parity "even"}
Derived graphs:
; Build a derived graph using a node mapping
(nodes (mapped-by #(+ 10 %) g))
=> #{11 12 13 14 15 16 17 18 19}
; Subgraphs of g
(edges (nodes-filtered-by #{1 2 3 5} dg))
=> ([1 2] [2 1] [2 3] [3 2])
(edges (subgraph-reachable-from dg 1))
=> ([1 2] [2 1] [2 3] [3 2] [3 4] [4 3])
Loom uses Clojure. It can use GraphViz for visualization.
See Loom TODO board.
clojure -M:test
clojure -T:build jar
clojure -T:build deploy
Names are in no order:
The dependency graph of Loom namespaces. lein-ns-dep-graph generates it.

Copyright © 2010-2016 Aysylu Greenberg & Justin Kramer (jkkramer@gmail.com).
Savyasachi maintains this fork (2026). Original: https://github.com/aysylu/loom. The project uses the Eclipse Public License 1.0. It keeps the original license.
Can you improve this documentation? These fine people already did:
Aysylu, Savyasachi, Aysylu Greenberg, Justin Kramer, aysylu, Horst Duchene, Andrea Crotti, Jim Berlage, bpringe, Ertuğrul Çetin, Francois Rey, Paul L. Snyder, Robert Lachlan, Daniel Compton & Kevin DowneyEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |