diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift index f1a72af..7c5cc76 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift @@ -11,6 +11,7 @@ //import CoreGraphics // // +import Grape // @@ -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) + } + } + } +} diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift index e6358a5..cc2cc82 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift @@ -176,3 +176,4 @@ struct MermaidVisualization: View { } } + diff --git a/Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md b/Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md index 526d382..571270c 100644 --- a/Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md +++ b/Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md @@ -1,3 +1,118 @@ # Creating a Force Directed Graph -## Overview \ No newline at end of file +## 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 \ No newline at end of file diff --git a/Sources/Grape/Grape.docc/CustomizingAppearances.md b/Sources/Grape/Grape.docc/CustomizingAppearances.md deleted file mode 100644 index 6ef0dee..0000000 --- a/Sources/Grape/Grape.docc/CustomizingAppearances.md +++ /dev/null @@ -1 +0,0 @@ -# Customizing Appearances diff --git a/Sources/Grape/Grape.docc/DescribingForces.md b/Sources/Grape/Grape.docc/DescribingForces.md deleted file mode 100644 index 0d34dc2..0000000 --- a/Sources/Grape/Grape.docc/DescribingForces.md +++ /dev/null @@ -1,6 +0,0 @@ -# Describing Forces - - - -## Overview - diff --git a/Sources/Grape/Grape.docc/Documentation.md b/Sources/Grape/Grape.docc/Documentation.md index 24c054c..8924437 100644 --- a/Sources/Grape/Grape.docc/Documentation.md +++ b/Sources/Grape/Grape.docc/Documentation.md @@ -15,9 +15,6 @@ If you’re looking for a more detailed control of force-directed layouts, pleas * -* -* -* * ``ForceDirectedGraph`` diff --git a/Sources/Grape/Grape.docc/Resources/BasicExample.png b/Sources/Grape/Grape.docc/Resources/BasicExample.png new file mode 100644 index 0000000..8547f27 Binary files /dev/null and b/Sources/Grape/Grape.docc/Resources/BasicExample.png differ diff --git a/Sources/Grape/Grape.docc/RespondingToInteractionsAndEvents.md b/Sources/Grape/Grape.docc/RespondingToInteractionsAndEvents.md deleted file mode 100644 index 1a4020c..0000000 --- a/Sources/Grape/Grape.docc/RespondingToInteractionsAndEvents.md +++ /dev/null @@ -1 +0,0 @@ -# Responding to Interactions and Events diff --git a/Sources/Grape/Views/ForceDirectedGraph.swift b/Sources/Grape/Views/ForceDirectedGraph.swift index f96d4b2..f284ff9 100644 --- a/Sources/Grape/Views/ForceDirectedGraph.swift +++ b/Sources/Grape/Views/ForceDirectedGraph.swift @@ -1,7 +1,8 @@ import ForceSimulation import SwiftUI -public struct ForceDirectedGraph where NodeID == Content.NodeID { +public struct ForceDirectedGraph +where NodeID == Content.NodeID { // public typealias NodeID = Content.NodeID @@ -72,6 +73,13 @@ public struct ForceDirectedGraph where // } // } + @SealedForce2DBuilder + @inlinable + static public func defaulForce() -> [SealedForce2D.ForceEntry] { + ManyBodyForce() + LinkForce() + } + @inlinable public init( _ isRunning: Binding = .constant(true), @@ -79,7 +87,7 @@ public struct ForceDirectedGraph where ticksPerSecond: Double = 60.0, initialViewportTransform: ViewportTransform = .identity, @GraphContentBuilder _ graph: () -> Content, - @SealedForce2DBuilder force: () -> [SealedForce2D.ForceEntry] = { [] }, + @SealedForce2DBuilder force: () -> [SealedForce2D.ForceEntry] = Self.defaulForce, emittingNewNodesWithStates state: @escaping (NodeID) -> KineticState = { _ in .init(position: .zero) } @@ -90,15 +98,15 @@ public struct ForceDirectedGraph 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 ) }