├── 3D Graph screenshots ├── 2NoRot.png ├── 2Rot.png ├── 3DGraph.gif ├── 3x1NoRot.png ├── 3x1Rot.png ├── 3x3NoRot.png ├── 3x3Rot.png ├── 4NoRot.png └── 4Rot.png ├── 3DGraph.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── 3DGraph ├── 3DGraph │ ├── BarElement.swift │ ├── BarNode.swift │ ├── GraphNode.swift │ ├── GraphView.swift │ └── LabelNode.swift ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── ViewController.swift ├── 3DGraphTests ├── Info.plist └── _DGraphTests.swift └── README.md /3D Graph screenshots/2NoRot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/2NoRot.png -------------------------------------------------------------------------------- /3D Graph screenshots/2Rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/2Rot.png -------------------------------------------------------------------------------- /3D Graph screenshots/3DGraph.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/3DGraph.gif -------------------------------------------------------------------------------- /3D Graph screenshots/3x1NoRot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/3x1NoRot.png -------------------------------------------------------------------------------- /3D Graph screenshots/3x1Rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/3x1Rot.png -------------------------------------------------------------------------------- /3D Graph screenshots/3x3NoRot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/3x3NoRot.png -------------------------------------------------------------------------------- /3D Graph screenshots/3x3Rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/3x3Rot.png -------------------------------------------------------------------------------- /3D Graph screenshots/4NoRot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/4NoRot.png -------------------------------------------------------------------------------- /3D Graph screenshots/4Rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greglecki/3DGraph/8019c71b4df1ed0f5ab9abe5185d27368e8bbfc7/3D Graph screenshots/4Rot.png -------------------------------------------------------------------------------- /3DGraph.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CAD098381B9D7908006E4E2D /* 2NoRot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAD098361B9D7908006E4E2D /* 2NoRot.png */; }; 11 | CAD098391B9D7908006E4E2D /* 2Rot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAD098371B9D7908006E4E2D /* 2Rot.png */; }; 12 | CAD0983D1B9EF085006E4E2D /* 3DGraph.gif in Resources */ = {isa = PBXBuildFile; fileRef = CAD0983C1B9EF085006E4E2D /* 3DGraph.gif */; }; 13 | CAFB8BF11B98AA090065B917 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8BF01B98AA090065B917 /* AppDelegate.swift */; }; 14 | CAFB8BF31B98AA090065B917 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8BF21B98AA090065B917 /* ViewController.swift */; }; 15 | CAFB8BF61B98AA090065B917 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8BF41B98AA090065B917 /* Main.storyboard */; }; 16 | CAFB8BF81B98AA090065B917 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8BF71B98AA090065B917 /* Images.xcassets */; }; 17 | CAFB8BFB1B98AA090065B917 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8BF91B98AA090065B917 /* LaunchScreen.xib */; }; 18 | CAFB8C071B98AA090065B917 /* _DGraphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C061B98AA090065B917 /* _DGraphTests.swift */; }; 19 | CAFB8C161B98AA4E0065B917 /* BarElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C111B98AA4E0065B917 /* BarElement.swift */; }; 20 | CAFB8C171B98AA4E0065B917 /* BarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C121B98AA4E0065B917 /* BarNode.swift */; }; 21 | CAFB8C181B98AA4E0065B917 /* GraphNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C131B98AA4E0065B917 /* GraphNode.swift */; }; 22 | CAFB8C191B98AA4E0065B917 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C141B98AA4E0065B917 /* GraphView.swift */; }; 23 | CAFB8C1A1B98AA4E0065B917 /* LabelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFB8C151B98AA4E0065B917 /* LabelNode.swift */; }; 24 | CAFB8C311B998FE80065B917 /* 3x1NoRot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C291B998FE80065B917 /* 3x1NoRot.png */; }; 25 | CAFB8C321B998FE80065B917 /* 3x1Rot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C2A1B998FE80065B917 /* 3x1Rot.png */; }; 26 | CAFB8C331B998FE80065B917 /* 3x3NoRot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C2B1B998FE80065B917 /* 3x3NoRot.png */; }; 27 | CAFB8C341B998FE80065B917 /* 3x3Rot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C2C1B998FE80065B917 /* 3x3Rot.png */; }; 28 | CAFB8C351B998FE80065B917 /* 4NoRot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C2D1B998FE80065B917 /* 4NoRot.png */; }; 29 | CAFB8C361B998FE80065B917 /* 4Rot.png in Resources */ = {isa = PBXBuildFile; fileRef = CAFB8C2E1B998FE80065B917 /* 4Rot.png */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | CAFB8C011B98AA090065B917 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = CAFB8BE31B98AA090065B917 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = CAFB8BEA1B98AA090065B917; 38 | remoteInfo = 3DGraph; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | CAD098361B9D7908006E4E2D /* 2NoRot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 2NoRot.png; sourceTree = ""; }; 44 | CAD098371B9D7908006E4E2D /* 2Rot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 2Rot.png; sourceTree = ""; }; 45 | CAD0983C1B9EF085006E4E2D /* 3DGraph.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = 3DGraph.gif; sourceTree = ""; }; 46 | CAFB8BEB1B98AA090065B917 /* 3DGraph.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 3DGraph.app; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | CAFB8BEF1B98AA090065B917 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | CAFB8BF01B98AA090065B917 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | CAFB8BF21B98AA090065B917 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 50 | CAFB8BF51B98AA090065B917 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 51 | CAFB8BF71B98AA090065B917 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 52 | CAFB8BFA1B98AA090065B917 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 53 | CAFB8C001B98AA090065B917 /* 3DGraphTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = 3DGraphTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | CAFB8C051B98AA090065B917 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | CAFB8C061B98AA090065B917 /* _DGraphTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _DGraphTests.swift; sourceTree = ""; }; 56 | CAFB8C111B98AA4E0065B917 /* BarElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarElement.swift; sourceTree = ""; }; 57 | CAFB8C121B98AA4E0065B917 /* BarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarNode.swift; sourceTree = ""; }; 58 | CAFB8C131B98AA4E0065B917 /* GraphNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphNode.swift; sourceTree = ""; }; 59 | CAFB8C141B98AA4E0065B917 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; 60 | CAFB8C151B98AA4E0065B917 /* LabelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelNode.swift; sourceTree = ""; }; 61 | CAFB8C291B998FE80065B917 /* 3x1NoRot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 3x1NoRot.png; sourceTree = ""; }; 62 | CAFB8C2A1B998FE80065B917 /* 3x1Rot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 3x1Rot.png; sourceTree = ""; }; 63 | CAFB8C2B1B998FE80065B917 /* 3x3NoRot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 3x3NoRot.png; sourceTree = ""; }; 64 | CAFB8C2C1B998FE80065B917 /* 3x3Rot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 3x3Rot.png; sourceTree = ""; }; 65 | CAFB8C2D1B998FE80065B917 /* 4NoRot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 4NoRot.png; sourceTree = ""; }; 66 | CAFB8C2E1B998FE80065B917 /* 4Rot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 4Rot.png; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | CAFB8BE81B98AA090065B917 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | CAFB8BFD1B98AA090065B917 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | CAFB8BE21B98AA090065B917 = { 88 | isa = PBXGroup; 89 | children = ( 90 | CAFB8C261B998FE80065B917 /* 3D Graph screenshots */, 91 | CAFB8BED1B98AA090065B917 /* 3DGraph */, 92 | CAFB8C031B98AA090065B917 /* 3DGraphTests */, 93 | CAFB8BEC1B98AA090065B917 /* Products */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | CAFB8BEC1B98AA090065B917 /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | CAFB8BEB1B98AA090065B917 /* 3DGraph.app */, 101 | CAFB8C001B98AA090065B917 /* 3DGraphTests.xctest */, 102 | ); 103 | name = Products; 104 | sourceTree = ""; 105 | }; 106 | CAFB8BED1B98AA090065B917 /* 3DGraph */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | CAFB8C101B98AA310065B917 /* 3DGraph */, 110 | CAFB8BF01B98AA090065B917 /* AppDelegate.swift */, 111 | CAFB8BF21B98AA090065B917 /* ViewController.swift */, 112 | CAFB8BF41B98AA090065B917 /* Main.storyboard */, 113 | CAFB8BF71B98AA090065B917 /* Images.xcassets */, 114 | CAFB8BF91B98AA090065B917 /* LaunchScreen.xib */, 115 | CAFB8BEE1B98AA090065B917 /* Supporting Files */, 116 | ); 117 | path = 3DGraph; 118 | sourceTree = ""; 119 | }; 120 | CAFB8BEE1B98AA090065B917 /* Supporting Files */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | CAFB8BEF1B98AA090065B917 /* Info.plist */, 124 | ); 125 | name = "Supporting Files"; 126 | sourceTree = ""; 127 | }; 128 | CAFB8C031B98AA090065B917 /* 3DGraphTests */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | CAFB8C061B98AA090065B917 /* _DGraphTests.swift */, 132 | CAFB8C041B98AA090065B917 /* Supporting Files */, 133 | ); 134 | path = 3DGraphTests; 135 | sourceTree = ""; 136 | }; 137 | CAFB8C041B98AA090065B917 /* Supporting Files */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | CAFB8C051B98AA090065B917 /* Info.plist */, 141 | ); 142 | name = "Supporting Files"; 143 | sourceTree = ""; 144 | }; 145 | CAFB8C101B98AA310065B917 /* 3DGraph */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | CAFB8C111B98AA4E0065B917 /* BarElement.swift */, 149 | CAFB8C121B98AA4E0065B917 /* BarNode.swift */, 150 | CAFB8C131B98AA4E0065B917 /* GraphNode.swift */, 151 | CAFB8C141B98AA4E0065B917 /* GraphView.swift */, 152 | CAFB8C151B98AA4E0065B917 /* LabelNode.swift */, 153 | ); 154 | path = 3DGraph; 155 | sourceTree = ""; 156 | }; 157 | CAFB8C261B998FE80065B917 /* 3D Graph screenshots */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | CAD0983C1B9EF085006E4E2D /* 3DGraph.gif */, 161 | CAD098361B9D7908006E4E2D /* 2NoRot.png */, 162 | CAD098371B9D7908006E4E2D /* 2Rot.png */, 163 | CAFB8C291B998FE80065B917 /* 3x1NoRot.png */, 164 | CAFB8C2A1B998FE80065B917 /* 3x1Rot.png */, 165 | CAFB8C2B1B998FE80065B917 /* 3x3NoRot.png */, 166 | CAFB8C2C1B998FE80065B917 /* 3x3Rot.png */, 167 | CAFB8C2D1B998FE80065B917 /* 4NoRot.png */, 168 | CAFB8C2E1B998FE80065B917 /* 4Rot.png */, 169 | ); 170 | path = "3D Graph screenshots"; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXGroup section */ 174 | 175 | /* Begin PBXNativeTarget section */ 176 | CAFB8BEA1B98AA090065B917 /* 3DGraph */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = CAFB8C0A1B98AA090065B917 /* Build configuration list for PBXNativeTarget "3DGraph" */; 179 | buildPhases = ( 180 | CAFB8BE71B98AA090065B917 /* Sources */, 181 | CAFB8BE81B98AA090065B917 /* Frameworks */, 182 | CAFB8BE91B98AA090065B917 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = 3DGraph; 189 | productName = 3DGraph; 190 | productReference = CAFB8BEB1B98AA090065B917 /* 3DGraph.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | CAFB8BFF1B98AA090065B917 /* 3DGraphTests */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = CAFB8C0D1B98AA090065B917 /* Build configuration list for PBXNativeTarget "3DGraphTests" */; 196 | buildPhases = ( 197 | CAFB8BFC1B98AA090065B917 /* Sources */, 198 | CAFB8BFD1B98AA090065B917 /* Frameworks */, 199 | CAFB8BFE1B98AA090065B917 /* Resources */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | CAFB8C021B98AA090065B917 /* PBXTargetDependency */, 205 | ); 206 | name = 3DGraphTests; 207 | productName = 3DGraphTests; 208 | productReference = CAFB8C001B98AA090065B917 /* 3DGraphTests.xctest */; 209 | productType = "com.apple.product-type.bundle.unit-test"; 210 | }; 211 | /* End PBXNativeTarget section */ 212 | 213 | /* Begin PBXProject section */ 214 | CAFB8BE31B98AA090065B917 /* Project object */ = { 215 | isa = PBXProject; 216 | attributes = { 217 | LastUpgradeCheck = 0640; 218 | ORGANIZATIONNAME = "Greg Lecki"; 219 | TargetAttributes = { 220 | CAFB8BEA1B98AA090065B917 = { 221 | CreatedOnToolsVersion = 6.4; 222 | }; 223 | CAFB8BFF1B98AA090065B917 = { 224 | CreatedOnToolsVersion = 6.4; 225 | TestTargetID = CAFB8BEA1B98AA090065B917; 226 | }; 227 | }; 228 | }; 229 | buildConfigurationList = CAFB8BE61B98AA090065B917 /* Build configuration list for PBXProject "3DGraph" */; 230 | compatibilityVersion = "Xcode 3.2"; 231 | developmentRegion = English; 232 | hasScannedForEncodings = 0; 233 | knownRegions = ( 234 | en, 235 | Base, 236 | ); 237 | mainGroup = CAFB8BE21B98AA090065B917; 238 | productRefGroup = CAFB8BEC1B98AA090065B917 /* Products */; 239 | projectDirPath = ""; 240 | projectRoot = ""; 241 | targets = ( 242 | CAFB8BEA1B98AA090065B917 /* 3DGraph */, 243 | CAFB8BFF1B98AA090065B917 /* 3DGraphTests */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | CAFB8BE91B98AA090065B917 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | CAD0983D1B9EF085006E4E2D /* 3DGraph.gif in Resources */, 254 | CAFB8C321B998FE80065B917 /* 3x1Rot.png in Resources */, 255 | CAFB8BF61B98AA090065B917 /* Main.storyboard in Resources */, 256 | CAFB8C361B998FE80065B917 /* 4Rot.png in Resources */, 257 | CAFB8C341B998FE80065B917 /* 3x3Rot.png in Resources */, 258 | CAFB8BFB1B98AA090065B917 /* LaunchScreen.xib in Resources */, 259 | CAFB8BF81B98AA090065B917 /* Images.xcassets in Resources */, 260 | CAFB8C351B998FE80065B917 /* 4NoRot.png in Resources */, 261 | CAD098381B9D7908006E4E2D /* 2NoRot.png in Resources */, 262 | CAD098391B9D7908006E4E2D /* 2Rot.png in Resources */, 263 | CAFB8C311B998FE80065B917 /* 3x1NoRot.png in Resources */, 264 | CAFB8C331B998FE80065B917 /* 3x3NoRot.png in Resources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | CAFB8BFE1B98AA090065B917 /* Resources */ = { 269 | isa = PBXResourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | /* End PBXResourcesBuildPhase section */ 276 | 277 | /* Begin PBXSourcesBuildPhase section */ 278 | CAFB8BE71B98AA090065B917 /* Sources */ = { 279 | isa = PBXSourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | CAFB8C181B98AA4E0065B917 /* GraphNode.swift in Sources */, 283 | CAFB8C161B98AA4E0065B917 /* BarElement.swift in Sources */, 284 | CAFB8BF31B98AA090065B917 /* ViewController.swift in Sources */, 285 | CAFB8BF11B98AA090065B917 /* AppDelegate.swift in Sources */, 286 | CAFB8C1A1B98AA4E0065B917 /* LabelNode.swift in Sources */, 287 | CAFB8C171B98AA4E0065B917 /* BarNode.swift in Sources */, 288 | CAFB8C191B98AA4E0065B917 /* GraphView.swift in Sources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | CAFB8BFC1B98AA090065B917 /* Sources */ = { 293 | isa = PBXSourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | CAFB8C071B98AA090065B917 /* _DGraphTests.swift in Sources */, 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | }; 300 | /* End PBXSourcesBuildPhase section */ 301 | 302 | /* Begin PBXTargetDependency section */ 303 | CAFB8C021B98AA090065B917 /* PBXTargetDependency */ = { 304 | isa = PBXTargetDependency; 305 | target = CAFB8BEA1B98AA090065B917 /* 3DGraph */; 306 | targetProxy = CAFB8C011B98AA090065B917 /* PBXContainerItemProxy */; 307 | }; 308 | /* End PBXTargetDependency section */ 309 | 310 | /* Begin PBXVariantGroup section */ 311 | CAFB8BF41B98AA090065B917 /* Main.storyboard */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | CAFB8BF51B98AA090065B917 /* Base */, 315 | ); 316 | name = Main.storyboard; 317 | sourceTree = ""; 318 | }; 319 | CAFB8BF91B98AA090065B917 /* LaunchScreen.xib */ = { 320 | isa = PBXVariantGroup; 321 | children = ( 322 | CAFB8BFA1B98AA090065B917 /* Base */, 323 | ); 324 | name = LaunchScreen.xib; 325 | sourceTree = ""; 326 | }; 327 | /* End PBXVariantGroup section */ 328 | 329 | /* Begin XCBuildConfiguration section */ 330 | CAFB8C081B98AA090065B917 /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 335 | CLANG_CXX_LIBRARY = "libc++"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_WARN_BOOL_CONVERSION = YES; 339 | CLANG_WARN_CONSTANT_CONVERSION = YES; 340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 341 | CLANG_WARN_EMPTY_BODY = YES; 342 | CLANG_WARN_ENUM_CONVERSION = YES; 343 | CLANG_WARN_INT_CONVERSION = YES; 344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | GCC_C_LANGUAGE_STANDARD = gnu99; 352 | GCC_DYNAMIC_NO_PIC = NO; 353 | GCC_NO_COMMON_BLOCKS = YES; 354 | GCC_OPTIMIZATION_LEVEL = 0; 355 | GCC_PREPROCESSOR_DEFINITIONS = ( 356 | "DEBUG=1", 357 | "$(inherited)", 358 | ); 359 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 367 | MTL_ENABLE_DEBUG_INFO = YES; 368 | ONLY_ACTIVE_ARCH = YES; 369 | SDKROOT = iphoneos; 370 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 371 | TARGETED_DEVICE_FAMILY = 2; 372 | }; 373 | name = Debug; 374 | }; 375 | CAFB8C091B98AA090065B917 /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ALWAYS_SEARCH_USER_PATHS = NO; 379 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 380 | CLANG_CXX_LIBRARY = "libc++"; 381 | CLANG_ENABLE_MODULES = YES; 382 | CLANG_ENABLE_OBJC_ARC = YES; 383 | CLANG_WARN_BOOL_CONVERSION = YES; 384 | CLANG_WARN_CONSTANT_CONVERSION = YES; 385 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 386 | CLANG_WARN_EMPTY_BODY = YES; 387 | CLANG_WARN_ENUM_CONVERSION = YES; 388 | CLANG_WARN_INT_CONVERSION = YES; 389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | SDKROOT = iphoneos; 408 | TARGETED_DEVICE_FAMILY = 2; 409 | VALIDATE_PRODUCT = YES; 410 | }; 411 | name = Release; 412 | }; 413 | CAFB8C0B1B98AA090065B917 /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | INFOPLIST_FILE = 3DGraph/Info.plist; 418 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | }; 421 | name = Debug; 422 | }; 423 | CAFB8C0C1B98AA090065B917 /* Release */ = { 424 | isa = XCBuildConfiguration; 425 | buildSettings = { 426 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 427 | INFOPLIST_FILE = 3DGraph/Info.plist; 428 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | }; 431 | name = Release; 432 | }; 433 | CAFB8C0E1B98AA090065B917 /* Debug */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | BUNDLE_LOADER = "$(TEST_HOST)"; 437 | FRAMEWORK_SEARCH_PATHS = ( 438 | "$(SDKROOT)/Developer/Library/Frameworks", 439 | "$(inherited)", 440 | ); 441 | GCC_PREPROCESSOR_DEFINITIONS = ( 442 | "DEBUG=1", 443 | "$(inherited)", 444 | ); 445 | INFOPLIST_FILE = 3DGraphTests/Info.plist; 446 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 447 | PRODUCT_NAME = "$(TARGET_NAME)"; 448 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/3DGraph.app/3DGraph"; 449 | }; 450 | name = Debug; 451 | }; 452 | CAFB8C0F1B98AA090065B917 /* Release */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | BUNDLE_LOADER = "$(TEST_HOST)"; 456 | FRAMEWORK_SEARCH_PATHS = ( 457 | "$(SDKROOT)/Developer/Library/Frameworks", 458 | "$(inherited)", 459 | ); 460 | INFOPLIST_FILE = 3DGraphTests/Info.plist; 461 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/3DGraph.app/3DGraph"; 464 | }; 465 | name = Release; 466 | }; 467 | /* End XCBuildConfiguration section */ 468 | 469 | /* Begin XCConfigurationList section */ 470 | CAFB8BE61B98AA090065B917 /* Build configuration list for PBXProject "3DGraph" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | CAFB8C081B98AA090065B917 /* Debug */, 474 | CAFB8C091B98AA090065B917 /* Release */, 475 | ); 476 | defaultConfigurationIsVisible = 0; 477 | defaultConfigurationName = Release; 478 | }; 479 | CAFB8C0A1B98AA090065B917 /* Build configuration list for PBXNativeTarget "3DGraph" */ = { 480 | isa = XCConfigurationList; 481 | buildConfigurations = ( 482 | CAFB8C0B1B98AA090065B917 /* Debug */, 483 | CAFB8C0C1B98AA090065B917 /* Release */, 484 | ); 485 | defaultConfigurationIsVisible = 0; 486 | defaultConfigurationName = Release; 487 | }; 488 | CAFB8C0D1B98AA090065B917 /* Build configuration list for PBXNativeTarget "3DGraphTests" */ = { 489 | isa = XCConfigurationList; 490 | buildConfigurations = ( 491 | CAFB8C0E1B98AA090065B917 /* Debug */, 492 | CAFB8C0F1B98AA090065B917 /* Release */, 493 | ); 494 | defaultConfigurationIsVisible = 0; 495 | defaultConfigurationName = Release; 496 | }; 497 | /* End XCConfigurationList section */ 498 | }; 499 | rootObject = CAFB8BE31B98AA090065B917 /* Project object */; 500 | } 501 | -------------------------------------------------------------------------------- /3DGraph.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /3DGraph/3DGraph/BarElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarElement.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 27/08/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /// Single bar element to be added to barElements array in GraphView 13 | 14 | class BarElement: NSObject { 15 | 16 | private(set) var width: CGFloat = 10 17 | private(set) var height: CGFloat = 10 18 | private(set) var color: UIColor 19 | private(set) var cornerRadius: CGFloat 20 | 21 | /// Use internally by library. DON'T set the value manually. 22 | var index = 0 23 | 24 | /// Leght of the bar 25 | var length: CGFloat = 10 26 | 27 | /// Array of labels - each element in the array is draw in a separate line 28 | var labels: [NSAttributedString]? 29 | 30 | /** 31 | Initializes a new bar element with the provided pameters. 32 | 33 | :param: width with of the bar element 34 | :param: height height of the bar element 35 | :param: length length of the bar element 36 | :param: color color of the bar element 37 | :param: cornerRadius corner radius of the bar element represented in units not pixel 38 | 39 | :returns: A new bar element. 40 | */ 41 | init(width: CGFloat = 10, height: CGFloat = 10, length: CGFloat = 10, color: UIColor = UIColor.grayColor(), cornerRadius: CGFloat = 0.5) { 42 | 43 | self.width = width 44 | self.height = height 45 | self.length = length 46 | self.color = color 47 | self.cornerRadius = cornerRadius 48 | 49 | super.init() 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /3DGraph/3DGraph/BarNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarNode.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 27/08/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | 13 | class BarNode: SCNNode { 14 | 15 | var width: CGFloat = 10 16 | var height: CGFloat = 10 17 | private(set) var color: UIColor 18 | private(set) var cornerRadius: CGFloat 19 | private var bar: SCNBox! 20 | private var barNode: SCNNode! 21 | private var textNode: SCNNode? 22 | var labelOffset: CGPoint = CGPointZero 23 | 24 | var index = 0 25 | 26 | var length: CGFloat = 10 /*{ 27 | didSet { 28 | SCNTransaction.begin() 29 | SCNTransaction.setAnimationDuration(0.5) 30 | 31 | bar.length = length 32 | barNode.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 33 | 34 | if let textNode = textNode { 35 | textNode.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 36 | } 37 | SCNTransaction.commit() 38 | } 39 | }*/ 40 | 41 | var labels: [NSAttributedString]? /*{ 42 | didSet { 43 | addLabels(labels?.reverse()) 44 | } 45 | }*/ 46 | 47 | init(width: CGFloat = 10, height: CGFloat = 10, length: CGFloat = 10, color: UIColor = UIColor.grayColor(), cornerRadius: CGFloat = 0.5) { 48 | 49 | self.width = width 50 | self.height = height 51 | self.length = length 52 | self.color = color 53 | self.cornerRadius = cornerRadius 54 | 55 | super.init() 56 | 57 | } 58 | 59 | required init(coder aDecoder: NSCoder) { 60 | fatalError("not implemented") 61 | } 62 | 63 | func prepareBarNode() { 64 | 65 | barNode = SCNNode(geometry: createBar(width, height: height, length: length, color: color, cornerRadius: cornerRadius)) 66 | barNode.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 67 | 68 | addLabels(labels?.reverse()) 69 | 70 | addChildNode(barNode) 71 | } 72 | 73 | func setLength(length: CGFloat, animated: Bool) { 74 | 75 | let duration: CFTimeInterval = animated ? 0.5 : 0.0 76 | SCNTransaction.begin() 77 | SCNTransaction.setAnimationDuration(duration) 78 | 79 | bar.length = length 80 | barNode.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 81 | 82 | if let textNode = textNode { 83 | textNode.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 84 | } 85 | SCNTransaction.commit() 86 | } 87 | 88 | private func createBar(width: CGFloat, height: CGFloat, length: CGFloat, color: UIColor, cornerRadius: CGFloat) -> SCNBox { 89 | 90 | let geometry = SCNBox(width: width, height: height, length: length, chamferRadius: cornerRadius) 91 | geometry.firstMaterial!.diffuse.contents = color 92 | geometry.firstMaterial!.specular.contents = UIColor.whiteColor() 93 | bar = geometry 94 | 95 | return geometry 96 | } 97 | 98 | private func addLabels(labels: [NSAttributedString]?) { 99 | 100 | if let labels = labels { 101 | 102 | textNode = SCNNode() 103 | 104 | let space = height*0.8 / CGFloat(labels.count) 105 | var yPoint = (-height/2) + (space/2) 106 | 107 | for attString in labels { 108 | 109 | if attString.length < 1 { return } 110 | 111 | var range = NSMakeRange(0, 1) 112 | 113 | let myText = SCNText(string: attString, extrusionDepth: 0.1) 114 | myText.alignmentMode = kCAAlignmentRight 115 | if let color = attString.attribute(NSForegroundColorAttributeName, atIndex: 0, effectiveRange:&range) as? UIColor { 116 | myText.firstMaterial!.diffuse.contents = color 117 | } 118 | 119 | myText.firstMaterial!.specular.contents = UIColor.whiteColor() 120 | let myTextNode = SCNNode(geometry: myText) 121 | 122 | myTextNode.position = SCNVector3(x: Float(-attString.size().width + width*0.9/2 + labelOffset.x), y: Float(yPoint + labelOffset.y), z: 0) 123 | myTextNode.orientation = SCNQuaternion(x: 0.1, y: 0, z: 0, w: 0) 124 | 125 | textNode?.addChildNode(myTextNode) 126 | 127 | yPoint += space 128 | } 129 | 130 | textNode!.pivot = SCNMatrix4MakeTranslation(0, 0, Float(-length/2)) 131 | barNode.addChildNode(textNode!) 132 | } 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /3DGraph/3DGraph/GraphNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphNode.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 25/08/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | 13 | class GraphNode: SCNNode { 14 | 15 | private var barNodes: [BarNode] 16 | private var columns: Int 17 | var horizontalSpace: Float = 2 18 | var verticalSpace: Float = 2 19 | var xAxisLabels: [NSAttributedString]? 20 | var yAxisLabels: [NSAttributedString]? 21 | var xLabelsOnTop = true 22 | var yLabelsOnLeft = true 23 | 24 | init(barNodes: [BarNode], columns: Int = 3) { 25 | 26 | self.barNodes = barNodes 27 | self.columns = columns 28 | 29 | super.init() 30 | } 31 | 32 | required init(coder aDecoder: NSCoder) { 33 | fatalError("not implemented") 34 | } 35 | 36 | func reDrawGraph() { 37 | prepareGraph() 38 | } 39 | 40 | private var startPositionX: Float = 0 41 | private var startPositionY: Float = 0 42 | 43 | private func prepareGraph() { 44 | 45 | let mainNode = SCNNode() 46 | 47 | var maxColsOrBars = columns > barNodes.count ? barNodes.count : columns 48 | var totalRowLength: Float = Float(barNodes[0.. maxLabelWidth) ? Float(lab.size().width) : maxLabelWidth 55 | } 56 | totalRowLength += horizontalSpace 57 | } 58 | totalRowLength += maxLabelWidth 59 | 60 | var totalColumnLength: Float = 0.0 61 | for (index, element) in enumerate(barNodes) { 62 | if index % columns == 0 { 63 | totalColumnLength += Float(element.height) 64 | } 65 | } 66 | var maxLabelHeight: Float = 0 67 | if let ylabels = yAxisLabels { 68 | for lab in ylabels { 69 | maxLabelHeight = (Float(lab.size().height) > maxLabelHeight) ? Float(lab.size().height) : maxLabelHeight 70 | totalColumnLength += verticalSpace 71 | } 72 | } 73 | totalColumnLength + maxLabelHeight 74 | 75 | var rowsCount = Int(ceilf(Float(barNodes.count)/Float(columns))) 76 | totalColumnLength += verticalSpace * Float(rowsCount-1) 77 | 78 | if xLabelsOnTop { 79 | startPositionY = totalColumnLength/2 - (maxLabelHeight + verticalSpace) 80 | } else { 81 | startPositionY = totalColumnLength/2 82 | } 83 | 84 | if yLabelsOnLeft { 85 | startPositionX = -totalRowLength/2 + maxLabelWidth/2 + horizontalSpace 86 | } else { 87 | startPositionX = -totalRowLength/2 + (2 * horizontalSpace) 88 | } 89 | 90 | var positionX: Float = startPositionX 91 | var positionY: Float = startPositionY 92 | 93 | for (index, bar) in enumerate(barNodes) { 94 | 95 | bar.index = index 96 | 97 | if index % columns == 0 { 98 | var aboveHeight: Float = 0.0 99 | if index - columns >= 0 { 100 | aboveHeight = Float(barNodes[index-columns].height) 101 | } 102 | 103 | positionY -= aboveHeight/2 + Float(verticalSpace) + Float(bar.height/2) 104 | positionX = startPositionX 105 | } 106 | 107 | positionX += Float(bar.width/2) 108 | 109 | bar.position = SCNVector3Make(positionX, positionY, 0) 110 | positionX += Float(horizontalSpace) + Float(bar.width/2) 111 | 112 | mainNode.addChildNode(bar) 113 | } 114 | 115 | addChildNode(mainNode) 116 | 117 | addLabel() 118 | } 119 | 120 | private func addLabel() { 121 | 122 | if let xAxisLab = xAxisLabels { 123 | var maxCol = columns <= barNodes.count ? columns : barNodes.count 124 | for index in 0.. columns-1 { 179 | let tempBar = barNodes[columns-1] 180 | xPos = tempBar.position.x + Float(tempBar.width/2) + horizontalSpace 181 | 182 | var idx = index * columns 183 | if idx < barNodes.count { 184 | 185 | let bar = barNodes[idx] 186 | yPos = bar.position.y 187 | } 188 | } 189 | } 190 | 191 | let yLabelNode = LabelNode(text:textLabel) 192 | 193 | if let n = yLabelNode.getLabel() { 194 | n.position = SCNVector3(x: xPos, y: yPos, z: 0) 195 | addChildNode(n) 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /3DGraph/3DGraph/GraphView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphView.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 26/08/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | 13 | let MINRANGE: CGFloat = 0 14 | let MAXRANGE: CGFloat = 60 15 | 16 | /// Place where the graph will be displayed 17 | class GraphView: UIView { 18 | 19 | private var sceneView: SCNView! 20 | private var mainNode: SCNNode = SCNNode() 21 | private var barNodes = [BarNode]() 22 | private var minRange: CGFloat = 0 23 | private var maxRange: CGFloat = 0 24 | private var animated = false 25 | 26 | /// If rotateAtTapEnabled is true that's amount of degree the bar will rotate around the X Axis when user tapped the graph view 27 | var rotAngleX: CGFloat = -35.0 28 | /// If rotateAtTapEnabled is true that's amount of degree the bar will rotate around the Y Axis when user tapped the graph view 29 | var rotAngleY: CGFloat = -5.0 30 | 31 | /// Enable rotation of the graph view on touch, the default is true 32 | var rotateAtTapEnabled = true 33 | /// Handler which is triggered after user touch bar element 34 | var selectedItemHandler: ((Int) -> ())? 35 | 36 | /// Labels describe the X Axis 37 | var xAxisLabels: [NSAttributedString]? { 38 | didSet { 39 | drawGraph() 40 | } 41 | } 42 | 43 | /// Labels describe the Y Axis 44 | var yAxisLabels: [NSAttributedString]? { 45 | didSet { 46 | drawGraph() 47 | } 48 | } 49 | 50 | /// Set the X Axis labels on the top of the graph, the default is true. Set to false if you want to move it to the bottom 51 | var xLabelsOnTop = true { 52 | didSet { 53 | drawGraph() 54 | } 55 | } 56 | 57 | /// Set the Y Axis labels on the left of the graph, the default is true. Set to false if you want to move it to the right 58 | var yLabelsOnLeft = true { 59 | didSet { 60 | drawGraph() 61 | } 62 | } 63 | 64 | /// Array of BarElements which will be draw on the graph 65 | var barElements = [BarElement]() { 66 | didSet { 67 | drawGraph() 68 | } 69 | } 70 | 71 | /// Number of bar elements in one row 72 | var columns: Int = 3 { 73 | didSet { 74 | drawGraph() 75 | } 76 | } 77 | 78 | /// Horizontal space betweene each bar element 79 | var horizontalSpace: Float = 2 { 80 | didSet { 81 | drawGraph() 82 | } 83 | } 84 | 85 | /// Vertical space betweene each bar element 86 | var verticalSpace: Float = 2 { 87 | didSet { 88 | drawGraph() 89 | } 90 | } 91 | 92 | /// The graph position offset, the default is CGPointZero (centre of the view). Change the offset is you want to move in relation to the view 93 | var offset: CGPoint = CGPointZero { 94 | didSet { 95 | offset = translatePointTo3DWorld(offset) 96 | drawGraph() 97 | } 98 | } 99 | 100 | /// BarElement's labels offset, the default is CGPointZero. Change the offset is you want to move in relation to the bar element 101 | var labelOffset: CGPoint = CGPointZero { 102 | didSet { 103 | labelOffset = translatePointTo3DWorld(labelOffset) 104 | drawGraph() 105 | } 106 | } 107 | 108 | // MARK: - UIVIew Lifecycle 109 | required init(coder aDecoder: NSCoder) { 110 | super.init(coder: aDecoder) 111 | 112 | prepareGraphView() 113 | } 114 | 115 | override init(frame: CGRect) { 116 | super.init(frame: frame) 117 | 118 | prepareGraphView() 119 | } 120 | 121 | override var bounds: CGRect { 122 | didSet { 123 | //println("Size: \(NSStringFromCGRect(bounds))") 124 | sceneView.frame = bounds 125 | if let _ = mainNode.parentNode { 126 | drawGraph() 127 | } 128 | } 129 | } 130 | 131 | // MARK: - Custom Methods 132 | /** 133 | Changes single bar element length with animation, which could be disabled 134 | 135 | :param: length The new length of the bar element 136 | :param: atIndex index of bar element you want to change the length 137 | :param: animated animate the change if set to true 138 | */ 139 | func setBarLength(length: CGFloat, atIndex index: Int, animated: Bool = true) { 140 | 141 | var newLength = length 142 | if length > maxRange { 143 | newLength = maxRange 144 | } 145 | if length < minRange { 146 | newLength = minRange 147 | } 148 | if let node = barNodes.filter( { $0.index == index } ).first { 149 | let val = getNewValue(newLength, min: minRange, max: maxRange) 150 | node.setLength(val, animated: animated) 151 | } 152 | } 153 | 154 | /** 155 | Changes all bar elements length with animation, which could be disabled 156 | 157 | :param: lengths Array of the new lengths. Array count needs to match bar elements count added to the graph view 158 | :param: animated animate the change if set to true 159 | */ 160 | func reloadBarLengths(lengths: [CGFloat], animated: Bool = true) { 161 | 162 | if lengths.count < barNodes.count { 163 | println("ReloadBarLengths: array of lengths must be the same length as bar elements count passed to graph view") 164 | return 165 | } 166 | 167 | minRange = 0 168 | maxRange = 0 169 | for len in lengths { 170 | if len > maxRange { 171 | maxRange = len 172 | } 173 | if len < minRange { 174 | minRange = len 175 | } 176 | } 177 | 178 | for idx in 0.. 0 { 241 | 242 | minRange = 0 243 | maxRange = 0 244 | for b in barElements { 245 | if b.length > maxRange { 246 | maxRange = b.length 247 | } 248 | if b.length < minRange { 249 | minRange = b.length 250 | } 251 | } 252 | 253 | barNodes = barElements.map { 254 | [unowned self] (bar) -> BarNode in 255 | 256 | let newSize = self.translateSizeTo3DWorld(CGSize(width: bar.width, height: bar.height)) 257 | let barNode = BarNode(width: newSize.width, height: newSize.height, length:self.getNewValue(bar.length, min: self.minRange, max: self.maxRange), color: bar.color, cornerRadius: bar.cornerRadius) 258 | barNode.labels = bar.labels 259 | 260 | barNode.index = bar.index 261 | barNode.labelOffset = self.labelOffset 262 | barNode.prepareBarNode() 263 | 264 | return barNode 265 | } 266 | 267 | let graph = GraphNode(barNodes: barNodes, columns: columns) 268 | graph.horizontalSpace = horizontalSpace 269 | graph.verticalSpace = verticalSpace 270 | 271 | graph.xAxisLabels = xAxisLabels 272 | graph.yAxisLabels = yAxisLabels 273 | graph.xLabelsOnTop = xLabelsOnTop 274 | graph.yLabelsOnLeft = yLabelsOnLeft 275 | 276 | graph.reDrawGraph() 277 | 278 | graph.pivot = SCNMatrix4MakeTranslation(graph.position.x - Float(offset.x), graph.position.y - Float(offset.y), 0) 279 | 280 | mainNode.removeFromParentNode() 281 | 282 | mainNode = graph 283 | sceneView.scene!.rootNode.addChildNode(mainNode) 284 | } 285 | } 286 | 287 | 288 | // MARK - Gesture Recognizer 289 | func tapGesture(sender: UITapGestureRecognizer) { 290 | 291 | if (!rotateAtTapEnabled && selectedItemHandler == nil) { 292 | return 293 | } 294 | if !animated { 295 | 296 | let vp = sender.locationInView(sceneView) 297 | 298 | if let node = getNodeAtPoint(vp) { 299 | if let handler = selectedItemHandler { 300 | handler(node.index) 301 | } 302 | // Dont animate call itemSelected block 303 | return 304 | } 305 | } 306 | 307 | rotateGraph() 308 | } 309 | 310 | 311 | // MARK - Animation Delegate 312 | override func animationDidStop(anim: CAAnimation!, finished flag: Bool) { 313 | 314 | var rotX = rotAngleX 315 | var rotY = rotAngleY 316 | if animated { 317 | rotX = 0.0 318 | rotY = 0.0 319 | } 320 | if (flag) { 321 | mainNode.transform = SCNMatrix4Mult(SCNMatrix4MakeRotation(Float(degreeToRadian(rotX)), 1, 0, 0), SCNMatrix4MakeRotation(Float(degreeToRadian(rotY)), 0, 1, 0)) 322 | mainNode.removeAllAnimations() 323 | animated = !animated 324 | } 325 | } 326 | 327 | 328 | // MARK: - Helpers 329 | private func getNodeAtPoint(point: CGPoint) -> BarNode? { 330 | 331 | let worldPoint = mapPointTo3DWorld(point) 332 | 333 | var nodes = mainNode.childNodesPassingTest { (child, stop) -> Bool in 334 | if let n = child as? BarNode { 335 | 336 | let nodeRect = CGRect(x: CGFloat(n.position.x) + self.offset.x - n.width/2, y: CGFloat(n.position.y) + self.offset.y - n.height/2, width: n.width, height: n.height) 337 | 338 | if CGRectContainsPoint(nodeRect, CGPoint(x: CGFloat(worldPoint.x), y: CGFloat(worldPoint.y))) { 339 | return true 340 | } 341 | } 342 | return false 343 | } 344 | 345 | return nodes.first as? BarNode 346 | } 347 | 348 | private func translatePointTo3DWorld(point: CGPoint) -> CGPoint { 349 | 350 | let viewSize = bounds 351 | 352 | let p = mapPointTo3DWorld(CGPoint(x: viewSize.width, y: viewSize.height)) 353 | 354 | let widthRatio = viewSize.width / (abs(p.x)*2) 355 | let heightRatio = viewSize.height / (abs(p.y)*2) 356 | 357 | return CGPoint(x: point.x/widthRatio, y: point.y/heightRatio) 358 | } 359 | 360 | private func translateSizeTo3DWorld(size: CGSize) -> CGSize { 361 | 362 | let viewSize = bounds 363 | 364 | let p = mapPointTo3DWorld(CGPoint(x: viewSize.width, y: viewSize.height)) 365 | 366 | let widthRatio = viewSize.width / (abs(p.x)*2) 367 | let heightRatio = viewSize.height / (abs(p.y)*2) 368 | 369 | return CGSize(width: size.width/widthRatio, height: size.height/heightRatio) 370 | } 371 | 372 | private func mapPointTo3DWorld(point: CGPoint) -> CGPoint { 373 | 374 | let projectedOrigin = sceneView.projectPoint(SCNVector3Zero) 375 | 376 | let vpWithZ = SCNVector3(x: Float(point.x), y: Float(point.y), z: projectedOrigin.z) 377 | let worldPoint = sceneView.unprojectPoint(vpWithZ) 378 | 379 | return CGPoint(x: CGFloat(worldPoint.x), y: CGFloat(worldPoint.y)) 380 | } 381 | 382 | private func rotateGraph() { 383 | 384 | var rotX = rotAngleX 385 | var rotY = rotAngleY 386 | if animated { 387 | rotX = 0.0 388 | rotY = 0.0 389 | } 390 | 391 | var rotAnim = CABasicAnimation(keyPath: "transform"); 392 | rotAnim.duration = 0.5; 393 | rotAnim.toValue = NSValue(SCNMatrix4: SCNMatrix4Mult(SCNMatrix4MakeRotation(Float(degreeToRadian(rotX)), 1, 0, 0), SCNMatrix4MakeRotation(Float(degreeToRadian(rotY)), 0, 1, 0))) 394 | rotAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 395 | rotAnim.delegate = self 396 | rotAnim.removedOnCompletion = false 397 | rotAnim.fillMode = kCAFillModeForwards 398 | mainNode.addAnimation(rotAnim, forKey: "transform") 399 | } 400 | 401 | private func degreeToRadian(angle: CGFloat) -> CGFloat { 402 | 403 | return angle * CGFloat(M_PI) / 180.0 404 | } 405 | 406 | private func getNewValue(value: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { 407 | 408 | let oldRange = max - min 409 | if oldRange == 0 { 410 | return MINRANGE 411 | } else { 412 | let newRange = (MAXRANGE*0.9) - MINRANGE 413 | return (((value - min) * newRange) / oldRange) + MINRANGE 414 | } 415 | } 416 | 417 | } 418 | -------------------------------------------------------------------------------- /3DGraph/3DGraph/LabelNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelNode.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 01/09/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | class LabelNode: SCNNode { 13 | 14 | private var text: NSAttributedString 15 | 16 | init(text: NSAttributedString) { 17 | 18 | self.text = text 19 | 20 | super.init() 21 | 22 | } 23 | 24 | required init(coder aDecoder: NSCoder) { 25 | fatalError("not implemented") 26 | } 27 | 28 | func getLabel() -> SCNNode? { 29 | 30 | var textNode = SCNNode() 31 | 32 | if text.length < 1 { return nil } 33 | 34 | var range = NSMakeRange(0, 1) 35 | 36 | let myText = SCNText(string: text, extrusionDepth: 0.1) 37 | myText.alignmentMode = kCAAlignmentRight 38 | 39 | if let color = text.attribute(NSForegroundColorAttributeName, atIndex: 0, effectiveRange:&range) as? UIColor { 40 | myText.firstMaterial!.diffuse.contents = color 41 | } 42 | 43 | myText.firstMaterial!.specular.contents = UIColor.whiteColor() 44 | let myTextNode = SCNNode(geometry: myText) 45 | 46 | myTextNode.orientation = SCNQuaternion(x: 0.1, y: 0, z: 0, w: 0) 47 | 48 | textNode.addChildNode(myTextNode) 49 | 50 | return textNode 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /3DGraph/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 03/09/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /3DGraph/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /3DGraph/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 37 | 46 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /3DGraph/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "40x40", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "76x76", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "76x76", 31 | "scale" : "2x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /3DGraph/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.gl.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /3DGraph/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 3DGraph 4 | // 5 | // Created by Greg Lecki on 03/09/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | //import SceneKit 12 | 13 | 14 | class ViewController: UIViewController { 15 | 16 | @IBOutlet weak var graphView: GraphView! 17 | 18 | 19 | override func viewDidAppear(animated: Bool) { 20 | super.viewWillAppear(animated) 21 | 22 | graphView.barElements = getBars() 23 | graphView.selectedItemHandler = { 24 | idx in 25 | println("Item \(idx) pressed") 26 | } 27 | 28 | graphView.xAxisLabels = getHorizontalLabels() 29 | graphView.xLabelsOnTop = false 30 | 31 | graphView.yAxisLabels = getVerticalLabels() 32 | graphView.yLabelsOnLeft = false 33 | 34 | //graphView.columns = 4 35 | //graphView.offset = CGPoint(x: 0, y: -60) 36 | //graphView.labelOffset = CGPoint(x: -10, y: 5) 37 | //graphView.rotAngleX = -60 38 | //graphView.rotAngleY = -18 39 | 40 | } 41 | 42 | @IBAction func columnsPressed(sender: AnyObject) { 43 | 44 | graphView.columns = graphView.columns == 3 ? 4 : 3 45 | } 46 | 47 | @IBAction func horSpaceButtonPressed(sender: AnyObject) { 48 | graphView.horizontalSpace = graphView.horizontalSpace == 2 ? 4 : 2 49 | } 50 | 51 | @IBAction func vertSpaceButtonPressed(sender: AnyObject) { 52 | graphView.verticalSpace = graphView.verticalSpace == 2 ? 4 : 2 53 | } 54 | 55 | @IBAction func animationButtonPressed(sender: AnyObject) { 56 | 57 | graphView.setBarLength(150, atIndex: 0, animated: true) 58 | graphView.setBarLength(80, atIndex: 2, animated: true) 59 | graphView.setBarLength(70, atIndex: 5, animated: true) 60 | } 61 | 62 | // MARK: - TEST Methods 63 | func getBars() -> [BarElement] { 64 | 65 | var label1 = NSMutableAttributedString(string: "Count 76") 66 | label1.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 5)) 67 | label1.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(5, label1.length-5)) 68 | 69 | var label2 = NSMutableAttributedString(string: "Value 663.4k") 70 | label2.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 5)) 71 | label2.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(5, label2.length-5)) 72 | 73 | var label3 = NSMutableAttributedString(string: "Avg demand 178.9k") 74 | label3.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 10)) 75 | label3.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(10, label3.length-10)) 76 | 77 | var label4 = NSMutableAttributedString(string: "Fill(90.04%) 91.96%") 78 | label4.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 12)) 79 | label4.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(12, label4.length-12)) 80 | 81 | let bar1 = BarElement(width: 200, height:150, length: 200, color: UIColor.orangeColor(), cornerRadius: 2.5) 82 | bar1.labels = [label1, label2, label3, label4] 83 | 84 | let bar2 = BarElement(width: 200, height: 150, length: 150, color: UIColor.redColor(), cornerRadius: 2.5) 85 | bar2.labels = [label1, label2, label3, label4] 86 | 87 | let bar3 = BarElement(width: 200, height: 150, length: 180, color: UIColor.blueColor(), cornerRadius: 2.5) 88 | bar3.labels = [label1, label2, label3, label4] 89 | 90 | let bar4 = BarElement(width: 200, height: 150, length: 140, color: UIColor.orangeColor(), cornerRadius: 1.5) 91 | let bar5 = BarElement(width: 200, height: 150, length: 100, color: UIColor.blueColor(), cornerRadius: 1.5) 92 | let bar6 = BarElement(width: 200, height: 150, length: 120, color: UIColor.redColor(), cornerRadius: 1.5) 93 | let bar7 = BarElement(width: 200, height: 150, length: 80, color: UIColor.redColor(), cornerRadius: 1.5) 94 | let bar8 = BarElement(width: 200, height: 150, length: 60, color: UIColor.orangeColor(), cornerRadius: 1.5) 95 | let bar9 = BarElement(width: 200, height: 150, length: 100, color: UIColor.blueColor(), cornerRadius: 1.5) 96 | 97 | return [bar1, bar2, bar3, bar4, bar5, bar6, bar7, bar8, bar9] 98 | } 99 | 100 | func getHorizontalLabels() -> [NSAttributedString] { 101 | var labelX1 = NSMutableAttributedString(string: "Low") 102 | labelX1.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 3)) 103 | 104 | var labelX2 = NSMutableAttributedString(string: "Medium") 105 | labelX2.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 6)) 106 | 107 | var labelX3 = NSMutableAttributedString(string: "High") 108 | labelX3.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 4)) 109 | 110 | var labelX4 = NSMutableAttributedString(string: "Very High") 111 | labelX4.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 9)) 112 | 113 | return [labelX1, labelX2, labelX3, labelX4] 114 | } 115 | 116 | func getVerticalLabels() -> [NSAttributedString] { 117 | var labelY1 = NSMutableAttributedString(string: "A") 118 | labelY1.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 1)) 119 | 120 | var labelY2 = NSMutableAttributedString(string: "B") 121 | labelY2.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 1)) 122 | 123 | var labelY3 = NSMutableAttributedString(string: "C") 124 | labelY3.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(5), NSForegroundColorAttributeName: UIColor.darkGrayColor()], range: NSMakeRange(0, 1)) 125 | return [labelY1, labelY2, labelY3] 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /3DGraphTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.gl.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /3DGraphTests/_DGraphTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // _DGraphTests.swift 3 | // 3DGraphTests 4 | // 5 | // Created by Greg Lecki on 03/09/2015. 6 | // Copyright (c) 2015 Greg Lecki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class _DGraphTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3DGraph 2 | An interactive Swift 3D Graph. Simple, very easy to setup and use. 3 | 4 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/3DGraph.gif "3DGraph GIF") 5 | 6 | 7 | #### Installation 8 | - Copy `BarNode.swift`, `LabelNode.swift`, `GraphNode.swift`, `BarElement.swift` and `GraphView.swift` to your project. 9 | 10 | #### Usage 11 | 12 | Add a UIView to storyboard or xib and assign a subclass of `GraphView` (you can also create GraphView in code) 13 | Allocate the required constraints should be necessary and appropriate measures according to our needs. 14 | The next step would be a sight IBOutlet to assign attributes and data. 15 | Once these steps, we pass the data we want to show in the graph, it is an array of `BarElement` (`[BarElement]`) 16 | 17 | 18 | ```swift 19 | 20 | //Create `BarElement` 21 | 22 | let bar1 = BarElement(width: 200, height:150, length: 200, color: UIColor.orangeColor(), cornerRadius: 2.5) 23 | let bar2 = BarElement(width: 200, height: 150, length: 150, color: UIColor.redColor(), cornerRadius: 2.5) 24 | let bar3 = BarElement(width: 200, height: 150, length: 180, color: UIColor.blueColor(), cornerRadius: 2.5) 25 | 26 | // Optionally you can add labels to the bar element, it needs to be ann array of AttributedString 27 | var label1 = NSMutableAttributedString(string: "Count 76") 28 | label1.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 5)) 29 | label1.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(5, label1.length-5)) 30 | 31 | var label2 = NSMutableAttributedString(string: "Value 663.4k") 32 | label2.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 5)) 33 | label2.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(5, label2.length-5)) 34 | 35 | var label3 = NSMutableAttributedString(string: "Avg demand 178.9k") 36 | label3.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 10)) 37 | label3.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(10, label3.length-10)) 38 | 39 | var label4 = NSMutableAttributedString(string: "Fill(90.04%) 91.96%") 40 | label4.addAttributes([NSFontAttributeName: UIFont.systemFontOfSize(4), NSForegroundColorAttributeName: UIColor.whiteColor()], range: NSMakeRange(0, 12)) 41 | label4.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(4.5)], range: NSMakeRange(12, label4.length-12)) 42 | 43 | bar1.labels = [label1, label2, label3, label4] 44 | bar2.labels = [label1, label2, label3, label4] 45 | bar3.labels = [label1, label2, label3, label4] 46 | ``` 47 | 48 | To display the bar elements add it to garElements property of graphView: 49 | 50 | ```swift 51 | class ViewController: UIViewController { 52 | 53 | @IBOutlet weak var graphView: GraphView! 54 | 55 | 56 | override func viewDidAppear(animated: Bool) { 57 | super.viewWillAppear(animated) 58 | 59 | graphView.barElements = [bar1, bar2, bar3] 60 | graphView.selectedItemHandler = { 61 | idx in 62 | println("Item \(idx) pressed") 63 | } 64 | } 65 | } 66 | ``` 67 | ##### Customize 68 | ```swift 69 | graphView.columns = 4 // Columns in single row 70 | graphView.offset = CGPoint(x: 0, y: -60) // Let you move the graph inside the view 71 | graphView.labelOffset = CGPoint(x: -10, y: 5) // Let you move the labels inside bar element 72 | graphView.rotAngleX = -60 // Rotation in degrees for animation after user tap the graph around X axis 73 | graphView.rotAngleY = -18 // Rotation in degrees for animation after user tap the graph around Y axis 74 | graphView.rotateAtTapEnabled = true // Enable rotation of the graph view on touch 75 | graphView.selectedItemHandler: ((Int) -> ())? // Handler which is triggered after user touch bar element 76 | graphView.horizontalSpace = 2 // horizontal space between bar elements 77 | graphView.verticalSpace = 2 // vertical space between bar elements 78 | graphView.xAxisLabels // Set the X Axis labels on the top/bottom of the graph. It's array of NSAttributedString 79 | graphView.yAxisLabels // Set the Y Axis labels on the left/right of the graph. It's array of NSAttributedString 80 | graphView.xLabelsOnTop = true // Set the position of xAxisLabels (top/bottom) 81 | graphView.yLabelsOnLeft = true // Set the position of yAxisLabels (left/right) 82 | ``` 83 | 84 | ##### Preview Image 85 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/2NoRot.png "3DGraph Screenshot") 86 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/2Rot.png "3DGraph Screenshot") 87 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/3x1NoRot.png "3DGraph Screenshot") 88 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/3x1Rot.png "3DGraph Screenshot") 89 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/3x3NoRot.png "3DGraph Screenshot") 90 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/3x3Rot.png "3DGraph Screenshot") 91 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/4NoRot.png "3DGraph Screenshot") 92 | ![3DGraph](https://github.com/greglecki/3DGraph/blob/master/3D%20Graph%20screenshots/4Rot.png "3DGraph Screenshot") 93 | --------------------------------------------------------------------------------