What packages can help with drawing a bipartite graph?

I would like to draw something like this in Typst, only with arrows rather than lines. Cetz can certainly achieve this, but since it is very basic, is there a package that can let me do it more easily? The purpose is to showcase the difference between relations and different types of functions between 2 sets:


(Bipartite Graph -- from Wolfram MathWorld)

Here is one basic way, using fletcher:

#import "@preview/fletcher:0.5.8": diagram, node, edge

#let (c1, c2) = (red.mix(orange), orange.mix(yellow))

#let dot(pos, color, name, ..args) = node(pos, radius: 5pt, fill: color, name: name, ..args)

#diagram(
  node-stroke: 0.5pt,
  node-outset: 2pt,
  spacing: (20mm, 5mm),

  dot((0,0.5), c1, <a1>),
  dot((0,1.5), c1, <a2>),
  dot((0,2.5), c1, <a3>),
  dot((0,3.5), c1, <a4>),

  dot((1,0), c2, <b1>),
  dot((1,1), c2, <b2>),
  dot((1,2), c2, <b3>),
  dot((1,3), c2, <b4>),
  dot((1,4), c2, <b5>),

  edge(<a1>, "-|>", <b1>),
  edge(<a1>, "-|>", <b5>),
  edge(<a1>, "-|>", <b2>),
  edge(<a2>, "-|>", <b3>),
  edge(<a3>, "-|>", <b4>),
  edge(<a4>, "-|>", <b4>),
)

If you have lots of similar diagrams like this, you could probably write a function that takes in a data structure like a dictionary and produces a diagram automatically. To give you a hint in the right direction, you can add for loops into diagrams like so:

#diagram(
  node-stroke: 0.5pt,
  node-outset: 2pt,
  spacing: (20mm, 5mm),

  for y in range(5) {
    dot((0,y), c1, "a" + str(y))
  },

  for y in range(5) {
    dot((1,y), c2, "b" + str(y))
  },

  for (src, tgt) in (
    (0, 1),
    (1, 1),
    (3, 2),
    (3, 3),
  ) {
    edge(label("a" + str(src)), label("b" + str(tgt)), "-|>")
  }
)
1 Like

Fletcher is definitely the way to go.

Since I already finished my own stab at it, here it is:

#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge, shapes
#set page(width: auto, height: auto, margin: 5mm, fill: white)

#let a-nodes = 4
#let b-nodes = 5

#let edges = (
  (<A0>, <B0>),
  (<A0>, <B1>),
  (<A0>, <B4>),
  (<A1>, <B2>),
  (<A2>, <B3>),
  (<A3>, <B3>),
)

#diagram(spacing: (45mm, 40mm), {
  let pos(x, i, n) = {
    let y = if n == 1 { 1 / 2 } else { i / (n - 1) }
    (x, y)
  }

  let node = node.with(radius: 4mm, stroke: 0.6pt, outset: 1mm)

  for i in range(a-nodes) {
    node(pos(0, i, a-nodes), name: "A" + str(i))
  }
  for i in range(b-nodes) {
    node(pos(1, i, b-nodes), name: "B" + str(i))
  }
  for (from, to) in edges {
    edge(from, to)
  }
})

The pos() function takes care of adjusting the y coordinates for even spacing. The coordinates are between 0 and 1, meaning that the spacing effectively determines the diagram dimensions (after also accounting for node sizes).