Grape Documentation

repository·main·Indexed 19 days ago

https://github.com/li3zhen1/grape

A Swift library for graph visualization and force simulation. It includes the Grape module, providing high-level SwiftUI components like ForceDirectedGraph, NodeMark, and LinkMark for rendering graphs, and the ForceSimulation module, a low-level, dimension-agnostic engine using velocity Verlet integration. The simulation engine utilizes Kinetics, ForceProtocol, and Simulation to manage kinetic states and compose forces such as ManyBodyForce, LinkForce, CenterForce, and CollideForce.

Tokens
5.1K
Snippets
10
Records
21
Agent score
64%

What's inside Grape

  1. Create a graph visualization with Grape

    main

    Grape is a SwiftUI framework for constructing and visualizing force-directed graphs on Apple platforms. You build graphs using nodes, links, and forces as fundamental building blocks.

    To create a visualization, you primarily use the ForceDirectedGraph view and describe your graph structure using GraphContent.

  2. Configure forces in the simulation

    main

    Grape uses a force-directed layout engine. You can customize the behavior of the graph by applying different forces using SealedForceDescriptor or its builder. Available forces include:

    • CenterForce: Pulls nodes toward the center.
    • CollideForce: Prevents nodes from overlapping.
    • LinkForce: Maintains the distance between linked nodes.
    • ManyBodyForce: Simulates gravitational or electrostatic forces between nodes.
    • PositionForce: Pulls nodes toward specific coordinates.
    • RadialForce: Applies forces relative to a radial pattern.
  3. Manage graph view state and transforms

    main

    To control how the graph is viewed and how it moves, Grape provides several state management types:

    • ForceDirectedGraphModel: Manages the underlying model of the graph.
    • ViewportTransform: Controls the visible area of the graph.
    • TransformProtocol: An interface for defining transformations.
    • KineticState: Manages the physical/motion state of the graph.
    • KeyFrame: Used for defining specific states in animations or transitions.
  4. Describe a graph using GraphContent and Marks

    main

    Graphs in Grape are described using a declarative syntax. You use GraphContent (and its GraphContentBuilder) to define the elements of your graph:

    • NodeMark: Represents a node in the graph.
    • LinkMark: Represents a connection between nodes.
    • Series: Groups related marks.
    • GraphComponent: A generic building block for graph elements.

    Localization is supported by providing localized string keys for labels within the visualization.

  5. How the ForceSimulation module works

    main

    The ForceSimulation module is the underlying engine for Grape. It is composed of three main concepts:

    1. Kinetics: Describes the kinetic state of the system, including positions, velocities, link connections, and the alpha value (system activity level).
    2. ForceProtocol: Defines forces that mutate the Kinetics. A force must implement bindKinetics(_:) to reference the state and apply() to perform the mutation (e.g., adding velocity).
    3. Simulation: A shell class that manages a Kinetics instance and a single force conforming to ForceProtocol. It uses velocity Verlet integration to step the simulation forward.

    Because Simulation only stores one force, you must compose multiple forces into a single force object (e.g., using SealedForce2D) to create complex behaviors.

  6. Decorate graph marks with modifiers

    main

    You can customize the appearance of nodes and links using GraphContentModifier.

    Common decoration options include:

    • StrokeColor: Sets the color of the outlines.
    • LinkShape: Defines the geometry of links (e.g., StraightLineLinkShape).
    • PlainLineLink and ArrowLineLink: Specific link styles for different visual requirements.
  7. Customize graph forces in ForceDirectedGraph

    main

    By default, ForceDirectedGraph applies a LinkForce and a ManyBodyForce. If you provide a custom force: closure, you override the defaults.

    If you override the forces, you must manually re-add the forces you want to keep (such as .link() or .manyBody()), otherwise the nodes may remain static. You can also add .center() to keep the graph's mass center at the center of the view.

    ForceDirectedGraph {
        Series(myNodes) { id in
            NodeMark(id: id)
        }
        Series(myLinks) { from, to in
            LinkMark(from: from, to: to)
        }
    } force: {
        .manyBody()
        .link()
        .center()
    }
  8. Add interactivity to your graph

    main

    Grape provides several SwiftUI view modifiers and tools to make graphs interactive:

    • GraphProxy: Used to interact with the graph state.
    • graphOverlay(alignment:content:): Adds an overlay to the graph.
    • graphBackground(alignment:content:): Adds a background to the graph.
    • withGraphTapGesture(_:action:): Attaches a tap gesture specifically for graph elements.
    • withGraphDragGesture(_:action:): Attaches a drag gesture for interacting with nodes or links.
    • withGraphMagnifyGesture(_:action:): Attaches a magnification (zoom) gesture.
  9. Eliminate redundant rerenders in large graphs

    main

    Because re-evaluating the body of a ForceDirectedGraph can be computationally expensive (especially with large graphs or heavy rich text labels), you should avoid referencing individual observed properties directly within the main graph view's body.

    Instead, pass the entire Observable object (ForceDirectedGraphState) to subviews. By referencing the object itself rather than its specific properties (like graphStates.isRunning), the parent view's body will not re-evaluate when those properties change. Use @Bindable in subviews to allow them to mutate the state.

    import Grape
    
    struct MyStatefulGraph: View {
        @State var graphStates = ForceDirectedGraphState()
        
        var body: some View {
            HStack {
                // Pass the entire object to avoid body re-evaluation on property changes
                ForceDirectedGraph(states: graphStates) { 
                    // ...
                } force: { 
                    // ...
                }
                
                // Use a separate view to handle interactions
                GraphStateToggle(graphStates: graphStates)
            }
        }
    }
    
    struct GraphStateToggle: View {
        @Bindable var graphStates: ForceDirectedGraphState
        
        var body: some View {
            Button {
                graphStates.isRunning.toggle()
            } label: {
                Text("Toggle Running")
            }
        }
    }
  10. How Kinetics, ForceProtocol, and Simulation work together

    main

    The ForceSimulation module is built around three core concepts:

    1. Kinetics<Vector>: Describes the kinetic state of the system, including position, velocity, link connections, and an alpha value representing system activity. The Vector type (e.g., SIMD2<Double> or SIMD3<Float>) defines the coordinate space and must conform to SimulatableVector.
    2. ForceProtocol: Defines the forces acting on the system. A force is responsible for two tasks:
      • bindKinetics(_ kinetics: Kinetics<Vector>): Binding to a Kinetics instance to maintain a reference for mutation.
      • apply(): Mutating the states within Kinetics (e.g., a gravity force adding velocity to nodes).
    3. Simulation: The primary interface for the user. It manages a Kinetics object and a single ForceProtocol object, performing velocity Verlet integration. Because a Simulation only holds one force, you must compose multiple forces into a single force object (like SealedForce2D) to use them together.