# ForceGraph

a growing network that lays itself out live: repulsion spreads the nodes, edges pull them together, hubs emerge from rich-get-richer growth, and you can drag any node while the web reflows around it (ForceLayout)

Run it with `swift run --package-path Examples Example-Patterns-ForceGraph`.

```swift
import Ollin

/// A network laying itself out as it grows: nodes repel, edges pull, and the
/// graph continually untangles itself into an even web with no coordinates
/// ever given (`ForceLayout`). A new node joins every stretch of frames,
/// linking to old nodes with a preference for the already-popular ones, so
/// hubs emerge and the web keeps reflowing to make room.
///
/// Drag any node and the rest of the graph reflows around your hand; let go
/// and it settles again. Node size is degree: the big disks are the hubs the
/// growth rule made rich.
@main
final class ForceGraph: Sketch {
    private var layout: ForceLayout?
    private var degrees: [Int] = []
    /// Every edge end once, so a uniform pick lands on a node in proportion
    /// to its degree (the rich-get-richer rule).
    private var stubs: [Int] = []
    private var grabbed: Int?

    private let paper = Color(hex: 0xF3F0E7)
    private let ink = Color(hex: 0x22364A)
    private let accent = Color(hex: 0xA83C51)
    private let maxNodes = 110

    override func draw() {
        let frame = canvasRectangle.inset(by: .all(100 * scale))
        if layout == nil {
            let seeded = ForceLayout(count: 3, edges: [(0, 1), (1, 2), (2, 0)],
                                     in: frame, seed: variation)
            seeded.idealDistance = spacing(for: 3, in: frame)
            seeded.gravity = 0.008
            layout = seeded
            degrees = [2, 2, 2]
            stubs = [0, 1, 1, 2, 2, 0]
        }
        guard let layout else { return }

        // Growth: every so often a newcomer appears beside a parent picked
        // by degree, sometimes closing a loop to a second one.
        if frameCount % 22 == 0, layout.count < maxNodes {
            let parent = stubs[Int(random(0, Double(stubs.count)))]
            let offset = Vector2(angle: random(0, .tau), length: 26 * scale)
            let newcomer = layout.addNode(at: layout.positions[parent] + offset)
            connect(layout, newcomer, parent)
            if random(0, 1) < 0.35 {
                let second = stubs[Int(random(0, Double(stubs.count)))]
                if second != newcomer, second != parent {
                    connect(layout, newcomer, second)
                }
            }
            // Re-derive the spacing for the new population, so the web keeps
            // filling the frame as it fills in.
            layout.idealDistance = spacing(for: layout.count, in: frame)
            layout.reheat(0.25)
        }

        // A grabbed node moves with the mouse; the reheat keeps the web following.
        if let grabbed, mouseIsPressed {
            layout.positions[grabbed] = mouse
            layout.reheat(0.15)
        }
        layout.step()

        background(paper)
        stroke(ink.withAlpha(0.55))
        strokeWeight(1.3 * scale)
        for edge in layout.edges {
            drawLine(layout.positions[edge.a], layout.positions[edge.b])
        }
        noStroke()
        for i in 0 ..< layout.count {
            fill(i == grabbed ? accent : ink)
            let radius = (3.4 + Double(degrees[i]).squareRoot() * 2.6) * scale
            drawCircle(center: layout.positions[i], radius: radius)
        }

        drawCaption("\(layout.count) nodes" + (layout.count < maxNodes ? ", growing" : "")
                    + (grabbed != nil ? ", dragging" : ", drag a node"))
    }

    override func mousePressed() {
        guard let layout, let nearest = layout.nearestNode(to: mouse),
              layout.positions[nearest].distance(to: mouse) < 80 * scale
        else { return }
        grabbed = nearest
        layout.pinned[nearest] = true
    }

    override func mouseReleased() {
        if let grabbed { layout?.pinned[grabbed] = false }
        grabbed = nil
        layout?.reheat(0.3)
    }

    /// The frame-filling spacing for `count` nodes: the `√(area / count)`
    /// default opened up (a big sparse web compresses well below its spacing
    /// constant), capped so the youngest graph doesn't slam its three nodes
    /// into the corners.
    private func spacing(for count: Int, in frame: Rectangle) -> Double {
        min(220 * scale,
            1.6 * (frame.width * frame.height / Double(max(count, 1))).squareRoot())
    }

    private func connect(_ layout: ForceLayout, _ a: Int, _ b: Int) {
        layout.connect(a, b)
        while degrees.count < layout.count { degrees.append(0) }
        degrees[a] += 1
        degrees[b] += 1
        stubs.append(a)
        stubs.append(b)
    }
}
```
