├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── CollectionGraph.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── CollectionGraph.xcscheme ├── CollectionGraph ├── CollectionGraph.h ├── CollectionGraphDataSource.swift ├── CollectionGraphDelegate.swift ├── CollectionGraphView.swift ├── GraphCollectionView.xib ├── GraphDelegates.swift ├── GraphLayout.swift ├── Info.plist ├── LabelView.swift ├── LineConnectorAttributes.swift ├── LineConnectorView.swift ├── LineGraphLayout.swift ├── RangeFinder.swift ├── Spinner.swift ├── XLabelViewAttributes.swift ├── XibLoader.swift ├── YDividerLayoutAttributes.swift └── YDividerLineView.swift ├── CollectionGraphExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── DefaultUser.imageset │ │ ├── Contents.json │ │ ├── DefaultUser.png │ │ ├── DefaultUser@2x.png │ │ └── DefaultUser@3x.png │ ├── first.imageset │ │ ├── Contents.json │ │ └── first.pdf │ ├── second.imageset │ │ ├── Contents.json │ │ └── second.pdf │ ├── user1.imageset │ │ ├── Contents.json │ │ ├── user1.png │ │ ├── user1@2x.png │ │ └── user1@3x.png │ ├── user2.imageset │ │ ├── Contents.json │ │ ├── user2.png │ │ ├── user2@2x.png │ │ └── user2@3x.png │ ├── user3.imageset │ │ ├── Contents.json │ │ ├── user3.png │ │ ├── user3@2x.png │ │ └── user3@3x.png │ └── user4.imageset │ │ ├── Contents.json │ │ ├── user4.png │ │ ├── user4@2x.png │ │ └── user4@3x.png ├── BarReusableView.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ColorHelper.swift ├── DataModels.swift ├── DataService.swift ├── FirstViewController.swift ├── Info.plist ├── MilesPerDayData.json ├── MyGraphCell.swift ├── Parsers.swift ├── PeopleCollectionViewCell.swift ├── SecondViewController.swift ├── SideBarReusableView.swift ├── ThirdViewController.swift ├── TotalMilesRan.json └── ppm_sample_data.json ├── CollectionGraphTests ├── CollectionGraphTests.swift └── Info.plist ├── README.md └── ReadmeImages ├── LotsOfDataGraph.gif ├── MilesRanGraph.gif └── TotalMilesRanGraph.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - line_length 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8.1 3 | 4 | xcode_project: CollectionGraph.xcodeproj 5 | xcode_scheme: CollectionGraph 6 | xcode_sdk: iphonesimulator10.0 7 | 8 | script: xcodebuild test -project CollectionGraph.xcodeproj -scheme CollectionGraph -destination 'platform=iOS Simulator,name=iPhone 6s,OS=10.0' 9 | 10 | # notifications: 11 | # slack: collectiveidea:UAduo5riNzgVWHyExXgSnZWk 12 | -------------------------------------------------------------------------------- /CollectionGraph.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 570379C11DAFFD2A00A88C6B /* YDividerLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570379C01DAFFD2A00A88C6B /* YDividerLayoutAttributes.swift */; }; 11 | 570379C31DB0006B00A88C6B /* YDividerLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570379C21DB0006B00A88C6B /* YDividerLineView.swift */; }; 12 | 570379C81DB02C5700A88C6B /* LineConnectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570379C71DB02C5700A88C6B /* LineConnectorView.swift */; }; 13 | 570379CA1DB02CB500A88C6B /* LineConnectorAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570379C91DB02CB500A88C6B /* LineConnectorAttributes.swift */; }; 14 | 5714C1AC1E3797F500844C25 /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5714C1AB1E3797F500844C25 /* Spinner.swift */; }; 15 | 5715599D1E315DFE00D8E0AA /* ThirdViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5715599C1E315DFE00D8E0AA /* ThirdViewController.swift */; }; 16 | 5717848E1D95A2B20074879A /* CollectionGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5717848D1D95A2B20074879A /* CollectionGraphView.swift */; }; 17 | 5717848F1D95A3540074879A /* CollectionGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57EA98971D958C82000D6C55 /* CollectionGraph.framework */; }; 18 | 571784901D95A3540074879A /* CollectionGraph.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 57EA98971D958C82000D6C55 /* CollectionGraph.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 19 | 571B7EC81D9D9AA000FCE88E /* XibLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571B7EC71D9D9AA000FCE88E /* XibLoader.swift */; }; 20 | 571B7ECA1D9D9CF500FCE88E /* GraphCollectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 571B7EC91D9D9CF500FCE88E /* GraphCollectionView.xib */; }; 21 | 571B7ECC1D9DAD4B00FCE88E /* GraphLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571B7ECB1D9DAD4B00FCE88E /* GraphLayout.swift */; }; 22 | 571B7ED21D9DBAC300FCE88E /* CollectionGraphDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571B7ED11D9DBAC300FCE88E /* CollectionGraphDataSource.swift */; }; 23 | 571F01AC1E2D03ED0053C400 /* PeopleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571F01AB1E2D03ED0053C400 /* PeopleCollectionViewCell.swift */; }; 24 | 571F01B01E2D2AB70053C400 /* DataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571F01AF1E2D2AB70053C400 /* DataService.swift */; }; 25 | 571F01B21E2D5FE20053C400 /* Parsers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571F01B11E2D5FE20053C400 /* Parsers.swift */; }; 26 | 571F01B41E2D60810053C400 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571F01B31E2D60810053C400 /* DataModels.swift */; }; 27 | 571F01B81E2D6AFD0053C400 /* ppm_sample_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 571F01B71E2D6AFD0053C400 /* ppm_sample_data.json */; }; 28 | 5728A01C1E324C73006CCEBA /* BarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5728A01B1E324C73006CCEBA /* BarReusableView.swift */; }; 29 | 5728A01E1E3250B2006CCEBA /* TotalMilesRan.json in Resources */ = {isa = PBXBuildFile; fileRef = 5728A01D1E3250B2006CCEBA /* TotalMilesRan.json */; }; 30 | 574F020C1DE393B9001ACC27 /* CollectionGraphDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574F020B1DE393B9001ACC27 /* CollectionGraphDelegate.swift */; }; 31 | 5756122A1DB1098700F94EAF /* LabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575612291DB1098700F94EAF /* LabelView.swift */; }; 32 | 5756122C1DB109F100F94EAF /* XLabelViewAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5756122B1DB109F100F94EAF /* XLabelViewAttributes.swift */; }; 33 | 576457D41E008C4200FA3432 /* GraphDelegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576457D31E008C4200FA3432 /* GraphDelegates.swift */; }; 34 | 576DD7841DB55BA90084498B /* MyGraphCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576DD7831DB55BA90084498B /* MyGraphCell.swift */; }; 35 | 577853411E1EDF4A00058C19 /* RangeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577853401E1EDF4A00058C19 /* RangeFinder.swift */; }; 36 | 579788B81E29182D000503AF /* MilesPerDayData.json in Resources */ = {isa = PBXBuildFile; fileRef = 579788B71E29182D000503AF /* MilesPerDayData.json */; }; 37 | 579788F71E297C56000503AF /* ColorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579788F61E297C56000503AF /* ColorHelper.swift */; }; 38 | 57C8AD551DF9ADB50097D1A8 /* SideBarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C8AD541DF9ADB50097D1A8 /* SideBarReusableView.swift */; }; 39 | 57EA989B1D958C82000D6C55 /* CollectionGraph.h in Headers */ = {isa = PBXBuildFile; fileRef = 57EA989A1D958C82000D6C55 /* CollectionGraph.h */; settings = {ATTRIBUTES = (Public, ); }; }; 40 | 57EA98A21D958C82000D6C55 /* CollectionGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57EA98971D958C82000D6C55 /* CollectionGraph.framework */; }; 41 | 57EA98A71D958C82000D6C55 /* CollectionGraphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EA98A61D958C82000D6C55 /* CollectionGraphTests.swift */; }; 42 | A7B2B0FC1D958F1500F3A410 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B2B0FB1D958F1500F3A410 /* AppDelegate.swift */; }; 43 | A7B2B0FE1D958F1500F3A410 /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B2B0FD1D958F1500F3A410 /* FirstViewController.swift */; }; 44 | A7B2B1001D958F1500F3A410 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B2B0FF1D958F1500F3A410 /* SecondViewController.swift */; }; 45 | A7B2B1031D958F1500F3A410 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A7B2B1011D958F1500F3A410 /* Main.storyboard */; }; 46 | A7B2B1051D958F1500F3A410 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A7B2B1041D958F1500F3A410 /* Assets.xcassets */; }; 47 | A7B2B1081D958F1500F3A410 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A7B2B1061D958F1500F3A410 /* LaunchScreen.storyboard */; }; 48 | /* End PBXBuildFile section */ 49 | 50 | /* Begin PBXContainerItemProxy section */ 51 | 571784911D95A3540074879A /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 57EA988E1D958C82000D6C55 /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = 57EA98961D958C82000D6C55; 56 | remoteInfo = CollectionGraph; 57 | }; 58 | 57EA98A31D958C82000D6C55 /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = 57EA988E1D958C82000D6C55 /* Project object */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 57EA98961D958C82000D6C55; 63 | remoteInfo = CollectionGraph; 64 | }; 65 | /* End PBXContainerItemProxy section */ 66 | 67 | /* Begin PBXCopyFilesBuildPhase section */ 68 | 571784931D95A3540074879A /* Embed Frameworks */ = { 69 | isa = PBXCopyFilesBuildPhase; 70 | buildActionMask = 2147483647; 71 | dstPath = ""; 72 | dstSubfolderSpec = 10; 73 | files = ( 74 | 571784901D95A3540074879A /* CollectionGraph.framework in Embed Frameworks */, 75 | ); 76 | name = "Embed Frameworks"; 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXCopyFilesBuildPhase section */ 80 | 81 | /* Begin PBXFileReference section */ 82 | 570379C01DAFFD2A00A88C6B /* YDividerLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YDividerLayoutAttributes.swift; sourceTree = ""; }; 83 | 570379C21DB0006B00A88C6B /* YDividerLineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YDividerLineView.swift; sourceTree = ""; }; 84 | 570379C71DB02C5700A88C6B /* LineConnectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineConnectorView.swift; sourceTree = ""; }; 85 | 570379C91DB02CB500A88C6B /* LineConnectorAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineConnectorAttributes.swift; sourceTree = ""; }; 86 | 5714C1AB1E3797F500844C25 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; }; 87 | 5715599C1E315DFE00D8E0AA /* ThirdViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThirdViewController.swift; sourceTree = ""; }; 88 | 5717848D1D95A2B20074879A /* CollectionGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionGraphView.swift; sourceTree = ""; }; 89 | 571B7EC71D9D9AA000FCE88E /* XibLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XibLoader.swift; sourceTree = ""; }; 90 | 571B7EC91D9D9CF500FCE88E /* GraphCollectionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = GraphCollectionView.xib; sourceTree = ""; }; 91 | 571B7ECB1D9DAD4B00FCE88E /* GraphLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphLayout.swift; sourceTree = ""; }; 92 | 571B7ED11D9DBAC300FCE88E /* CollectionGraphDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionGraphDataSource.swift; sourceTree = ""; }; 93 | 571F01AB1E2D03ED0053C400 /* PeopleCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeopleCollectionViewCell.swift; sourceTree = ""; }; 94 | 571F01AF1E2D2AB70053C400 /* DataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataService.swift; sourceTree = ""; }; 95 | 571F01B11E2D5FE20053C400 /* Parsers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parsers.swift; sourceTree = ""; }; 96 | 571F01B31E2D60810053C400 /* DataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = ""; }; 97 | 571F01B71E2D6AFD0053C400 /* ppm_sample_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ppm_sample_data.json; sourceTree = ""; }; 98 | 5728A01B1E324C73006CCEBA /* BarReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarReusableView.swift; sourceTree = ""; }; 99 | 5728A01D1E3250B2006CCEBA /* TotalMilesRan.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TotalMilesRan.json; sourceTree = ""; }; 100 | 574F020B1DE393B9001ACC27 /* CollectionGraphDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionGraphDelegate.swift; sourceTree = ""; }; 101 | 575612291DB1098700F94EAF /* LabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelView.swift; sourceTree = ""; }; 102 | 5756122B1DB109F100F94EAF /* XLabelViewAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XLabelViewAttributes.swift; sourceTree = ""; }; 103 | 576457D31E008C4200FA3432 /* GraphDelegates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphDelegates.swift; sourceTree = ""; }; 104 | 576DD7831DB55BA90084498B /* MyGraphCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyGraphCell.swift; sourceTree = ""; }; 105 | 577853401E1EDF4A00058C19 /* RangeFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RangeFinder.swift; sourceTree = ""; }; 106 | 579788B71E29182D000503AF /* MilesPerDayData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = MilesPerDayData.json; sourceTree = ""; }; 107 | 579788F61E297C56000503AF /* ColorHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorHelper.swift; sourceTree = ""; }; 108 | 57C8AD541DF9ADB50097D1A8 /* SideBarReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideBarReusableView.swift; sourceTree = ""; }; 109 | 57EA98971D958C82000D6C55 /* CollectionGraph.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CollectionGraph.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 110 | 57EA989A1D958C82000D6C55 /* CollectionGraph.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CollectionGraph.h; sourceTree = ""; }; 111 | 57EA989C1D958C82000D6C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 112 | 57EA98A11D958C82000D6C55 /* CollectionGraphTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CollectionGraphTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 113 | 57EA98A61D958C82000D6C55 /* CollectionGraphTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionGraphTests.swift; sourceTree = ""; }; 114 | 57EA98A81D958C82000D6C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 115 | A7B2B0F91D958F1500F3A410 /* CollectionGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CollectionGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 116 | A7B2B0FB1D958F1500F3A410 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 117 | A7B2B0FD1D958F1500F3A410 /* FirstViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 118 | A7B2B0FF1D958F1500F3A410 /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 119 | A7B2B1021D958F1500F3A410 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 120 | A7B2B1041D958F1500F3A410 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 121 | A7B2B1071D958F1500F3A410 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 122 | A7B2B1091D958F1500F3A410 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | /* End PBXFileReference section */ 124 | 125 | /* Begin PBXFrameworksBuildPhase section */ 126 | 57EA98931D958C82000D6C55 /* Frameworks */ = { 127 | isa = PBXFrameworksBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | 57EA989E1D958C82000D6C55 /* Frameworks */ = { 134 | isa = PBXFrameworksBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | 57EA98A21D958C82000D6C55 /* CollectionGraph.framework in Frameworks */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | A7B2B0F61D958F1500F3A410 /* Frameworks */ = { 142 | isa = PBXFrameworksBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 5717848F1D95A3540074879A /* CollectionGraph.framework in Frameworks */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXFrameworksBuildPhase section */ 150 | 151 | /* Begin PBXGroup section */ 152 | 570379C41DB02B1800A88C6B /* Layouts */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 571B7ECB1D9DAD4B00FCE88E /* GraphLayout.swift */, 156 | 570379C51DB02B3D00A88C6B /* Layout Attributes */, 157 | ); 158 | name = Layouts; 159 | sourceTree = ""; 160 | }; 161 | 570379C51DB02B3D00A88C6B /* Layout Attributes */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 570379C01DAFFD2A00A88C6B /* YDividerLayoutAttributes.swift */, 165 | 570379C91DB02CB500A88C6B /* LineConnectorAttributes.swift */, 166 | 5756122B1DB109F100F94EAF /* XLabelViewAttributes.swift */, 167 | 570379C61DB02B5E00A88C6B /* Supplementary Views */, 168 | ); 169 | name = "Layout Attributes"; 170 | sourceTree = ""; 171 | }; 172 | 570379C61DB02B5E00A88C6B /* Supplementary Views */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 570379C21DB0006B00A88C6B /* YDividerLineView.swift */, 176 | 570379C71DB02C5700A88C6B /* LineConnectorView.swift */, 177 | 575612291DB1098700F94EAF /* LabelView.swift */, 178 | ); 179 | name = "Supplementary Views"; 180 | sourceTree = ""; 181 | }; 182 | 5714C1A81E378D0900844C25 /* JSON files */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 579788B71E29182D000503AF /* MilesPerDayData.json */, 186 | 571F01B71E2D6AFD0053C400 /* ppm_sample_data.json */, 187 | 5728A01D1E3250B2006CCEBA /* TotalMilesRan.json */, 188 | ); 189 | name = "JSON files"; 190 | sourceTree = ""; 191 | }; 192 | 5714C1A91E378D2200844C25 /* View Controllers */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | A7B2B0FD1D958F1500F3A410 /* FirstViewController.swift */, 196 | A7B2B0FF1D958F1500F3A410 /* SecondViewController.swift */, 197 | 5715599C1E315DFE00D8E0AA /* ThirdViewController.swift */, 198 | ); 199 | name = "View Controllers"; 200 | sourceTree = ""; 201 | }; 202 | 5714C1AA1E378D4200844C25 /* Views */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 576DD7831DB55BA90084498B /* MyGraphCell.swift */, 206 | 571F01AB1E2D03ED0053C400 /* PeopleCollectionViewCell.swift */, 207 | 5728A01B1E324C73006CCEBA /* BarReusableView.swift */, 208 | 57C8AD541DF9ADB50097D1A8 /* SideBarReusableView.swift */, 209 | ); 210 | name = Views; 211 | sourceTree = ""; 212 | }; 213 | 57EA988D1D958C82000D6C55 = { 214 | isa = PBXGroup; 215 | children = ( 216 | 57EA98991D958C82000D6C55 /* CollectionGraph */, 217 | 57EA98A51D958C82000D6C55 /* CollectionGraphTests */, 218 | A7B2B0FA1D958F1500F3A410 /* CollectionGraphExample */, 219 | 57EA98981D958C82000D6C55 /* Products */, 220 | ); 221 | sourceTree = ""; 222 | }; 223 | 57EA98981D958C82000D6C55 /* Products */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 57EA98971D958C82000D6C55 /* CollectionGraph.framework */, 227 | 57EA98A11D958C82000D6C55 /* CollectionGraphTests.xctest */, 228 | A7B2B0F91D958F1500F3A410 /* CollectionGraphExample.app */, 229 | ); 230 | name = Products; 231 | sourceTree = ""; 232 | }; 233 | 57EA98991D958C82000D6C55 /* CollectionGraph */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 57EA989A1D958C82000D6C55 /* CollectionGraph.h */, 237 | 57EA989C1D958C82000D6C55 /* Info.plist */, 238 | 5717848D1D95A2B20074879A /* CollectionGraphView.swift */, 239 | 576457D31E008C4200FA3432 /* GraphDelegates.swift */, 240 | 571B7ED11D9DBAC300FCE88E /* CollectionGraphDataSource.swift */, 241 | 574F020B1DE393B9001ACC27 /* CollectionGraphDelegate.swift */, 242 | 570379C41DB02B1800A88C6B /* Layouts */, 243 | 571B7EC71D9D9AA000FCE88E /* XibLoader.swift */, 244 | 5714C1AB1E3797F500844C25 /* Spinner.swift */, 245 | 577853401E1EDF4A00058C19 /* RangeFinder.swift */, 246 | 571B7EC91D9D9CF500FCE88E /* GraphCollectionView.xib */, 247 | ); 248 | path = CollectionGraph; 249 | sourceTree = ""; 250 | }; 251 | 57EA98A51D958C82000D6C55 /* CollectionGraphTests */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | 57EA98A61D958C82000D6C55 /* CollectionGraphTests.swift */, 255 | 57EA98A81D958C82000D6C55 /* Info.plist */, 256 | ); 257 | path = CollectionGraphTests; 258 | sourceTree = ""; 259 | }; 260 | A7B2B0FA1D958F1500F3A410 /* CollectionGraphExample */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | A7B2B0FB1D958F1500F3A410 /* AppDelegate.swift */, 264 | 5714C1A91E378D2200844C25 /* View Controllers */, 265 | 5714C1AA1E378D4200844C25 /* Views */, 266 | 571F01B31E2D60810053C400 /* DataModels.swift */, 267 | 571F01AF1E2D2AB70053C400 /* DataService.swift */, 268 | 571F01B11E2D5FE20053C400 /* Parsers.swift */, 269 | 5714C1A81E378D0900844C25 /* JSON files */, 270 | 579788F61E297C56000503AF /* ColorHelper.swift */, 271 | A7B2B1011D958F1500F3A410 /* Main.storyboard */, 272 | A7B2B1061D958F1500F3A410 /* LaunchScreen.storyboard */, 273 | A7B2B1041D958F1500F3A410 /* Assets.xcassets */, 274 | A7B2B1091D958F1500F3A410 /* Info.plist */, 275 | ); 276 | path = CollectionGraphExample; 277 | sourceTree = ""; 278 | }; 279 | /* End PBXGroup section */ 280 | 281 | /* Begin PBXHeadersBuildPhase section */ 282 | 57EA98941D958C82000D6C55 /* Headers */ = { 283 | isa = PBXHeadersBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | 57EA989B1D958C82000D6C55 /* CollectionGraph.h in Headers */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXHeadersBuildPhase section */ 291 | 292 | /* Begin PBXNativeTarget section */ 293 | 57EA98961D958C82000D6C55 /* CollectionGraph */ = { 294 | isa = PBXNativeTarget; 295 | buildConfigurationList = 57EA98AB1D958C82000D6C55 /* Build configuration list for PBXNativeTarget "CollectionGraph" */; 296 | buildPhases = ( 297 | 57EA98921D958C82000D6C55 /* Sources */, 298 | 57EA98931D958C82000D6C55 /* Frameworks */, 299 | 57EA98941D958C82000D6C55 /* Headers */, 300 | 57EA98951D958C82000D6C55 /* Resources */, 301 | 076041C7A9FFC1CFFEC3FC6B /* Swift Lint */, 302 | ); 303 | buildRules = ( 304 | ); 305 | dependencies = ( 306 | ); 307 | name = CollectionGraph; 308 | productName = CollectionGraph; 309 | productReference = 57EA98971D958C82000D6C55 /* CollectionGraph.framework */; 310 | productType = "com.apple.product-type.framework"; 311 | }; 312 | 57EA98A01D958C82000D6C55 /* CollectionGraphTests */ = { 313 | isa = PBXNativeTarget; 314 | buildConfigurationList = 57EA98AE1D958C82000D6C55 /* Build configuration list for PBXNativeTarget "CollectionGraphTests" */; 315 | buildPhases = ( 316 | 57EA989D1D958C82000D6C55 /* Sources */, 317 | 57EA989E1D958C82000D6C55 /* Frameworks */, 318 | 57EA989F1D958C82000D6C55 /* Resources */, 319 | ); 320 | buildRules = ( 321 | ); 322 | dependencies = ( 323 | 57EA98A41D958C82000D6C55 /* PBXTargetDependency */, 324 | ); 325 | name = CollectionGraphTests; 326 | productName = CollectionGraphTests; 327 | productReference = 57EA98A11D958C82000D6C55 /* CollectionGraphTests.xctest */; 328 | productType = "com.apple.product-type.bundle.unit-test"; 329 | }; 330 | A7B2B0F81D958F1500F3A410 /* CollectionGraphExample */ = { 331 | isa = PBXNativeTarget; 332 | buildConfigurationList = A7B2B10A1D958F1500F3A410 /* Build configuration list for PBXNativeTarget "CollectionGraphExample" */; 333 | buildPhases = ( 334 | A7B2B0F51D958F1500F3A410 /* Sources */, 335 | A7B2B0F61D958F1500F3A410 /* Frameworks */, 336 | A7B2B0F71D958F1500F3A410 /* Resources */, 337 | 571784931D95A3540074879A /* Embed Frameworks */, 338 | ); 339 | buildRules = ( 340 | ); 341 | dependencies = ( 342 | 571784921D95A3540074879A /* PBXTargetDependency */, 343 | ); 344 | name = CollectionGraphExample; 345 | productName = CollectionGraphExample; 346 | productReference = A7B2B0F91D958F1500F3A410 /* CollectionGraphExample.app */; 347 | productType = "com.apple.product-type.application"; 348 | }; 349 | /* End PBXNativeTarget section */ 350 | 351 | /* Begin PBXProject section */ 352 | 57EA988E1D958C82000D6C55 /* Project object */ = { 353 | isa = PBXProject; 354 | attributes = { 355 | LastSwiftUpdateCheck = 0800; 356 | LastUpgradeCheck = 0920; 357 | ORGANIZATIONNAME = "Collective Idea"; 358 | TargetAttributes = { 359 | 57EA98961D958C82000D6C55 = { 360 | CreatedOnToolsVersion = 7.3.1; 361 | LastSwiftMigration = 0920; 362 | }; 363 | 57EA98A01D958C82000D6C55 = { 364 | CreatedOnToolsVersion = 7.3.1; 365 | LastSwiftMigration = 0920; 366 | }; 367 | A7B2B0F81D958F1500F3A410 = { 368 | CreatedOnToolsVersion = 8.0; 369 | DevelopmentTeam = 3MXD5J8GME; 370 | LastSwiftMigration = 0920; 371 | ProvisioningStyle = Automatic; 372 | }; 373 | }; 374 | }; 375 | buildConfigurationList = 57EA98911D958C82000D6C55 /* Build configuration list for PBXProject "CollectionGraph" */; 376 | compatibilityVersion = "Xcode 3.2"; 377 | developmentRegion = English; 378 | hasScannedForEncodings = 0; 379 | knownRegions = ( 380 | en, 381 | Base, 382 | ); 383 | mainGroup = 57EA988D1D958C82000D6C55; 384 | productRefGroup = 57EA98981D958C82000D6C55 /* Products */; 385 | projectDirPath = ""; 386 | projectRoot = ""; 387 | targets = ( 388 | 57EA98961D958C82000D6C55 /* CollectionGraph */, 389 | 57EA98A01D958C82000D6C55 /* CollectionGraphTests */, 390 | A7B2B0F81D958F1500F3A410 /* CollectionGraphExample */, 391 | ); 392 | }; 393 | /* End PBXProject section */ 394 | 395 | /* Begin PBXResourcesBuildPhase section */ 396 | 57EA98951D958C82000D6C55 /* Resources */ = { 397 | isa = PBXResourcesBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | 571B7ECA1D9D9CF500FCE88E /* GraphCollectionView.xib in Resources */, 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | 57EA989F1D958C82000D6C55 /* Resources */ = { 405 | isa = PBXResourcesBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | A7B2B0F71D958F1500F3A410 /* Resources */ = { 412 | isa = PBXResourcesBuildPhase; 413 | buildActionMask = 2147483647; 414 | files = ( 415 | A7B2B1081D958F1500F3A410 /* LaunchScreen.storyboard in Resources */, 416 | 5728A01E1E3250B2006CCEBA /* TotalMilesRan.json in Resources */, 417 | 579788B81E29182D000503AF /* MilesPerDayData.json in Resources */, 418 | 571F01B81E2D6AFD0053C400 /* ppm_sample_data.json in Resources */, 419 | A7B2B1051D958F1500F3A410 /* Assets.xcassets in Resources */, 420 | A7B2B1031D958F1500F3A410 /* Main.storyboard in Resources */, 421 | ); 422 | runOnlyForDeploymentPostprocessing = 0; 423 | }; 424 | /* End PBXResourcesBuildPhase section */ 425 | 426 | /* Begin PBXShellScriptBuildPhase section */ 427 | 076041C7A9FFC1CFFEC3FC6B /* Swift Lint */ = { 428 | isa = PBXShellScriptBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | ); 432 | inputPaths = ( 433 | ); 434 | name = "Swift Lint"; 435 | outputPaths = ( 436 | ); 437 | runOnlyForDeploymentPostprocessing = 0; 438 | shellPath = /bin/sh; 439 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 440 | }; 441 | /* End PBXShellScriptBuildPhase section */ 442 | 443 | /* Begin PBXSourcesBuildPhase section */ 444 | 57EA98921D958C82000D6C55 /* Sources */ = { 445 | isa = PBXSourcesBuildPhase; 446 | buildActionMask = 2147483647; 447 | files = ( 448 | 570379C11DAFFD2A00A88C6B /* YDividerLayoutAttributes.swift in Sources */, 449 | 571B7ED21D9DBAC300FCE88E /* CollectionGraphDataSource.swift in Sources */, 450 | 5756122A1DB1098700F94EAF /* LabelView.swift in Sources */, 451 | 5714C1AC1E3797F500844C25 /* Spinner.swift in Sources */, 452 | 574F020C1DE393B9001ACC27 /* CollectionGraphDelegate.swift in Sources */, 453 | 570379CA1DB02CB500A88C6B /* LineConnectorAttributes.swift in Sources */, 454 | 570379C31DB0006B00A88C6B /* YDividerLineView.swift in Sources */, 455 | 571B7EC81D9D9AA000FCE88E /* XibLoader.swift in Sources */, 456 | 5717848E1D95A2B20074879A /* CollectionGraphView.swift in Sources */, 457 | 570379C81DB02C5700A88C6B /* LineConnectorView.swift in Sources */, 458 | 571B7ECC1D9DAD4B00FCE88E /* GraphLayout.swift in Sources */, 459 | 577853411E1EDF4A00058C19 /* RangeFinder.swift in Sources */, 460 | 5756122C1DB109F100F94EAF /* XLabelViewAttributes.swift in Sources */, 461 | 576457D41E008C4200FA3432 /* GraphDelegates.swift in Sources */, 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | }; 465 | 57EA989D1D958C82000D6C55 /* Sources */ = { 466 | isa = PBXSourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | 57EA98A71D958C82000D6C55 /* CollectionGraphTests.swift in Sources */, 470 | ); 471 | runOnlyForDeploymentPostprocessing = 0; 472 | }; 473 | A7B2B0F51D958F1500F3A410 /* Sources */ = { 474 | isa = PBXSourcesBuildPhase; 475 | buildActionMask = 2147483647; 476 | files = ( 477 | 57C8AD551DF9ADB50097D1A8 /* SideBarReusableView.swift in Sources */, 478 | A7B2B1001D958F1500F3A410 /* SecondViewController.swift in Sources */, 479 | 571F01B41E2D60810053C400 /* DataModels.swift in Sources */, 480 | 571F01B01E2D2AB70053C400 /* DataService.swift in Sources */, 481 | A7B2B0FC1D958F1500F3A410 /* AppDelegate.swift in Sources */, 482 | A7B2B0FE1D958F1500F3A410 /* FirstViewController.swift in Sources */, 483 | 576DD7841DB55BA90084498B /* MyGraphCell.swift in Sources */, 484 | 5728A01C1E324C73006CCEBA /* BarReusableView.swift in Sources */, 485 | 571F01AC1E2D03ED0053C400 /* PeopleCollectionViewCell.swift in Sources */, 486 | 5715599D1E315DFE00D8E0AA /* ThirdViewController.swift in Sources */, 487 | 579788F71E297C56000503AF /* ColorHelper.swift in Sources */, 488 | 571F01B21E2D5FE20053C400 /* Parsers.swift in Sources */, 489 | ); 490 | runOnlyForDeploymentPostprocessing = 0; 491 | }; 492 | /* End PBXSourcesBuildPhase section */ 493 | 494 | /* Begin PBXTargetDependency section */ 495 | 571784921D95A3540074879A /* PBXTargetDependency */ = { 496 | isa = PBXTargetDependency; 497 | target = 57EA98961D958C82000D6C55 /* CollectionGraph */; 498 | targetProxy = 571784911D95A3540074879A /* PBXContainerItemProxy */; 499 | }; 500 | 57EA98A41D958C82000D6C55 /* PBXTargetDependency */ = { 501 | isa = PBXTargetDependency; 502 | target = 57EA98961D958C82000D6C55 /* CollectionGraph */; 503 | targetProxy = 57EA98A31D958C82000D6C55 /* PBXContainerItemProxy */; 504 | }; 505 | /* End PBXTargetDependency section */ 506 | 507 | /* Begin PBXVariantGroup section */ 508 | A7B2B1011D958F1500F3A410 /* Main.storyboard */ = { 509 | isa = PBXVariantGroup; 510 | children = ( 511 | A7B2B1021D958F1500F3A410 /* Base */, 512 | ); 513 | name = Main.storyboard; 514 | sourceTree = ""; 515 | }; 516 | A7B2B1061D958F1500F3A410 /* LaunchScreen.storyboard */ = { 517 | isa = PBXVariantGroup; 518 | children = ( 519 | A7B2B1071D958F1500F3A410 /* Base */, 520 | ); 521 | name = LaunchScreen.storyboard; 522 | sourceTree = ""; 523 | }; 524 | /* End PBXVariantGroup section */ 525 | 526 | /* Begin XCBuildConfiguration section */ 527 | 57EA98A91D958C82000D6C55 /* Debug */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ALWAYS_SEARCH_USER_PATHS = NO; 531 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 532 | CLANG_ANALYZER_NONNULL = YES; 533 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 534 | CLANG_CXX_LIBRARY = "libc++"; 535 | CLANG_ENABLE_MODULES = YES; 536 | CLANG_ENABLE_OBJC_ARC = YES; 537 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 538 | CLANG_WARN_BOOL_CONVERSION = YES; 539 | CLANG_WARN_COMMA = YES; 540 | CLANG_WARN_CONSTANT_CONVERSION = YES; 541 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 542 | CLANG_WARN_EMPTY_BODY = YES; 543 | CLANG_WARN_ENUM_CONVERSION = YES; 544 | CLANG_WARN_INFINITE_RECURSION = YES; 545 | CLANG_WARN_INT_CONVERSION = YES; 546 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 547 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 548 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 549 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 550 | CLANG_WARN_STRICT_PROTOTYPES = YES; 551 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 552 | CLANG_WARN_UNREACHABLE_CODE = YES; 553 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 554 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 555 | COPY_PHASE_STRIP = NO; 556 | CURRENT_PROJECT_VERSION = 1; 557 | DEBUG_INFORMATION_FORMAT = dwarf; 558 | ENABLE_STRICT_OBJC_MSGSEND = YES; 559 | ENABLE_TESTABILITY = YES; 560 | GCC_C_LANGUAGE_STANDARD = gnu99; 561 | GCC_DYNAMIC_NO_PIC = NO; 562 | GCC_NO_COMMON_BLOCKS = YES; 563 | GCC_OPTIMIZATION_LEVEL = 0; 564 | GCC_PREPROCESSOR_DEFINITIONS = ( 565 | "DEBUG=1", 566 | "$(inherited)", 567 | ); 568 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 569 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 570 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 571 | GCC_WARN_UNDECLARED_SELECTOR = YES; 572 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 573 | GCC_WARN_UNUSED_FUNCTION = YES; 574 | GCC_WARN_UNUSED_VARIABLE = YES; 575 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 576 | MTL_ENABLE_DEBUG_INFO = YES; 577 | ONLY_ACTIVE_ARCH = YES; 578 | SDKROOT = iphoneos; 579 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 580 | TARGETED_DEVICE_FAMILY = "1,2"; 581 | VERSIONING_SYSTEM = "apple-generic"; 582 | VERSION_INFO_PREFIX = ""; 583 | }; 584 | name = Debug; 585 | }; 586 | 57EA98AA1D958C82000D6C55 /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ALWAYS_SEARCH_USER_PATHS = NO; 590 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 591 | CLANG_ANALYZER_NONNULL = YES; 592 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 593 | CLANG_CXX_LIBRARY = "libc++"; 594 | CLANG_ENABLE_MODULES = YES; 595 | CLANG_ENABLE_OBJC_ARC = YES; 596 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 597 | CLANG_WARN_BOOL_CONVERSION = YES; 598 | CLANG_WARN_COMMA = YES; 599 | CLANG_WARN_CONSTANT_CONVERSION = YES; 600 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 601 | CLANG_WARN_EMPTY_BODY = YES; 602 | CLANG_WARN_ENUM_CONVERSION = YES; 603 | CLANG_WARN_INFINITE_RECURSION = YES; 604 | CLANG_WARN_INT_CONVERSION = YES; 605 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 606 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 607 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 608 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 609 | CLANG_WARN_STRICT_PROTOTYPES = YES; 610 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 611 | CLANG_WARN_UNREACHABLE_CODE = YES; 612 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 613 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 614 | COPY_PHASE_STRIP = NO; 615 | CURRENT_PROJECT_VERSION = 1; 616 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 617 | ENABLE_NS_ASSERTIONS = NO; 618 | ENABLE_STRICT_OBJC_MSGSEND = YES; 619 | GCC_C_LANGUAGE_STANDARD = gnu99; 620 | GCC_NO_COMMON_BLOCKS = YES; 621 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 622 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 623 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 624 | GCC_WARN_UNDECLARED_SELECTOR = YES; 625 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 626 | GCC_WARN_UNUSED_FUNCTION = YES; 627 | GCC_WARN_UNUSED_VARIABLE = YES; 628 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 629 | MTL_ENABLE_DEBUG_INFO = NO; 630 | SDKROOT = iphoneos; 631 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 632 | TARGETED_DEVICE_FAMILY = "1,2"; 633 | VALIDATE_PRODUCT = YES; 634 | VERSIONING_SYSTEM = "apple-generic"; 635 | VERSION_INFO_PREFIX = ""; 636 | }; 637 | name = Release; 638 | }; 639 | 57EA98AC1D958C82000D6C55 /* Debug */ = { 640 | isa = XCBuildConfiguration; 641 | buildSettings = { 642 | CLANG_ENABLE_MODULES = YES; 643 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 644 | DEFINES_MODULE = YES; 645 | DYLIB_COMPATIBILITY_VERSION = 1; 646 | DYLIB_CURRENT_VERSION = 1; 647 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 648 | INFOPLIST_FILE = CollectionGraph/Info.plist; 649 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 650 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 651 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraph; 652 | PRODUCT_NAME = "$(TARGET_NAME)"; 653 | SKIP_INSTALL = YES; 654 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 655 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 656 | SWIFT_VERSION = 4.0; 657 | }; 658 | name = Debug; 659 | }; 660 | 57EA98AD1D958C82000D6C55 /* Release */ = { 661 | isa = XCBuildConfiguration; 662 | buildSettings = { 663 | CLANG_ENABLE_MODULES = YES; 664 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 665 | DEFINES_MODULE = YES; 666 | DYLIB_COMPATIBILITY_VERSION = 1; 667 | DYLIB_CURRENT_VERSION = 1; 668 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 669 | INFOPLIST_FILE = CollectionGraph/Info.plist; 670 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 671 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 672 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraph; 673 | PRODUCT_NAME = "$(TARGET_NAME)"; 674 | SKIP_INSTALL = YES; 675 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 676 | SWIFT_VERSION = 4.0; 677 | }; 678 | name = Release; 679 | }; 680 | 57EA98AF1D958C82000D6C55 /* Debug */ = { 681 | isa = XCBuildConfiguration; 682 | buildSettings = { 683 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 684 | INFOPLIST_FILE = CollectionGraphTests/Info.plist; 685 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 686 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraphTests; 687 | PRODUCT_NAME = "$(TARGET_NAME)"; 688 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 689 | SWIFT_VERSION = 4.0; 690 | }; 691 | name = Debug; 692 | }; 693 | 57EA98B01D958C82000D6C55 /* Release */ = { 694 | isa = XCBuildConfiguration; 695 | buildSettings = { 696 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 697 | INFOPLIST_FILE = CollectionGraphTests/Info.plist; 698 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 699 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraphTests; 700 | PRODUCT_NAME = "$(TARGET_NAME)"; 701 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 702 | SWIFT_VERSION = 4.0; 703 | }; 704 | name = Release; 705 | }; 706 | A7B2B10B1D958F1500F3A410 /* Debug */ = { 707 | isa = XCBuildConfiguration; 708 | buildSettings = { 709 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 710 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 711 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 712 | CLANG_WARN_INFINITE_RECURSION = YES; 713 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 714 | DEVELOPMENT_TEAM = 3MXD5J8GME; 715 | INFOPLIST_FILE = CollectionGraphExample/Info.plist; 716 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 717 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 718 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraphExample; 719 | PRODUCT_NAME = "$(TARGET_NAME)"; 720 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 721 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 722 | SWIFT_VERSION = 4.0; 723 | }; 724 | name = Debug; 725 | }; 726 | A7B2B10C1D958F1500F3A410 /* Release */ = { 727 | isa = XCBuildConfiguration; 728 | buildSettings = { 729 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 730 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 731 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 732 | CLANG_WARN_INFINITE_RECURSION = YES; 733 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 734 | DEVELOPMENT_TEAM = 3MXD5J8GME; 735 | INFOPLIST_FILE = CollectionGraphExample/Info.plist; 736 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 737 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 738 | PRODUCT_BUNDLE_IDENTIFIER = com.collectiveidea.CollectionGraphExample; 739 | PRODUCT_NAME = "$(TARGET_NAME)"; 740 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 741 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 742 | SWIFT_VERSION = 4.0; 743 | }; 744 | name = Release; 745 | }; 746 | /* End XCBuildConfiguration section */ 747 | 748 | /* Begin XCConfigurationList section */ 749 | 57EA98911D958C82000D6C55 /* Build configuration list for PBXProject "CollectionGraph" */ = { 750 | isa = XCConfigurationList; 751 | buildConfigurations = ( 752 | 57EA98A91D958C82000D6C55 /* Debug */, 753 | 57EA98AA1D958C82000D6C55 /* Release */, 754 | ); 755 | defaultConfigurationIsVisible = 0; 756 | defaultConfigurationName = Release; 757 | }; 758 | 57EA98AB1D958C82000D6C55 /* Build configuration list for PBXNativeTarget "CollectionGraph" */ = { 759 | isa = XCConfigurationList; 760 | buildConfigurations = ( 761 | 57EA98AC1D958C82000D6C55 /* Debug */, 762 | 57EA98AD1D958C82000D6C55 /* Release */, 763 | ); 764 | defaultConfigurationIsVisible = 0; 765 | defaultConfigurationName = Release; 766 | }; 767 | 57EA98AE1D958C82000D6C55 /* Build configuration list for PBXNativeTarget "CollectionGraphTests" */ = { 768 | isa = XCConfigurationList; 769 | buildConfigurations = ( 770 | 57EA98AF1D958C82000D6C55 /* Debug */, 771 | 57EA98B01D958C82000D6C55 /* Release */, 772 | ); 773 | defaultConfigurationIsVisible = 0; 774 | defaultConfigurationName = Release; 775 | }; 776 | A7B2B10A1D958F1500F3A410 /* Build configuration list for PBXNativeTarget "CollectionGraphExample" */ = { 777 | isa = XCConfigurationList; 778 | buildConfigurations = ( 779 | A7B2B10B1D958F1500F3A410 /* Debug */, 780 | A7B2B10C1D958F1500F3A410 /* Release */, 781 | ); 782 | defaultConfigurationIsVisible = 0; 783 | defaultConfigurationName = Release; 784 | }; 785 | /* End XCConfigurationList section */ 786 | }; 787 | rootObject = 57EA988E1D958C82000D6C55 /* Project object */; 788 | } 789 | -------------------------------------------------------------------------------- /CollectionGraph.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CollectionGraph.xcodeproj/xcshareddata/xcschemes/CollectionGraph.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /CollectionGraph/CollectionGraph.h: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionGraph.h 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/23/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CollectionGraph. 12 | FOUNDATION_EXPORT double CollectionGraphVersionNumber; 13 | 14 | //! Project version string for CollectionGraph. 15 | FOUNDATION_EXPORT const unsigned char CollectionGraphVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /CollectionGraph/CollectionGraphDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionGraphDataSource.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/29/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionGraphDataSource: NSObject, UICollectionViewDataSource, RangeFinder { 12 | 13 | internal weak var collectionGraphCellDelegate: CollectionGraphCellDelegate? 14 | internal weak var collectionGraphBarDelegate: CollectionGraphBarDelegate? 15 | internal weak var collectionGraphLineDelegate: CollectionGraphLineDelegate? 16 | internal weak var collectionGraphLineFillDelegate: CollectionGraphLineFillDelegate? 17 | internal weak var collectionGraphLabelsDelegate: CollectionGraphLabelsDelegate? 18 | internal weak var collectionGraphYDividerLineDelegate: CollectionGraphYDividerLineDelegate? 19 | 20 | var graphData: [[GraphDatum]]? 21 | 22 | internal var yDividerLineColor: UIColor = UIColor.lightGray 23 | 24 | internal var textColor: UIColor = UIColor.darkText 25 | 26 | internal var textSize: CGFloat = 8 27 | 28 | internal var fontName: String? 29 | 30 | internal var ySteps: Int = 6 31 | internal var xSteps: Int = 3 32 | 33 | func numberOfSections(in collectionView: UICollectionView) -> Int { 34 | guard let graphData = graphData else { 35 | return 1 36 | } 37 | 38 | let sectionCount = graphData.count == 0 ? 1 : graphData.count 39 | return sectionCount 40 | } 41 | 42 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 43 | 44 | guard let graphData = graphData else { 45 | return 0 46 | } 47 | 48 | if graphData.isEmpty { 49 | return 0 50 | } 51 | 52 | return graphData[section].count 53 | } 54 | 55 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | 57 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReuseIDs.graphCell.rawValue, for: indexPath) 58 | 59 | if let graphData = graphData { 60 | collectionGraphCellDelegate?.collectionGraph(cell: cell, forData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 61 | } 62 | 63 | return cell 64 | } 65 | 66 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 67 | 68 | switch kind { 69 | case ReuseIDs.yDividerView.rawValue: 70 | 71 | let yDividerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.yDividerView.rawValue, for: indexPath) 72 | 73 | yDividerViewSetup(yDividerView: yDividerView, indexPath: indexPath) 74 | 75 | return yDividerView 76 | 77 | case ReuseIDs.lineConnectorView.rawValue: 78 | 79 | let line = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.lineConnectorView.rawValue, for: indexPath) 80 | 81 | lineConnectorViewSetup(line: line, indexPath: indexPath) 82 | 83 | return line 84 | 85 | case ReuseIDs.yLabelView.rawValue: 86 | 87 | let labelView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.yLabelView.rawValue, for: indexPath) 88 | 89 | yLabelViewSetup(labelView: labelView, indexPath: indexPath) 90 | 91 | return labelView 92 | 93 | case ReuseIDs.xLabelView.rawValue: 94 | 95 | let labelView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.xLabelView.rawValue, for: indexPath) 96 | 97 | xLabelViewSetup(labelView: labelView, indexPath: indexPath) 98 | 99 | return labelView 100 | 101 | case ReuseIDs.barView.rawValue: 102 | 103 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.barView.rawValue, for: indexPath) 104 | 105 | barViewSetup(barView: view, indexPath: indexPath) 106 | 107 | return view 108 | 109 | case ReuseIDs.sideBarView.rawValue: 110 | 111 | let sideBar = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseIDs.sideBarView.rawValue, for: indexPath) 112 | 113 | return sideBar 114 | 115 | default: 116 | 117 | return UICollectionReusableView() 118 | } 119 | } 120 | 121 | func yDividerViewSetup(yDividerView: UICollectionReusableView, indexPath: IndexPath) { 122 | if let yDividerView = yDividerView as? YDividerLineView { 123 | yDividerView.line.strokeColor = yDividerLineColor.cgColor 124 | 125 | collectionGraphYDividerLineDelegate?.collectionGraph(yDividerLine: yDividerView.line, atItem: indexPath.item) 126 | } 127 | } 128 | 129 | func lineConnectorViewSetup(line: UICollectionReusableView, indexPath: IndexPath) { 130 | if let line = line as? LineConnectorView, let graphData = graphData { 131 | 132 | collectionGraphLineDelegate?.collectionGraph(connectorLine: line.line, withData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 133 | 134 | line.fillColor = collectionGraphLineFillDelegate?.collectionGraph(fillColorForGraphSectionWithData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 135 | } 136 | } 137 | 138 | func yLabelViewSetup(labelView: UICollectionReusableView, indexPath: IndexPath) { 139 | if let labelView = labelView as? LabelView, let graphData = graphData { 140 | 141 | if let fontName = fontName { 142 | labelView.label.font = UIFont(name: fontName, size: textSize) 143 | } else { 144 | labelView.label.font = UIFont(name: labelView.label.font.fontName, size: textSize) 145 | } 146 | 147 | labelView.label.textColor = textColor 148 | 149 | let yRange = yDataRange(graphData: graphData, numberOfSteps: ySteps) 150 | 151 | let amountPerStep = yRange.max / CGFloat(ySteps) 152 | 153 | let yText = "\(Int((yRange.max - (amountPerStep * CGFloat(indexPath.item)))))" 154 | labelView.label.text = yText 155 | } 156 | } 157 | 158 | func xLabelViewSetup(labelView: UICollectionReusableView, indexPath: IndexPath) { 159 | if let labelView = labelView as? LabelView, let graphData = graphData { 160 | 161 | if let fontName = fontName { 162 | labelView.label.font = UIFont(name: fontName, size: textSize) 163 | } else { 164 | labelView.label.font = UIFont(name: labelView.label.font.fontName, size: textSize) 165 | } 166 | 167 | labelView.label.textColor = textColor 168 | 169 | let range = xDataRange(graphData: graphData) 170 | let xDelta = range.max - range.min 171 | 172 | var stepAmount = xDelta / CGFloat(xSteps - 1) 173 | 174 | if stepAmount.isNaN { stepAmount = 1 } 175 | 176 | let labelText = "\(stepAmount * CGFloat(indexPath.item) + range.min)" 177 | 178 | if let collectionGraphLabelsDelegate = collectionGraphLabelsDelegate { 179 | labelView.label.text = collectionGraphLabelsDelegate.collectionGraph(textForXLabelWithCurrentText: labelText, item: indexPath.item) 180 | } else { 181 | labelView.label.text = labelText 182 | } 183 | } 184 | } 185 | 186 | func barViewSetup(barView: UICollectionReusableView, indexPath: IndexPath) { 187 | if let graphData = graphData { 188 | 189 | collectionGraphBarDelegate?.collectionGraph(barView: barView, withData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 190 | } 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /CollectionGraph/CollectionGraphDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionGraphDelegate.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 11/21/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionGraphDelegate: NSObject, UICollectionViewDelegate { 12 | 13 | internal weak var collectionGraphViewDelegate: CollectionGraphViewDelegate? 14 | 15 | let collectionView: UICollectionView 16 | 17 | var visibleIndices = Set() { 18 | didSet { 19 | let sections = visibleIndices.map { 20 | $0.section 21 | } 22 | 23 | let sectionSet = Set(sections) 24 | 25 | collectionGraphViewDelegate?.collectionGraph(updatedVisibleIndexPaths: visibleIndices, sections: sectionSet) 26 | } 27 | } 28 | 29 | internal var didUpdateVisibleIndicesCallback: ((_ indexPaths: Set, _ sections: Set) -> ())? 30 | 31 | public init(_ collectionView: UICollectionView) { 32 | self.collectionView = collectionView 33 | } 34 | 35 | func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 36 | visibleIndices.insert(indexPath) 37 | } 38 | 39 | func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 40 | visibleIndices.remove(indexPath) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /CollectionGraph/CollectionGraphView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionGraphView.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/23/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | CollectionGraphView requires its data to conform to GraphDatum. 13 | 14 | You may create a struct that conforms to, but also supplies more information. 15 | You will be able to access that information during callbacks so you can customize Cells, Bar lines, and Line views. 16 | */ 17 | public protocol GraphDatum { 18 | var point: CGPoint { get set } 19 | } 20 | 21 | public enum ReuseIDs: String { 22 | case graphCell = "GraphCell" 23 | case lineConnectorView = "Line" 24 | case barView = "Bar" 25 | case yDividerView = "YDivider" 26 | case yLabelView = "YLabel" 27 | case xLabelView = "XLabel" 28 | case sideBarView = "SideBar" 29 | } 30 | 31 | @IBDesignable 32 | public class CollectionGraphView: UIView, UICollectionViewDelegate { 33 | 34 | public weak var collectionGraphViewDelegate: CollectionGraphViewDelegate? { 35 | didSet { 36 | collectionGraphDelegate.collectionGraphViewDelegate = collectionGraphViewDelegate 37 | } 38 | } 39 | 40 | public weak var collectionGraphCellDelegate: CollectionGraphCellDelegate? { 41 | didSet { 42 | collectionGraphDataSource.collectionGraphCellDelegate = collectionGraphCellDelegate 43 | layout.collectionGraphCellDelegate = collectionGraphCellDelegate 44 | } 45 | } 46 | 47 | public weak var collectionGraphBarDelegate: CollectionGraphBarDelegate? { 48 | didSet { 49 | if barView == nil { 50 | barView = UICollectionReusableView() 51 | } 52 | 53 | collectionGraphDataSource.collectionGraphBarDelegate = collectionGraphBarDelegate 54 | layout.collectionGraphBarDelegate = collectionGraphBarDelegate 55 | } 56 | } 57 | 58 | public weak var collectionGraphLineDelegate: CollectionGraphLineDelegate? { 59 | didSet { 60 | collectionGraphDataSource.collectionGraphLineDelegate = collectionGraphLineDelegate 61 | 62 | layout.displayLineConnectors = true 63 | 64 | self.graphCollectionView.register( 65 | LineConnectorView.classForCoder(), 66 | forSupplementaryViewOfKind: ReuseIDs.lineConnectorView.rawValue, 67 | withReuseIdentifier: ReuseIDs.lineConnectorView.rawValue) 68 | } 69 | } 70 | 71 | public weak var collectionGraphLineFillDelegate: CollectionGraphLineFillDelegate? { 72 | didSet { 73 | collectionGraphDataSource.collectionGraphLineFillDelegate = collectionGraphLineFillDelegate 74 | } 75 | } 76 | 77 | public weak var collectionGraphLabelsDelegate: CollectionGraphLabelsDelegate? { 78 | didSet { 79 | collectionGraphDataSource.collectionGraphLabelsDelegate = collectionGraphLabelsDelegate 80 | } 81 | } 82 | 83 | public weak var collectionGraphYDividerLineDelegate: CollectionGraphYDividerLineDelegate? { 84 | didSet { 85 | collectionGraphDataSource.collectionGraphYDividerLineDelegate = collectionGraphYDividerLineDelegate 86 | } 87 | } 88 | 89 | /// Each GraphDatum array will define a new section in the graph. 90 | public var graphData: [[GraphDatum]]? { 91 | didSet { 92 | if let graphData = graphData { 93 | layout.graphData = graphData 94 | collectionGraphDataSource.graphData = graphData 95 | graphCollectionView.reloadData() 96 | } 97 | } 98 | } 99 | 100 | private var collectionGraphDataSource = CollectionGraphDataSource() 101 | 102 | private var collectionGraphDelegate: CollectionGraphDelegate! 103 | 104 | /** A graphCell represents a data point on the graph. 105 | 106 | C = graphCell 107 | 108 | | C 109 | | C 110 | | C 111 | | C 112 | | C 113 | |____________________ 114 | 1 2 3 4 5 115 | 116 | */ 117 | public var graphCell: UICollectionViewCell? { 118 | didSet { 119 | if let graphCell = graphCell { 120 | self.graphCollectionView.register(graphCell.classForCoder, forCellWithReuseIdentifier: ReuseIDs.graphCell.rawValue) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | Used for a bar graph 127 | 128 | A barView represents the bar that sits under a graphCell and extends to the bottom of the graph. 129 | 130 | In order to display the graph with bars you need to initialize a barCell 131 | */ 132 | public var barView: UICollectionReusableView? { 133 | didSet { 134 | if let barView = barView { 135 | self.graphCollectionView.register(barView.classForCoder, forSupplementaryViewOfKind: ReuseIDs.barView.rawValue, withReuseIdentifier: ReuseIDs.barView.rawValue) 136 | layout.displayBars = true 137 | } 138 | } 139 | } 140 | 141 | /** 142 | A view that lies behind the y axis labels and above the plotted graph. Useful for covering the graph when it scrolls behind the y labels. 143 | 144 | **Note!** 145 | You need to provide a subclass of UICollectionReusableView and override ````init(frame: CGRect)````. 146 | Inside the init block is where you set your customizations 147 | 148 | Initializiing a UICollectionReusableView() and then settings its background color will not work. 149 | 150 | **Example** 151 | 152 | ```` 153 | // MySideBarClass.swift 154 | override init(frame: CGRect) { 155 | super.init(frame: frame) 156 | backgroundColor = UIColor.red 157 | } 158 | ```` 159 | */ 160 | public var ySideBarView: UICollectionReusableView? { 161 | didSet { 162 | if let ySideBarView = ySideBarView { 163 | layout.ySideBarView = ySideBarView 164 | 165 | graphCollectionView.collectionViewLayout.register(ySideBarView.classForCoder, forDecorationViewOfKind: ReuseIDs.sideBarView.rawValue) 166 | } 167 | } 168 | } 169 | 170 | private var layout = GraphLayout() 171 | 172 | /** 173 | The width of the scrollable graph content. 174 | 175 | - Default is is the width of the CollectionGraphView. 176 | - All data points will plot to fit within specified width. 177 | */ 178 | @IBInspectable public var graphContentWidth: CGFloat = 0 { 179 | didSet { 180 | self.layout.graphContentWidth = self.graphContentWidth 181 | self.graphCollectionView.contentOffset.x = -self.leftInset 182 | } 183 | } 184 | 185 | /// The color of the labels on the x and y axes. 186 | @IBInspectable public var textColor: UIColor = UIColor.darkText { 187 | didSet { 188 | collectionGraphDataSource.textColor = textColor 189 | graphCollectionView.reloadData() 190 | } 191 | } 192 | 193 | @IBInspectable public var textSize: CGFloat = 8 { 194 | didSet { 195 | collectionGraphDataSource.textSize = textSize 196 | } 197 | } 198 | 199 | public var fontName: String? { 200 | didSet { 201 | collectionGraphDataSource.fontName = fontName 202 | } 203 | } 204 | 205 | /// The color of the horizontal lines that run across the graph. 206 | @IBInspectable public var yDividerLineColor: UIColor = UIColor.lightGray { 207 | didSet { 208 | collectionGraphDataSource.yDividerLineColor = yDividerLineColor 209 | graphCollectionView.reloadData() 210 | } 211 | } 212 | 213 | /// The number of horizonal lines and labels to display on the graph along the y axis 214 | @IBInspectable public var ySteps: Int = 6 { 215 | didSet { 216 | layout.ySteps = ySteps 217 | collectionGraphDataSource.ySteps = ySteps 218 | graphCollectionView.reloadData() 219 | } 220 | } 221 | 222 | /// The number of labels to display along the x axis. 223 | @IBInspectable public var xSteps: Int = 3 { 224 | didSet { 225 | layout.xSteps = xSteps 226 | collectionGraphDataSource.xSteps = xSteps 227 | graphCollectionView.reloadData() 228 | } 229 | } 230 | 231 | /// Distance offset from the top of the view 232 | @IBInspectable public var topInset: CGFloat = 10 { 233 | didSet { 234 | graphCollectionView.contentInset.top = topInset 235 | graphCollectionView.reloadData() 236 | } 237 | } 238 | 239 | /** 240 | Distance offset from the left side of the view. 241 | 242 | This makes space for the y labels. 243 | */ 244 | @IBInspectable public var leftInset: CGFloat = 20 { 245 | didSet { 246 | graphCollectionView.contentInset.left = leftInset 247 | graphCollectionView.reloadData() 248 | } 249 | } 250 | 251 | /** 252 | Distance offset from the bottom of the view. 253 | 254 | This makes space for the x labels. 255 | */ 256 | @IBInspectable public var bottomInset: CGFloat = 20 { 257 | didSet { 258 | graphCollectionView.contentInset.bottom = bottomInset 259 | graphCollectionView.reloadData() 260 | } 261 | } 262 | 263 | /// Distance offset from the right of the view 264 | @IBInspectable public var rightInset: CGFloat = 20 { 265 | didSet { 266 | graphCollectionView.contentInset.right = rightInset 267 | graphCollectionView.reloadData() 268 | } 269 | } 270 | 271 | @IBOutlet internal weak var graphCollectionView: UICollectionView! { 272 | didSet { 273 | graphCollectionView.dataSource = collectionGraphDataSource 274 | 275 | collectionGraphDelegate = CollectionGraphDelegate(graphCollectionView) 276 | graphCollectionView.delegate = collectionGraphDelegate 277 | 278 | graphCollectionView.collectionViewLayout = layout 279 | 280 | graphCollectionView.contentInset = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) 281 | graphCollectionView.contentOffset.x = -leftInset 282 | 283 | registerDefaultCells() 284 | } 285 | } 286 | 287 | private func registerDefaultCells() { 288 | self.graphCollectionView.register(YDividerLineView.classForCoder(), forSupplementaryViewOfKind: ReuseIDs.yDividerView.rawValue, withReuseIdentifier: ReuseIDs.yDividerView.rawValue) 289 | 290 | self.graphCollectionView.register(LabelView.classForCoder(), forSupplementaryViewOfKind: ReuseIDs.xLabelView.rawValue, withReuseIdentifier: ReuseIDs.xLabelView.rawValue) 291 | 292 | self.graphCollectionView.register(LabelView.classForCoder(), forSupplementaryViewOfKind: ReuseIDs.yLabelView.rawValue, withReuseIdentifier: ReuseIDs.yLabelView.rawValue) 293 | } 294 | 295 | public var contentOffset: CGPoint { 296 | get { 297 | return graphCollectionView.contentOffset 298 | } 299 | set { 300 | self.graphCollectionView.contentOffset = newValue 301 | self.graphCollectionView.layoutIfNeeded() 302 | self.graphCollectionView.collectionViewLayout.invalidateLayout() 303 | } 304 | } 305 | 306 | /// Scroll the graph to a data point 307 | public func scrollToDataPoint(graphDatum: GraphDatum, withAnimation animation: Bool, andScrollPosition scrollPosition: UICollectionViewScrollPosition) { 308 | 309 | var sectionNumber: Int? 310 | var itemNumber: Int? 311 | 312 | // go thru graphData find matching datum 313 | if let graphData = graphData { 314 | for section in 0 ... graphData.count - 1 { 315 | 316 | itemNumber = graphData[section].index(where: { (data) -> Bool in 317 | sectionNumber = section 318 | return data.point == graphDatum.point 319 | }) 320 | } 321 | } 322 | 323 | if let sectionNumber = sectionNumber, let itemNumber = itemNumber { 324 | let indexPath = IndexPath(item: itemNumber, section: sectionNumber) 325 | graphCollectionView.scrollToItem(at: indexPath, at: scrollPosition, animated: animation) 326 | } 327 | } 328 | 329 | // MARK: - View Lifecycle 330 | 331 | required public init(frame: CGRect, graphCell: UICollectionViewCell) { 332 | super.init(frame: frame) 333 | 334 | addCollectionView() 335 | 336 | self.graphCell = graphCell 337 | } 338 | 339 | override init(frame: CGRect) { 340 | super.init(frame: frame) 341 | 342 | addCollectionView() 343 | } 344 | 345 | required public init?(coder aDecoder: NSCoder) { 346 | super.init(coder: aDecoder) 347 | 348 | addCollectionView() 349 | 350 | defer { 351 | graphCell = UICollectionViewCell() 352 | } 353 | } 354 | 355 | func addCollectionView() { 356 | let xibView = XibLoader.viewFromXib(name: "GraphCollectionView", owner: self) 357 | 358 | xibView?.frame = bounds 359 | 360 | if let xibView = xibView { 361 | addSubview(xibView) 362 | } 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /CollectionGraph/GraphCollectionView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /CollectionGraph/GraphDelegates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphDelegates.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 12/13/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol CollectionGraphCellDelegate: class { 12 | /** 13 | Returns the graphCell and corresponding GraphDatum. 14 | 15 | Use this to set any properties on the graphCell like color, layer properties, or any custom visual properties from your subclass. 16 | 17 | - parameter cell: The corresponding graphCell 18 | - parameter data: The corresponding GraphDatum 19 | - parameter section: The section number of [GraphDatum] 20 | */ 21 | func collectionGraph(cell: UICollectionViewCell, forData data: GraphDatum, atIndexPath indexPath: IndexPath) 22 | 23 | /** 24 | Set the size of the graphCell 25 | 26 | - parameter data: The corresponding GraphDatum 27 | - parameter section: The section number of [GraphDatum] 28 | */ 29 | func collectionGraph(sizeForGraphCellWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> CGSize 30 | } 31 | 32 | public protocol CollectionGraphBarDelegate: class { 33 | /** 34 | Returns the barCell and corresponding GraphDatum. 35 | 36 | Use this to set any properties on the barCell like color, layer properties, or any custom visual properties from your subclass. 37 | 38 | - parameter barView: The corresponding barView 39 | - parameter data: The corresponding GraphDatum 40 | - parameter section: The section number of [GraphDatum] 41 | */ 42 | func collectionGraph(barView: UICollectionReusableView, withData data: GraphDatum, atIndexPath indexPath: IndexPath) 43 | 44 | /** 45 | Set the width of the barCell with corresponding GraphDatum in Section 46 | 47 | - parameter data: The corresponding GraphDatum 48 | - parameter section: The section number of [GraphDatum] 49 | */ 50 | func collectionGraph(widthForBarViewWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> CGFloat 51 | } 52 | 53 | public protocol CollectionGraphLineDelegate: class { 54 | /** 55 | Returns the Connector Lines and corresponding GraphDatum. 56 | 57 | Use this to set any properties on the line like color, dot pattern, cap, or any custom visual properties from your subclass. 58 | 59 | - parameter connectorLine: GraphLineShapeLayer is a CAShapeLayer subclass with an extra straightLines Bool you can set. The default is false. 60 | 61 | - parameter data: the corresponding GraphDatum 62 | - parameter section: The section number in [[GraphDatum]] 63 | */ 64 | func collectionGraph(connectorLine: GraphLineShapeLayer, withData data: GraphDatum, atIndexPath indexPath: IndexPath) 65 | } 66 | 67 | public protocol CollectionGraphLineFillDelegate: class { 68 | /** 69 | Set the color of the fill below the graph line 70 | 71 | - parameter data: the corresponding GraphDatum 72 | - parameter section: The section number in [[GraphDatum]] 73 | */ 74 | func collectionGraph(fillColorForGraphSectionWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> UIColor 75 | } 76 | 77 | public protocol CollectionGraphLabelsDelegate: class { 78 | /** 79 | Set the text of label along the x axis 80 | 81 | ## Tip: 82 | Useful for converting Dates that were converted to Ints back to Dates 83 | 84 | - parameter currentString: The labels current string 85 | - parameter section: The labels current section number 86 | */ 87 | func collectionGraph(textForXLabelWithCurrentText currentText: String, item: Int) -> String 88 | } 89 | 90 | public protocol CollectionGraphViewDelegate: class { 91 | /** 92 | Returns the visible IndexPaths and Sections as Sets<> when scrolling 93 | 94 | - parameter indexPaths: Set of visible GraphDatum 95 | - parameter sections: Set of visible sections of [GraphDatum] 96 | */ 97 | func collectionGraph(updatedVisibleIndexPaths indexPaths: Set, sections: Set) 98 | } 99 | 100 | public protocol CollectionGraphYDividerLineDelegate: class { 101 | /** 102 | Returns the Y Divider Lines. 103 | 104 | Use this to set any properties on the line like color, dot pattern, cap. 105 | 106 | - parameter yDividerLine: CAShapeLayer 107 | */ 108 | func collectionGraph(yDividerLine: CAShapeLayer, atItem item: Int) 109 | } 110 | -------------------------------------------------------------------------------- /CollectionGraph/GraphLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphLayout.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/29/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class GraphLayout: UICollectionViewLayout, RangeFinder { 12 | 13 | internal weak var collectionGraphCellDelegate: CollectionGraphCellDelegate? 14 | internal weak var collectionGraphBarDelegate: CollectionGraphBarDelegate? 15 | 16 | internal var graphData: [[GraphDatum]]? 17 | 18 | internal var displayBars = false 19 | internal var displayLineConnectors = false 20 | 21 | internal var ySideBarView: UICollectionReusableView? 22 | 23 | internal var ySteps: Int = 6 24 | internal var xSteps: Int = 3 25 | 26 | internal var graphContentWidth: CGFloat? // width of graph in points 27 | 28 | internal var cellSize: CGSize = CGSize(width: 3.0, height: 3.0) 29 | 30 | private var yIncrements: CGFloat { 31 | get { 32 | if let graphData = graphData { 33 | return yDataRange(graphData: graphData, numberOfSteps: ySteps).max / CGFloat(ySteps) 34 | } 35 | return 0 36 | } 37 | } 38 | 39 | private let labelsZIndex = Int.max 40 | private let sideBarZIndex = Int.max - 1 41 | 42 | internal var staticAttributes: [UICollectionViewLayoutAttributes]? 43 | 44 | // MARK: - Layout Setup 45 | 46 | public override func prepare() { 47 | super.prepare() 48 | 49 | createStaticAttributes() 50 | } 51 | 52 | public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 53 | 54 | return true 55 | } 56 | 57 | func createStaticAttributes() { 58 | 59 | var tempAttributes = [UICollectionViewLayoutAttributes]() 60 | 61 | tempAttributes += self.layoutAttributesForCell() 62 | 63 | tempAttributes += self.layoutAttributesForXLabels() 64 | 65 | if self.displayLineConnectors { 66 | tempAttributes += self.layoutAttributesForLineConnector() 67 | } 68 | 69 | if self.displayBars { 70 | tempAttributes += self.layoutAttributesForBar() 71 | } 72 | 73 | self.staticAttributes = tempAttributes 74 | } 75 | 76 | internal func temporaryAttributes() -> [UICollectionViewLayoutAttributes] { 77 | var tempAttributes = [UICollectionViewLayoutAttributes]() 78 | 79 | tempAttributes += self.layoutAttributesForYDividerLines() 80 | 81 | tempAttributes += self.layoutAttributesForYLabels() 82 | 83 | if ySideBarView != nil { 84 | tempAttributes += self.layoutAttributesForSideBar() 85 | } 86 | 87 | return tempAttributes 88 | } 89 | 90 | public override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { 91 | let context = super.invalidationContext(forBoundsChange: newBounds) 92 | 93 | guard let collectionView = self.collectionView else { 94 | return context 95 | } 96 | 97 | if collectionView.bounds.size != newBounds.size { 98 | 99 | return context 100 | } 101 | 102 | invalidateIntermediateIndices(with: context) 103 | 104 | return context 105 | } 106 | 107 | private func invalidateIntermediateIndices(with context: UICollectionViewLayoutInvalidationContext) { 108 | 109 | guard let collectionView = self.collectionView else { 110 | return 111 | } 112 | 113 | let dividerLineIndices = collectionView.indexPathsForVisibleSupplementaryElements(ofKind: ReuseIDs.yDividerView.rawValue) 114 | context.invalidateSupplementaryElements(ofKind: ReuseIDs.yDividerView.rawValue, at: dividerLineIndices) 115 | 116 | let yLabelIndices = collectionView.indexPathsForVisibleSupplementaryElements(ofKind: ReuseIDs.yLabelView.rawValue) 117 | context.invalidateSupplementaryElements(ofKind: ReuseIDs.yLabelView.rawValue, at: yLabelIndices) 118 | 119 | let ySideBarIndices = [IndexPath(item: 0, section: 0)] 120 | context.invalidateDecorationElements(ofKind: ReuseIDs.sideBarView.rawValue, at: ySideBarIndices) 121 | } 122 | 123 | private func layoutAttributesForCell() -> [UICollectionViewLayoutAttributes] { 124 | 125 | var tempAttributes = [UICollectionViewLayoutAttributes]() 126 | 127 | if let collectionView = collectionView { 128 | 129 | for sectionNumber in 0 ..< collectionView.numberOfSections { 130 | 131 | for itemNumber in 0 ..< collectionView.numberOfItems(inSection: sectionNumber) { 132 | 133 | let indexPath = IndexPath(item: itemNumber, section: sectionNumber) 134 | 135 | if let attributes = layoutAttributesForItem(at: indexPath) { 136 | tempAttributes += [attributes] 137 | } 138 | } 139 | } 140 | } 141 | return tempAttributes 142 | } 143 | 144 | private func layoutAttributesForYDividerLines() -> [UICollectionViewLayoutAttributes] { 145 | 146 | var tempAttributes = [UICollectionViewLayoutAttributes]() 147 | 148 | if let collectionView = collectionView { 149 | 150 | if collectionView.numberOfSections > 0 { 151 | for number in 0 ..< ySteps { 152 | 153 | let indexPath = IndexPath(item: number, section: 0) 154 | 155 | let supplementaryAttribute = layoutAttributesForSupplementaryView(ofKind: ReuseIDs.yDividerView.rawValue, at: indexPath) 156 | 157 | if let supplementaryAttribute = supplementaryAttribute { 158 | tempAttributes += [supplementaryAttribute] 159 | } 160 | } 161 | } 162 | } 163 | return tempAttributes 164 | } 165 | 166 | private func layoutAttributesForYLabels() -> [UICollectionViewLayoutAttributes] { 167 | 168 | var tempAttributes = [UICollectionViewLayoutAttributes]() 169 | 170 | for number in 0 ..< ySteps { 171 | 172 | let indexPath = IndexPath(item: number, section: 0) 173 | 174 | let supplementaryAttribute = layoutAttributesForSupplementaryView(ofKind: ReuseIDs.yLabelView.rawValue, at: indexPath) 175 | 176 | if let supplementaryAttribute = supplementaryAttribute { 177 | tempAttributes += [supplementaryAttribute] 178 | } 179 | } 180 | return tempAttributes 181 | } 182 | 183 | private func layoutAttributesForXLabels() -> [UICollectionViewLayoutAttributes] { 184 | 185 | var tempAttributes = [UICollectionViewLayoutAttributes]() 186 | 187 | for number in 0 ..< xSteps { 188 | 189 | let indexPath = IndexPath(item: number, section: 0) 190 | 191 | let supplementaryAttribute = layoutAttributesForSupplementaryView(ofKind: ReuseIDs.xLabelView.rawValue, at: indexPath) 192 | 193 | if let supplementaryAttribute = supplementaryAttribute { 194 | tempAttributes += [supplementaryAttribute] 195 | } 196 | } 197 | return tempAttributes 198 | } 199 | 200 | private func layoutAttributesForLineConnector() -> [UICollectionViewLayoutAttributes] { 201 | 202 | var tempAttributes = [UICollectionViewLayoutAttributes]() 203 | 204 | if let collectionView = collectionView { 205 | 206 | for sectionNumber in 0.. [UICollectionViewLayoutAttributes] { 223 | 224 | var tempAttributes = [UICollectionViewLayoutAttributes]() 225 | 226 | if let _ = collectionView { 227 | let indexPath = IndexPath(item: 0, section: 0) 228 | let attribute = layoutAttributesForDecorationView(ofKind: ReuseIDs.sideBarView.rawValue, at: indexPath) 229 | 230 | if let attribute = attribute { 231 | tempAttributes += [attribute] 232 | } 233 | } 234 | 235 | return tempAttributes 236 | } 237 | 238 | private func layoutAttributesForBar() -> [UICollectionViewLayoutAttributes] { 239 | 240 | var tempAttributes = [UICollectionViewLayoutAttributes]() 241 | 242 | if let collectionView = collectionView { 243 | 244 | for sectionNumber in 0.. UICollectionViewLayoutAttributes? { 263 | let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) 264 | 265 | if let graphData = graphData, let collectionGraphCellDelegate = collectionGraphCellDelegate { 266 | cellSize = collectionGraphCellDelegate.collectionGraph(sizeForGraphCellWithData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 267 | } 268 | 269 | let frame = CGRect(x: xGraphPosition(indexPath: indexPath) - cellSize.width / 2, 270 | y: yGraphPosition(indexPath: indexPath) - cellSize.height / 2, 271 | width: cellSize.width, 272 | height: cellSize.height) 273 | 274 | attributes.frame = frame 275 | attributes.zIndex = sideBarZIndex - 1 - indexPath.item 276 | 277 | return attributes 278 | } 279 | 280 | public override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 281 | 282 | let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: ReuseIDs.sideBarView.rawValue, with: indexPath) 283 | 284 | if elementKind == ReuseIDs.sideBarView.rawValue { 285 | if let collectionView = collectionView { 286 | 287 | let width = collectionView.contentInset.left 288 | let height = collectionView.frame.height 289 | let verticleInsets = collectionView.contentInset.bottom + collectionView.contentInset.top 290 | 291 | attributes.zIndex = sideBarZIndex 292 | 293 | attributes.frame = CGRect(x: collectionView.contentOffset.x, y: -verticleInsets, width: width, height: height) 294 | } 295 | } 296 | return attributes 297 | } 298 | 299 | public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 300 | 301 | if elementKind == ReuseIDs.yDividerView.rawValue { 302 | 303 | return setAttributesForYDivider(fromIndex: indexPath) 304 | 305 | } else if elementKind == ReuseIDs.lineConnectorView.rawValue { 306 | 307 | return setAttributesForLineConnector(fromIndex: indexPath) 308 | 309 | } else if elementKind == ReuseIDs.yLabelView.rawValue { 310 | 311 | return setAttributesForYLabel(fromIndex: indexPath) 312 | 313 | } else if elementKind == ReuseIDs.xLabelView.rawValue { 314 | 315 | return setAttributesForXLabel(fromIndex: indexPath) 316 | 317 | } else if elementKind == ReuseIDs.barView.rawValue { 318 | 319 | return setAttributesForBar(fromIndex: indexPath) 320 | 321 | } 322 | return nil 323 | } 324 | 325 | // attribute creation 326 | 327 | private func setAttributesForYDivider(fromIndex indexPath: IndexPath) -> YDividerLayoutAttributes { 328 | 329 | let attributes = YDividerLayoutAttributes(forSupplementaryViewOfKind: ReuseIDs.yDividerView.rawValue, with: indexPath) 330 | 331 | if let collectionView = collectionView { 332 | 333 | let height = (collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height)) / CGFloat(ySteps) 334 | let width = collectionView.bounds.width 335 | 336 | let frame = CGRect(x: collectionView.contentOffset.x, 337 | y: height * CGFloat(indexPath.row) + cellSize.height / 2, 338 | width: width, 339 | height: height) 340 | 341 | attributes.frame = frame 342 | attributes.inset = collectionView.contentInset.left 343 | 344 | attributes.zIndex = -1 345 | } 346 | return attributes 347 | } 348 | 349 | private func setAttributesForLineConnector(fromIndex indexPath: IndexPath) -> LineConnectorAttributes? { 350 | 351 | let attributes = LineConnectorAttributes(forSupplementaryViewOfKind: ReuseIDs.lineConnectorView.rawValue, with: indexPath) 352 | 353 | if let graphData = graphData { 354 | 355 | if indexPath.item < graphData[indexPath.section].count - 1 { 356 | 357 | let xOffset = xGraphPosition(indexPath: indexPath) 358 | let yOffset = yGraphPosition(indexPath: indexPath) - cellSize.height / 2 359 | 360 | let nextIndex = IndexPath(item: indexPath.item + 1, section: indexPath.section) 361 | 362 | let xOffset2 = xGraphPosition(indexPath: nextIndex) 363 | let yOffset2 = yGraphPosition(indexPath: nextIndex) - cellSize.height / 2 364 | 365 | let p1 = CGPoint(x: xOffset, 366 | y: yOffset) 367 | 368 | let p2 = CGPoint(x: xOffset2, 369 | y: yOffset2) 370 | 371 | // create a Rect between the two points 372 | if let collectionView = collectionView { 373 | 374 | let height = collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height) 375 | 376 | let rect = CGRect(x: min(p1.x, p2.x), 377 | y: cellSize.height / 2, 378 | width: fabs(p1.x - p2.x), 379 | height: height) 380 | 381 | attributes.frame = rect 382 | } 383 | 384 | attributes.points = (first: p1, second: p2) 385 | 386 | attributes.zIndex = indexPath.section 387 | 388 | return attributes 389 | } 390 | } 391 | return nil 392 | } 393 | 394 | private func setAttributesForYLabel(fromIndex indexPath: IndexPath) -> XLabelViewAttributes { 395 | 396 | let attributes = XLabelViewAttributes(forSupplementaryViewOfKind: ReuseIDs.yLabelView.rawValue, with: indexPath) 397 | 398 | if let collectionView = collectionView { 399 | 400 | let height = (collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height)) / CGFloat(ySteps) 401 | let width = collectionView.contentInset.left 402 | 403 | let frame = CGRect(x: collectionView.contentOffset.x, 404 | y: (height * CGFloat(indexPath.row)) - (height / 2) + cellSize.height / 2, 405 | width: width, 406 | height: height) 407 | 408 | attributes.frame = frame 409 | 410 | attributes.zIndex = labelsZIndex 411 | 412 | } 413 | return attributes 414 | } 415 | 416 | private func setAttributesForXLabel(fromIndex indexPath: IndexPath) -> XLabelViewAttributes { 417 | 418 | let attributes = XLabelViewAttributes(forSupplementaryViewOfKind: ReuseIDs.xLabelView.rawValue, with: indexPath) 419 | 420 | if let collectionView = collectionView { 421 | 422 | let height = collectionView.contentInset.bottom 423 | 424 | let collectionWidth = graphContentWidth ?? collectionView.bounds.width - (collectionView.contentInset.left + collectionView.contentInset.right + cellSize.width) 425 | 426 | let width = xSteps == 1 ? collectionWidth : collectionWidth / CGFloat(xSteps - 1) 427 | 428 | let xPosition = (width * CGFloat(indexPath.item) - width / 2) + cellSize.width / 2 429 | 430 | let yPosition = collectionView.frame.height - collectionView.contentInset.top - collectionView.contentInset.bottom 431 | 432 | let frame = CGRect(x: xPosition, 433 | y: yPosition, 434 | width: width, 435 | height: height) 436 | 437 | attributes.frame = frame 438 | 439 | attributes.zIndex = labelsZIndex 440 | } 441 | return attributes 442 | } 443 | 444 | private func setAttributesForBar(fromIndex indexPath: IndexPath) -> UICollectionViewLayoutAttributes { 445 | 446 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: ReuseIDs.barView.rawValue, with: indexPath) 447 | 448 | var width: CGFloat = cellSize.width 449 | 450 | if let graphData = graphData, let collectionGraphBarDelegate = collectionGraphBarDelegate { 451 | width = collectionGraphBarDelegate.collectionGraph(widthForBarViewWithData: graphData[indexPath.section][indexPath.item], atIndexPath: indexPath) 452 | } 453 | 454 | var heightOfCollectionView: CGFloat = 0 455 | 456 | if let collectionView = collectionView { 457 | heightOfCollectionView = collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height) 458 | } 459 | 460 | let barHeight = heightOfCollectionView - yGraphPosition(indexPath: indexPath) + cellSize.height / 2 461 | let yPosition = heightOfCollectionView - (heightOfCollectionView - yGraphPosition(indexPath: indexPath)) 462 | 463 | attributes.frame = CGRect(x: xGraphPosition(indexPath: indexPath) - width / 2, 464 | y: yPosition, 465 | width: width, 466 | height: barHeight) 467 | 468 | attributes.zIndex = indexPath.item 469 | 470 | return attributes 471 | } 472 | 473 | // MARK: - Layout 474 | 475 | public override var collectionViewContentSize: CGSize { 476 | if let collectionView = collectionView { 477 | 478 | let initialSize = collectionView.bounds.width - (collectionView.contentInset.left + collectionView.contentInset.right) 479 | 480 | var width = initialSize 481 | 482 | if let graphContentWidth = graphContentWidth { 483 | width = graphContentWidth + cellSize.width 484 | } 485 | 486 | let height = collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height) 487 | 488 | let contentSize = CGSize(width: width, height: height) 489 | 490 | return contentSize 491 | } 492 | 493 | return CGSize.zero 494 | } 495 | 496 | override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 497 | 498 | var attributes = [UICollectionViewLayoutAttributes]() 499 | 500 | attributes += temporaryAttributes() 501 | 502 | for staticAttributes in staticAttributes! { 503 | if staticAttributes.frame.intersects(rect) { 504 | attributes += [staticAttributes] 505 | } 506 | } 507 | 508 | return attributes 509 | } 510 | 511 | // MARK: - Helpers 512 | 513 | private func xGraphPosition(indexPath: IndexPath) -> CGFloat { 514 | if let graphData = graphData, let collectionView = collectionView { 515 | 516 | let width = graphContentWidth ?? collectionView.bounds.width - (collectionView.contentInset.left + collectionView.contentInset.right + cellSize.width) 517 | 518 | let xRange = xDataRange(graphData: graphData) 519 | 520 | let xDeltaRange = xRange.max - xRange.min 521 | 522 | var xValPercent = (graphData[indexPath.section][indexPath.item].point.x - xRange.min) / xDeltaRange 523 | 524 | if xValPercent.isNaN { 525 | xValPercent = 0 526 | } 527 | 528 | let xPos = width * xValPercent + cellSize.width / 2 529 | 530 | return xPos 531 | } 532 | return 0 533 | } 534 | 535 | private func yGraphPosition(indexPath: IndexPath) -> CGFloat { 536 | if let collectionView = collectionView, let graphData = graphData { 537 | let delta = collectionView.bounds.height - (collectionView.contentInset.top + collectionView.contentInset.bottom + cellSize.height) 538 | 539 | let yRange = yDataRange(graphData: graphData, numberOfSteps: ySteps) 540 | 541 | let position = delta - (delta * (graphData[indexPath.section][indexPath.item].point.y / yRange.max)) + cellSize.height / 2 542 | 543 | return position.isNaN ? 0 : position 544 | } 545 | return 0 546 | } 547 | 548 | } 549 | -------------------------------------------------------------------------------- /CollectionGraph/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CollectionGraph/LabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLabelView.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/14/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LabelView: UICollectionReusableView { 12 | 13 | var label = UILabel() 14 | var textColor: UIColor = UIColor.darkText 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | setup() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | setup() 24 | } 25 | 26 | func setup() { 27 | label.font = UIFont.systemFont(ofSize: 8) 28 | label.textAlignment = .center 29 | label.textColor = textColor 30 | label.text = "" 31 | addSubview(label) 32 | } 33 | 34 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 35 | 36 | if let attributes = layoutAttributes as? XLabelViewAttributes { 37 | label.sizeToFit() 38 | label.frame = attributes.bounds 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /CollectionGraph/LineConnectorAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineConnectorAttributes.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/13/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LineConnectorAttributes: UICollectionViewLayoutAttributes { 12 | var points:(first: CGPoint, second: CGPoint) = (first: CGPoint.zero, second: CGPoint.zero) 13 | 14 | override func copy(with zone: NSZone? = nil) -> Any { 15 | let copy = super.copy(with: zone) 16 | if let copy = copy as? LineConnectorAttributes { 17 | copy.points = points 18 | } 19 | return copy 20 | } 21 | 22 | override func isEqual(_ object: Any?) -> Bool { 23 | if let attributes = object as? LineConnectorAttributes { 24 | if attributes.points == points { 25 | return super.isEqual(object) 26 | } 27 | } 28 | return false 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /CollectionGraph/LineConnectorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineConnectorView.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/13/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class GraphLineShapeLayer: CAShapeLayer { 12 | public var straightLines: Bool = false 13 | 14 | public override var strokeColor: CGColor? { 15 | didSet { 16 | removeAllAnimations() 17 | CATransaction.begin() 18 | CATransaction.setDisableActions(true) 19 | 20 | super.strokeColor = strokeColor 21 | 22 | CATransaction.commit() 23 | } 24 | } 25 | } 26 | 27 | class LineConnectorView: UICollectionReusableView { 28 | 29 | var line: GraphLineShapeLayer = GraphLineShapeLayer() 30 | 31 | var fillShape = CAShapeLayer() 32 | 33 | var points:(first: CGPoint, second: CGPoint)? 34 | 35 | var lineWidth: CGFloat = 1 36 | 37 | var fillColor: UIColor? 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | setup() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | setup() 47 | } 48 | 49 | func setup() { 50 | line.fillColor = UIColor.clear.cgColor 51 | 52 | layer.addSublayer(line) 53 | 54 | layer.addSublayer(fillShape) 55 | } 56 | 57 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 58 | if let attributes = layoutAttributes as? LineConnectorAttributes { 59 | self.points = attributes.points 60 | setNeedsLayout() 61 | } 62 | } 63 | 64 | func drawLine() { 65 | if let bezPath = connectingLine() { 66 | self.line.path = bezPath.cgPath 67 | } 68 | } 69 | 70 | func connectingLine() -> UIBezierPath? { 71 | if let points = points { 72 | let path = UIBezierPath() 73 | 74 | let startingPoint = CGPoint(x: 0, y: points.first.y) 75 | let endingPoint = CGPoint(x: bounds.width, y: points.second.y) 76 | 77 | path.move(to: startingPoint) 78 | 79 | if line.straightLines { 80 | path.addLine(to: endingPoint) 81 | } else { 82 | let cp1 = CGPoint(x: (startingPoint.x + endingPoint.x) / 2, y: startingPoint.y) 83 | let cp2 = CGPoint(x: (startingPoint.x + endingPoint.x) / 2, y: endingPoint.y) 84 | path.addCurve(to: endingPoint, controlPoint1: cp1, controlPoint2: cp2) 85 | } 86 | 87 | return path 88 | } 89 | return nil 90 | } 91 | 92 | func fillLineWithColor() { 93 | if let fillColor = fillColor { 94 | 95 | CATransaction.begin() 96 | CATransaction.setDisableActions(true) 97 | 98 | var path = UIBezierPath() 99 | 100 | if let bezPath = connectingLine() { 101 | path = bezPath 102 | 103 | let bottomRight = CGPoint(x: bounds.width, y: bounds.height) 104 | let bottomLeft = CGPoint(x: 0, y: bounds.height) 105 | 106 | path.addLine(to: bottomRight) 107 | path.addLine(to: bottomLeft) 108 | } 109 | 110 | fillShape.path = path.cgPath 111 | 112 | fillShape.fillColor = fillColor.cgColor 113 | 114 | CATransaction.commit() 115 | } 116 | } 117 | 118 | override func layoutSubviews() { 119 | super.layoutSubviews() 120 | drawLine() 121 | 122 | fillLineWithColor() 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /CollectionGraph/LineGraphLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineGraphLayout.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/29/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | public class LineGraphLayout: GraphLayout { 13 | 14 | var colors = [UIColor.gray] 15 | @IBInspectable var straightLines: Bool = false 16 | @IBInspectable var lineWidth: CGFloat = 1 17 | 18 | override public func prepare() { 19 | super.prepare() 20 | 21 | if let collectionView = collectionView { 22 | 23 | var tempAttributes = [UICollectionViewLayoutAttributes]() 24 | 25 | for sectionNumber in 0.. UICollectionViewLayoutAttributes? { 43 | 44 | let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) 45 | 46 | if elementKind == ReuseIDs.LineSupplementaryView.rawValue { 47 | let attributes = LineConnectorAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath) 48 | if let graphData = graphData { 49 | 50 | if indexPath.item < graphData.values[indexPath.section].count - 1 { 51 | 52 | let xOffset = xGraphPosition(indexPath: indexPath) + cellSize.width / 2 53 | let yOffset = yGraphPosition(indexPath: indexPath) 54 | 55 | let p1 = CGPoint(x: xOffset, 56 | y: yOffset) 57 | 58 | let nextIndex = IndexPath(item: indexPath.item + 1, section: indexPath.section) 59 | 60 | let xOffset2 = xGraphPosition(indexPath: nextIndex) + cellSize.width / 2 61 | let yOffset2 = yGraphPosition(indexPath: nextIndex) 62 | 63 | let p2 = CGPoint(x: xOffset2, 64 | y: yOffset2) 65 | 66 | // create a Rect between the two points 67 | let rect = CGRect(x: min(p1.x, p2.x), 68 | y: min(p1.y, p2.y), 69 | width: fabs(p1.x - p2.x), 70 | height: fabs(p1.y - p2.y)) 71 | 72 | attributes.frame = rect 73 | 74 | // decide which way the line should go 75 | attributes.lineStartsAtTop = 76 | (xOffset < xOffset2 && yOffset > yOffset2) || 77 | (xOffset > xOffset2 && yOffset < yOffset2) 78 | ? false : true 79 | 80 | attributes.lineWidth = lineWidth 81 | attributes.straightLines = straightLines 82 | 83 | return attributes 84 | } 85 | } 86 | } 87 | return attributes 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /CollectionGraph/RangeFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangeFinder.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/5/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RangeFinder { 12 | func xDataRange(graphData: [[GraphDatum]]) -> (min: CGFloat, max: CGFloat) 13 | func yDataRange(graphData: [[GraphDatum]], numberOfSteps steps: Int) -> (min: CGFloat, max: CGFloat) 14 | } 15 | 16 | extension RangeFinder { 17 | func xDataRange(graphData: [[GraphDatum]]) -> (min: CGFloat, max: CGFloat) { 18 | let xVals = graphData.flatMap { 19 | return $0.map { return $0.point.x } 20 | } 21 | 22 | if let min = xVals.min(), let max = xVals.max() { 23 | return (min, max) 24 | } 25 | 26 | return (0, 0) 27 | } 28 | 29 | func yDataRange(graphData: [[GraphDatum]], numberOfSteps steps: Int) -> (min: CGFloat, max: CGFloat) { 30 | let yVals = graphData.flatMap { 31 | return $0.map { return $0.point.y } 32 | } 33 | 34 | let maxY = yVals.max() ?? 0 35 | 36 | var remainder = maxY.truncatingRemainder(dividingBy: CGFloat(steps)) 37 | if remainder.isNaN { 38 | remainder = 0 39 | } 40 | 41 | var max: CGFloat = 0 42 | 43 | if remainder == 0 { 44 | max = maxY 45 | } else { 46 | max = maxY - remainder + CGFloat(steps) 47 | } 48 | 49 | let min = yVals.min() ?? 0 50 | 51 | return (min, max) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /CollectionGraph/Spinner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spinner.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/24/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Spinner: UIView { 12 | 13 | let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray) 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | stylize() 19 | 20 | addSpinner() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | func stylize() { 28 | 29 | backgroundColor = UIColor.white.withAlphaComponent(0.8) 30 | layer.cornerRadius = 3 31 | 32 | alpha = 0 33 | } 34 | 35 | func addConstraints() { 36 | translatesAutoresizingMaskIntoConstraints = false 37 | 38 | if let superview = superview { 39 | centerXAnchor.constraint(equalTo: superview.centerXAnchor).isActive = true 40 | centerYAnchor.constraint(equalTo: superview.centerYAnchor).isActive = true 41 | widthAnchor.constraint(equalToConstant: 35).isActive = true 42 | heightAnchor.constraint(equalToConstant: 35).isActive = true 43 | } 44 | } 45 | 46 | func addSpinner() { 47 | addSubview(spinner) 48 | 49 | spinner.translatesAutoresizingMaskIntoConstraints = false 50 | spinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 51 | spinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 52 | 53 | spinner.startAnimating() 54 | } 55 | 56 | func fadeOut() { 57 | UIView.animate(withDuration: 0.25, delay: 0, options: .beginFromCurrentState, animations: { 58 | self.alpha = 0 59 | }, completion: { _ in 60 | self.spinner.stopAnimating() 61 | }) 62 | } 63 | 64 | func fadeIn() { 65 | spinner.startAnimating() 66 | 67 | UIView.animate(withDuration: 0.25, delay: 0, options: .beginFromCurrentState, animations: { 68 | self.alpha = 1 69 | }, completion: nil) 70 | } 71 | 72 | override func didMoveToSuperview() { 73 | addConstraints() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CollectionGraph/XLabelViewAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLabelViewAttributes.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/14/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class XLabelViewAttributes: UICollectionViewLayoutAttributes { 12 | var text = "" 13 | 14 | override func copy(with zone: NSZone? = nil) -> Any { 15 | let copy = super.copy(with: zone) 16 | if let copy = copy as? XLabelViewAttributes { 17 | copy.text = text 18 | } 19 | return copy 20 | } 21 | 22 | override func isEqual(_ object: Any?) -> Bool { 23 | if let attributes = object as? XLabelViewAttributes { 24 | if attributes.text == text { 25 | return super.isEqual(object) 26 | } 27 | } 28 | return false 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /CollectionGraph/XibLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XibLoader.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 9/29/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class XibLoader: NSObject { 12 | 13 | class func viewFromXib(name: String, owner: NSObject) -> UIView? { 14 | 15 | let bundle = Bundle(for: self.classForCoder()) 16 | 17 | let nib = UINib(nibName: name, bundle: bundle) 18 | 19 | let nibView = nib.instantiate(withOwner: owner, options: nil)[0] as? UIView 20 | 21 | return nibView 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /CollectionGraph/YDividerLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YDividerLayoutAttributes.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/13/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class YDividerLayoutAttributes: UICollectionViewLayoutAttributes { 12 | var text: String = "" 13 | var inset: CGFloat = 0 14 | 15 | override func copy(with zone: NSZone? = nil) -> Any { 16 | let copy = super.copy(with: zone) 17 | if let copy = copy as? YDividerLayoutAttributes { 18 | copy.text = text 19 | copy.inset = inset 20 | } 21 | return copy 22 | } 23 | 24 | override func isEqual(_ object: Any?) -> Bool { 25 | if let attributes = object as? YDividerLayoutAttributes { 26 | if attributes.text == text && attributes.inset == inset { 27 | return super.isEqual(object) 28 | } 29 | } 30 | return false 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /CollectionGraph/YDividerLineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YDividerLineView.swift 3 | // Graph 4 | // 5 | // Created by Ben Lambert on 9/13/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class YDividerLineView: UICollectionReusableView { 12 | 13 | var line: CAShapeLayer = CAShapeLayer() 14 | var lineWidth: CGFloat = 1 15 | var lineColor: UIColor = UIColor.lightGray 16 | var textColor: UIColor = UIColor.darkText 17 | var inset: CGFloat = 0 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | setup() 22 | } 23 | 24 | required init?(coder aDecoder: NSCoder) { 25 | super.init(coder: aDecoder) 26 | setup() 27 | } 28 | 29 | func setup() { 30 | line.fillColor = UIColor.clear.cgColor 31 | line.strokeColor = lineColor.cgColor 32 | line.lineWidth = lineWidth 33 | layer.addSublayer(line) 34 | } 35 | 36 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 37 | if let attributes = layoutAttributes as? YDividerLayoutAttributes { 38 | inset = attributes.inset 39 | } 40 | } 41 | 42 | func drawLine() { 43 | let start = CGPoint(x: self.bounds.minX + inset, y: self.bounds.minY) 44 | let end = CGPoint(x: self.bounds.maxX, y: self.bounds.minY) 45 | 46 | let path = UIBezierPath() 47 | path.move(to: start) 48 | path.addLine(to: end) 49 | self.line.path = path.cgPath 50 | } 51 | 52 | override func layoutSubviews() { 53 | super.layoutSubviews() 54 | drawLine() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /CollectionGraphExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CollectionGraphExample 4 | // 5 | // Created by Chris Rittersdorf on 9/23/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // 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. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // 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. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // 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. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "DefaultUser.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "DefaultUser@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "DefaultUser@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser@2x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/DefaultUser.imageset/DefaultUser@3x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "user1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "user1@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "user1@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user1.imageset/user1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user1.imageset/user1.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user1.imageset/user1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user1.imageset/user1@2x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user1.imageset/user1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user1.imageset/user1@3x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "user2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "user2@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "user2@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user2.imageset/user2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user2.imageset/user2.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user2.imageset/user2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user2.imageset/user2@2x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user2.imageset/user2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user2.imageset/user2@3x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "user3.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "user3@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "user3@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user3.imageset/user3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user3.imageset/user3.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user3.imageset/user3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user3.imageset/user3@2x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user3.imageset/user3@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user3.imageset/user3@3x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "user4.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "user4@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "user4@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user4.imageset/user4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user4.imageset/user4.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user4.imageset/user4@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user4.imageset/user4@2x.png -------------------------------------------------------------------------------- /CollectionGraphExample/Assets.xcassets/user4.imageset/user4@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/CollectionGraphExample/Assets.xcassets/user4.imageset/user4@3x.png -------------------------------------------------------------------------------- /CollectionGraphExample/BarReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarReusableView.swift 3 | // [i]Kegerator 4 | // 5 | // Created by Ben Lambert on 12/21/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BarReusableView: UICollectionReusableView { 12 | 13 | let gradient = CAGradientLayer() 14 | 15 | var colors = [CGColor]() { 16 | didSet { 17 | gradient.colors = colors 18 | } 19 | } 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | 24 | self.backgroundColor = UIColor.clear 25 | 26 | let topColor = UIColor(red: 251.0/255.0, green: 246.0/255.0, blue: 0.0/255.0, alpha: 1).cgColor 27 | let bottomColor = UIColor(red: 24.0/255.0, green: 193.0/255.0, blue: 215.0/255.0, alpha: 1).cgColor 28 | 29 | gradient.colors = [topColor, bottomColor] 30 | gradient.locations = [0.0, 1.0] 31 | 32 | gradient.frame = bounds 33 | 34 | layer.addSublayer(gradient) 35 | } 36 | 37 | required init?(coder aDecoder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 42 | super.apply(layoutAttributes) 43 | 44 | // It might be important make adjustment to any CALayers added in case the frame changes. 45 | // If the gradient's frame isnt adjusted, it will still have its old size upon screen rotation. 46 | // Actions are disabled to prevent animation from taking place. 47 | 48 | CATransaction.begin() 49 | CATransaction.setDisableActions(true) 50 | 51 | gradient.frame = bounds 52 | 53 | CATransaction.commit() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /CollectionGraphExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /CollectionGraphExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /CollectionGraphExample/ColorHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorHelper.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/13/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ColorHelpers { 12 | func colorForSection(section: Int) -> UIColor 13 | } 14 | 15 | extension ColorHelpers { 16 | 17 | // http://www.colorhunt.co/c/43813 18 | 19 | func colorForSection(section: Int) -> UIColor { 20 | 21 | switch section { 22 | case 0: 23 | // blue 24 | return UIColor(red: 69.0 / 255.0, green: 193.0 / 255.0, blue: 201.0 / 255.0, alpha: 1) 25 | case 1: 26 | // pink 27 | return UIColor(red: 252.0 / 255.0, green: 81.0 / 255.0, blue: 133.0 / 255.0, alpha: 1) 28 | case 2: 29 | // yellow 30 | return UIColor(red: 252.0 / 255.0, green: 227.0 / 255.0, blue: 138.0 / 255.0, alpha: 1) 31 | case 3: 32 | // purple 33 | return UIColor(red: 187.0 / 255.0, green: 88.0 / 255.0, blue: 176.0 / 255.0, alpha: 1) 34 | case 4: 35 | // grey 36 | return UIColor(red: 125.0 / 255.0, green: 124.0 / 255.0, blue: 122.0 / 255.0, alpha: 1) 37 | default: 38 | return UIColor.darkGray 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /CollectionGraphExample/DataModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataModels.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/16/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CollectionGraph 11 | 12 | struct SmogData: GraphDatum { 13 | // x = dates 14 | // y = Particles Per Million 15 | var point: CGPoint 16 | } 17 | 18 | struct MilesPerDayDatum: GraphDatum { 19 | // x = dates ran 20 | // y = miles that date 21 | var point: CGPoint 22 | var name: String 23 | var imageName: String 24 | } 25 | 26 | struct TotalMilesRanDatum: GraphDatum { 27 | // x = Order Number 28 | // y = Total miles that date 29 | var point: CGPoint 30 | var name: String 31 | var imageName: String 32 | } 33 | -------------------------------------------------------------------------------- /CollectionGraphExample/DataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SmogService.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/16/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CollectionGraph 11 | 12 | class DataService { 13 | 14 | let parser: Parsable 15 | 16 | init(parser: Parsable) { 17 | self.parser = parser 18 | } 19 | 20 | func fetchData(fromFile file: String, completion: @escaping ([[GraphDatum]]) -> Void) { 21 | 22 | let json = self.serializeJsonFile(fromFile: file) 23 | 24 | // Simulate server wait time 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 26 | 27 | if let json = json { 28 | 29 | let datumArray = self.parser.parse(json: json) 30 | 31 | completion(datumArray) 32 | } 33 | }) 34 | } 35 | 36 | func serializeJsonFile(fromFile file: String) -> [[String: Any]]? { 37 | 38 | let path = Bundle.main.path(forResource: file, ofType: "json") 39 | 40 | if let path = path { 41 | 42 | do { 43 | let data = try Data(referencing: NSData(contentsOfFile: path)) 44 | 45 | let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]] 46 | 47 | if let json = json { 48 | return json 49 | } 50 | 51 | } catch { 52 | print("Missing the .json file") 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /CollectionGraphExample/FirstViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.swift 3 | // CollectionGraphExample 4 | // 5 | // Created by Chris Rittersdorf on 9/23/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionGraph 11 | 12 | class FirstViewController: UIViewController, CollectionGraphViewDelegate, CollectionGraphCellDelegate, CollectionGraphLineDelegate, CollectionGraphLineFillDelegate, CollectionGraphLabelsDelegate, CollectionGraphYDividerLineDelegate, ColorHelpers { 13 | 14 | @IBOutlet weak var graph: CollectionGraphView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | setGraphDelegates() 20 | 21 | generalGraphSetup() 22 | 23 | fetchGraphData() 24 | } 25 | 26 | func generalGraphSetup() { 27 | 28 | // Change the Font of the X and Y labels 29 | // graph.fontName = "chalkduster" 30 | 31 | graph.ySideBarView = SideBarReusableView() 32 | 33 | graph.ySteps = 5 34 | 35 | graph.textColor = UIColor(red: 171.0 / 255.0, green: 170.0 / 255.0, blue: 198.0 / 255.0, alpha: 1) 36 | 37 | // Provide a custom cell with a user image property 38 | graph.graphCell = PeopleCollectionViewCell() 39 | 40 | graph.yDividerLineColor = UIColor(red: 112.0 / 255.0, green: 110.0 / 255.0, blue: 171.0 / 255.0, alpha: 1) 41 | } 42 | 43 | func setGraphDelegates() { 44 | 45 | graph.collectionGraphCellDelegate = self 46 | graph.collectionGraphLineDelegate = self 47 | graph.collectionGraphLineFillDelegate = self 48 | graph.collectionGraphLabelsDelegate = self 49 | graph.collectionGraphYDividerLineDelegate = self 50 | } 51 | 52 | func fetchGraphData() { 53 | 54 | let parser = MilesPerDayParser() 55 | 56 | let service = DataService(parser: parser) 57 | 58 | service.fetchData(fromFile: "MilesPerDayData") { (data) in 59 | self.graph.graphData = data 60 | 61 | // each set of data has the same amount of data points so we'll just use the count from the first set 62 | self.graph.xSteps = data[0].count 63 | 64 | // Adjusts the width of the graph. The Cells are spaced out depending on this size 65 | self.graph.graphContentWidth = 800 66 | 67 | // self.graph.contentOffset = CGPoint(x: 30, y: self.graph.contentOffset.y) 68 | } 69 | } 70 | 71 | // MARK: - Graph Delegates 72 | 73 | // CollectionGraphViewDelegate 74 | 75 | func collectionGraph(updatedVisibleIndexPaths indexPaths: Set, sections: Set) { 76 | indexPaths.forEach { 77 | let data = self.graph.graphData?[$0.section][$0.item] 78 | print("Data: \(data)") 79 | } 80 | } 81 | 82 | // CollectionGraphCellDelegate 83 | 84 | func collectionGraph(cell: UICollectionViewCell, forData data: GraphDatum, atIndexPath indexPath: IndexPath) { 85 | 86 | if let peopleCell = cell as? PeopleCollectionViewCell { 87 | 88 | if let data = data as? MilesPerDayDatum { 89 | 90 | let imageName = data.imageName 91 | peopleCell.image = UIImage(named: imageName) ?? UIImage() 92 | } 93 | 94 | } 95 | 96 | cell.backgroundColor = UIColor.white 97 | cell.layer.borderWidth = 1 98 | cell.layer.borderColor = colorForSection(section: indexPath.section).cgColor 99 | cell.layer.cornerRadius = cell.frame.width / 2 100 | } 101 | 102 | func collectionGraph(sizeForGraphCellWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> CGSize { 103 | return CGSize(width: 20, height: 20) 104 | } 105 | 106 | // CollectionGraphLineDelegate 107 | 108 | func collectionGraph(connectorLine: GraphLineShapeLayer, withData data: GraphDatum, atIndexPath indexPath: IndexPath) { 109 | // connectorLine.lineWidth = 2 110 | // connectorLine.lineDashPattern = [4, 2] 111 | // connectorLine.straightLines = true 112 | // connectorLine.lineCap = kCALineCapRound 113 | connectorLine.strokeColor = colorForSection(section: indexPath.section).cgColor 114 | } 115 | 116 | // CollectionGraphLineFillDelegate 117 | 118 | func collectionGraph(fillColorForGraphSectionWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> UIColor { 119 | return colorForSection(section: indexPath.section).withAlphaComponent(0.1) 120 | } 121 | 122 | // CollectionGraphLabelsDelegate 123 | 124 | func collectionGraph(textForXLabelWithCurrentText currentText: String, item: Int) -> String { 125 | 126 | let timeInterval = Double(currentText) 127 | 128 | if let timeInterval = timeInterval { 129 | let date = Date(timeIntervalSince1970: timeInterval) 130 | 131 | let customFormat = DateFormatter.dateFormat(fromTemplate: "MMM d hh:mm", options: 0, locale: Locale(identifier: "us"))! 132 | 133 | let formatter = DateFormatter() 134 | formatter.timeZone = TimeZone(abbreviation: "UTC") 135 | formatter.dateFormat = customFormat 136 | 137 | return formatter.string(from: date) 138 | } 139 | 140 | //return "•" 141 | return currentText 142 | } 143 | 144 | // CollectionGraphYDividerLineDelegate 145 | 146 | func collectionGraph(yDividerLine: CAShapeLayer, atItem: Int) { 147 | yDividerLine.lineDashPattern = [1, 8] 148 | 149 | // this will override the graphs yDividerLineColor property 150 | // yDividerLine.strokeColor = UIColor.white.cgColor 151 | 152 | // yDividerLine.lineWidth = 2 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /CollectionGraphExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /CollectionGraphExample/MilesPerDayData.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Janice Anderson", 3 | "date": [ 4 | "2017-01-01T00:00:00Z", 5 | "2017-01-02T00:00:00Z", 6 | "2017-01-03T00:00:00Z", 7 | "2017-01-04T00:00:00Z", 8 | "2017-01-05T00:00:00Z", 9 | "2017-01-06T00:00:00Z", 10 | "2017-01-07T00:00:00Z" 11 | ], 12 | "miles": [ 13 | 10, 14 | 8, 15 | 3, 16 | 2, 17 | 8, 18 | 7, 19 | 3 20 | ] 21 | }, { 22 | "name": "Scott Alvarez", 23 | "date": [ 24 | "2017-01-01T00:00:00Z", 25 | "2017-01-02T00:00:00Z", 26 | "2017-01-03T00:00:00Z", 27 | "2017-01-04T00:00:00Z", 28 | "2017-01-05T00:00:00Z", 29 | "2017-01-06T00:00:00Z", 30 | "2017-01-07T00:00:00Z" 31 | ], 32 | "miles": [ 33 | 8, 34 | 2, 35 | 4, 36 | 7, 37 | 2, 38 | 3, 39 | 4 40 | ] 41 | }, { 42 | "name": "Beverly Baker", 43 | "date": [ 44 | "2017-01-01T00:00:00Z", 45 | "2017-01-02T00:00:00Z", 46 | "2017-01-03T00:00:00Z", 47 | "2017-01-04T00:00:00Z", 48 | "2017-01-05T00:00:00Z", 49 | "2017-01-06T00:00:00Z", 50 | "2017-01-07T00:00:00Z" 51 | ], 52 | "miles": [ 53 | 0, 54 | 9, 55 | 10, 56 | 9, 57 | 4, 58 | 10, 59 | 2 60 | ] 61 | }, { 62 | "name": "Ron Dingle", 63 | "date": [ 64 | "2017-01-01T00:00:00Z", 65 | "2017-01-02T00:00:00Z", 66 | "2017-01-03T00:00:00Z", 67 | "2017-01-04T00:00:00Z", 68 | "2017-01-05T00:00:00Z", 69 | "2017-01-06T00:00:00Z", 70 | "2017-01-07T00:00:00Z" 71 | ], 72 | "miles": [ 73 | 9, 74 | 7, 75 | 9, 76 | 8, 77 | 8, 78 | 5, 79 | 10 80 | ] 81 | }] 82 | -------------------------------------------------------------------------------- /CollectionGraphExample/MyGraphCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyGraphCell.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 10/17/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionGraph 11 | 12 | class MyGraphCell: UICollectionViewCell { 13 | 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | backgroundColor = UIColor.red 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /CollectionGraphExample/Parsers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parsers.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/16/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CollectionGraph 11 | 12 | protocol Parsable { 13 | func parse(json: [[String: Any]]) -> [[GraphDatum]] 14 | } 15 | 16 | struct MilesPerDayParser: Parsable { 17 | 18 | func parse(json: [[String: Any]]) -> [[GraphDatum]] { 19 | 20 | var data = [[MilesPerDayDatum]]() 21 | 22 | for sectionNumber in 0.. [[GraphDatum]] { 68 | 69 | var data = [[TotalMilesRanDatum]]() 70 | 71 | var sectionData = [TotalMilesRanDatum]() 72 | 73 | for sectionNumber in 0.. [[GraphDatum]] { 102 | 103 | var data = [[SmogData]]() 104 | var smogData = [SmogData]() 105 | 106 | for number in 0.. CGSize { 72 | return CGSize(width: 3, height: 3) 73 | } 74 | 75 | // CollectionGraphLineDelegate 76 | 77 | func collectionGraph(connectorLine: GraphLineShapeLayer, withData data: GraphDatum, atIndexPath indexPath: IndexPath) { 78 | connectorLine.strokeColor = colorForSection(section: indexPath.section).cgColor 79 | } 80 | 81 | // CollectionGraphLabelsDelegate 82 | 83 | func collectionGraph(textForXLabelWithCurrentText currentText: String, item: Int) -> String { 84 | 85 | let timeInterval = Double(currentText) 86 | 87 | if let timeInterval = timeInterval { 88 | let date = Date(timeIntervalSince1970: timeInterval) 89 | 90 | let customFormat = DateFormatter.dateFormat(fromTemplate: "MMM d hh:mm", options: 0, locale: Locale(identifier: "us"))! 91 | 92 | let formatter = DateFormatter() 93 | formatter.timeZone = TimeZone(abbreviation: "UTC") 94 | formatter.dateFormat = customFormat 95 | 96 | return formatter.string(from: date) 97 | } 98 | 99 | return currentText 100 | } 101 | 102 | // CollectionGraphYDividerLineDelegate 103 | 104 | func collectionGraph(yDividerLine: CAShapeLayer, atItem: Int) { 105 | yDividerLine.lineDashPattern = [1, 8] 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /CollectionGraphExample/SideBarReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SideBarReusableView.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 12/8/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SideBarReusableView: UICollectionReusableView { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | backgroundColor = UIColor(red: 60.0 / 255.0, green: 59.0 / 255.0, blue: 92.0 / 255.0, alpha: 0.9) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CollectionGraphExample/ThirdViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThirdViewController.swift 3 | // CollectionGraph 4 | // 5 | // Created by Ben Lambert on 1/19/17. 6 | // Copyright © 2017 Collective Idea. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CollectionGraph 11 | 12 | class ThirdViewController: UIViewController, CollectionGraphCellDelegate, CollectionGraphLabelsDelegate, CollectionGraphBarDelegate, CollectionGraphYDividerLineDelegate, ColorHelpers { 13 | 14 | @IBOutlet weak var graph: CollectionGraphView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | setGraphDelegates() 20 | 21 | generalGraphSetup() 22 | 23 | fetchGraphData() 24 | } 25 | 26 | func generalGraphSetup() { 27 | 28 | graph.ySideBarView = SideBarReusableView() 29 | 30 | graph.ySteps = 10 31 | 32 | graph.textColor = UIColor(red: 171.0 / 255.0, green: 170.0 / 255.0, blue: 198.0 / 255.0, alpha: 1) 33 | 34 | // Provide a custom cell with a user image property 35 | graph.graphCell = PeopleCollectionViewCell() 36 | 37 | // Provide a custom bar cell that has a gradient layer 38 | graph.barView = BarReusableView() 39 | 40 | graph.yDividerLineColor = UIColor(red: 112.0 / 255.0, green: 110.0 / 255.0, blue: 171.0 / 255.0, alpha: 1) 41 | } 42 | 43 | func setGraphDelegates() { 44 | graph.collectionGraphCellDelegate = self 45 | graph.collectionGraphLabelsDelegate = self 46 | graph.collectionGraphBarDelegate = self 47 | graph.collectionGraphYDividerLineDelegate = self 48 | } 49 | 50 | func fetchGraphData() { 51 | 52 | let parser = TotalMilesRanParser() 53 | 54 | let service = DataService(parser: parser) 55 | 56 | service.fetchData(fromFile: "TotalMilesRan") { (data) in 57 | self.graph.graphData = data 58 | 59 | // each set of data has the same amount of data points so we'll just use the count from the first set 60 | self.graph.xSteps = data[0].count 61 | 62 | // Adjusts the width of the graph. The Cells are spaced out depending on this size 63 | let spacing = 80 64 | self.graph.graphContentWidth = CGFloat(self.graph.xSteps) * CGFloat(spacing) 65 | } 66 | 67 | } 68 | 69 | // MARK: - Graph Delegates 70 | 71 | // CollectionGraphCellDelegate 72 | 73 | func collectionGraph(cell: UICollectionViewCell, forData data: GraphDatum, atIndexPath indexPath: IndexPath) { 74 | 75 | if let peopleCell = cell as? PeopleCollectionViewCell { 76 | 77 | if let data = data as? TotalMilesRanDatum { 78 | 79 | let imageName = data.imageName 80 | peopleCell.image = UIImage(named: imageName) ?? UIImage(named: "DefaultUser")! 81 | } 82 | } 83 | 84 | cell.backgroundColor = UIColor.white 85 | cell.layer.borderWidth = 2 86 | 87 | let colorNumber = indexPath.item % 4 88 | 89 | cell.layer.borderColor = colorForSection(section: colorNumber).cgColor 90 | cell.layer.cornerRadius = 5 91 | } 92 | 93 | func collectionGraph(sizeForGraphCellWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> CGSize { 94 | return CGSize(width: 30, height: 30) 95 | } 96 | 97 | // CollectionGraphLabelsDelegate 98 | 99 | func collectionGraph(textForXLabelWithCurrentText currentText: String, item: Int) -> String { 100 | 101 | if let totalMilesData = graph.graphData?[0] as? [TotalMilesRanDatum] { 102 | let personName = totalMilesData[item].name 103 | 104 | return personName 105 | } 106 | 107 | return currentText 108 | } 109 | 110 | // CollectionGraphBarDelegate 111 | 112 | func collectionGraph(widthForBarViewWithData data: GraphDatum, atIndexPath indexPath: IndexPath) -> CGFloat { 113 | return 7 114 | } 115 | 116 | func collectionGraph(barView: UICollectionReusableView, withData data: GraphDatum, atIndexPath indexPath: IndexPath) { 117 | if let barView = barView as? BarReusableView { 118 | 119 | let colorNumber = indexPath.item % 4 120 | 121 | let color1 = colorForSection(section: colorNumber).cgColor 122 | let color2 = UIColor(red: 112.0 / 255.0, green: 110.0 / 255.0, blue: 171.0 / 255.0, alpha: 1).cgColor 123 | barView.colors = [color1, color2] 124 | } 125 | } 126 | 127 | // CollectionGraphYDividerLineDelegate 128 | 129 | func collectionGraph(yDividerLine: CAShapeLayer, atItem item: Int) { 130 | 131 | let length = NSNumber(integerLiteral: item) 132 | 133 | yDividerLine.lineDashPattern = [length, 8] 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /CollectionGraphExample/TotalMilesRan.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Anna Peterson", 3 | "date": [ 4 | "2017-01-06T23:13:45Z", 5 | "2017-01-02T05:41:57Z", 6 | "2017-01-06T04:36:55Z", 7 | "2017-01-02T22:39:43Z", 8 | "2017-01-05T06:51:36Z", 9 | "2017-01-02T16:40:35Z", 10 | "2017-01-06T02:50:41Z" 11 | ], 12 | "miles": [ 13 | 7, 14 | 10, 15 | 7, 16 | 10, 17 | 2, 18 | 5, 19 | 8 20 | ] 21 | }, { 22 | "name": "Howard Turner", 23 | "date": [ 24 | "2017-01-06T03:03:27Z", 25 | "2017-01-04T17:13:23Z", 26 | "2017-01-02T08:24:59Z", 27 | "2017-01-04T14:58:14Z", 28 | "2017-01-06T09:58:19Z", 29 | "2017-01-05T02:52:58Z", 30 | "2017-01-01T02:15:57Z" 31 | ], 32 | "miles": [ 33 | 3, 34 | 7, 35 | 6, 36 | 7, 37 | 2, 38 | 6, 39 | 4 40 | ] 41 | }, { 42 | "name": "Julie Day", 43 | "date": [ 44 | "2017-01-02T21:32:21Z", 45 | "2017-01-05T20:30:42Z", 46 | "2017-01-02T19:07:25Z", 47 | "2017-01-06T12:38:48Z", 48 | "2017-01-05T23:53:29Z", 49 | "2017-01-04T03:32:50Z", 50 | "2017-01-01T06:29:36Z" 51 | ], 52 | "miles": [ 53 | 2, 54 | 8, 55 | 9, 56 | 2, 57 | 10, 58 | 5, 59 | 3 60 | ] 61 | }, { 62 | "name": "Louise Warren", 63 | "date": [ 64 | "2017-01-05T07:40:20Z", 65 | "2017-01-03T04:51:32Z", 66 | "2017-01-03T05:00:55Z", 67 | "2017-01-03T08:01:56Z", 68 | "2017-01-01T01:09:19Z", 69 | "2017-01-04T05:31:59Z", 70 | "2017-01-04T22:14:11Z" 71 | ], 72 | "miles": [ 73 | 4, 74 | 10, 75 | 8, 76 | 4, 77 | 2, 78 | 9, 79 | 2 80 | ] 81 | }, { 82 | "name": "Robert Garza", 83 | "date": [ 84 | "2017-01-02T09:49:02Z", 85 | "2017-01-05T22:32:37Z", 86 | "2017-01-03T22:48:17Z", 87 | "2017-01-05T02:57:32Z", 88 | "2017-01-04T13:10:56Z", 89 | "2017-01-04T12:28:47Z", 90 | "2017-01-05T22:56:56Z" 91 | ], 92 | "miles": [ 93 | 5, 94 | 2, 95 | 3, 96 | 5, 97 | 9, 98 | 10, 99 | 10 100 | ] 101 | }, { 102 | "name": "Jeffrey Gordon", 103 | "date": [ 104 | "2017-01-01T20:37:52Z", 105 | "2017-01-04T23:06:49Z", 106 | "2017-01-03T14:40:59Z", 107 | "2017-01-03T08:40:04Z", 108 | "2017-01-04T02:14:52Z", 109 | "2017-01-05T09:37:46Z", 110 | "2017-01-02T18:11:45Z" 111 | ], 112 | "miles": [ 113 | 4, 114 | 8, 115 | 2, 116 | 3, 117 | 7, 118 | 4, 119 | 5 120 | ] 121 | }, { 122 | "name": "Russell Crawford", 123 | "date": [ 124 | "2017-01-05T09:28:19Z", 125 | "2017-01-03T21:40:10Z", 126 | "2017-01-06T01:09:47Z", 127 | "2017-01-01T09:08:34Z", 128 | "2017-01-03T00:18:43Z", 129 | "2017-01-06T23:29:17Z", 130 | "2017-01-02T04:43:39Z" 131 | ], 132 | "miles": [ 133 | 10, 134 | 10, 135 | 9, 136 | 9, 137 | 10, 138 | 4, 139 | 1 140 | ] 141 | }, { 142 | "name": "Jimmy Thomas", 143 | "date": [ 144 | "2017-01-02T11:23:59Z", 145 | "2017-01-01T06:46:13Z", 146 | "2017-01-06T19:31:07Z", 147 | "2017-01-04T12:13:42Z", 148 | "2017-01-02T18:59:37Z", 149 | "2017-01-03T18:34:20Z", 150 | "2017-01-03T18:18:10Z" 151 | ], 152 | "miles": [ 153 | 6, 154 | 10, 155 | 5, 156 | 10, 157 | 8, 158 | 8, 159 | 4 160 | ] 161 | }, { 162 | "name": "Margaret Burton", 163 | "date": [ 164 | "2017-01-06T21:58:49Z", 165 | "2017-01-05T19:29:57Z", 166 | "2017-01-02T02:51:56Z", 167 | "2017-01-06T14:32:23Z", 168 | "2017-01-04T14:59:55Z", 169 | "2017-01-01T16:44:39Z", 170 | "2017-01-04T00:12:24Z" 171 | ], 172 | "miles": [ 173 | 6, 174 | 9, 175 | 1, 176 | 5, 177 | 6, 178 | 5, 179 | 2 180 | ] 181 | }, { 182 | "name": "Todd Hicks", 183 | "date": [ 184 | "2017-01-06T08:29:39Z", 185 | "2017-01-01T17:52:23Z", 186 | "2017-01-03T15:25:17Z", 187 | "2017-01-05T08:34:14Z", 188 | "2017-01-03T01:14:41Z", 189 | "2017-01-03T08:50:28Z", 190 | "2017-01-06T13:26:38Z" 191 | ], 192 | "miles": [ 193 | 6, 194 | 2, 195 | 9, 196 | 2, 197 | 4, 198 | 1, 199 | 5 200 | ] 201 | }, { 202 | "name": "Brian Robertson", 203 | "date": [ 204 | "2017-01-03T16:29:08Z", 205 | "2017-01-06T15:38:01Z", 206 | "2017-01-05T19:05:43Z", 207 | "2017-01-01T12:17:59Z", 208 | "2017-01-06T04:31:46Z", 209 | "2017-01-04T02:16:39Z", 210 | "2017-01-05T00:44:30Z" 211 | ], 212 | "miles": [ 213 | 5, 214 | 2, 215 | 8, 216 | 6, 217 | 8, 218 | 4, 219 | 4 220 | ] 221 | }, { 222 | "name": "Henry Johnston", 223 | "date": [ 224 | "2017-01-05T07:55:48Z", 225 | "2017-01-03T18:18:24Z", 226 | "2017-01-04T16:20:48Z", 227 | "2017-01-04T07:18:41Z", 228 | "2017-01-03T15:10:46Z", 229 | "2017-01-03T09:01:58Z", 230 | "2017-01-02T09:39:11Z" 231 | ], 232 | "miles": [ 233 | 7, 234 | 4, 235 | 3, 236 | 4, 237 | 5, 238 | 7, 239 | 9 240 | ] 241 | }, { 242 | "name": "Edward Bishop", 243 | "date": [ 244 | "2017-01-03T16:56:20Z", 245 | "2017-01-02T07:01:52Z", 246 | "2017-01-01T12:08:17Z", 247 | "2017-01-03T01:54:00Z", 248 | "2017-01-06T05:37:50Z", 249 | "2017-01-06T04:15:17Z", 250 | "2017-01-02T04:13:48Z" 251 | ], 252 | "miles": [ 253 | 6, 254 | 8, 255 | 4, 256 | 7, 257 | 7, 258 | 10, 259 | 4 260 | ] 261 | }, { 262 | "name": "Evelyn Austin", 263 | "date": [ 264 | "2017-01-03T18:57:07Z", 265 | "2017-01-01T17:36:12Z", 266 | "2017-01-02T01:20:55Z", 267 | "2017-01-03T00:39:44Z", 268 | "2017-01-06T09:28:20Z", 269 | "2017-01-01T22:01:09Z", 270 | "2017-01-01T02:05:40Z" 271 | ], 272 | "miles": [ 273 | 6, 274 | 1, 275 | 2, 276 | 8, 277 | 1, 278 | 10, 279 | 2 280 | ] 281 | }, { 282 | "name": "Howard Cruz", 283 | "date": [ 284 | "2017-01-01T05:11:36Z", 285 | "2017-01-06T07:28:06Z", 286 | "2017-01-03T13:03:52Z", 287 | "2017-01-06T07:03:00Z", 288 | "2017-01-03T21:00:52Z", 289 | "2017-01-03T08:39:42Z", 290 | "2017-01-01T03:57:53Z" 291 | ], 292 | "miles": [ 293 | 1, 294 | 3, 295 | 2, 296 | 10, 297 | 5, 298 | 1, 299 | 10 300 | ] 301 | }] 302 | -------------------------------------------------------------------------------- /CollectionGraphTests/CollectionGraphTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionGraphTests.swift 3 | // CollectionGraphTests 4 | // 5 | // Created by Ben Lambert on 9/23/16. 6 | // Copyright © 2016 Collective Idea. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CollectionGraph 11 | 12 | class CollectionGraphTests: 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 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /CollectionGraphTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CollectionGraph ![Travis CI](https://api.travis-ci.org/collectiveidea/CollectionGraph.svg) 2 | 3 | A flexible and customizable scatter, bar, and line graphing tool for iOS. Backed by UICollectionViews. 4 | 5 | ## Features 6 | * Plot numerous collections of data on one graph. 7 | * Supply custom views for data points, and other elements. 8 | * Callbacks to customize specific elements. 9 | * Layout using storyboards (However, `IBDesignables` do not carry over in linked frameworks) 10 | 11 | ## Installation 12 | 13 | ### Carthage 14 | To integrate CollectionGraph into your Xcode project using Carthage, specify it in your `Cartfile`: 15 |

