Skip to content

Commit

Permalink
Add examples
Browse files Browse the repository at this point in the history
  • Loading branch information
li3zhen1 committed Jan 7, 2024
1 parent a3acd9f commit a2933e1
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//import CoreGraphics
//
//
import Grape


//
Expand Down Expand Up @@ -96,25 +97,42 @@ struct ContentView: View {
@State var selection: ExampleKind = .classicMiserable

var body: some View {
NavigationSplitView {
List(ExampleKind.list, id:\.self, selection: $selection) { kind in
Text(kind.description)
}
} detail: {
switch selection {
case .ring:
MyRing()
case .classicMiserable:
MiserableGraph()
case .lattice:
Lattice()
case .mermaid:
MermaidVisualization()
}
}
MyGraph()
// NavigationSplitView {
// List(ExampleKind.list, id:\.self, selection: $selection) { kind in
// Text(kind.description)
// }
// } detail: {
// switch selection {
// case .ring:
// MyRing()
// case .classicMiserable:
// MiserableGraph()
// case .lattice:
// Lattice()
// case .mermaid:
// MermaidVisualization()
// }
// }
}
}

#Preview {
ContentView()
}

struct MyGraph: View {
let myNodes = ["A", "B", "C"]
let myLinks = [("A", "B"), ("B", "C")]

var body: some View {
ForceDirectedGraph {
Series(myNodes) { id in
NodeMark(id: id)
}
Series(myLinks) { from, to in
LinkMark(from: from, to: to)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,4 @@ struct MermaidVisualization: View {

}
}

117 changes: 116 additions & 1 deletion Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,118 @@
# Creating a Force Directed Graph

## Overview
## Overview

A graph is a collection of nodes and links. Each node is connected to other nodes by links. In Grape, you describe a node with a `NodeMark` and a link with a `LinkMark`. `NodeMark` and `LinkMark` are associated with an `id` or `id`s that identifies them. An `id` can be any type that conforms to `Hashable`.

Grape provides a `ForceDirectedGraph` view to visualize a graph. You can easily initialize it like you would do in SwiftUI.

```swift

struct MyGraph: View {
var body: some View {
ForceDirectedGraph {
NodeMark(id: "A")
NodeMark(id: "B")
LinkMark(from: "A", to: "B")
}
}
}

```

For the array data, `Series` comes handy for describing a collection of nodes and links. Consider it a simplified version of `ForEach` in SwiftUI.

```swift

struct MyGraph: View {
let myNodes = ["A", "B", "C"]
let myLinks = [("A", "B"), ("B", "C")]

var body: some View {
ForceDirectedGraph {
Series(myNodes) { id in
NodeMark(id: id)
}
Series(myLinks) { from, to in
LinkMark(from: from, to: to)
}
}
}
}

```

@Image(source: "BasicExample.png", alt: "A basic force directied graph.")

> **Note**: Grape currently does not protect you from linking to non-existing nodes. If you link to a node that does not exist, view crashes.

## Customizing forces

You can customize the forces that interfere with the nodes and links. By default, Grape uses a `LinkForce` and a `ManyBodyForce`.

For example, the `CenterForce` can keep the mass center of the graph at the center of the view, so it does not drift away. To add a `CenterForce`, you can do the following.


```swift
struct MyGraph: View {
let myNodes = ["A", "B", "C"]
let myLinks = [("A", "B"), ("B", "C")]

var body: some View {
ForceDirectedGraph {
Series(myNodes) { id in
NodeMark(id: id)
}
Series(myLinks) { from, to in
LinkMark(from: from, to: to)
}
} force: {
ManyBodyForce()
LinkForce()
CenterForce()
}
}
}
```

Note that when you override the default forces, you may need to add the `LinkForce` and `ManyBodyForce` back. Otherwise, the nodes may stay static since no forces are moving them to other places.

## Customizing appearances

Add modifiers like you would do in SwiftUI to style your nodes and links.

```swift

struct MyGraph: View {
let myNodes = ["A", "B", "C"]
let myLinks = [("A", "B"), ("B", "C")]

var body: some View {
ForceDirectedGraph {
Series(myNodes) { id in
NodeMark(id: id)
.foregroundColor(.blue)
}
Series(myLinks) { from, to in
LinkMark(from: from, to: to)
}
}
}
}

```


## Responding to interactions and events

Grape provides a set of interactions and events to help you respond to user interactions, including dragging, zooming, and tapping. They are mostly supported by default, and you can install your callbacks to respond to them.


For detailed usages, please refer to [MermaidVisualization.swift](https://github.com/li3zhen1/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift).


@Video(source: "https://github.com/li3zhen1/Grape/assets/45376537/80d933c1-8b5b-4b1a-9062-9628577bd2e0", alt: "A screen record of mermaid")


// TODO: Add examples
1 change: 0 additions & 1 deletion Sources/Grape/Grape.docc/CustomizingAppearances.md

This file was deleted.

6 changes: 0 additions & 6 deletions Sources/Grape/Grape.docc/DescribingForces.md

This file was deleted.

3 changes: 0 additions & 3 deletions Sources/Grape/Grape.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ If you’re looking for a more detailed control of force-directed layouts, pleas


* <doc:CreatingAForceDirectedGraph>
* <doc:DescribingForces>
* <doc:CustomizingAppearances>
* <doc:RespondingToInteractionsAndEvents>

* ``ForceDirectedGraph``

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

20 changes: 14 additions & 6 deletions Sources/Grape/Views/ForceDirectedGraph.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ForceSimulation
import SwiftUI

public struct ForceDirectedGraph<NodeID: Hashable, Content: GraphContent> where NodeID == Content.NodeID {
public struct ForceDirectedGraph<NodeID: Hashable, Content: GraphContent>
where NodeID == Content.NodeID {

// public typealias NodeID = Content.NodeID

Expand Down Expand Up @@ -72,14 +73,21 @@ public struct ForceDirectedGraph<NodeID: Hashable, Content: GraphContent> where
// }
// }

@SealedForce2DBuilder
@inlinable
static public func defaulForce() -> [SealedForce2D.ForceEntry] {
ManyBodyForce()
LinkForce()
}

@inlinable
public init(
_ isRunning: Binding<Bool> = .constant(true),
_ modelTransform: Binding<ViewportTransform> = .constant(.identity),
ticksPerSecond: Double = 60.0,
initialViewportTransform: ViewportTransform = .identity,
@GraphContentBuilder<NodeID> _ graph: () -> Content,
@SealedForce2DBuilder force: () -> [SealedForce2D.ForceEntry] = { [] },
@SealedForce2DBuilder force: () -> [SealedForce2D.ForceEntry] = Self.defaulForce,
emittingNewNodesWithStates state: @escaping (NodeID) -> KineticState = { _ in
.init(position: .zero)
}
Expand All @@ -90,15 +98,15 @@ public struct ForceDirectedGraph<NodeID: Hashable, Content: GraphContent> where

self._graphRenderingContextShadow = gctx
self._isRunning = isRunning

self._forceDescriptors = force()

let force = SealedForce2D(self._forceDescriptors)
self.model = .init(
gctx,
force,
gctx,
force,
modelTransform: modelTransform,
emittingNewNodesWith: state,
emittingNewNodesWith: state,
ticksPerSecond: ticksPerSecond
)
}
Expand Down

0 comments on commit a2933e1

Please sign in to comment.