From a2933e1b1aa74eb9708455df4adee5e8e959ecc3 Mon Sep 17 00:00:00 2001 From: "Li, Zhen" Date: Sun, 7 Jan 2024 17:53:00 -0500 Subject: [PATCH] Add examples --- .../ContentView.swift | 50 +++++--- .../MermaidVisualization.swift | 1 + .../Grape.docc/CreatingAForceDirectedGraph.md | 117 +++++++++++++++++- .../Grape.docc/CustomizingAppearances.md | 1 - Sources/Grape/Grape.docc/DescribingForces.md | 6 - Sources/Grape/Grape.docc/Documentation.md | 3 - .../Grape.docc/Resources/BasicExample.png | Bin 0 -> 12571 bytes .../RespondingToInteractionsAndEvents.md | 1 - Sources/Grape/Views/ForceDirectedGraph.swift | 20 ++- 9 files changed, 165 insertions(+), 34 deletions(-) delete mode 100644 Sources/Grape/Grape.docc/CustomizingAppearances.md delete mode 100644 Sources/Grape/Grape.docc/DescribingForces.md create mode 100644 Sources/Grape/Grape.docc/Resources/BasicExample.png delete mode 100644 Sources/Grape/Grape.docc/RespondingToInteractionsAndEvents.md 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 0000000000000000000000000000000000000000..8547f2746ef2c6f8760cf41fed2270a2d1012548 GIT binary patch literal 12571 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 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 ) }