16 | ``` 17 | github "collectiveidea/CollectionGraph" 18 | ``` 19 | 20 | ## Usage 21 | * Check out the CollectionGraphExample target in the project to see some examples 22 | 23 | ### Plotting Points 24 | 25 | /////////////// 26 | example image 27 | ////////////// 28 | 29 | To plot a graph of simple points, assign the CollectionGraph's `graphData` property. 30 | 31 | ```Swift 32 | import CollectionGraph 33 | 34 | var graphData: [[GraphDatum]]? 35 | ``` 36 | 37 | Each array of `[GraphDatum]` represents a new "section" in the graph. Each section should represent a different set of data. 38 | 39 | For example:
40 | 41 | ```swift 42 | 43 | func losAngelesGraphData() -> [GraphhDatum] { ... } 44 | func newYorkGraphData() -> [GraphhDatum] { ... } 45 | func grandRapidsGraphData() -> [GraphhDatum] { ... } 46 | 47 | let data = [losAngelesGraphData(), newYorkGraphData(), grandRapidsGraphData()] 48 | ``` 49 | Three different sets of points (each city) will be plotted on the same graph. 50 | 51 | `GraphDatum` is a protocol that requires a CGPoint. 52 | You can add other properties you may want to surface during the Callbacks to better identify the data. 53 | 54 | ##### Example 55 | ```swift 56 | struct Data: GraphDatum { 57 | var point: CGPoint 58 | var id: String 59 | } 60 | ``` 61 | ## Customization 62 | 63 | ### Graph Cells 64 | 65 | #### Custom graphCells 66 | If you want a custom graphCell instead of the default square, you can set the `graphCell` property. 67 | ```swift 68 | var graphCell: UICollectionViewCell? 69 | ``` 70 | Using the provided Delegate Callbacks, you can set your `graphCell` properties for specific `graphData` points 71 | 72 | This is great for setting images or labels that change depending on the data. 73 | 74 | ![Gif of Miles Ran](/ReadmeImages/MilesRanGraph.gif) 75 | 76 | #### Cell Delegate Callbacks 77 | 78 | ```swift 79 | protocol CollectionGraphCellDelegate: class { 80 | 81 | func collectionGraph( 82 | cell: UICollectionViewCell, 83 | forData data: GraphDatum, 84 | atSection section: Int 85 | ) 86 | 87 | func collectionGraph( 88 | sizeForGraphCellWithData data: GraphDatum, 89 | inSection section: Int 90 | ) -> CGSize 91 | } 92 | ``` 93 | 94 | 1) Properties Callback - Set the visual treatment on a cell.
95 | `func collectionGraph(cell: UICollectionViewCell, forData data: GraphDatum, atSection section: Int)` 96 | 97 | Example properties: 98 | * backgroundColor 99 | * layer.cornerRadius 100 | * Any custom properties you added in a subclass 101 | * etc... 102 | 103 | 104 | ##### Example usage: 105 | ```swift 106 | collectionGraph.collectionGraphCellDelegate = self 107 | ``` 108 | ```swift 109 | func collectionGraph(cell: UICollectionViewCell, forData data: GraphDatum, atSection section: Int) { 110 | cell.backgroundColor = UIColor.darkText 111 | cell.layer.cornerRadius = cell.frame.width / 2 112 | 113 | // custom graphCell 114 | if let myCell = cell as? myCell { 115 | myCell.image = UIImage(named: "donut.png") 116 | } 117 | } 118 | ``` 119 | 120 | 2) Size Callback 121 | 122 | ```swift 123 | func collectionGraph(sizeForGraphCellWithData data: GraphDatum, inSection section: Int) -> CGSize { 124 | if section == 0 { 125 | return CGSize(width: 3, height: 3) 126 | } 127 | 128 | return CGSize(width: 8, height: 8) 129 | } 130 | ``` 131 | 132 | ### Bar Graph Views 133 | Used for bar graphs. These are the views that start at the bottom of the graph and extend up to the graphCell. Can be used with line graphs as well. 134 | 135 | #### Custom barViews 136 | 137 | If you want a custom barView instead of the default view, you can set the `barView` property 138 | 139 | ```swift 140 | var barView: UICollectionReusableView? 141 | ``` 142 | ![Gif of bar Graph](/ReadmeImages/TotalMilesRanGraph.gif) 143 | 144 | #### BarView Delegate Callbacks 145 | 146 | ```swift 147 | protocol CollectionGraphBarDelegate: class { 148 | 149 | func collectionGraph( 150 | barView: UICollectionReusableView, 151 | withData data: GraphDatum, 152 | inSection section: Int 153 | ) 154 | 155 | func collectionGraph( 156 | widthForBarViewWithData data: GraphDatum, 157 | inSection section: Int 158 | ) -> CGFloat 159 | } 160 | ``` 161 | 162 | 1) Propery Callback - Set the visual treatment on a bar view.
163 | `func collectionGraph(barView: UICollectionReusableView, withData data: GraphDatum, inSection section: Int)` 164 | 165 | Example properties: 166 | * backgroundColor 167 | * Any custom properties you added in a subclass 168 | * etc... 169 | 170 | 171 | ##### Example usage: 172 | ```swift 173 | collectionGraph.collectionGraphBarDelegate = self 174 | ``` 175 | ```swift 176 | func collectionGraph(barView: UICollectionReusableView, withData data: GraphDatum, inSection section: Int) { 177 | barView.backgroundColor = UIColor.lightGray 178 | } 179 | ``` 180 | 181 | ### Line Graph Connector Lines 182 | 183 | Used for line graphs. These are the lines that connect the `graphCells`. Can be used with bar graphs as well. 184 | 185 | 186 | ![Gif of line Graph](/ReadmeImages/LotsOfDataGraph.gif) 187 | 188 | #### Customizing a Connector Line with Delegate Callbacks 189 | To display connector lines, you need to use the delegate callback and set the line's properties. 190 | 191 | ```swift 192 | protocol CollectionGraphLineDelegate: class { 193 | 194 | func collectionGraph( 195 | connectorLine: GraphLineShapeLayer, 196 | withData data: GraphDatum, 197 | inSection section: Int) 198 | } 199 | ``` 200 | 201 | The `connectorLine` property provides a `GraphLineShapeLayer` which is a subclass of `CAShapeLayer`, and provides an additional property of `straightLines`. 202 | 203 | ```swift 204 | class GraphLineShapeLayer: CAShapeLayer { 205 | public var straightLines: Bool = false 206 | } 207 | ``` 208 | 209 | You can set all the visual properties of a CAShapeLayer to achieve the look you want. 210 | 211 | Example properties: 212 | * lineWidth 213 | * straightLines 214 | * lineDashPattern 215 | * strokeColor 216 | 217 | #### Example usage: 218 | ```swift 219 | collectionGraph.collectionGraphLineDelegate = self 220 | ``` 221 | ```swift 222 | func collectionGraph(connectorLine: GraphLineShapeLayer, withData data: GraphDatum, inSection section: Int) { 223 | connectorLine.lineWidth = 2 224 | connectorLine.lineDashPattern = [4, 2] 225 | connectorLine.straightLines = true 226 | connectorLine.lineCap = kCALineCapRound 227 | connectorLine.strokeColor = UIColor.darkGray.cgColor 228 | } 229 | ``` 230 | 231 | ### X Labels 232 | To change the text displayed along the X axis, use `CollectionGraphLabelsDelegate` 233 | 234 | ```swift 235 | public protocol CollectionGraphLabelsDelegate: class { 236 | 237 | func collectionGraph( 238 | textForXLabelWithCurrentText currentText: String, 239 | inSection section: Int 240 | ) -> String 241 | } 242 | ``` 243 | 244 | #### Example usage: 245 | 246 | ```swift 247 | collectionGraph.collectionGraphLabelsDelegate = self 248 | ``` 249 | ```swift 250 | func collectionGraph(textForXLabelWithCurrentText currentText: String, inSection section: Int) -> String { 251 | return "•" 252 | } 253 | ``` 254 | 255 | ### Horizontal Y Divider Lines 256 | These are the horizontal lines next to the Y labels that span the length of the graph. 257 | 258 | ```swift 259 | public protocol CollectionGraphYDividerLineDelegate: class { 260 | 261 | func collectionGraph(yDividerLine: CAShapeLayer) 262 | } 263 | ``` 264 | 265 | #### Example usage: 266 | 267 | ```swift 268 | collectionGraph.collectionGraphYDividerLineDelegate = self 269 | ``` 270 | ```swift 271 | func collectionGraph(yDividerLine: CAShapeLayer) { 272 | yDividerLine.lineDashPattern = [1, 8] 273 | yDividerLine.lineWidth = 2 274 | } 275 | ``` 276 | 277 | ## Settable Properties 278 | 279 | ### Graph width and Insets 280 | Width:
281 | By default the graphCells are plotted within the width of the CollectionGraph.
282 | By setting `graphContentWidth` you can extend the content width to allow scrolling. 283 | ```swift 284 | var graphContentWidth: CGFloat 285 | ``` 286 | 287 | Insets:
288 | Adjust space along the edges of the CollectionGraph.
289 | The left and bottom space is used to layout the labels. 290 | ```swift 291 | var topInset: CGFloat 292 | 293 | var leftInset: CGFloat 294 | 295 | var bottomInset: CGFloat 296 | 297 | var rightInset: CGFloat 298 | ``` 299 | ### Side Bar View 300 | A view that lies behind the y axis labels and above the plotted graph. Useful for covering the graph when it scrolls behind the y labels. 301 | ```swift 302 | var ySideBarView: UICollectionReusableView? 303 | ``` 304 | **Note!** 305 | You need to provide a subclass of UICollectionReusableView and override `init(frame: CGRect)`. 306 | Inside the init block is where you set your customizations 307 | 308 | Initializiing a UICollectionReusableView() and then settings its background color will not work. 309 | 310 | #### Example: 311 | ```swift 312 | class MySideBarClass: UICollectionReusableView { 313 | 314 | override init(frame: CGRect) { 315 | super.init(frame: frame) 316 | backgroundColor = UIColor.red 317 | } 318 | 319 | } 320 | ``` 321 | ### Y Divider Lines 322 | Set the color of the horizontal lines that go across the graph along the Y axis. 323 | ```swift 324 | var yDividerLineColor: UIColor 325 | ``` 326 | 327 | ### Number of labels along the X and Y axes 328 | X: 329 | ```swift 330 | var xSteps: Int 331 | ``` 332 | 333 | Y: 334 | ```swift 335 | var ySteps: Int 336 | ``` 337 | 338 | ### Text for X and Y labels 339 | 340 | Color: 341 | ```swift 342 | var textColor: UIColor 343 | ``` 344 | 345 | Size: 346 | ```swift 347 | var textSize: CGFloat 348 | ``` 349 | 350 | Font Name: 351 | ```swift 352 | var fontName: String? 353 | ``` 354 | 355 | ### Graph Offset 356 | 357 | Get and Set the current offset of the CollectionGraph. 358 | ```swift 359 | var contentOffset: CGPoint 360 | ``` 361 | 362 | Scroll to a specific data point. 363 | ```swift 364 | func scrollToDataPoint( 365 | graphDatum: GraphDatum, 366 | withAnimation animation: Bool, 367 | andScrollPosition scrollPosition: UICollectionViewScrollPosition) 368 | ``` 369 | 370 | ## Develoment 371 | 372 | Install SwiftLint for static analysis: 373 | 374 | ```bash 375 | $ brew install swiftlint 376 | ``` 377 | -------------------------------------------------------------------------------- /ReadmeImages/LotsOfDataGraph.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/ReadmeImages/LotsOfDataGraph.gif -------------------------------------------------------------------------------- /ReadmeImages/MilesRanGraph.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/ReadmeImages/MilesRanGraph.gif -------------------------------------------------------------------------------- /ReadmeImages/TotalMilesRanGraph.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collectiveidea/CollectionGraph/6a0d675fc0bd8140e7cb9191b71eb145ff585bf6/ReadmeImages/TotalMilesRanGraph.gif --------------------------------------------------------------------------------