├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .swiftlint.yml ├── .xcovignore ├── Brewfile ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── MobileAnalyticsChartSwift.podspec ├── MobileAnalyticsChartSwift.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── MobileAnalyticsChartSwift.xcscheme │ └── MobileAnalyticsChartSwiftExamplePods.xcscheme ├── MobileAnalyticsChartSwift.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── MobileAnalyticsChartSwift ├── Calculator │ ├── AnimationFunction.swift │ ├── Calculator.swift │ ├── CalculatorConfiguration.swift │ ├── CalculatorImpl.swift │ └── Models │ │ ├── BoundaryValue.swift │ │ ├── ChartData.swift │ │ ├── ChartDefinitionValue.swift │ │ ├── ChartXAxisValue.swift │ │ ├── RangeValue.swift │ │ └── StaticValueState.swift ├── Factories │ ├── AnalyticsDefinition │ │ ├── AnalyticsDefinitionFactory.swift │ │ └── AnalyticsDefinitionFactoryImpl.swift │ └── AnalyticsYAxisLocalization │ │ ├── AnalyticsYAxisLocalizationFactory.swift │ │ └── AnalyticsYAxisLocalizationFactoryImpl.swift ├── Formatters │ └── MonetaryAmountFormatter.swift ├── Info.plist ├── MobileAnalyticsChartSwift.h ├── Models │ ├── AnalyticsUnit.swift │ ├── CurrencyCode.swift │ └── MonetaryAmount.swift ├── Modules │ └── AnalyticsChartSpriteKit │ │ ├── AnalyticsChartSpriteKitModuleIO.swift │ │ ├── AnalyticsChartSpriteKitViewIO.swift │ │ ├── Assembly │ │ └── AnalyticsChartSpriteKitAssembly.swift │ │ ├── Presenter │ │ └── AnalyticsChartSpriteKitPresenter.swift │ │ └── View │ │ ├── AnalyticsChartSpriteKitView.swift │ │ └── ViewModel │ │ └── AnalyticsChartViewModel.swift └── Render │ ├── Actions │ ├── ColorTransition.swift │ └── FadeColorTransition.swift │ ├── ChartRenderConfiguration.swift │ ├── Configuration │ ├── ChartAnimation.swift │ ├── ChartDefinition.swift │ ├── ChartFadeAnimation.swift │ ├── ChartGestureState.swift │ ├── ChartPath.swift │ ├── ChartPathType.swift │ ├── ChartRangeLabel.swift │ ├── ChartXAxis.swift │ ├── ChartYAxis.swift │ └── ChartZeroLine.swift │ ├── Gradient │ ├── Gradient.swift │ └── SKTexture+Gradient.swift │ ├── Nodes │ ├── BasicNode.swift │ ├── RoundNode.swift │ └── SquareNode.swift │ ├── Paths │ ├── HorizontalQuadraticPath.swift │ ├── LinearPath.swift │ └── QuadraticPath.swift │ ├── RenderConfiguration.swift │ ├── RenderDrawer.swift │ ├── RenderDrawerModuleIO.swift │ └── SpriteKit │ └── RenderSpriteKitImpl.swift ├── MobileAnalyticsChartSwiftExamplePods ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── Categories │ ├── CGFloat+Space.swift │ ├── UIColor+Style.swift │ └── UIFont+Style.swift ├── Factories │ └── AnalyticsChart │ │ ├── AnalyticsChartFactory.swift │ │ └── AnalyticsChartFactoryImpl.swift ├── Info.plist ├── Modules │ ├── Chart │ │ ├── Assembly │ │ │ └── ChartAssembly.swift │ │ ├── ChartViewIO.swift │ │ ├── Presenter │ │ │ └── ChartPresenter.swift │ │ └── View │ │ │ └── ChartViewController.swift │ ├── CustomChart │ │ ├── Assembly │ │ │ └── CustomChartAssembly.swift │ │ ├── CustomChartViewIO.swift │ │ ├── Presenter │ │ │ └── CustomChartPresenter.swift │ │ └── View │ │ │ └── CustomChartViewController.swift │ ├── ListCharts │ │ ├── Assembly │ │ │ └── ListChartsAssembly.swift │ │ ├── ListChartsRouterIO.swift │ │ ├── ListChartsViewIO.swift │ │ ├── Presenter │ │ │ └── ListChartsPresenter.swift │ │ ├── Router │ │ │ └── ListChartsRouter.swift │ │ └── View │ │ │ ├── ListChartsViewController.swift │ │ │ └── ViewModel │ │ │ └── ListChartsViewModel.swift │ └── MultiselectCharts │ │ ├── Assembly │ │ └── MultiselectChartsAssembly.swift │ │ ├── MultiselectChartsViewIO.swift │ │ ├── Presenter │ │ └── MultiselectChartsPresenter.swift │ │ └── View │ │ └── MultiselectChartsViewController.swift └── Podfile-example ├── Package.swift ├── Podfile ├── Podfile.lock ├── README.md └── Resources ├── definition.gif ├── gesture.gif ├── loading.gif └── redraw.gif /.gitignore: -------------------------------------------------------------------------------- 1 | ## Various settings 2 | *.pbxuser 3 | !default.pbxuser 4 | *.mode1v3 5 | !default.mode1v3 6 | *.mode2v3 7 | !default.mode2v3 8 | *.perspectivev3 9 | !default.perspectivev3 10 | xcuserdata/ 11 | 12 | ## Other 13 | *.moved-aside 14 | *.xccheckout 15 | *.xcscmblueprint 16 | 17 | ## Obj-C/Swift specific 18 | *.hmap 19 | *.ipa 20 | *.dSYM.zip 21 | *.dSYM 22 | 23 | ## Playgrounds 24 | timeline.xctimeline 25 | playground.xcworkspace 26 | 27 | # CocoaPods 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | Pods/ 34 | 35 | # Carthage 36 | # 37 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 38 | Carthage/Checkouts 39 | 40 | Carthage/Build 41 | 42 | # SPM 43 | # 44 | .swiftpm/ 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots 56 | fastlane/test_output 57 | 58 | # Appcode 59 | .idea/ 60 | 61 | .DS_Store -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | mobile-analytics-chart-swift -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.6.3 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - class_delegate_protocol 3 | # Delegate protocols should be class-only so they can be weakly referenced. 4 | # 5 | # ↓protocol FooDelegate {} 6 | # 7 | # ↓protocol FooDelegate: Bar {} 8 | 9 | 10 | 11 | - closing_brace 12 | # Closing brace with closing parenthesis should not have any whitespaces in the middle. 13 | # 14 | # [].map({ ↓} ) 15 | 16 | 17 | 18 | - closure_end_indentation 19 | # Closure end should have the same indentation as the line that started it. 20 | # 21 | # SignalProducer(values: [1, 2, 3]) 22 | # .startWithNext { number in 23 | # print(number) 24 | # ↓} 25 | # 26 | # return match(pattern: pattern, with: [.comment]).flatMap { range in 27 | # return Command(string: contents, range: range) 28 | # ↓}.flatMap { command in 29 | # return command.expand() 30 | # ↓} 31 | 32 | 33 | 34 | - closure_parameter_position 35 | # Closure parameters should be on the same line as opening brace. 36 | # 37 | # [1, 2].map { 38 | # ↓number in 39 | # number + 1 40 | # } 41 | # 42 | # [1, 2].map { 43 | # ↓number -> Int in 44 | # number + 1 45 | # } 46 | 47 | 48 | 49 | - closure_spacing 50 | # Closure expressions should have a single space inside each brace. 51 | # 52 | # [].filter(↓{$0.contains(location)}) 53 | # 54 | # [].map(↓{$0}) 55 | 56 | 57 | 58 | - colon 59 | # Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. 60 | # 61 | # let ↓abc:Void 62 | # 63 | # let ↓abc: Void 64 | # 65 | # let ↓abc :Void 66 | # 67 | # let ↓abc : Void 68 | # 69 | # let ↓abc : [Void: Void] 70 | # 71 | # func abc(↓def:Void) {} 72 | # 73 | # func abc(def: Void, ↓ghi :Void) {} 74 | # 75 | # let abc = [Void↓ : Void]() 76 | # 77 | # let abc = [Void↓: Void]() 78 | # 79 | # let abc = [1: [3↓ : 2], 3: 4] 80 | 81 | 82 | 83 | - comma 84 | # There should be no space before and one after any comma. 85 | # 86 | # func abc(a: String↓ ,b: String) { } 87 | # 88 | # func abc(a: String↓ ,b: String↓ ,c: String↓ ,d: String) { } 89 | # 90 | # enum a { case a↓ ,b } 91 | # 92 | # let result = plus( 93 | # first: 3↓ , // #683 94 | # second: 4 95 | # ) 96 | 97 | 98 | 99 | - compiler_protocol_init 100 | # The initializers declared in compiler protocols such as `ExpressibleByArrayLiteral` shouldn't be called directly. 101 | # 102 | # let set = ↓Set(arrayLiteral: 1, 2) 103 | # 104 | # let set = ↓Set.init(arrayLiteral: 1, 2) 105 | 106 | 107 | 108 | - control_statement 109 | # if, for, while, do, guard, switch statements shouldn't wrap their conditionals in parentheses. 110 | # 111 | # do { ; } ↓while (condition) { 112 | # 113 | # ↓switch (foo) { 114 | # 115 | # do { ; } ↓while(condition) { 116 | # 117 | # } ↓while(condition) { 118 | # 119 | # ↓guard (condition) else { 120 | # 121 | # ↓for(item in collection) { 122 | # 123 | # ↓if ((min...max).contains(value)) { 124 | # 125 | # ↓if ((a || b) && (c || d)) { 126 | 127 | 128 | 129 | - cyclomatic_complexity 130 | # Complexity of function bodies should be limited. 131 | # 132 | # ↓func f1() { 133 | # if true { 134 | # if true { 135 | # if false {} 136 | # } 137 | # } 138 | # if false {} 139 | # let i = 0 140 | # switch i { 141 | # case 1: break 142 | # case 2: break 143 | # case 3: break 144 | # case 4: break 145 | # default: break 146 | # } 147 | # for _ in 1...5 { 148 | # guard true else { 149 | # return 150 | # } 151 | # } 152 | # } 153 | 154 | 155 | - discarded_notification_center_observer 156 | # When registing for a notification using a block, the 157 | # opaque observer that is returned should be stored so it 158 | # can be removed later. 159 | # 160 | # ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { } 161 | # 162 | # ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }) 163 | 164 | 165 | 166 | - dynamic_inline 167 | # Avoid using 'dynamic' and '@inline(__always)' together. 168 | # 169 | # class C { 170 | # @inline(__always) dynamic ↓func f() {} 171 | # } 172 | # 173 | # class C { 174 | # @inline(__always) public dynamic ↓func f() {} 175 | # } 176 | # 177 | # class C { 178 | # @inline(__always) dynamic internal ↓func f() {} 179 | # } 180 | # 181 | # class C { 182 | # @inline(__always) 183 | # dynamic ↓func f() {} 184 | # } 185 | # 186 | # class C { 187 | # @inline(__always) 188 | # dynamic 189 | # ↓func f() {} 190 | # } 191 | 192 | 193 | 194 | - empty_count 195 | # Prefer checking `isEmpty` over comparing `count` to zero. 196 | # 197 | # [Int]().↓count == 0 198 | # 199 | # [Int]().↓count > 0 200 | # 201 | # [Int]().↓count != 0 202 | 203 | 204 | 205 | - empty_parameters 206 | # Prefer `() -> ` over `Void -> `. 207 | # 208 | # let abc: ↓Void -> Void = {} 209 | # 210 | # func foo(completion: ↓Void -> Void) 211 | # 212 | # func foo(completion: ↓Void throws -> Void) 213 | # 214 | # let foo: ↓Void -> () throws -> Void) 215 | 216 | 217 | 218 | - empty_parentheses_with_trailing_closure 219 | # When using trailing closures, empty parentheses should be 220 | # avoided after the method call. 221 | # 222 | # [1, 2].map↓() { $0 + 1 } 223 | # 224 | # [1, 2].map↓( ) { $0 + 1 } 225 | # 226 | # [1, 2].map↓() { number in 227 | # number + 1 228 | # } 229 | # 230 | # [1, 2].map↓( ) { number in 231 | # number + 1 232 | # } 233 | 234 | 235 | 236 | - explicit_init 237 | # Explicitly calling .init() should be avoided. 238 | # 239 | # [1].flatMap{String↓.init($0)} 240 | # 241 | # [String.self].map { Type in Type↓.init(1) } 242 | 243 | 244 | 245 | - fatal_error_message 246 | # A fatalError call should have a message. 247 | # 248 | # func foo() { 249 | # ↓fatalError("") 250 | # } 251 | # 252 | # func foo() { 253 | # ↓fatalError() 254 | # } 255 | 256 | 257 | 258 | # - file_length 259 | # Files should not span too many lines. 260 | 261 | 262 | 263 | - first_where 264 | # Prefer using `.first(where:)` over `.filter { }.first` in collections. 265 | # 266 | # ↓myList.filter { $0 % 2 == 0 }.first 267 | # 268 | # ↓myList.filter({ $0 % 2 == 0 }).first 269 | # 270 | # ↓myList.map { $0 + 1 }.filter({ $0 % 2 == 0 }).first 271 | # 272 | # ↓myList.map { $0 + 1 }.filter({ $0 % 2 == 0 }).first?.something() 273 | # 274 | # ↓myList.filter(someFunction).first 275 | # 276 | # ↓myList.filter({ $0 % 2 == 0 }) 277 | # .first 278 | 279 | 280 | 281 | - for_where 282 | # `where` clauses are preferred over a single `if` inside a `for`. 283 | # 284 | # for user in users { 285 | # ↓if user.id == 1 { return true } 286 | # } 287 | 288 | 289 | 290 | - force_cast 291 | # Force casts should be avoided. 292 | # 293 | # NSNumber() ↓as! Int 294 | 295 | 296 | 297 | - force_try 298 | # Force tries should be avoided. 299 | # 300 | # func a() throws {}; ↓try! a() 301 | 302 | 303 | 304 | - force_unwrapping 305 | # Force unwrapping should be avoided. 306 | # 307 | # let url = NSURL(string: query)↓! 308 | # 309 | # navigationController↓!.pushViewController(viewController, animated: true) 310 | # 311 | # let unwrapped = optional↓! 312 | # 313 | # return cell↓! 314 | # 315 | # let url = NSURL(string: "http://www.google.com")↓! 316 | # 317 | # let dict = ["Boooo": "👻"]func bla() -> String { return dict["Boooo"]↓! } 318 | 319 | 320 | 321 | # - function_body_length 322 | # Functions bodies should not span too many lines. 323 | 324 | 325 | 326 | # - function_parameter_count 327 | # Number of function parameters should be low. 328 | # 329 | # ↓func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} 330 | # 331 | # ↓func initialValue(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} 332 | # 333 | # ↓func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int = 2, g: Int) {} 334 | # 335 | # struct Foo { 336 | # init(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} 337 | # ↓func bar(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} 338 | # } 339 | 340 | 341 | 342 | - generic_type_name 343 | # Generic type name should only contain alphanumeric characters, start with an uppercase character and span between 1 and 20 characters in length. 344 | # 345 | # func foo<↓T_Foo>() {} 346 | # 347 | # func foo<↓TTTTTTTTTTTTTTTTTTTTT>() {} 348 | # 349 | # func foo<↓type>() {} 350 | 351 | 352 | 353 | # - identifier_name 354 | # Identifier names should only contain alphanumeric characters and start with a lowercase character or should only contain capital letters. In an exception to the above, variable names may start with a capital letter when they are declared static and immutable. Variable names should not be too long or too short. 355 | # 356 | # ↓let MyLet = 0 357 | # 358 | # ↓let _myLet = 0 359 | # 360 | # private ↓let myLet_ = 0 361 | # 362 | # ↓let myExtremelyVeryVeryVeryVeryVeryVeryLongLet = 0 363 | # 364 | # ↓let i = 0 365 | # 366 | # enum Foo { case ↓MyEnum } 367 | # 368 | # ↓func IsOperator(name: String) -> Bool 369 | # 370 | # private ↓let _i = 0 371 | 372 | 373 | 374 | - implicit_getter 375 | # Computed read-only properties should avoid using the get keyword. 376 | # 377 | # class Foo { 378 | # var foo: Int { 379 | # ↓get { 380 | # return 20 381 | # } 382 | # } 383 | # } 384 | 385 | 386 | 387 | # - implicitly_unwrapped_optional 388 | # Implicitly unwrapped optionals should be avoided when possible. 389 | # 390 | # let label: UILabel! 391 | # 392 | # let IBOutlet: UILabel! 393 | # 394 | # let labels: [UILabel!] 395 | # 396 | # var ints: [Int!] = [42, nil, 42] 397 | # 398 | # let label: IBOutlet! 399 | 400 | 401 | 402 | - large_tuple 403 | # Tuples shouldn't have too many members. Create a custom type instead. 404 | # 405 | # ↓let foo: (Int, Int, Int) 406 | # 407 | # func foo(↓bar: (Int, Int, Int)) 408 | # 409 | # func foo() -> ↓(Int, Int, Int) 410 | # 411 | # func foo() throws -> ↓(Int, ↓(String, String, String), Int) {} 412 | # 413 | # func getDictionaryAndInt() -> (Dictionary, Int)? 414 | 415 | 416 | 417 | - leading_whitespace 418 | # Files should not contain leading whitespace. 419 | 420 | 421 | 422 | - legacy_cggeometry_functions 423 | # Struct extension properties and methods are preferred over legacy functions 424 | # 425 | # ↓CGRectGetWidth(rect) 426 | # 427 | # ↓CGRectIsNull(rect) 428 | # 429 | # ↓CGRectIntersectsRect(rect1, rect2) 430 | 431 | 432 | 433 | - legacy_constant 434 | # Struct-scoped constants are preferred over legacy global constants. 435 | # 436 | # ↓CGRectInfinite 437 | # 438 | # ↓CGSizeZero 439 | # 440 | # ↓CGRectNull 441 | # 442 | # ↓CGFloat(M_PI) 443 | 444 | 445 | 446 | - legacy_constructor 447 | # Swift constructors are preferred over legacy convenience functions. 448 | # 449 | # ↓CGPointMake(10, 10) 450 | # 451 | # ↓CGSizeMake(aWidth, aHeight) 452 | # 453 | # ↓NSMakeRect(xVal, yVal, width, height) 454 | # 455 | # ↓NSEdgeInsetsMake(top, left, bottom, right) 456 | 457 | 458 | 459 | - line_length 460 | # Lines should not span too many characters. 461 | 462 | 463 | 464 | - mark 465 | # MARK comment should be in valid format. 466 | # 467 | # ↓//MARK: bad 468 | # 469 | # ↓// MARK: bad 470 | # 471 | # ↓// MARK: bad 472 | # 473 | # ↓// MARK:bad 474 | # 475 | # ↓// MARK: -bad 476 | # 477 | # ↓//MARK: - bad 478 | # 479 | # ↓//MARK:- bad 480 | 481 | 482 | 483 | - missing_docs 484 | # Public declarations should be documented. 485 | # 486 | # ↓public func a() {} 487 | # 488 | # ↓// regular comment 489 | # public func a() {} 490 | # 491 | # ↓/* regular comment */ 492 | # public func a() {} 493 | # 494 | # /// docs 495 | # public protocol A { 496 | # ↓// no docs 497 | # var b: Int { get } } 498 | # /// docs 499 | # public struct C: A { 500 | # 501 | # ↓public let b: Int 502 | # } 503 | 504 | 505 | 506 | - nesting 507 | # Types should be nested at most 1 level deep, and statements should be nested at most 5 levels deep. 508 | # 509 | # class A { class B { ↓class C {} } } 510 | # 511 | # struct A { struct B { ↓struct C {} } } 512 | # 513 | # enum A { enum B { ↓enum C {} } } 514 | # 515 | # func func0() { 516 | # func func1() { 517 | # func func2() { 518 | # func func3() { 519 | # func func4() { 520 | # func func5() { 521 | # ↓func func6() { 522 | # } 523 | # } 524 | # } 525 | # } 526 | # } 527 | # } 528 | # } 529 | 530 | 531 | 532 | - opening_brace 533 | # Opening braces should be preceded by a single space and on the same line as the declaration. 534 | # 535 | # func abc(↓){ 536 | # } 537 | # 538 | # func abc()↓ 539 | # { } 540 | # 541 | # [].map(↓){ $0 } 542 | # 543 | # [].map↓( { } ) 544 | # 545 | # if let a = b{ } 546 | # 547 | # while a == b{ } 548 | # 549 | # guard let a = b else{ } 550 | 551 | 552 | 553 | - operator_usage_whitespace 554 | # Operators should be surrounded by a single whitespace when they are being used. 555 | # 556 | # let foo = 1↓+2 557 | # 558 | # let foo = 1↓ + 2 559 | # 560 | # let foo = 1↓ + 2 561 | # 562 | # let foo = 1↓ + 2 563 | # 564 | # let foo↓=1↓+2 565 | # 566 | # let foo = bar↓??0 567 | 568 | 569 | 570 | - operator_whitespace 571 | # Operators should be surrounded by a single whitespace when defining them. 572 | # 573 | # ↓func <|(lhs: Int, rhs: Int) -> Int {} 574 | # 575 | # ↓func <|<(lhs: A, rhs: A) -> A {} 576 | # 577 | # ↓func <| (lhs: Int, rhs: Int) -> Int {} 578 | 579 | 580 | 581 | # - private_outlet 582 | # IBOutlets should be private to avoid leaking UIKit to higher layers. 583 | # 584 | # class Foo { 585 | # @IBOutlet ↓var label: UILabel? 586 | # } 587 | 588 | 589 | 590 | - redundant_discardable_let 591 | # Prefer `_ = foo()` over `let _ = foo()` when discarding a result from a function. 592 | # 593 | # ↓let _ = foo() 594 | # 595 | # if _ = foo() { ↓let _ = bar() } 596 | 597 | 598 | 599 | - redundant_nil_coalescing 600 | # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant 601 | # 602 | # - var myVar: Int? = nil 603 | # myVar↓ ?? nil 604 | 605 | 606 | 607 | - redundant_optional_initialization 608 | # Initializing an optional variable with nil is redundant. 609 | # 610 | # var myVar: Int?↓ = nil 611 | # 612 | # var myVar: Optional↓ = nil 613 | # 614 | # var myVar: Int?↓=nil 615 | # 616 | # var myVar: Optional↓=nil 617 | 618 | 619 | 620 | - redundant_string_enum_value 621 | # String enum values can be omitted when they are equal to the enumcase name. 622 | # 623 | # enum Numbers: String { 624 | # case one = ↓"one" 625 | # case two = ↓"two" 626 | # } 627 | # 628 | # enum Numbers: String { 629 | # case one = ↓"one", two = ↓"two" 630 | # } 631 | 632 | 633 | 634 | - redundant_void_return 635 | # Returning Void in a function declaration is redundant. 636 | # 637 | # func foo()↓ -> Void {} 638 | # 639 | # protocol Foo { 640 | # func foo()↓ -> Void 641 | # } 642 | # 643 | # func foo()↓ -> () {} 644 | # 645 | # protocol Foo { 646 | # func foo()↓ -> () 647 | # } 648 | 649 | 650 | 651 | - return_arrow_whitespace 652 | # Return arrow and return type should be separated by a single space or on a separate line. 653 | # 654 | # func abc()↓->Int {} 655 | # 656 | # func abc()↓->[Int] {} 657 | # 658 | # func abc()↓->(Int, Int) {} 659 | # 660 | # func abc()↓-> Int {} 661 | # 662 | # func abc()↓ ->Int {} 663 | # 664 | # var abc = {(param: Int)↓ ->Bool in } 665 | # 666 | # var abc = {(param: Int)↓->Bool in } 667 | 668 | 669 | 670 | - shorthand_operator 671 | # Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning. 672 | # 673 | # ↓foo = foo - 1 674 | # 675 | # ↓foo = foo - aVariable 676 | # 677 | # ↓foo = foo - bar.method() 678 | # 679 | # ↓foo.aProperty = foo.aProperty - 1 680 | 681 | 682 | 683 | - sorted_imports 684 | # Imports should be sorted. 685 | # 686 | # import AAA 687 | # import ZZZ 688 | # import ↓BBB 689 | # import CCC 690 | 691 | 692 | 693 | - statement_position 694 | # Else and catch should be on the same line, one space after the previous declaration. 695 | # 696 | # ↓}else if { 697 | # 698 | # ↓} else { 699 | # 700 | # ↓} 701 | # catch { 702 | # 703 | # ↓} 704 | # catch { 705 | 706 | 707 | 708 | - syntactic_sugar 709 | # Shorthand syntactic sugar should be used, i.e. [Int] instead of Array 710 | # 711 | # let x: ↓Array 712 | # 713 | # let x: ↓Dictionary 714 | # 715 | # let x: ↓Optional 716 | # 717 | # let x: ↓ImplicitlyUnwrappedOptional 718 | 719 | 720 | 721 | - trailing_comma 722 | # Multi-line collection literals should have trailing commas 723 | 724 | 725 | 726 | - trailing_newline 727 | # Files should have a single trailing newline. 728 | 729 | 730 | 731 | - trailing_semicolon 732 | # Lines should not have trailing semicolons. 733 | # 734 | # let a = 0↓ 735 | 736 | 737 | 738 | - trailing_whitespace 739 | # Lines should not have trailing whitespace. 740 | 741 | 742 | 743 | - type_name 744 | # Type name should only contain alphanumeric characters, start with an uppercase character and span between 3 and 40 characters in length. 745 | 746 | 747 | 748 | - unused_enumerated 749 | # When the index or the item is not used, `.enumerated()` can be removed. 750 | # 751 | # for (↓_, foo) in bar.enumerated() { } 752 | # 753 | # for (↓_, foo) in abc.bar.enumerated() { } 754 | # 755 | # for (↓_, foo) in abc.something().enumerated() { } 756 | # 757 | # for (idx, ↓_) in bar.enumerated() { } 758 | 759 | 760 | 761 | - unused_optional_binding 762 | # Prefer `!= nil` over `let _ =` 763 | # 764 | # if let ↓_ = Foo.optionalValue { } 765 | # 766 | # if let a = Foo.optionalValue, let ↓_ = Foo.optionalValue2 { } 767 | # 768 | # if let ↓(_, _, _) = getOptionalTuple(), let bar = Foo.optionalValue { } 769 | # 770 | # guard let a = Foo.optionalValue, let ↓_ = Foo.optionalValue2 { } 771 | 772 | 773 | 774 | # - valid_ibinspectable 775 | # @IBInspectable should be applied to variables only, have its type explicit and be of a supported type 776 | # 777 | # @IBInspectable private ↓let count: Int 778 | # 779 | # @IBInspectable private ↓var count = 0 780 | # 781 | # @IBInspectable private ↓var count: Int? 782 | # 783 | # @IBInspectable private ↓var x: ImplicitlyUnwrappedOptional 784 | 785 | 786 | 787 | - vertical_parameter_alignment 788 | # Function parameters should be aligned vertically if they're in multiple lines in a declaration. 789 | # 790 | # func validateFunction(_ file: File, kind: SwiftDeclarationKind, 791 | # ↓dictionary: [String: SourceKitRepresentable]) { } 792 | # 793 | # func validateFunction(_ file: File, kind: SwiftDeclarationKind, 794 | # ↓dictionary: [String: SourceKitRepresentable]) { } 795 | # 796 | # func validateFunction(_ file: File, 797 | # ↓kind: SwiftDeclarationKind, 798 | # ↓dictionary: [String: SourceKitRepresentable]) { } 799 | 800 | 801 | 802 | - vertical_whitespace 803 | # Limit vertical whitespace to a single empty line. 804 | 805 | 806 | 807 | - void_return 808 | # Prefer `-> Void` over `-> ()`. 809 | # 810 | # let abc: () -> ↓() = {} 811 | # 812 | # func foo(completion: () -> ↓()) 813 | # 814 | # func foo(completion: () -> ↓( )) 815 | # 816 | # let foo: (ConfigurationTests) -> () throws -> ↓()) 817 | 818 | 819 | 820 | # - weak_delegate 821 | # Delegates should be weak to avoid reference cycles. 822 | # 823 | # ↓var delegate: SomeProtocol? 824 | # 825 | # ↓var scrollDelegate: ScrollDelegate? 826 | 827 | 828 | 829 | file_length: 500 830 | function_body_length: 40 831 | identifier_name: 832 | min_length: 2 833 | max_length: 20 834 | large_tuple: 3 835 | line_length: 836 | warning: 120 837 | ignores_urls: true 838 | # statement_position: 839 | # statement_mode: "uncuddled_else" 840 | trailing_comma: 841 | mandatory_comma: true 842 | vertical_whitespace: 843 | max_empty_lines: 1 844 | nesting: 845 | type_level: 4 846 | statement_level: 4 847 | type_name: 848 | max_length: 1000 849 | cyclomatic_complexity: 11 850 | 851 | 852 | excluded: 853 | - Pods 854 | - Carthage 855 | - .idea 856 | - vendor 857 | -------------------------------------------------------------------------------- /.xcovignore: -------------------------------------------------------------------------------- 1 | - Pods 2 | - MobileAnalyticsChartSwiftExamplePods -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "Swiftlint" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | > #### Before create pull request 2 | > - You must specify one of the versions in the field **NEXT_VERSION_TYPE** 3 | > - Also you need to indicate descriptions of changes between fields **NEXT_VERSION_DESCRIPTION_BEGIN** and **NEXT_VERSION_DESCRIPTION_END** 4 | ### NEXT_VERSION_TYPE=MAJOR|MINOR|PATCH 5 | ### NEXT_VERSION_DESCRIPTION_BEGIN 6 | ### NEXT_VERSION_DESCRIPTION_END 7 | 8 | ## [1.3.0](30-04-2021) 9 | 10 | Add support swift package manager. Update Readme. Add AnalyticsChartSpriteKitModuleOutput. 11 | 12 | ## [1.2.0](26-04-2021) 13 | 14 | Change blend mode of chart line to alpha. Change dash pattern for zero line. 15 | 16 | ## [1.1.0](11-12-2020) 17 | 18 | Added draw zero line. 19 | 20 | ## [1.0.0](25-11-2020) 21 | 22 | Move mobile analytics chart library to open-source -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '~> 1.9.3' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.3) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | algoliasearch (1.27.5) 11 | httpclient (~> 2.8, >= 2.8.3) 12 | json (>= 1.5.1) 13 | atomos (0.1.3) 14 | claide (1.0.3) 15 | cocoapods (1.9.3) 16 | activesupport (>= 4.0.2, < 5) 17 | claide (>= 1.0.2, < 2.0) 18 | cocoapods-core (= 1.9.3) 19 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 20 | cocoapods-downloader (>= 1.2.2, < 2.0) 21 | cocoapods-plugins (>= 1.0.0, < 2.0) 22 | cocoapods-search (>= 1.0.0, < 2.0) 23 | cocoapods-stats (>= 1.0.0, < 2.0) 24 | cocoapods-trunk (>= 1.4.0, < 2.0) 25 | cocoapods-try (>= 1.1.0, < 2.0) 26 | colored2 (~> 3.1) 27 | escape (~> 0.0.4) 28 | fourflusher (>= 2.3.0, < 3.0) 29 | gh_inspector (~> 1.0) 30 | molinillo (~> 0.6.6) 31 | nap (~> 1.0) 32 | ruby-macho (~> 1.4) 33 | xcodeproj (>= 1.14.0, < 2.0) 34 | cocoapods-core (1.9.3) 35 | activesupport (>= 4.0.2, < 6) 36 | algoliasearch (~> 1.0) 37 | concurrent-ruby (~> 1.1) 38 | fuzzy_match (~> 2.0.4) 39 | nap (~> 1.0) 40 | netrc (~> 0.11) 41 | typhoeus (~> 1.0) 42 | cocoapods-deintegrate (1.0.4) 43 | cocoapods-downloader (1.4.0) 44 | cocoapods-plugins (1.0.0) 45 | nap 46 | cocoapods-search (1.0.0) 47 | cocoapods-stats (1.1.0) 48 | cocoapods-trunk (1.5.0) 49 | nap (>= 0.8, < 2.0) 50 | netrc (~> 0.11) 51 | cocoapods-try (1.2.0) 52 | colored2 (3.1.2) 53 | concurrent-ruby (1.1.7) 54 | escape (0.0.4) 55 | ethon (0.12.0) 56 | ffi (>= 1.3.0) 57 | ffi (1.11.1) 58 | fourflusher (2.3.1) 59 | fuzzy_match (2.0.4) 60 | gh_inspector (1.1.3) 61 | httpclient (2.8.3) 62 | i18n (0.9.5) 63 | concurrent-ruby (~> 1.0) 64 | json (2.3.1) 65 | minitest (5.14.2) 66 | molinillo (0.6.6) 67 | nanaimo (0.3.0) 68 | nap (1.1.0) 69 | netrc (0.11.0) 70 | ruby-macho (1.4.0) 71 | thread_safe (0.3.6) 72 | typhoeus (1.4.0) 73 | ethon (>= 0.9.0) 74 | tzinfo (1.2.8) 75 | thread_safe (~> 0.1) 76 | xcodeproj (1.19.0) 77 | CFPropertyList (>= 2.3.3, < 4.0) 78 | atomos (~> 0.1.3) 79 | claide (>= 1.0.2, < 2.0) 80 | colored2 (~> 3.1) 81 | nanaimo (~> 0.3.0) 82 | 83 | PLATFORMS 84 | ruby 85 | 86 | DEPENDENCIES 87 | cocoapods (~> 1.9.3) 88 | 89 | BUNDLED WITH 90 | 1.17.3 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2007—2020 NBCO YooMoney LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MobileAnalyticsChartSwift' 3 | s.version = '1.3.0' 4 | s.homepage = 'https://github.com/yoomoney/mobile-analytics-chart-swift/browse' 5 | s.license = { 6 | :type => "MIT", 7 | :file => "LICENSE" 8 | } 9 | s.authors = 'YooMoney' 10 | s.summary = 'Mobile Analytics Chart' 11 | 12 | s.source = { 13 | :git => 'https://github.com/yoomoney/mobile-analytics-chart-swift.git', 14 | :tag => s.version.to_s 15 | } 16 | 17 | s.ios.deployment_target = '11.0' 18 | s.swift_version = '5.0' 19 | 20 | s.ios.source_files = 'MobileAnalyticsChartSwift/**/*.{h,swift}', 'MobileAnalyticsChartSwift/*.{h,swift}' 21 | 22 | s.ios.framework = 'UIKit' 23 | end 24 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcodeproj/xcshareddata/xcschemes/MobileAnalyticsChartSwift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcodeproj/xcshareddata/xcschemes/MobileAnalyticsChartSwiftExamplePods.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/AnimationFunction.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Cubic ease out function. 4 | /// x and result in [0...1]. 5 | public func cubicEaseOut( 6 | _ x: CGFloat 7 | ) -> CGFloat { 8 | let p = x - 1 9 | return p * p * p + 1 10 | } 11 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Calculator.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import UIKit 3 | 4 | /// Protocol of Calculator. 5 | public protocol Calculator { 6 | 7 | /// Minimum value in visible range. 8 | var minValue: CGFloat? { get } 9 | 10 | /// Maximum value in the visible range. 11 | var maxValue: CGFloat? { get } 12 | 13 | /// Create points for charts. 14 | func makeChartsPoints( 15 | frame: CGRect, 16 | chartMargins: UIEdgeInsets, 17 | minValue: CGFloat?, 18 | maxValue: CGFloat? 19 | ) -> [[CGPoint]] 20 | 21 | /// Create a new range based on a change location. 22 | func makeRangeValue( 23 | deltaLocation: CGFloat, 24 | frameWidth: CGFloat 25 | ) -> RangeValue? 26 | 27 | /// Create a new range based on a change scale. 28 | func makeRangeValue( 29 | scale: CGFloat 30 | ) -> RangeValue? 31 | 32 | /// Set a new range of values. 33 | func setRangeValue( 34 | rangeValue: RangeValue 35 | ) 36 | 37 | /// Make total count values on x axis and 38 | /// x axis visible values with a position on the x axis and a value at that point. 39 | func makeXAxis( 40 | frame: CGRect, 41 | leftInset: CGFloat, 42 | rightInset: CGFloat, 43 | minCount: Int, 44 | zoomFactor: CGFloat 45 | ) -> [ChartXAxisValue] 46 | 47 | /// Make y coordinate for lines on the y axis. 48 | func makeYAxisLines( 49 | frame: CGRect, 50 | labelHeight: CGFloat, 51 | labelInsets: UIEdgeInsets, 52 | linesCount: Int 53 | ) -> [(start: CGPoint, end: CGPoint)] 54 | 55 | /// Make position and value for labels on the y axis. 56 | func makeYAxisValues( 57 | frame: CGRect, 58 | chartMargins: UIEdgeInsets, 59 | labelHeight: CGFloat, 60 | labelInsets: UIEdgeInsets, 61 | linesCount: Int 62 | ) -> [(position: CGPoint, value: CGFloat)] 63 | 64 | /// Make zero line coordinate for zero line. 65 | func makeZeroLine( 66 | frame: CGRect, 67 | chartMargins: UIEdgeInsets, 68 | minValue: CGFloat?, 69 | maxValue: CGFloat? 70 | ) -> (start: CGPoint, end: CGPoint)? 71 | 72 | /// Make dates for range diapasone. 73 | func makeRangeDates() -> (start: Date, end: Date?)? 74 | 75 | /// Make definition value, position for point and positions for line. 76 | func makeDefinition( 77 | frame: CGRect, 78 | chartMargins: UIEdgeInsets, 79 | definitionPosition: CGPoint, 80 | minValue: CGFloat?, 81 | maxValue: CGFloat? 82 | ) -> ChartDefinitionValues? 83 | 84 | /// Calculate line width. 85 | func calculateLineWidth( 86 | index: Int, 87 | minLineWidth: CGFloat, 88 | maxLineWidth: CGFloat 89 | ) -> CGFloat 90 | 91 | /// Return true if number of values equal 1. 92 | func isAlwaysDrawDefinition() -> Bool 93 | 94 | /// Return true if values is not empty. 95 | func isNeedDrawChart() -> Bool 96 | } 97 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/CalculatorConfiguration.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Type of calculator configuration. 4 | public struct CalculatorConfiguration { 5 | 6 | /// Minimum static value. If minStaticValue == nil then minimum will change dynamically. 7 | public let minStaticValue: StaticValueState? 8 | 9 | /// Maximum static value. If maxStaticValue == nil then Maximum will change dynamically. 10 | public let maxStaticValue: StaticValueState? 11 | 12 | /// Initial value of the lower. Takes a value in the range [0...1]. 13 | public let initialLowerValue: CGFloat 14 | 15 | /// Initial value of the upper. Takes a value in the range [0...1]. 16 | public let initialUpperValue: CGFloat 17 | 18 | /// Minimum count of visible points on the chart. 19 | public let minimumCountVisibleValues: Int 20 | 21 | /// Line width reduction range based on the number of points on the chart. 22 | /// Before lower bound line width will be equal maximum line width. 23 | /// After upper bound line width will be equal minimum line width. 24 | public let rangeLineWidthReduction: Range 25 | 26 | /// Creates instance of `CalculatorConfiguration`. 27 | /// 28 | /// - Parameters: 29 | /// - minStaticValue: Minimum static value. If minStaticValue == nil then minimum will change dynamically. 30 | /// - maxStaticValue: Maximum static value. If maxStaticValue == nil then Maximum will change dynamically. 31 | /// - initialLowerValue: Initial value of the lower. Takes a value in the range [0...1]. 32 | /// - initialUpperValue: Initial value of the upper. Takes a value in the range [0...1]. 33 | /// - minimumCountVisibleValues: Minimum count of visible points on the chart. 34 | /// - rangeLineWidthReduction: Line width reduction range based on the number of points on the chart. 35 | /// Before lower bound line width will be equal maximum line width. 36 | /// After upper bound line width will be equal minimum line width. 37 | /// 38 | /// - Returns: Instance of `CalculatorConfiguration`. 39 | public init( 40 | minStaticValue: StaticValueState?, 41 | maxStaticValue: StaticValueState?, 42 | initialLowerValue: CGFloat = 0.0, 43 | initialUpperValue: CGFloat = 1.0, 44 | minimumCountVisibleValues: Int = 4, 45 | rangeLineWidthReduction: Range = 10 ..< 100 46 | ) { 47 | self.minStaticValue = minStaticValue 48 | self.maxStaticValue = maxStaticValue 49 | self.initialLowerValue = initialLowerValue 50 | self.initialUpperValue = initialUpperValue 51 | self.minimumCountVisibleValues = minimumCountVisibleValues 52 | self.rangeLineWidthReduction = rangeLineWidthReduction 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/CalculatorImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// Implementation of calculator. 5 | public class CalculatorImpl { 6 | 7 | // MARK: - Init data 8 | 9 | /// Array of chart data. 10 | private var data: [ChartData] 11 | 12 | /// Calculator configuration. 13 | private var configuration: CalculatorConfiguration 14 | 15 | /// Number of values. 16 | private(set) var valuesCount: Int 17 | 18 | /// Number of segments. 19 | private(set) var segmentsCount: Int? 20 | 21 | /// Value range. Takes a value in range [0...1]. 22 | private(set) var rangeValue: RangeValue 23 | 24 | /// Index range. Takes a value in range [0...segmentsCount]. 25 | private(set) var rangeIndex: RangeValue? 26 | 27 | /// Boundary value range. Takes a value in range [values.min...values.max] or can take custom values. 28 | private(set) var rangeBoundaryValue: RangeValue? 29 | 30 | // MARK: - Init 31 | 32 | /// Creates instance of `CalculatorImpl`. 33 | /// 34 | /// - Parameters: 35 | /// - data: Array of chart data. 36 | /// - configuration: Calculator configuration. 37 | /// - valuesCount: Number of values. 38 | /// - segmentsCount: Number of segments. 39 | /// - rangeValue: Value range. Takes a value in range [0...1]. 40 | /// - rangeIndex: Index range. Takes a value in range [0...segmentsCount]. 41 | /// - rangeBoundaryValue: Boundary value range. Takes a value in range [values.min...values.max] 42 | /// or can take custom values. 43 | /// 44 | /// - Returns: Instance of `CalculatorImpl`. 45 | public init( 46 | data: [ChartData], 47 | configuration: CalculatorConfiguration 48 | ) { 49 | self.data = data 50 | self.configuration = configuration 51 | 52 | self.valuesCount = data.first?.values.count ?? 0 53 | self.segmentsCount = data.first?.values.isEmpty ?? true ? nil : valuesCount - 1 54 | self.rangeValue = RangeValue( 55 | lowerValue: configuration.initialLowerValue, 56 | upperValue: configuration.initialUpperValue 57 | ) 58 | 59 | guard let segmentsCount = self.segmentsCount else { return } 60 | 61 | self.rangeIndex = RangeValue( 62 | lowerValue: makeLowerIndex(rangeValue.lowerValue, segmentsCount: segmentsCount), 63 | upperValue: makeUpperIndex(rangeValue.upperValue, segmentsCount: segmentsCount) 64 | ) 65 | 66 | self.rangeBoundaryValue = makeBoundaryValues( 67 | values: visibleValuesInRange, 68 | minStaticValue: configuration.minStaticValue, 69 | maxStaticValue: configuration.maxStaticValue 70 | ) 71 | } 72 | 73 | // MARK: - Data 74 | 75 | /// Minimum value in visible range. 76 | public var minValue: CGFloat? { 77 | get { 78 | switch rangeBoundaryValue?.lowerValue { 79 | case .customValue(let value): 80 | return value 81 | case .staticValue(let value): 82 | return value 83 | case .boundaryValue(let rangeValue): 84 | return min(rangeValue.lowerValue, rangeValue.upperValue) 85 | case .none: 86 | return nil 87 | } 88 | } 89 | set { 90 | switch rangeBoundaryValue?.lowerValue { 91 | case .customValue: 92 | if let newValue = newValue { 93 | rangeBoundaryValue?.lowerValue = .customValue(newValue) 94 | } else { 95 | rangeBoundaryValue = nil 96 | } 97 | case .staticValue: 98 | break 99 | case .boundaryValue(let rangeValue): 100 | if let newValue = newValue { 101 | let newRangeValue = RangeValue( 102 | lowerValue: rangeValue.lowerValue, 103 | upperValue: newValue 104 | ) 105 | rangeBoundaryValue?.lowerValue = .boundaryValue(newRangeValue) 106 | } else { 107 | rangeBoundaryValue = nil 108 | } 109 | case .none: 110 | break 111 | } 112 | } 113 | } 114 | 115 | /// Maximum value in the visible range. 116 | public var maxValue: CGFloat? { 117 | get { 118 | switch rangeBoundaryValue?.upperValue { 119 | case .customValue(let value): 120 | return value 121 | case .staticValue(let value): 122 | return value 123 | case .boundaryValue(let rangeValue): 124 | return max(rangeValue.lowerValue, rangeValue.upperValue) 125 | case .none: 126 | return nil 127 | } 128 | } 129 | set { 130 | switch rangeBoundaryValue?.upperValue { 131 | case .customValue: 132 | if let newValue = newValue { 133 | rangeBoundaryValue?.upperValue = .customValue(newValue) 134 | } else { 135 | rangeBoundaryValue = nil 136 | } 137 | case .staticValue: 138 | break 139 | case .boundaryValue(let rangeValue): 140 | if let newValue = newValue { 141 | let newRangeValue = RangeValue( 142 | lowerValue: rangeValue.lowerValue, 143 | upperValue: newValue 144 | ) 145 | rangeBoundaryValue?.upperValue = .boundaryValue(newRangeValue) 146 | } else { 147 | rangeBoundaryValue = nil 148 | } 149 | case .none: 150 | break 151 | } 152 | } 153 | } 154 | 155 | /// Absolute value max value - min value in the visible range. 156 | private var boundaryDiff: CGFloat? { 157 | guard let maxValue = maxValue, 158 | let minValue = minValue, 159 | maxValue != minValue else { return nil } 160 | return abs(maxValue - minValue) 161 | } 162 | 163 | /// Lower index in the visible range. 164 | private var lowerIndexValue: CGFloat? { 165 | guard let segmentsCount = segmentsCount else { return nil } 166 | return CGFloat(segmentsCount) * rangeValue.lowerValue 167 | } 168 | 169 | /// Upper index in the visible range. 170 | private var upperIndexValue: CGFloat? { 171 | guard let segmentsCount = segmentsCount else { return nil } 172 | return CGFloat(segmentsCount) * rangeValue.upperValue 173 | } 174 | 175 | /// Visible range of values. 176 | private var visibleValuesInRange: [[CGFloat]] { 177 | guard let rangeIndex = rangeIndex, 178 | rangeIndex.lowerValue >= 0, 179 | rangeIndex.lowerValue <= rangeIndex.upperValue, 180 | rangeIndex.upperValue < valuesCount else { 181 | return [[]] 182 | } 183 | return data.map { Array($0.values[rangeIndex.lowerValue ... rangeIndex.upperValue]) } 184 | } 185 | } 186 | 187 | extension CalculatorImpl: Calculator { 188 | 189 | public func makeChartsPoints( 190 | frame: CGRect, 191 | chartMargins: UIEdgeInsets, 192 | minValue: CGFloat?, 193 | maxValue: CGFloat? 194 | ) -> [[CGPoint]] { 195 | guard valuesCount != 0, 196 | let minValue = minValue ?? self.minValue, 197 | let maxValue = maxValue ?? self.maxValue, 198 | maxValue != minValue else { 199 | return [] 200 | } 201 | 202 | let boundaryDiff = abs(maxValue - minValue) 203 | 204 | guard boundaryDiff > 0 else { 205 | return [] 206 | } 207 | 208 | let height = frame.height 209 | - chartMargins.top 210 | - chartMargins.bottom 211 | let y = frame.minY 212 | + chartMargins.bottom 213 | 214 | guard valuesCount > 1 else { 215 | return data.map { 216 | let y = y + ($0.values[0] - minValue) / boundaryDiff * height 217 | let startPoint = CGPoint( 218 | x: frame.minX, 219 | y: y 220 | ) 221 | let endPoint = CGPoint( 222 | x: frame.maxX, 223 | y: y 224 | ) 225 | return [startPoint, endPoint] 226 | } 227 | } 228 | 229 | guard let rangeIndex = rangeIndex, 230 | let upperIndexValue = upperIndexValue, 231 | let lowerIndexValue = lowerIndexValue, 232 | upperIndexValue > lowerIndexValue else { 233 | return [] 234 | } 235 | 236 | let segmentWidth = frame.width / (upperIndexValue - lowerIndexValue) 237 | let leftShift = segmentWidth * (lowerIndexValue - CGFloat(rangeIndex.lowerValue)) 238 | 239 | return visibleValuesInRange.map { 240 | $0.enumerated().map { 241 | CGPoint( 242 | x: frame.minX 243 | + segmentWidth * CGFloat($0.offset) 244 | - leftShift, 245 | y: y + ($0.element - minValue) / boundaryDiff * height 246 | ) 247 | } 248 | } 249 | } 250 | 251 | public func makeRangeValue( 252 | deltaLocation: CGFloat, 253 | frameWidth: CGFloat 254 | ) -> RangeValue? { 255 | guard data.first?.values.count ?? 0 > 1, 256 | deltaLocation != 0, 257 | frameWidth > 0 else { return nil } 258 | 259 | var lowerValue = rangeValue.lowerValue 260 | var upperValue = rangeValue.upperValue 261 | let diff = upperValue - lowerValue 262 | 263 | var deltaValue = (-deltaLocation / frameWidth) * diff 264 | 265 | guard deltaValue > 0 && upperValue < 1.0 266 | || deltaValue < 0 && lowerValue > 0.0 else { 267 | return nil 268 | } 269 | 270 | if lowerValue + deltaValue < 0 { 271 | deltaValue = -lowerValue 272 | } 273 | 274 | if upperValue + deltaValue > 1 { 275 | deltaValue = 1.0 - upperValue 276 | } 277 | 278 | lowerValue += deltaValue 279 | upperValue += deltaValue 280 | 281 | return RangeValue(lowerValue: lowerValue, upperValue: upperValue) 282 | } 283 | 284 | public func makeRangeValue( 285 | scale: CGFloat 286 | ) -> RangeValue? { 287 | guard data[0].values.count > 1, 288 | visibleValuesInRange[0].count > configuration.minimumCountVisibleValues || scale < 1 else { return nil } 289 | 290 | var lowerValue = rangeValue.lowerValue 291 | var upperValue = rangeValue.upperValue 292 | let diff = upperValue - lowerValue 293 | 294 | let deltaValue = (1 - scale) * diff 295 | 296 | lowerValue = boundValue(lowerValue - deltaValue, toLowerValue: 0.0, upperValue: 1.0) 297 | upperValue = boundValue(upperValue + deltaValue, toLowerValue: 0.0, upperValue: 1.0) 298 | 299 | return RangeValue(lowerValue: lowerValue, upperValue: upperValue) 300 | } 301 | 302 | public func setRangeValue( 303 | rangeValue: RangeValue 304 | ) { 305 | guard rangeValue.lowerValue < rangeValue.upperValue, 306 | let rangeIndex = rangeIndex, 307 | let segmentsCount = segmentsCount else { return } 308 | 309 | self.rangeValue = rangeValue 310 | 311 | let oldLowerIndex = rangeIndex.lowerValue 312 | let oldUpperIndex = rangeIndex.upperValue 313 | let newLowerIndex = makeLowerIndex(rangeValue.lowerValue, segmentsCount: segmentsCount) 314 | let newUpperIndex = makeUpperIndex(rangeValue.upperValue, segmentsCount: segmentsCount) 315 | var needFindBoundaryValues: Bool = false 316 | 317 | if newLowerIndex < oldLowerIndex { 318 | if let boundaryValues = findBoundaryValues(data.map { Array($0.values[newLowerIndex ..< oldLowerIndex]) }) { 319 | setBoundaryValuesIfNeeded(boundaryValues) 320 | } 321 | } 322 | if newLowerIndex > oldLowerIndex { 323 | if let boundaryValues = findBoundaryValues(data.map { Array($0.values[oldLowerIndex ..< newLowerIndex]) }), 324 | needFindNewBoundaryValues(boundaryValues) { 325 | needFindBoundaryValues = true 326 | } 327 | } 328 | self.rangeIndex?.lowerValue = newLowerIndex 329 | 330 | if newUpperIndex > oldUpperIndex { 331 | if let boundaryValues = findBoundaryValues( 332 | data.map { Array($0.values[oldUpperIndex + 1 ... newUpperIndex]) } 333 | ) { 334 | setBoundaryValuesIfNeeded(boundaryValues) 335 | } 336 | } 337 | if newUpperIndex < oldUpperIndex { 338 | if let boundaryValues = findBoundaryValues( 339 | data.map { Array($0.values[newUpperIndex + 1 ... oldUpperIndex]) } 340 | ), 341 | needFindNewBoundaryValues(boundaryValues) { 342 | needFindBoundaryValues = true 343 | } 344 | } 345 | self.rangeIndex?.upperValue = newUpperIndex 346 | 347 | if needFindBoundaryValues { 348 | if let boundaryValues = findBoundaryValues(visibleValuesInRange) { 349 | setBoundaryValues(boundaryValues) 350 | } 351 | } 352 | } 353 | 354 | public func calculateLineWidth( 355 | index: Int, 356 | minLineWidth: CGFloat, 357 | maxLineWidth: CGFloat 358 | ) -> CGFloat { 359 | guard visibleValuesInRange.indices.contains(index) else { 360 | return 0 361 | } 362 | let valuesInRangeCount = visibleValuesInRange[index].count 363 | let rangeReduction = configuration.rangeLineWidthReduction 364 | switch valuesInRangeCount { 365 | case 0 ..< rangeReduction.lowerBound: 366 | return maxLineWidth 367 | case rangeReduction.lowerBound ..< rangeReduction.upperBound: 368 | let rangeCount = CGFloat(rangeReduction.count) 369 | let factor = CGFloat(valuesInRangeCount - rangeReduction.lowerBound) 370 | / rangeCount 371 | let cubicEaseOutFactor = cubicEaseOut(factor) 372 | return maxLineWidth - (maxLineWidth - minLineWidth) * cubicEaseOutFactor 373 | default: 374 | return minLineWidth 375 | } 376 | } 377 | 378 | public func makeXAxis( 379 | frame: CGRect, 380 | leftInset: CGFloat, 381 | rightInset: CGFloat, 382 | minCount: Int, 383 | zoomFactor: CGFloat 384 | ) -> [ChartXAxisValue] { 385 | let minCount = min(minCount, valuesCount) 386 | guard minCount > 0, 387 | let segmentsCount = segmentsCount else { 388 | return [] 389 | } 390 | 391 | guard minCount > 1 else { 392 | return [ 393 | ChartXAxisValue( 394 | position: CGPoint(x: frame.width * 0.5 + frame.minX, y: frame.minY), 395 | date: data[0].dates[0] 396 | ), 397 | ] 398 | } 399 | 400 | let diff = rangeValue.upperValue - rangeValue.lowerValue 401 | guard diff > 0 else { 402 | return [] 403 | } 404 | 405 | let zoomCount = CGFloat(minCount) / diff 406 | let zoomPower = floor(log2(floor(zoomCount * zoomFactor))) - 2 407 | let totalCount = max(minCount, (minCount - 1) * Int(pow(2, zoomPower)) + 1) 408 | guard totalCount > 1 else { 409 | return [] 410 | } 411 | 412 | let width = frame.width / diff 413 | let segmentWidth = width / CGFloat(segmentsCount) 414 | let labelWidth: CGFloat = 40.0 // TODO: Need remove constant or rewrite algorithm 415 | let fillWidth = labelWidth * CGFloat(totalCount) + leftInset + rightInset 416 | let spaceWidth = (width - fillWidth) / CGFloat(totalCount - 1) 417 | let lowerPositionX = width * rangeValue.lowerValue 418 | let upperPositionX = width * rangeValue.upperValue 419 | 420 | let elementWidth = labelWidth + spaceWidth 421 | let lowerIndexPosition = (lowerPositionX - labelWidth - leftInset - labelWidth * 0.5) / elementWidth 422 | let upperIndexPosition = (upperPositionX - leftInset + labelWidth * 0.5) / elementWidth 423 | var lowerXAxisIndex = max(0, Int(floor(lowerIndexPosition))) 424 | var upperXAxisIndex = min(totalCount, Int(ceil(upperIndexPosition))) 425 | 426 | if lowerXAxisIndex >= upperXAxisIndex { 427 | lowerXAxisIndex = 0 428 | upperXAxisIndex = totalCount 429 | } 430 | 431 | let values = (lowerXAxisIndex ..< upperXAxisIndex).compactMap { 432 | makeXAxisValue( 433 | index: $0, 434 | frame: frame, 435 | leftInset: leftInset, 436 | lowerPositionX: lowerPositionX, 437 | labelWidth: labelWidth, 438 | spaceWidth: spaceWidth, 439 | segmentWidth: segmentWidth 440 | ) 441 | } 442 | return values 443 | } 444 | 445 | public func makeYAxisLines( 446 | frame: CGRect, 447 | labelHeight: CGFloat, 448 | labelInsets: UIEdgeInsets, 449 | linesCount: Int 450 | ) -> [(start: CGPoint, end: CGPoint)] { 451 | guard valuesCount > 0 else { 452 | return [] 453 | } 454 | 455 | guard linesCount > 1 else { 456 | assertionFailure("Amount of lines should be more then 1") 457 | return [] 458 | } 459 | 460 | let frameHeight = frame.height 461 | - labelHeight 462 | - labelInsets.top 463 | - labelInsets.bottom 464 | let segmentHeight = frameHeight / CGFloat(linesCount - 1) 465 | return (0 ..< linesCount).map { 466 | let y = frame.minY + segmentHeight * CGFloat($0) 467 | return ( 468 | start: CGPoint(x: frame.minX, y: y), 469 | end: CGPoint(x: frame.maxX, y: y) 470 | ) 471 | } 472 | } 473 | 474 | public func makeYAxisValues( 475 | frame: CGRect, 476 | chartMargins: UIEdgeInsets, 477 | labelHeight: CGFloat, 478 | labelInsets: UIEdgeInsets, 479 | linesCount: Int 480 | ) -> [(position: CGPoint, value: CGFloat)] { 481 | guard linesCount > 1 else { 482 | assertionFailure("Amount of lines should be more then 1") 483 | return [] 484 | } 485 | 486 | let chartMargin = chartMargins.bottom + chartMargins.top 487 | let chartHeight = frame.height - chartMargin 488 | guard chartHeight > 0, 489 | let boundaryDiff = self.boundaryDiff else { 490 | return [] 491 | } 492 | 493 | let labelAbsoluteHeight = labelHeight + labelInsets.top + labelInsets.bottom 494 | let alternativeBoundaryDiff = boundaryDiff * (1 + (chartMargin - labelAbsoluteHeight) / chartHeight) 495 | let alternativeMinValue = minValue ?? 0 * (1 + (chartMargin - labelAbsoluteHeight) / chartHeight) 496 | 497 | let frameHeight = frame.height - labelAbsoluteHeight 498 | let segmentHeight = frameHeight / CGFloat(linesCount - 1) 499 | let x = frame.minX + labelInsets.left 500 | return (0 ..< linesCount).map { 501 | let y = segmentHeight * CGFloat($0) 502 | return ( 503 | position: CGPoint(x: x, y: y + frame.minY + labelInsets.bottom), 504 | value: (y / frameHeight) * alternativeBoundaryDiff + alternativeMinValue 505 | ) 506 | } 507 | } 508 | 509 | public func makeZeroLine( 510 | frame: CGRect, 511 | chartMargins: UIEdgeInsets, 512 | minValue: CGFloat?, 513 | maxValue: CGFloat? 514 | ) -> (start: CGPoint, end: CGPoint)? { 515 | guard valuesCount != 0, 516 | let minValue = minValue ?? self.minValue, 517 | let maxValue = maxValue ?? self.maxValue, 518 | maxValue != minValue else { 519 | return nil 520 | } 521 | 522 | let boundaryDiff = abs(maxValue - minValue) 523 | 524 | guard boundaryDiff > 0 else { 525 | return nil 526 | } 527 | 528 | let height = frame.height 529 | - chartMargins.top 530 | - chartMargins.bottom 531 | let y = frame.minY 532 | + chartMargins.bottom 533 | - minValue / boundaryDiff * height 534 | 535 | return ( 536 | start: CGPoint(x: frame.minX, y: y), 537 | end: CGPoint(x: frame.maxX, y: y) 538 | ) 539 | } 540 | 541 | public func makeRangeDates() -> (start: Date, end: Date?)? { 542 | guard let start = data.first?.dates.first else { 543 | return nil 544 | } 545 | 546 | guard let end = data.first?.dates.last, 547 | end != start else { 548 | return (start: start, end: nil) 549 | } 550 | 551 | return (start: start, end: end) 552 | } 553 | 554 | public func makeDefinition( 555 | frame: CGRect, 556 | chartMargins: UIEdgeInsets, 557 | definitionPosition: CGPoint, 558 | minValue: CGFloat?, 559 | maxValue: CGFloat? 560 | ) -> ChartDefinitionValues? { 561 | guard let minValue = minValue ?? self.minValue, 562 | let maxValue = maxValue ?? self.maxValue, 563 | maxValue != minValue else { 564 | return nil 565 | } 566 | 567 | let boundaryDiff = abs(maxValue - minValue) 568 | 569 | guard let segmentsCount = segmentsCount, 570 | boundaryDiff > 0 else { 571 | return nil 572 | } 573 | 574 | let height = frame.height 575 | - chartMargins.top 576 | - chartMargins.bottom 577 | let y = frame.minY 578 | + chartMargins.bottom 579 | 580 | guard segmentsCount > 0 else { 581 | let positionX = frame.minX 582 | + frame.width * 0.5 583 | let lineStartPosition = CGPoint( 584 | x: positionX, 585 | y: frame.minY 586 | ) 587 | let lineEndPosition = CGPoint( 588 | x: positionX, 589 | y: frame.maxY 590 | ) 591 | 592 | let chartValues: [(value: CGFloat, pointPosition: CGPoint)] = data.map { 593 | let value = $0.values[0] 594 | let pointPosition = CGPoint( 595 | x: frame.minX + frame.width * 0.5, 596 | y: y + (value - minValue) / boundaryDiff * height 597 | ) 598 | return ( 599 | value: value, 600 | pointPosition: pointPosition 601 | ) 602 | } 603 | return ChartDefinitionValues( 604 | date: data[0].dates[0], 605 | linePosition: (start: lineStartPosition, end: lineEndPosition), 606 | chartValues: chartValues 607 | ) 608 | } 609 | 610 | let diff = rangeValue.upperValue - rangeValue.lowerValue 611 | guard diff > 0 else { 612 | return nil 613 | } 614 | 615 | let width = frame.width / diff 616 | guard width > 0 else { 617 | return nil 618 | } 619 | 620 | guard let upperIndexValue = upperIndexValue, 621 | let lowerIndexValue = lowerIndexValue, 622 | upperIndexValue > lowerIndexValue else { 623 | return nil 624 | } 625 | 626 | let segmentWidth = frame.width / (upperIndexValue - lowerIndexValue) 627 | let lowerPositionX = width * rangeValue.lowerValue 628 | let actualPositionX = lowerPositionX + definitionPosition.x 629 | let valueIndex = Int(round(actualPositionX / width * CGFloat(segmentsCount))) 630 | let positionX = frame.minX 631 | + segmentWidth * CGFloat(valueIndex) 632 | - width * rangeValue.lowerValue 633 | let lineStartPosition = CGPoint( 634 | x: positionX, 635 | y: frame.minY 636 | ) 637 | let lineEndPosition = CGPoint( 638 | x: positionX, 639 | y: frame.maxY 640 | ) 641 | 642 | let chartValues: [(value: CGFloat, pointPosition: CGPoint)] = data.map { 643 | let value = $0.values[valueIndex] 644 | let pointPosition = CGPoint( 645 | x: positionX, 646 | y: y + (value - minValue) / boundaryDiff * height 647 | ) 648 | return ( 649 | value: value, 650 | pointPosition: pointPosition 651 | ) 652 | } 653 | 654 | return ChartDefinitionValues( 655 | date: data[0].dates[valueIndex], 656 | linePosition: (start: lineStartPosition, end: lineEndPosition), 657 | chartValues: chartValues 658 | ) 659 | } 660 | 661 | public func isAlwaysDrawDefinition() -> Bool { 662 | return valuesCount == 1 663 | } 664 | 665 | public func isNeedDrawChart() -> Bool { 666 | return valuesCount > 0 667 | } 668 | 669 | /// A new minimum or maximum to be found in the case 670 | /// of adding or deleting points from the current minimum or maximum. 671 | private func needFindNewBoundaryValues( 672 | _ boundaryValues: RangeValue 673 | ) -> Bool { 674 | return boundaryValues.lowerValue == minValue || boundaryValues.upperValue == maxValue 675 | } 676 | 677 | /// Set new boundary values for maximum and minimum. 678 | private func setBoundaryValues( 679 | _ boundaryValues: RangeValue 680 | ) { 681 | minValue = boundaryValues.lowerValue 682 | maxValue = boundaryValues.upperValue 683 | } 684 | 685 | /// Set new boundary values for maximum and minimum if necessary. 686 | private func setBoundaryValuesIfNeeded( 687 | _ boundaryValues: RangeValue 688 | ) { 689 | if let minValue = minValue { 690 | if boundaryValues.lowerValue < minValue { 691 | self.minValue = boundaryValues.lowerValue 692 | } 693 | } else { 694 | self.minValue = boundaryValues.lowerValue 695 | } 696 | 697 | if let maxValue = maxValue { 698 | if boundaryValues.upperValue > maxValue { 699 | self.maxValue = boundaryValues.upperValue 700 | } 701 | } else { 702 | self.maxValue = boundaryValues.upperValue 703 | } 704 | } 705 | 706 | /// Make x axis value with a position on the x axis and a value at that point 707 | private func makeXAxisValue( 708 | index: Int, 709 | frame: CGRect, 710 | leftInset: CGFloat, 711 | lowerPositionX: CGFloat, 712 | labelWidth: CGFloat, 713 | spaceWidth: CGFloat, 714 | segmentWidth: CGFloat 715 | ) -> ChartXAxisValue? { 716 | guard segmentWidth > 0 else { 717 | assertionFailure("Segment width should be more then 0") 718 | return nil 719 | } 720 | let positionX = leftInset 721 | + CGFloat(index) * (labelWidth + spaceWidth) 722 | - lowerPositionX 723 | + labelWidth * 0.5 724 | let positionY = frame.minY 725 | let position = CGPoint(x: positionX, y: positionY) 726 | let currentIndex = Int(round((positionX + lowerPositionX) / segmentWidth)) 727 | 728 | return ChartXAxisValue( 729 | position: position, 730 | date: data[0].dates[currentIndex] 731 | ) 732 | } 733 | } 734 | 735 | // MARK: - Private scope 736 | 737 | /// Make boundary values based on values and static value state 738 | private func makeBoundaryValues( 739 | values: [[CGFloat]], 740 | minStaticValue: StaticValueState?, 741 | maxStaticValue: StaticValueState? 742 | ) -> RangeValue? { 743 | guard let boundaryValues = findBoundaryValues(values) else { return nil } 744 | 745 | let minCustomValue = values[0].count == 1 746 | ? makeMinCustomValue(from: values.map { $0[0] }.min() ?? 1) 747 | : boundaryValues.lowerValue 748 | let minBoundaryValue = makeBoundaryValue( 749 | from: minStaticValue, 750 | customValue: minCustomValue 751 | ) 752 | 753 | let maxCustomValue = values[0].count == 1 754 | ? makeMaxCustomValue(from: values.map { $0[0] }.max() ?? 1) 755 | : boundaryValues.upperValue 756 | let maxBoundaryValue = makeBoundaryValue( 757 | from: maxStaticValue, 758 | customValue: maxCustomValue 759 | ) 760 | 761 | return RangeValue( 762 | lowerValue: minBoundaryValue, 763 | upperValue: maxBoundaryValue 764 | ) 765 | } 766 | 767 | private func makeMinCustomValue( 768 | from value: CGFloat 769 | ) -> CGFloat { 770 | if value > 0 { 771 | return 0 772 | } else if value < 0 { 773 | return value * 2 774 | } else { 775 | return 0 776 | } 777 | } 778 | 779 | private func makeMaxCustomValue( 780 | from value: CGFloat 781 | ) -> CGFloat { 782 | if value > 0 { 783 | return value * 2 784 | } else if value < 0 { 785 | return 0 786 | } else { 787 | return 1 788 | } 789 | } 790 | 791 | /// Make boundary value based on static value state and custom value. 792 | private func makeBoundaryValue( 793 | from staticValue: StaticValueState?, 794 | customValue: CGFloat 795 | ) -> BoundaryValue { 796 | let boundaryValue: BoundaryValue 797 | switch staticValue { 798 | case .none: 799 | boundaryValue = .customValue(customValue) 800 | case .default: 801 | boundaryValue = .staticValue(customValue) 802 | case .customValue(let value): 803 | boundaryValue = .staticValue(value) 804 | case .boundaryValue(let value): 805 | let rangeValue = RangeValue( 806 | lowerValue: value, 807 | upperValue: customValue 808 | ) 809 | boundaryValue = .boundaryValue(rangeValue) 810 | } 811 | return boundaryValue 812 | } 813 | 814 | /// Find the boundary values of maximum and minimum in the data set. 815 | private func findBoundaryValues( 816 | _ arrayValues: [[CGFloat]] 817 | ) -> RangeValue? { 818 | guard arrayValues.isEmpty == false, 819 | arrayValues[0].isEmpty == false else { return nil } 820 | 821 | var min = arrayValues[0][0] 822 | var max = arrayValues[0][0] 823 | 824 | for values in arrayValues { 825 | for value in values { 826 | if value < min { 827 | min = value 828 | } 829 | if value > max { 830 | max = value 831 | } 832 | } 833 | } 834 | 835 | return RangeValue(lowerValue: min, upperValue: max) 836 | } 837 | 838 | /// Make lower index. 839 | private func makeLowerIndex( 840 | _ value: CGFloat, 841 | segmentsCount: Int 842 | ) -> Int { 843 | return makeIndex( 844 | value: value, 845 | segmentsCount: CGFloat(segmentsCount), 846 | roundRule: .down 847 | ) 848 | } 849 | 850 | /// Make upper index. 851 | private func makeUpperIndex( 852 | _ value: CGFloat, 853 | segmentsCount: Int 854 | ) -> Int { 855 | return makeIndex( 856 | value: value, 857 | segmentsCount: CGFloat(segmentsCount), 858 | roundRule: .up 859 | ) 860 | } 861 | 862 | /// Make index. 863 | private func makeIndex( 864 | value: CGFloat, 865 | segmentsCount: CGFloat, 866 | roundRule: FloatingPointRoundingRule 867 | ) -> Int { 868 | let index = (segmentsCount * value).rounded(roundRule) 869 | return Int(index) 870 | } 871 | 872 | /// Make bound value based on lower value and upper value. 873 | private func boundValue( 874 | _ value: CGFloat, 875 | toLowerValue lowerValue: CGFloat, 876 | upperValue: CGFloat 877 | ) -> CGFloat { 878 | return min(max(value, lowerValue), upperValue) 879 | } 880 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/BoundaryValue.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Type of boundary value. 4 | public enum BoundaryValue { 5 | 6 | /// Cusom value. Boundary value can change. 7 | case customValue(CGFloat) 8 | 9 | /// Static value. Boundary value can't change and always has a static value. 10 | case staticValue(CGFloat) 11 | 12 | /// Boundary value. 13 | /// For minimum boundary value = min(boundaryValue, minValue) 14 | /// For maximum boundary value = max(boundaryValue, maxValue) 15 | case boundaryValue(RangeValue) 16 | } 17 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/ChartData.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | /// Type of chart data. 5 | public struct ChartData { 6 | 7 | /// Chart value set. 8 | public let values: [CGFloat] 9 | 10 | /// Chart date set. 11 | public let dates: [Date] 12 | 13 | /// Creates instance of `ChartData`. 14 | /// 15 | /// - Parameters: 16 | /// - values: Chart value set. 17 | /// - dates: Chart date set. 18 | /// 19 | /// - Returns: Instance of `ChartData`. 20 | public init( 21 | values: [CGFloat], 22 | dates: [Date] 23 | ) { 24 | self.values = values 25 | self.dates = dates 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/ChartDefinitionValue.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | /// Type of chart definition value. 5 | public struct ChartDefinitionValues { 6 | 7 | /// Date for definition. 8 | public let date: Date 9 | 10 | /// Definition line position. 11 | public let linePosition: (start: CGPoint, end: CGPoint) 12 | 13 | /// Charts values and point position. 14 | public let chartValues: [(value: CGFloat, pointPosition: CGPoint)] 15 | 16 | /// Creates instance of `ChartDefinitionValues`. 17 | /// 18 | /// - Parameters: 19 | /// - date: Date in definition position. 20 | /// - linePosition: Definition line position. 21 | /// - chartValues: Charts values and point position. 22 | /// 23 | /// - Returns: Instance of `ChartDefinitionValues`. 24 | public init( 25 | date: Date, 26 | linePosition: (start: CGPoint, end: CGPoint), 27 | chartValues: [(value: CGFloat, pointPosition: CGPoint)] 28 | ) { 29 | self.date = date 30 | self.linePosition = linePosition 31 | self.chartValues = chartValues 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/ChartXAxisValue.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | /// Type of chart x axis value. 5 | public struct ChartXAxisValue { 6 | let position: CGPoint 7 | let date: Date 8 | } 9 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/RangeValue.swift: -------------------------------------------------------------------------------- 1 | /// Type of range value. 2 | public struct RangeValue { 3 | 4 | /// Lower value. 5 | public var lowerValue: T 6 | 7 | /// Upper value. 8 | public var upperValue: T 9 | 10 | /// Creates instance of `RangeValue`. 11 | /// 12 | /// - Parameters: 13 | /// - lowerValue: Lower value. 14 | /// - upperValue: Upper value. 15 | /// 16 | /// - Returns: Instance of `RangeValue`. 17 | public init( 18 | lowerValue: T, 19 | upperValue: T 20 | ) { 21 | self.lowerValue = lowerValue 22 | self.upperValue = upperValue 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Calculator/Models/StaticValueState.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Type of static value state. 4 | public enum StaticValueState { 5 | 6 | /// Static value will depend on the data set. 7 | case `default` 8 | 9 | /// Static value will have a specific value. 10 | case customValue(CGFloat) 11 | 12 | /// Static value will have boundary value. 13 | case boundaryValue(CGFloat) 14 | } 15 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Factories/AnalyticsDefinition/AnalyticsDefinitionFactory.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Protocol of analytics definition factory. 4 | public protocol AnalyticsDefinitionFactory { 5 | 6 | /// Make definition text from value and unit. 7 | func makeDefinitionText( 8 | _ value: CGFloat, 9 | unit: AnalyticsUnit 10 | ) -> String? 11 | } 12 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Factories/AnalyticsDefinition/AnalyticsDefinitionFactoryImpl.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Implementation of analytics definition factory. 4 | final public class AnalyticsDefinitionFactoryImpl { 5 | 6 | private lazy var monetaryAmountFormatter: MonetaryAmountFormatter = { 7 | let formatter = MonetaryAmountFormatter() 8 | return formatter 9 | }() 10 | 11 | /// Creates instance of `AnalyticsDefinitionFactoryImpl`. 12 | /// 13 | /// - Returns: Instance of `AnalyticsDefinitionFactoryImpl`. 14 | public init() {} 15 | } 16 | 17 | // MARK: - AnalyticsDefinitionFactory 18 | 19 | extension AnalyticsDefinitionFactoryImpl: AnalyticsDefinitionFactory { 20 | public func makeDefinitionText( 21 | _ value: CGFloat, 22 | unit: AnalyticsUnit 23 | ) -> String? { 24 | let text: String? 25 | switch unit { 26 | case .quantity: 27 | text = "\(Int(value))" 28 | case .currency(let currencyCode): 29 | text = monetaryAmountFormatter.format(value, currencySymbol: currencyCode.currencySymbol) 30 | case .unknown: 31 | assertionFailure("AnalyticsUnit shouldn't be unknown") 32 | text = nil 33 | } 34 | return text 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Factories/AnalyticsYAxisLocalization/AnalyticsYAxisLocalizationFactory.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// Protocol of analytics y axis localization factory. 4 | public protocol AnalyticsYAxisLocalizationFactory { 5 | 6 | /// Make y axis text from value. 7 | func makeYAxisText( 8 | _ value: CGFloat 9 | ) -> String 10 | } 11 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Factories/AnalyticsYAxisLocalization/AnalyticsYAxisLocalizationFactoryImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Implementation of analytics y axis localization factory. 4 | final public class AnalyticsYAxisLocalizationFactoryImpl {} 5 | 6 | // MARK: - AnalyticsYAxisLocalizationFactory 7 | 8 | extension AnalyticsYAxisLocalizationFactoryImpl: AnalyticsYAxisLocalizationFactory { 9 | public func makeYAxisText( 10 | _ value: CGFloat 11 | ) -> String { 12 | let text: String 13 | switch abs(value) { 14 | case Constants.thousandRange: 15 | let rangeValue = value / Constants.thousandRange.lowerBound 16 | let format = makeCorrectFormat(value: rangeValue) 17 | text = String( 18 | format: format + " %@", 19 | rangeValue, 20 | Localized.Text.thousand 21 | ) 22 | case Constants.millionRange: 23 | let rangeValue = value / Constants.millionRange.lowerBound 24 | let format = makeCorrectFormat(value: rangeValue) 25 | text = String( 26 | format: format + " %@", 27 | rangeValue, 28 | Localized.Text.million 29 | ) 30 | case Constants.billionRange: 31 | let rangeValue = value / Constants.billionRange.lowerBound 32 | let format = makeCorrectFormat(value: rangeValue) 33 | text = String( 34 | format: format + " %@", 35 | rangeValue, 36 | Localized.Text.billion 37 | ) 38 | default: 39 | let rangeValue = value 40 | let format = makeCorrectFormat(value: rangeValue) 41 | text = String( 42 | format: format, 43 | rangeValue 44 | ) 45 | } 46 | return text 47 | } 48 | } 49 | 50 | // MARK: - Localized 51 | 52 | private extension AnalyticsYAxisLocalizationFactoryImpl { 53 | enum Localized { 54 | // swiftlint:disable line_length 55 | enum Text { 56 | static let thousand = NSLocalizedString( 57 | "AnalyticsYAxis.Text.Thousand", 58 | value: "тыс.", 59 | comment: "Text для сокращения тысяч на оси Y" 60 | ) 61 | static let million = NSLocalizedString( 62 | "AnalyticsYAxis.Text.Million", 63 | value: "млн.", 64 | comment: "Text для сокращения миллионов на оси Y" 65 | ) 66 | static let billion = NSLocalizedString( 67 | "AnalyticsYAxis.Text.Billion", 68 | value: "млрд.", 69 | comment: "Text для сокращения миллиардов на оси Y" 70 | ) 71 | } 72 | // swiftlint:enable line_length 73 | } 74 | } 75 | 76 | // MARK: - Constants 77 | 78 | extension AnalyticsYAxisLocalizationFactoryImpl { 79 | private enum Constants { 80 | static let thousandRange: Range = pow(10, 3) ..< pow(10, 6) 81 | static let millionRange: Range = pow(10, 6) ..< pow(10, 9) 82 | static let billionRange: Range = pow(10, 9) ..< .greatestFiniteMagnitude 83 | } 84 | } 85 | 86 | // MARK: - Private scope 87 | 88 | func makeCorrectFormat( 89 | value: CGFloat 90 | ) -> String { 91 | let format: String 92 | let truncValue = CGFloat(round(10 * value) / 10) 93 | let roundedValue = round(value) 94 | if roundedValue != truncValue { 95 | format = "%.1f" 96 | } else { 97 | format = "%.0f" 98 | } 99 | return format 100 | } 101 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Formatters/MonetaryAmountFormatter.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MonetaryAmountFormatter { 4 | 5 | private lazy var numberFormatter: NumberFormatter = { 6 | let formatter = NumberFormatter() 7 | formatter.locale = .current 8 | formatter.numberStyle = .currency 9 | formatter.maximumFractionDigits = 2 10 | return formatter 11 | }() 12 | 13 | func format(_ amount: MonetaryAmount) -> String { 14 | numberFormatter.currencySymbol = amount.currency.currencySymbol 15 | return numberFormatter.string(for: amount.value) ?? "" 16 | } 17 | 18 | func format(_ value: Decimal, currencySymbol: String) -> String { 19 | numberFormatter.currencySymbol = currencySymbol 20 | return numberFormatter.string(for: value) ?? "" 21 | } 22 | 23 | func format(_ value: Double, currencySymbol: String) -> String { 24 | numberFormatter.currencySymbol = currencySymbol 25 | return numberFormatter.string(for: value) ?? "" 26 | } 27 | 28 | func format(_ value: CGFloat, currencySymbol: String) -> String { 29 | numberFormatter.currencySymbol = currencySymbol 30 | return numberFormatter.string(for: value) ?? "" 31 | } 32 | 33 | func format(_ value: String, currencySymbol: String) -> Decimal { 34 | numberFormatter.currencySymbol = currencySymbol 35 | return numberFormatter.number(from: value)?.decimalValue ?? 0 36 | } 37 | 38 | // MARK: - Init 39 | 40 | /// Creates instance of `MonetaryAmountFormatter`. 41 | /// 42 | /// - Returns: Instance of `MonetaryAmountFormatter`. 43 | public init() {} 44 | } 45 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/MobileAnalyticsChartSwift.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for MobileAnalyticsChartSwift. 4 | FOUNDATION_EXPORT double MobileAnalyticsChartSwiftVersionNumber; 5 | 6 | //! Project version string for MobileAnalyticsChartSwift. 7 | FOUNDATION_EXPORT const unsigned char MobileAnalyticsChartSwiftVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Models/AnalyticsUnit.swift: -------------------------------------------------------------------------------- 1 | /// Entity of unit. 2 | public enum AnalyticsUnit: Equatable { 3 | 4 | /// Unit is given as a quantity. 5 | case quantity 6 | 7 | /// Unit is given as a currency. 8 | case currency(CurrencyCode) 9 | 10 | /// Unsupported unit. 11 | case unknown(String) 12 | } 13 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Models/CurrencyCode.swift: -------------------------------------------------------------------------------- 1 | public enum CurrencyCode: RawRepresentable, Codable, Equatable { 2 | case rub 3 | case usd 4 | case eur 5 | case byn 6 | case kzt 7 | case unknown(String) 8 | } 9 | 10 | /// Making currency symbol. 11 | public extension CurrencyCode { 12 | 13 | /// Symbol of currency. 14 | var currencySymbol: String { 15 | let result: String 16 | switch self { 17 | case .rub: 18 | result = "₽" 19 | case .usd: 20 | result = "$" 21 | case .eur: 22 | result = "€" 23 | case .byn: 24 | result = "Br" 25 | case .kzt: 26 | result = "₸" 27 | case .unknown(let value): 28 | result = value 29 | } 30 | return result 31 | } 32 | } 33 | 34 | // MARK: - RawRepresentable. 35 | // swiftlint:disable missing_docs 36 | public extension CurrencyCode { 37 | 38 | private enum _SupportedCurrency: String { 39 | case rub = "RUB" 40 | case usd = "USD" 41 | case eur = "EUR" 42 | case byn = "BYN" 43 | case kzt = "KZT" 44 | } 45 | 46 | /// Typealias RawValue for CurrencyCode. 47 | typealias RawValue = String 48 | 49 | /// Creates instance of `CurrencyCode`. 50 | /// 51 | /// - Parameters: 52 | /// - rawValue: RawValue of CurrencyCode. 53 | /// 54 | /// - Returns: Instance of `CurrencyCode`. 55 | init(rawValue: CurrencyCode.RawValue) { 56 | if let supportedCurrency = _SupportedCurrency(rawValue: rawValue) { 57 | switch supportedCurrency { 58 | case .rub: 59 | self = .rub 60 | case .usd: 61 | self = .usd 62 | case .eur: 63 | self = .eur 64 | case .byn: 65 | self = .byn 66 | case .kzt: 67 | self = .kzt 68 | } 69 | } else { 70 | self = .unknown(rawValue) 71 | } 72 | } 73 | 74 | /// RawValue for CurrencyCode. 75 | var rawValue: CurrencyCode.RawValue { 76 | switch self { 77 | case .rub: 78 | return _SupportedCurrency.rub.rawValue 79 | case .usd: 80 | return _SupportedCurrency.usd.rawValue 81 | case .eur: 82 | return _SupportedCurrency.eur.rawValue 83 | case .byn: 84 | return _SupportedCurrency.byn.rawValue 85 | case .kzt: 86 | return _SupportedCurrency.kzt.rawValue 87 | case .unknown(let value): 88 | return value 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Codable. 94 | 95 | public extension CurrencyCode { 96 | init(from decoder: Decoder) throws { 97 | let container = try decoder.singleValueContainer() 98 | let value = try container.decode(String.self) 99 | self.init(rawValue: value) 100 | } 101 | 102 | func encode(to encoder: Encoder) throws { 103 | var container = encoder.singleValueContainer() 104 | try container.encode(rawValue) 105 | } 106 | } 107 | // swiftlint:enable missing_docs 108 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Models/MonetaryAmount.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Decimal 2 | 3 | public struct MonetaryAmount: Equatable { 4 | 5 | public let value: Decimal 6 | public let currency: CurrencyCode 7 | 8 | public init(value: Decimal, currency: CurrencyCode) { 9 | self.value = value 10 | self.currency = currency 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/AnalyticsChartSpriteKitModuleIO.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// AnalyticsChartSpriteKit module inputData. 4 | public struct AnalyticsChartSpriteKitModuleInputData { 5 | 6 | /// ViewModels of AnalyticsChartSpriteKitViewModel. 7 | public let viewModels: [AnalyticsChartSpriteKitViewModel] 8 | 9 | /// Entity of RenderConfiguration. 10 | public let renderConfiguration: RenderConfiguration 11 | 12 | /// Entity of CalculatorConfiguration. 13 | public let calculatorConfiguration: CalculatorConfiguration 14 | 15 | /// Accessibility identifier. 16 | public let accessibilityIdentifier: String? 17 | 18 | /// Creates instance of `AnalyticsChartSpriteKitModuleInputData`. 19 | /// 20 | /// - Parameters: 21 | /// - viewModels: ViewModels of AnalyticsChartSpriteKitViewModel. 22 | /// - renderConfiguration: Entity of RenderConfiguration. 23 | /// - calculatorConfiguration: Entity of CalculatorConfiguration. 24 | /// - accessibilityIdentifier: Accessibility identifier. 25 | /// 26 | /// - Returns: Instance of `AnalyticsChartSpriteKitModuleInputData`. 27 | public init( 28 | viewModels: [AnalyticsChartSpriteKitViewModel], 29 | renderConfiguration: RenderConfiguration, 30 | calculatorConfiguration: CalculatorConfiguration, 31 | accessibilityIdentifier: String? = nil 32 | ) { 33 | self.viewModels = viewModels 34 | self.renderConfiguration = renderConfiguration 35 | self.calculatorConfiguration = calculatorConfiguration 36 | self.accessibilityIdentifier = accessibilityIdentifier 37 | } 38 | } 39 | 40 | /// AnalyticsChartSpriteKit module input 41 | public protocol AnalyticsChartSpriteKitModuleInput: class { 42 | func setChartViewModels( 43 | viewModels: [AnalyticsChartSpriteKitViewModel], 44 | silent: Bool 45 | ) 46 | 47 | func setRangeValue( 48 | rangeValue: RangeValue 49 | ) 50 | 51 | func setChartLoadingState() 52 | 53 | func setChartIdleState() 54 | } 55 | 56 | /// AnalyticsChartSpriteKit module output 57 | public protocol AnalyticsChartSpriteKitModuleOutput: class { 58 | func didChangeRangeValue( 59 | rangeValue: RangeValue 60 | ) 61 | 62 | func didHandleLongPress() 63 | 64 | func didHandlePan( 65 | deltaLocation: CGFloat 66 | ) 67 | 68 | func didHandlePinch( 69 | scale: CGFloat 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/AnalyticsChartSpriteKitViewIO.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | /// AnalyticsChartSpriteKit view input 4 | protocol AnalyticsChartSpriteKitViewInput: class { 5 | func setScene(scene: SKScene) 6 | } 7 | 8 | /// AnalyticsChartSpriteKit view output 9 | protocol AnalyticsChartSpriteKitViewOutput: class { 10 | func setupView() 11 | 12 | func redraw() 13 | 14 | func traitCollectionDidChange() 15 | } 16 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/Assembly/AnalyticsChartSpriteKitAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// VIPER Assembly of AnalyticsChartSpriteKit module. 4 | public enum AnalyticsChartSpriteKitAssembly { 5 | 6 | /// Make AnalyticsChartSpriteKit module. 7 | public static func makeModule( 8 | inputData: AnalyticsChartSpriteKitModuleInputData, 9 | moduleOutput: AnalyticsChartSpriteKitModuleOutput? = nil 10 | ) -> (UIView, AnalyticsChartSpriteKitModuleInput) { 11 | let view = AnalyticsChartSpriteKitView() 12 | 13 | let analyticsYAxisLocalizationFactory = AnalyticsYAxisLocalizationFactoryImpl() 14 | let analyticsDefinitionFactory = AnalyticsDefinitionFactoryImpl() 15 | let presenter = AnalyticsChartSpriteKitPresenter( 16 | viewModels: inputData.viewModels, 17 | renderConfiguration: inputData.renderConfiguration, 18 | calculatorConfiguration: inputData.calculatorConfiguration, 19 | analyticsYAxisLocalizationFactory: analyticsYAxisLocalizationFactory, 20 | analyticsDefinitionFactory: analyticsDefinitionFactory 21 | ) 22 | 23 | view.output = presenter 24 | 25 | presenter.view = view 26 | presenter.moduleOutput = moduleOutput 27 | 28 | return (view, presenter) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/Presenter/AnalyticsChartSpriteKitPresenter.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// VIPER Presenter of AnalyticsChartSpriteKit module. 4 | final public class AnalyticsChartSpriteKitPresenter { 5 | 6 | // MARK: - VIPER 7 | 8 | weak var view: AnalyticsChartSpriteKitViewInput? 9 | weak var moduleOutput: AnalyticsChartSpriteKitModuleOutput? 10 | 11 | // MARK: - Init data 12 | 13 | private var viewModels: [AnalyticsChartSpriteKitViewModel] 14 | private var renderConfiguration: RenderConfiguration 15 | private var calculatorConfiguration: CalculatorConfiguration 16 | private let analyticsYAxisLocalizationFactory: AnalyticsYAxisLocalizationFactory 17 | private let analyticsDefinitionFactory: AnalyticsDefinitionFactory 18 | 19 | /// Creates instance of `AnalyticsChartSpriteKitPresenter`. 20 | /// 21 | /// - Parameters: 22 | /// - viewModels: ViewModels of AnalyticsChartSpriteKitViewModel. 23 | /// - renderConfiguration: Entity of RenderConfiguration. 24 | /// - calculatorConfiguration: Entity of CalculatorConfiguration. 25 | /// - analyticsYAxisLocalizationFactory: Entity of AnalyticsYAxisLocalizationFactory. 26 | /// - analyticsDefinitionFactory: Entity of AnalyticsDefinitionFactory. 27 | /// 28 | /// - Returns: Instance of `AnalyticsChartSpriteKitPresenter`. 29 | public init( 30 | viewModels: [AnalyticsChartSpriteKitViewModel], 31 | renderConfiguration: RenderConfiguration, 32 | calculatorConfiguration: CalculatorConfiguration, 33 | analyticsYAxisLocalizationFactory: AnalyticsYAxisLocalizationFactory, 34 | analyticsDefinitionFactory: AnalyticsDefinitionFactory 35 | ) { 36 | self.viewModels = viewModels 37 | self.renderConfiguration = renderConfiguration 38 | self.calculatorConfiguration = calculatorConfiguration 39 | self.analyticsYAxisLocalizationFactory = analyticsYAxisLocalizationFactory 40 | self.analyticsDefinitionFactory = analyticsDefinitionFactory 41 | } 42 | 43 | // MARK: - Module input 44 | 45 | private weak var renderDrawerModuleInput: RenderDrawerModuleInput? 46 | 47 | // MARK: - Stored data 48 | 49 | private var scene: RenderSpriteKitImpl? 50 | private var rangeValue: RangeValue? 51 | } 52 | 53 | // MARK: - AnalyticsChartSpriteKitViewOutput 54 | 55 | extension AnalyticsChartSpriteKitPresenter: AnalyticsChartSpriteKitViewOutput { 56 | func setupView() { 57 | guard let view = view else { return } 58 | 59 | let scene = makeScene( 60 | viewModels: viewModels 61 | ) 62 | self.scene = scene 63 | renderDrawerModuleInput = scene 64 | view.setScene(scene: scene) 65 | } 66 | 67 | func redraw() { 68 | guard let view = view, 69 | let scene = scene else { return } 70 | 71 | view.setScene(scene: scene) 72 | } 73 | 74 | func traitCollectionDidChange() { 75 | renderDrawerModuleInput?.setConfiguration(renderConfiguration) 76 | } 77 | 78 | private func makeScene( 79 | viewModels: [AnalyticsChartSpriteKitViewModel] 80 | ) -> RenderSpriteKitImpl { 81 | let calculator = CalculatorImpl( 82 | data: viewModels.map { $0.data }, 83 | configuration: calculatorConfiguration 84 | ) 85 | 86 | let renderSpriteKitImpl = RenderSpriteKitImpl( 87 | chartsConfiguration: viewModels.map { $0.configuration }, 88 | configuration: renderConfiguration, 89 | calculator: calculator, 90 | analyticsYAxisLocalizationFactory: analyticsYAxisLocalizationFactory, 91 | analyticsDefinitionFactory: analyticsDefinitionFactory 92 | ) 93 | renderSpriteKitImpl.moduleOutput = self 94 | return renderSpriteKitImpl 95 | } 96 | } 97 | 98 | // MARK: - RenderDrawerModuleOutput 99 | 100 | extension AnalyticsChartSpriteKitPresenter: RenderDrawerModuleOutput { 101 | public func didChangeRangeValue( 102 | rangeValue: RangeValue 103 | ) { 104 | self.rangeValue = rangeValue 105 | moduleOutput?.didChangeRangeValue(rangeValue: rangeValue) 106 | } 107 | 108 | public func didHandleLongPress() { 109 | moduleOutput?.didHandleLongPress() 110 | } 111 | 112 | public func didHandlePan( 113 | deltaLocation: CGFloat 114 | ) { 115 | moduleOutput?.didHandlePan(deltaLocation: deltaLocation) 116 | } 117 | 118 | public func didHandlePinch( 119 | scale: CGFloat 120 | ) { 121 | moduleOutput?.didHandlePinch(scale: scale) 122 | } 123 | } 124 | 125 | // MARK: - AnalyticsChartSpriteKitModuleInput 126 | 127 | extension AnalyticsChartSpriteKitPresenter: AnalyticsChartSpriteKitModuleInput { 128 | public func setChartViewModels( 129 | viewModels: [AnalyticsChartSpriteKitViewModel], 130 | silent: Bool 131 | ) { 132 | self.viewModels = viewModels 133 | 134 | guard !viewModels.isEmpty else { 135 | let calculator = makeCalculator(data: viewModels.map { $0.data }) 136 | renderDrawerModuleInput?.setCalculator(calculator) 137 | return 138 | } 139 | 140 | renderDrawerModuleInput?.stopFade() 141 | renderDrawerModuleInput?.fadeOutChart() 142 | 143 | renderDrawerModuleInput?.setChartsConfiguration( 144 | viewModels.map { $0.configuration } 145 | ) 146 | 147 | renderDrawerModuleInput?.fadeInChart() 148 | 149 | if silent { 150 | let calculator = CalculatorImpl( 151 | data: viewModels.map { $0.data }, 152 | configuration: CalculatorConfiguration( 153 | minStaticValue: calculatorConfiguration.minStaticValue, 154 | maxStaticValue: calculatorConfiguration.maxStaticValue, 155 | initialLowerValue: rangeValue?.lowerValue ?? 0.0, 156 | initialUpperValue: rangeValue?.upperValue ?? 1.0 157 | ) 158 | ) 159 | renderDrawerModuleInput?.setCalculator(calculator) 160 | } else { 161 | let calculator = makeCalculator(data: viewModels.map { $0.data }) 162 | renderDrawerModuleInput?.setCalculator(calculator) 163 | } 164 | } 165 | 166 | public func setChartLoadingState() { 167 | renderDrawerModuleInput?.startFade() 168 | } 169 | 170 | public func setChartIdleState() { 171 | renderDrawerModuleInput?.stopFade() 172 | } 173 | 174 | public func setRangeValue( 175 | rangeValue: RangeValue 176 | ) { 177 | self.rangeValue = rangeValue 178 | } 179 | 180 | private func makeCalculator( 181 | data: [ChartData] 182 | ) -> Calculator { 183 | return CalculatorImpl( 184 | data: data, 185 | configuration: calculatorConfiguration 186 | ) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/View/AnalyticsChartSpriteKitView.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | import UIKit 3 | 4 | final public class AnalyticsChartSpriteKitView: UIView { 5 | 6 | // MARK: - VIPER 7 | 8 | var output: AnalyticsChartSpriteKitViewOutput! 9 | 10 | // MARK: - Initializers 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | setup() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | setup() 20 | } 21 | 22 | // MARK: - UI properties 23 | 24 | private lazy var skView: SKView = { 25 | let view = SKView() 26 | view.shouldCullNonVisibleNodes = true 27 | view.ignoresSiblingOrder = true 28 | return view 29 | }() 30 | 31 | // MARK: - Managing the View 32 | 33 | private var isFirstLaunch = true 34 | 35 | public override func draw(_ rect: CGRect) { 36 | super.draw(rect) 37 | 38 | if isFirstLaunch { 39 | isFirstLaunch = false 40 | output.setupView() 41 | } else { 42 | output.redraw() 43 | } 44 | } 45 | 46 | public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 47 | super.traitCollectionDidChange(previousTraitCollection) 48 | output.traitCollectionDidChange() 49 | } 50 | 51 | // MARK: - Setup 52 | 53 | private func setup() { 54 | setupView() 55 | setupConstraints() 56 | } 57 | 58 | private func setupView() { 59 | [ 60 | skView, 61 | ].forEach { 62 | $0.translatesAutoresizingMaskIntoConstraints = false 63 | addSubview($0) 64 | } 65 | } 66 | 67 | private func setupConstraints() { 68 | let constraints = [ 69 | skView.topAnchor.constraint(equalTo: topAnchor), 70 | skView.bottomAnchor.constraint(equalTo: bottomAnchor), 71 | skView.leadingAnchor.constraint(equalTo: leadingAnchor), 72 | skView.trailingAnchor.constraint(equalTo: trailingAnchor), 73 | ] 74 | NSLayoutConstraint.activate(constraints) 75 | } 76 | } 77 | 78 | // MARK: - AnalyticsChartSpriteKitViewInput 79 | 80 | extension AnalyticsChartSpriteKitView: AnalyticsChartSpriteKitViewInput { 81 | func setScene(scene: SKScene) { 82 | skView.presentScene(scene) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Modules/AnalyticsChartSpriteKit/View/ViewModel/AnalyticsChartViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// ViewModel of AnalyticsChartSpriteKit. 4 | public struct AnalyticsChartSpriteKitViewModel { 5 | 6 | /// Entity of ChartData. 7 | public let data: ChartData 8 | 9 | /// Entity of ChartRenderConfiguration. 10 | public let configuration: ChartRenderConfiguration 11 | 12 | /// Accessibility identifier. 13 | public let accessibilityIdentifier: String? 14 | 15 | /// Creates instance of `AnalyticsChartSpriteKitViewModel`. 16 | /// 17 | /// - Parameters: 18 | /// - data: Entity of ChartData. 19 | /// - configuration: Entity of ChartRenderConfiguration. 20 | /// - accessibilityIdentifier: Accessibility identifier. 21 | /// 22 | /// - Returns: Instance of `AnalyticsChartSpriteKitViewModel`. 23 | public init( 24 | data: ChartData, 25 | configuration: ChartRenderConfiguration, 26 | accessibilityIdentifier: String? = nil 27 | ) { 28 | self.data = data 29 | self.configuration = configuration 30 | self.accessibilityIdentifier = accessibilityIdentifier 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Actions/ColorTransition.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension SKAction { 4 | static func strokeColorTransitionAction( 5 | fromColor: UIColor, 6 | toColor: UIColor, 7 | duration: TimeInterval = 0.5 8 | ) -> SKAction { 9 | return SKAction.customAction(withDuration: duration) { (node: SKNode, elapsedTime: CGFloat) in 10 | guard let shapeNode = node as? SKShapeNode else { return } 11 | 12 | let transColor = makeTransColor( 13 | elapsedTime: elapsedTime, 14 | duration: duration, 15 | fromColor: fromColor, 16 | toColor: toColor 17 | ) 18 | 19 | shapeNode.strokeColor = transColor 20 | } 21 | } 22 | 23 | static func gradientColorTransitionAction( 24 | fromColor: UIColor, 25 | toColor: UIColor, 26 | duration: TimeInterval = 0.5, 27 | direction: GradientDirection = .up 28 | ) -> SKAction { 29 | return SKAction.customAction(withDuration: duration) { (node: SKNode, elapsedTime: CGFloat) in 30 | guard let spriteNode = node as? SKSpriteNode else { return } 31 | 32 | let transColor = makeTransColor( 33 | elapsedTime: elapsedTime, 34 | duration: duration, 35 | fromColor: fromColor, 36 | toColor: toColor 37 | ) 38 | let size = spriteNode.size 39 | let textureSize = CGSize( 40 | width: size.width / 2, 41 | height: size.height / 2 42 | ) 43 | let texture = SKTexture( 44 | size: textureSize, 45 | color1: CIColor(color: transColor.withAlphaComponent(0.0)), 46 | color2: CIColor(color: transColor), 47 | direction: direction 48 | ) 49 | texture.filteringMode = .linear 50 | spriteNode.texture = texture 51 | } 52 | } 53 | } 54 | 55 | private func makeTransColor( 56 | elapsedTime: CGFloat, 57 | duration: TimeInterval, 58 | fromColor: UIColor, 59 | toColor: UIColor 60 | ) -> UIColor { 61 | let fraction = cubicEaseOut(CGFloat(elapsedTime / CGFloat(duration))) 62 | let startColorComponents = fromColor.toComponents() 63 | let endColorComponents = toColor.toComponents() 64 | return UIColor( 65 | red: lerp(a: startColorComponents.red, b: endColorComponents.red, fraction: fraction), 66 | green: lerp(a: startColorComponents.green, b: endColorComponents.green, fraction: fraction), 67 | blue: lerp(a: startColorComponents.blue, b: endColorComponents.blue, fraction: fraction), 68 | alpha: lerp(a: startColorComponents.alpha, b: endColorComponents.alpha, fraction: fraction) 69 | ) 70 | } 71 | 72 | private func lerp( 73 | a: CGFloat, 74 | b: CGFloat, 75 | fraction: CGFloat 76 | ) -> CGFloat { 77 | return (b - a) * fraction + a 78 | } 79 | 80 | private struct ColorComponents { 81 | var red = CGFloat(0) 82 | var green = CGFloat(0) 83 | var blue = CGFloat(0) 84 | var alpha = CGFloat(0) 85 | } 86 | 87 | private extension UIColor { 88 | func toComponents() -> ColorComponents { 89 | var components = ColorComponents() 90 | getRed( 91 | &components.red, 92 | green: &components.green, 93 | blue: &components.blue, 94 | alpha: &components.alpha 95 | ) 96 | return components 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Actions/FadeColorTransition.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension SKAction { 4 | static func fadeAction( 5 | startAction: SKAction, 6 | fadeOutAction: SKAction, 7 | fadeInAction: SKAction 8 | ) -> SKAction { 9 | let sequenceAction = SKAction.sequence([ 10 | fadeOutAction, 11 | fadeInAction, 12 | ]) 13 | let repeatAction = SKAction.repeatForever(sequenceAction) 14 | return SKAction.sequence([ 15 | startAction, 16 | repeatAction, 17 | ]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/ChartRenderConfiguration.swift: -------------------------------------------------------------------------------- 1 | /// Type of chat render configuration. 2 | public struct ChartRenderConfiguration { 3 | 4 | /// Entity of analytics unit. 5 | public var unit: AnalyticsUnit 6 | 7 | /// Entity of chart path. 8 | public let path: ChartPath 9 | 10 | /// Entity of chart gradient. 11 | public let gradient: Gradient? 12 | 13 | /// Creates instance of `ChartRenderConfiguration`. 14 | /// 15 | /// - Parameters: 16 | /// - unit: Entity of analytics unit. 17 | /// - path: Entity of chart path. 18 | /// - gradient: Entity of chart gradient. 19 | /// 20 | /// - Returns: Instance of `ChartRenderConfiguration`. 21 | public init( 22 | unit: AnalyticsUnit, 23 | path: ChartPath, 24 | gradient: Gradient? 25 | ) { 26 | self.unit = unit 27 | self.path = path 28 | self.gradient = gradient 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartAnimation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart animation. 4 | public struct ChartAnimation { 5 | 6 | /// Chart redraw duration. 7 | public let redrawDuration: TimeInterval 8 | 9 | /// Creates instance of `ChartAnimation`. 10 | /// 11 | /// - Parameters: 12 | /// - redrawDuration: Chart redraw duration. 13 | /// 14 | /// - Returns: Instance of `ChartAnimation`. 15 | public init( 16 | redrawDuration: TimeInterval 17 | ) { 18 | self.redrawDuration = redrawDuration 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartDefinition.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart definition line. 4 | public struct ChartDefinitionLine { 5 | 6 | /// Color of the defining line. 7 | public let color: UIColor 8 | 9 | /// Width of the defining line. 10 | public let width: CGFloat 11 | 12 | /// Creates instance of `ChartDefinitionLine`. 13 | /// 14 | /// - Parameters: 15 | /// - color: Color of the defining line. 16 | /// - width: Width of the defining line. 17 | /// 18 | /// - Returns: Instance of `ChartDefinitionLine`. 19 | public init( 20 | color: UIColor, 21 | width: CGFloat 22 | ) { 23 | self.color = color 24 | self.width = width 25 | } 26 | } 27 | 28 | /// Type of chart definition point. 29 | public struct ChartDefinitionPoint { 30 | 31 | /// Minimum radius of the defining point. 32 | public let minRadius: CGFloat 33 | 34 | /// Maximum radius of the defining point. 35 | public let maxRadius: CGFloat 36 | 37 | /// Creates instance of `ChartDefinitionPoint`. 38 | /// 39 | /// - Parameters: 40 | /// - minRadius: Minimum radius of the defining point. 41 | /// - maxRadius: Maximum radius of the defining point. 42 | /// 43 | /// - Returns: Instance of `ChartDefinitionPoint`. 44 | public init( 45 | minRadius: CGFloat, 46 | maxRadius: CGFloat 47 | ) { 48 | self.minRadius = minRadius 49 | self.maxRadius = maxRadius 50 | } 51 | } 52 | 53 | /// Type of chart definition view. 54 | public struct ChartDefinitionView { 55 | 56 | /// Background color of definition view. 57 | public let backgroundColor: UIColor 58 | 59 | /// Font of value label on definition view. 60 | public let valueLabelFont: UIFont 61 | 62 | /// Color of value label on definition view. 63 | public let valueLabelColor: UIColor 64 | 65 | /// Font of date label on definition view. 66 | public let dateLabelFont: UIFont 67 | 68 | /// Color of date label on definition view. 69 | public let dateLabelColor: UIColor 70 | 71 | /// Date formatter of label on definition view. 72 | public let dateFormatter: DateFormatter 73 | 74 | /// Creates instance of `ChartDefinitionView`. 75 | /// 76 | /// - Parameters: 77 | /// - backgroundColor: Background color of definition view. 78 | /// - valueLabelFont: Font of value label on definition view. 79 | /// - valueLabelColor: Color of value label on definition view. 80 | /// - dateLabelFont: Font of date label on definition view. 81 | /// - dateLabelColor: Color of date label on definition view. 82 | /// - dateFormatter: Date formatter of label on definition view. 83 | /// 84 | /// - Returns: Instance of `ChartDefinitionView`. 85 | public init( 86 | backgroundColor: UIColor, 87 | valueLabelFont: UIFont, 88 | valueLabelColor: UIColor, 89 | dateLabelFont: UIFont, 90 | dateLabelColor: UIColor, 91 | dateFormatter: DateFormatter 92 | ) { 93 | self.backgroundColor = backgroundColor 94 | self.valueLabelFont = valueLabelFont 95 | self.valueLabelColor = valueLabelColor 96 | self.dateLabelFont = dateLabelFont 97 | self.dateLabelColor = dateLabelColor 98 | self.dateFormatter = dateFormatter 99 | } 100 | } 101 | 102 | /// Type of chart definition. 103 | public struct ChartDefinition { 104 | 105 | /// Entity of chart definition line. 106 | public let line: ChartDefinitionLine 107 | 108 | /// Entity of chart definition point. 109 | public let point: ChartDefinitionPoint 110 | 111 | /// Entity of chart definition view. 112 | public let view: ChartDefinitionView 113 | 114 | /// Fade animation for the difining point. 115 | public let fadeAnimation: ChartFadeAnimation 116 | 117 | /// Creates instance of `ChartDefinition`. 118 | /// 119 | /// - Parameters: 120 | /// - line: Entity of chart definition line. 121 | /// - point: Entity of chart definition point. 122 | /// - view: Entity of chart definition view. 123 | /// - fadeAnimation: Fade animation for the difining point. 124 | /// 125 | /// - Returns: Instance of `ChartDefinition`. 126 | public init( 127 | line: ChartDefinitionLine, 128 | point: ChartDefinitionPoint, 129 | view: ChartDefinitionView, 130 | fadeAnimation: ChartFadeAnimation 131 | ) { 132 | self.line = line 133 | self.point = point 134 | self.view = view 135 | self.fadeAnimation = fadeAnimation 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartFadeAnimation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart fade animation. 4 | public struct ChartFadeAnimation { 5 | 6 | /// Key for fade animation. 7 | public let key: String 8 | 9 | /// Color of fadeOut animation. 10 | public let fadeOutColor: UIColor 11 | 12 | /// Color of fadeIn animation. 13 | public let fadeInColor: UIColor 14 | 15 | /// Animation start duration. 16 | public let startDuration: TimeInterval 17 | 18 | /// FadeOut animation duration. 19 | public let fadeOutDuration: TimeInterval 20 | 21 | /// FadeIn animation duration. 22 | public let fadeInDuration: TimeInterval 23 | 24 | /// Creates instance of `ChartFadeAnimation`. 25 | /// 26 | /// - Parameters: 27 | /// - key: Key for fade animation. 28 | /// - fadeOutColor: Color of fadeOut animation. 29 | /// - fadeInColor: Color of fadeIn animation. 30 | /// - startDuration: Animation start duration. 31 | /// - fadeOutDuration: FadeOut animation duration. 32 | /// - fadeInDuration: FadeIn animation duration. 33 | /// 34 | /// - Returns: Instance of `ChartFadeAnimation`. 35 | public init( 36 | key: String = "fade_key", 37 | fadeOutColor: UIColor, 38 | fadeInColor: UIColor, 39 | startDuration: TimeInterval, 40 | fadeOutDuration: TimeInterval, 41 | fadeInDuration: TimeInterval 42 | ) { 43 | self.key = key 44 | self.fadeOutColor = fadeOutColor 45 | self.fadeInColor = fadeInColor 46 | self.startDuration = startDuration 47 | self.fadeOutDuration = fadeOutDuration 48 | self.fadeInDuration = fadeInDuration 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartGestureState.swift: -------------------------------------------------------------------------------- 1 | /// Type of chart gesture state. 2 | public struct ChartGestureState { 3 | 4 | /// Active state of swipe gesture. 5 | public let swipeIsActive: Bool 6 | 7 | /// Active state of pinch gesture. 8 | public let pinchIsActive: Bool 9 | 10 | /// Active state of handle gesture. 11 | public let handleIsActive: Bool 12 | 13 | /// Creates instance of `ChartGestureState`. 14 | /// 15 | /// - Parameters: 16 | /// - swipeIsActive: Active state of swipe gesture. 17 | /// - pinchIsActive: Active state of pinch gesture. 18 | /// - handleIsActive: Active state of handle gesture. 19 | /// 20 | /// - Returns: Instance of `ChartGestureState`. 21 | public init( 22 | swipeIsActive: Bool, 23 | pinchIsActive: Bool, 24 | handleIsActive: Bool 25 | ) { 26 | self.swipeIsActive = swipeIsActive 27 | self.pinchIsActive = pinchIsActive 28 | self.handleIsActive = handleIsActive 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartPath.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart path. 4 | public struct ChartPath { 5 | 6 | /// Entity of path type. 7 | public let type: ChartPathType 8 | 9 | /// Color of chart path. 10 | public let color: UIColor 11 | 12 | /// Minimum width of chart path. 13 | public let minWidth: CGFloat 14 | 15 | /// Maximum width of chart path. 16 | public let maxWidth: CGFloat 17 | 18 | /// Entity of chart fade animation. 19 | public let fadeAnimation: ChartFadeAnimation 20 | 21 | /// Creates instance of `ChartPath`. 22 | /// 23 | /// - Parameters: 24 | /// - type: Entity of path type. 25 | /// - color: Color of chart path. 26 | /// - minWidth: Minimum width of chart path. 27 | /// - maxWidth: Maximum width of chart path. 28 | /// - fadeAnimation: Entity of chart fade animation. 29 | /// 30 | /// - Returns: Instance of `ChartPath`. 31 | public init( 32 | type: ChartPathType, 33 | color: UIColor, 34 | minWidth: CGFloat, 35 | maxWidth: CGFloat, 36 | fadeAnimation: ChartFadeAnimation 37 | ) { 38 | self.type = type 39 | self.color = color 40 | self.minWidth = minWidth 41 | self.maxWidth = maxWidth 42 | self.fadeAnimation = fadeAnimation 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartPathType.swift: -------------------------------------------------------------------------------- 1 | /// Type of chart path. 2 | public enum ChartPathType { 3 | 4 | /// Linear type drawing path. 5 | case linear 6 | 7 | /// Quadratic type drawing path. 8 | case quadratic 9 | 10 | /// Horizontal quadratic type drawing path. 11 | case horizontalQuadratic 12 | } 13 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartRangeLabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart range label. 4 | public struct ChartRangeLabel { 5 | 6 | /// Color of label. 7 | public let color: UIColor 8 | 9 | /// Font of label. 10 | public let font: UIFont 11 | 12 | /// Date formatter of label. 13 | public let dateFormatter: DateFormatter 14 | 15 | /// Insets of range label. 16 | public let insets: UIEdgeInsets 17 | 18 | /// Creates instance of `ChartRangeLabel`. 19 | /// 20 | /// - Parameters: 21 | /// - color: Color of label. 22 | /// - font: Font of label. 23 | /// - dateFormatter: Date formatter of label. 24 | /// - insets: Insets of range label. 25 | /// 26 | /// - Returns: Instance of `ChartRangeLabel`. 27 | public init( 28 | color: UIColor, 29 | font: UIFont, 30 | dateFormatter: DateFormatter, 31 | insets: UIEdgeInsets 32 | ) { 33 | self.color = color 34 | self.font = font 35 | self.dateFormatter = dateFormatter 36 | self.insets = insets 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartXAxis.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart x axis. 4 | public struct ChartXAxis { 5 | 6 | /// Color of labels. 7 | public let labelColor: UIColor 8 | 9 | /// Font of labels. 10 | public let labelFont: UIFont 11 | 12 | /// Date formatter for labels. 13 | public let dateFormatter: DateFormatter 14 | 15 | /// Insets of x axis. 16 | public let insets: UIEdgeInsets 17 | 18 | /// Margins of x axis. 19 | public let margins: UIEdgeInsets 20 | 21 | /// Minimum number of labels on x axis. 22 | public let minCountLabels: Int 23 | 24 | /// Factor of zoom for labels on x axis. 25 | public let zoomFactorLabels: CGFloat 26 | 27 | /// Creates instance of `ChartXAxis`. 28 | /// 29 | /// - Parameters: 30 | /// - labelColor: Color of labels. 31 | /// - labelFont: Font of labels. 32 | /// - dateFormatter: Date formatter for labels. 33 | /// - insets: Insets of x axis. 34 | /// - margins: Margins of x axis. 35 | /// - minCountLabels: Minimum number of labels on x axis. 36 | /// - zoomFactorLabels: Factor of zoom for labels on x axis. 37 | /// 38 | /// - Returns: Instance of `ChartXAxis`. 39 | public init( 40 | labelColor: UIColor, 41 | labelFont: UIFont, 42 | dateFormatter: DateFormatter, 43 | insets: UIEdgeInsets, 44 | margins: UIEdgeInsets, 45 | minCountLabels: Int = 4, 46 | zoomFactorLabels: CGFloat = 1.0 47 | ) { 48 | self.labelColor = labelColor 49 | self.labelFont = labelFont 50 | self.dateFormatter = dateFormatter 51 | self.insets = insets 52 | self.margins = margins 53 | self.minCountLabels = minCountLabels 54 | self.zoomFactorLabels = zoomFactorLabels 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartYAxis.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart y axis. 4 | public struct ChartYAxis { 5 | 6 | /// Color of labels. 7 | public let labelColor: UIColor 8 | 9 | /// Font of labels. 10 | public let labelFont: UIFont 11 | 12 | /// Insets of labels. 13 | public let labelInsets: UIEdgeInsets 14 | 15 | /// Color of lines. 16 | public let lineColor: UIColor 17 | 18 | /// Width of lines. 19 | public let lineWidth: CGFloat 20 | 21 | /// Number of lines. 22 | public let linesCount: Int 23 | 24 | /// Creates instance of `ChartYAxis`. 25 | /// 26 | /// - Parameters: 27 | /// - labelColor: Color of labels. 28 | /// - labelFont: Font of labels. 29 | /// - labelInsets: Insets of labels. 30 | /// - lineColor: Color of lines. 31 | /// - lineWidth: Width of lines. 32 | /// - linesCount: Number of lines. 33 | /// 34 | /// - Returns: Instance of `ChartYAxis`. 35 | public init( 36 | labelColor: UIColor, 37 | labelFont: UIFont, 38 | labelInsets: UIEdgeInsets, 39 | lineColor: UIColor, 40 | lineWidth: CGFloat, 41 | linesCount: Int = 4 42 | ) { 43 | self.labelColor = labelColor 44 | self.labelFont = labelFont 45 | self.labelInsets = labelInsets 46 | self.lineColor = lineColor 47 | self.lineWidth = lineWidth 48 | self.linesCount = linesCount 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Configuration/ChartZeroLine.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Type of chart zero line. 4 | public struct ChartZeroLine { 5 | 6 | /// Color of line. 7 | public let color: UIColor 8 | 9 | /// Width of line. 10 | public let width: CGFloat 11 | 12 | /// Creates instance of `ChartZeroLine`. 13 | /// 14 | /// - Parameters: 15 | /// - color: Color of line. 16 | /// - width: Width of line. 17 | /// 18 | /// - Returns: Instance of `ChartZeroLine`. 19 | public init( 20 | color: UIColor, 21 | width: CGFloat 22 | ) { 23 | self.color = color 24 | self.width = width 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Gradient/Gradient.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Gradient directions of the chart. 4 | public enum GradientDirection { 5 | 6 | /// From bottom to top. 7 | case up 8 | 9 | /// From left to right. 10 | case left 11 | 12 | /// From the lower right corner to the upper left. 13 | case upLeft 14 | 15 | /// From the lower left corner to the upper right. 16 | case upRight 17 | } 18 | 19 | /// Type of chart gradient. 20 | public struct Gradient { 21 | 22 | /// First color. 23 | public let firstColor: UIColor 24 | 25 | /// Second color. 26 | public let secondColor: UIColor 27 | 28 | /// Direction of the gradient. 29 | public let direction: GradientDirection 30 | 31 | /// Fade animation of the gradient. 32 | public let fadeAnimation: ChartFadeAnimation 33 | 34 | /// Creates instance of `Gradient`. 35 | /// 36 | /// - Parameters: 37 | /// - firstColor: Array of chart data. 38 | /// - secondColor: Calculator configuration. 39 | /// - direction: Number of values. 40 | /// - fadeAnimation: Number of segments. 41 | /// 42 | /// - Returns: Instance of `Gradient`. 43 | public init( 44 | firstColor: UIColor, 45 | secondColor: UIColor, 46 | direction: GradientDirection = .up, 47 | fadeAnimation: ChartFadeAnimation 48 | ) { 49 | self.firstColor = firstColor 50 | self.secondColor = secondColor 51 | self.direction = direction 52 | self.fadeAnimation = fadeAnimation 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Gradient/SKTexture+Gradient.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | extension SKTexture { 4 | 5 | convenience init( 6 | size: CGSize, 7 | color1: CIColor, 8 | color2: CIColor, 9 | direction: GradientDirection = .up 10 | ) { 11 | let context = CIContext(options: nil) 12 | guard let filter = CIFilter(name: "CILinearGradient") else { 13 | self.init() 14 | return 15 | } 16 | var startVector: CIVector 17 | var endVector: CIVector 18 | 19 | filter.setDefaults() 20 | 21 | switch direction { 22 | case .up: 23 | startVector = CIVector(x: size.width * 0.5, y: 0) 24 | endVector = CIVector(x: size.width * 0.5, y: size.height) 25 | case .left: 26 | startVector = CIVector(x: size.width, y: size.height * 0.5) 27 | endVector = CIVector(x: 0, y: size.height * 0.5) 28 | case .upLeft: 29 | startVector = CIVector(x: size.width, y: 0) 30 | endVector = CIVector(x: 0, y: size.height) 31 | case .upRight: 32 | startVector = CIVector(x: 0, y: 0) 33 | endVector = CIVector(x: size.width, y: size.height) 34 | } 35 | 36 | filter.setValue(startVector, forKey: "inputPoint0") 37 | filter.setValue(endVector, forKey: "inputPoint1") 38 | filter.setValue(color1, forKey: "inputColor0") 39 | filter.setValue(color2, forKey: "inputColor1") 40 | 41 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 42 | 43 | guard let outputImage = filter.outputImage, 44 | let image = context.createCGImage(outputImage, from: rect) else { 45 | self.init() 46 | return 47 | } 48 | self.init(cgImage: image) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Nodes/BasicNode.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | public class BasicNode: SKShapeNode { 4 | 5 | override init() { 6 | super.init() 7 | blendMode = .replace 8 | isAntialiased = false 9 | } 10 | 11 | required init?(coder aDecoder: NSCoder) { 12 | fatalError("init(coder:) has not been implemented") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Nodes/RoundNode.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | public class RoundNode: BasicNode { 4 | 5 | override init() { 6 | super.init() 7 | lineCap = .round 8 | lineJoin = .round 9 | } 10 | 11 | required init?(coder aDecoder: NSCoder) { 12 | fatalError("init(coder:) has not been implemented") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Nodes/SquareNode.swift: -------------------------------------------------------------------------------- 1 | import SpriteKit 2 | 3 | public class SquareNode: BasicNode { 4 | 5 | override init() { 6 | super.init() 7 | lineCap = .square 8 | lineJoin = .miter 9 | } 10 | 11 | required init?(coder aDecoder: NSCoder) { 12 | fatalError("init(coder:) has not been implemented") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Paths/HorizontalQuadraticPath.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class HorizontalQuadraticPath: UIBezierPath { 4 | convenience init( 5 | points: [CGPoint], 6 | phaseY: CGFloat = 1.0 7 | ) { 8 | self.init() 9 | guard points.isEmpty == false else { return } 10 | 11 | var prev = points[0] 12 | var cur = prev 13 | 14 | move(to: CGPoint(x: cur.x, y: cur.y * phaseY)) 15 | 16 | for j in 0 ..< points.count { 17 | prev = cur 18 | cur = points[j] 19 | 20 | let cpx = prev.x + (cur.x - prev.x) / 2.0 21 | addCurve( 22 | to: CGPoint( 23 | x: cur.x, 24 | y: cur.y * phaseY), 25 | controlPoint1: CGPoint( 26 | x: cpx, 27 | y: prev.y * phaseY), 28 | controlPoint2: CGPoint( 29 | x: cpx, 30 | y: cur.y * phaseY) 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Paths/LinearPath.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class LinearPath: UIBezierPath { 4 | convenience init( 5 | points: [CGPoint] 6 | ) { 7 | self.init() 8 | guard points.count > 1 else { return } 9 | 10 | move(to: points[0]) 11 | points[1 ..< points.endIndex].forEach { addLine(to: $0) } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwift/Render/Paths/QuadraticPath.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class QuadraticPath: UIBezierPath { 4 | convenience init( 5 | points: [CGPoint], 6 | phaseY: CGFloat = 1.0, 7 | intensity: CGFloat = 0.2 8 | ) { 9 | self.init() 10 | guard points.count > 1 else { return } 11 | 12 | var prevDx: CGFloat = 0.0 13 | var prevDy: CGFloat = 0.0 14 | var curDx: CGFloat = 0.0 15 | var curDy: CGFloat = 0.0 16 | 17 | var prevPrev: CGPoint = points[0] 18 | var prev: CGPoint = points[0] 19 | var cur: CGPoint = points[0] 20 | var next: CGPoint = points[0] 21 | var nextIndex: Int = -1 22 | 23 | move(to: CGPoint(x: cur.x, y: cur.y * phaseY)) 24 | 25 | for j in 0.. 23 | ) 24 | 25 | func didHandleLongPress() 26 | 27 | func didHandlePan( 28 | deltaLocation: CGFloat 29 | ) 30 | 31 | func didHandlePinch( 32 | scale: CGFloat 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application( 9 | _ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 | ) -> Bool { 12 | let window = UIWindow(frame: UIScreen.main.bounds) 13 | self.window = window 14 | 15 | let listChartsModule = ListChartsAssembly.makeModule() 16 | let rootViewController = UINavigationController( 17 | rootViewController: listChartsModule 18 | ) 19 | window.rootViewController = rootViewController 20 | window.makeKeyAndVisible() 21 | 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/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 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Categories/CGFloat+Space.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | enum Space { 4 | static let single: CGFloat = 8 5 | static let double: CGFloat = 16 6 | static let triple: CGFloat = 24 7 | static let quadruple: CGFloat = 32 8 | static let fivefold: CGFloat = 40 9 | } 10 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Categories/UIColor+Style.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | 5 | static let radicalRed = UIColor(red: 1, green: 51 / 255, blue: 102 / 255, alpha: 1) 6 | static let dodgerBlue = UIColor(red: 51 / 255, green: 102 / 255, blue: 1, alpha: 1) 7 | static let heliotrope = UIColor(red: 153 / 255, green: 102 / 255, blue: 1, alpha: 1) 8 | static let baliHai = UIColor(red: 134 / 255, green: 149 / 255, blue: 175 / 255, alpha: 1) 9 | 10 | static var border: UIColor = { 11 | if #available(iOS 13, *) { 12 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 13 | if UITraitCollection.userInterfaceStyle == .dark { 14 | return UIColor(white: 1, alpha: 0.05) 15 | } else { 16 | return UIColor(white: 0, alpha: 0.05) 17 | } 18 | } 19 | } else { 20 | return UIColor(white: 0, alpha: 0.05) 21 | } 22 | }() 23 | 24 | static var borderZeroLine: UIColor = { 25 | if #available(iOS 13, *) { 26 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 27 | if UITraitCollection.userInterfaceStyle == .dark { 28 | return UIColor(white: 1, alpha: 0.1) 29 | } else { 30 | return UIColor(white: 0, alpha: 0.1) 31 | } 32 | } 33 | } else { 34 | return UIColor(white: 0, alpha: 0.21) 35 | } 36 | }() 37 | 38 | static var alto: UIColor = { 39 | if #available(iOS 13, *) { 40 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 41 | if UITraitCollection.userInterfaceStyle == .dark { 42 | return UIColor(white: 219 / 255, alpha: 1) 43 | } else { 44 | return UIColor(white: 219 / 255, alpha: 1) 45 | } 46 | } 47 | } else { 48 | return UIColor(white: 219 / 255, alpha: 1) 49 | } 50 | }() 51 | 52 | static var gallery: UIColor = { 53 | if #available(iOS 13, *) { 54 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 55 | if UITraitCollection.userInterfaceStyle == .dark { 56 | return UIColor(white: 236 / 255, alpha: 1) 57 | } else { 58 | return UIColor(white: 236 / 255, alpha: 1) 59 | } 60 | } 61 | } else { 62 | return UIColor(white: 236 / 255, alpha: 1) 63 | } 64 | }() 65 | 66 | static var cararra: UIColor = { 67 | if #available(iOS 13, *) { 68 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 69 | if UITraitCollection.userInterfaceStyle == .dark { 70 | return UIColor(red: 28 / 255, green: 31 / 255, blue: 33 / 255, alpha: 1) 71 | } else { 72 | return UIColor(white: 247 / 255, alpha: 1) 73 | } 74 | } 75 | } else { 76 | return UIColor(white: 247 / 255, alpha: 1) 77 | } 78 | }() 79 | 80 | static var doveGray: UIColor = { 81 | if #available(iOS 13, *) { 82 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 83 | if UITraitCollection.userInterfaceStyle == .dark { 84 | return UIColor(white: 165 / 255, alpha: 1) 85 | } else { 86 | return UIColor(white: 102 / 255, alpha: 1) 87 | } 88 | } 89 | } else { 90 | return UIColor(white: 102 / 255, alpha: 1) 91 | } 92 | }() 93 | 94 | static var ghost: UIColor = { 95 | if #available(iOS 13, *) { 96 | return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in 97 | if UITraitCollection.userInterfaceStyle == .dark { 98 | return UIColor(white: 97 / 255, alpha: 1) 99 | } else { 100 | return UIColor(white: 179 / 255, alpha: 1) 101 | } 102 | } 103 | } else { 104 | return UIColor(white: 179 / 255, alpha: 1) 105 | } 106 | }() 107 | } 108 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Categories/UIFont+Style.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | private class _FontsCache { 5 | init() { 6 | NotificationCenter.default.addObserver( 7 | self, 8 | selector: #selector(contentSizeCategoryDidChangeNotification(_:)), 9 | name: UIContentSizeCategory.didChangeNotification, 10 | object: nil 11 | ) 12 | } 13 | 14 | deinit { 15 | NotificationCenter.default.removeObserver(self) 16 | } 17 | 18 | static let shared = _FontsCache() 19 | 20 | var cache = NSCache() 21 | 22 | @objc 23 | private func contentSizeCategoryDidChangeNotification(_ notification: Notification) { 24 | cache.removeAllObjects() 25 | } 26 | } 27 | 28 | static var dynamicCaption1: UIFont { 29 | return UIFont.makeFont(style: .footnote, face: "Regular") 30 | } 31 | 32 | static var dynamicCaption2: UIFont { 33 | return UIFont.makeFont(style: .caption2, face: "Regular") 34 | } 35 | 36 | private static func makeFont(style: UIFont.TextStyle, face: String) -> UIFont { 37 | let cacheKey = NSString(string: style.rawValue + face) 38 | if let font = _FontsCache.shared.cache.object(forKey: cacheKey) { 39 | return font 40 | } else { 41 | var descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) 42 | let size = descriptor.fontAttributes[UIFontDescriptor.AttributeName.size] as? Float ?? 0 43 | descriptor = UIFontDescriptor() 44 | let font = UIFont.systemFont(ofSize: CGFloat(size)) 45 | descriptor = descriptor.withFamily(font.familyName) 46 | descriptor = descriptor.withSize(CGFloat(size)) 47 | descriptor = descriptor.withFace(face) 48 | let resultFont = UIFont(descriptor: descriptor, size: 0) 49 | _FontsCache.shared.cache.setObject(resultFont, forKey: cacheKey) 50 | return resultFont 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Factories/AnalyticsChart/AnalyticsChartFactory.swift: -------------------------------------------------------------------------------- 1 | protocol AnalyticsChartFactory { 2 | func makeViewModels() -> [AnalyticsChartViewModel] 3 | } 4 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/Chart/Assembly/ChartAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum ChartAssembly { 4 | static func makeModule( 5 | inputData: AnalyticsChartViewModel 6 | ) -> UIViewController { 7 | let view = ChartViewController() 8 | 9 | let presenter = ChartPresenter(analyticsChartViewModel: inputData) 10 | 11 | view.output = presenter 12 | 13 | presenter.view = view 14 | 15 | return view 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/Chart/ChartViewIO.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Chart view input 4 | protocol ChartViewInput: class { 5 | func setTitle(_ title: String) 6 | func setChartView(_ chartView: UIView) 7 | } 8 | 9 | /// Chart view output 10 | protocol ChartViewOutput: class { 11 | func setupView() 12 | } 13 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/Chart/Presenter/ChartPresenter.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import MobileAnalyticsChartSwift 3 | 4 | final class ChartPresenter { 5 | 6 | // MARK: - VIPER 7 | 8 | weak var view: ChartViewInput? 9 | 10 | weak var analyticsChartSpriteKitModuleInput: AnalyticsChartSpriteKitModuleInput? 11 | 12 | private let analyticsChartViewModel: AnalyticsChartViewModel 13 | 14 | init(analyticsChartViewModel: AnalyticsChartViewModel) { 15 | self.analyticsChartViewModel = analyticsChartViewModel 16 | } 17 | } 18 | 19 | extension ChartPresenter: ChartViewOutput { 20 | func setupView() { 21 | let (chartView, moduleInput) = AnalyticsChartSpriteKitAssembly.makeModule( 22 | inputData: analyticsChartViewModel.data, 23 | moduleOutput: self 24 | ) 25 | 26 | analyticsChartSpriteKitModuleInput = moduleInput 27 | 28 | view?.setTitle(analyticsChartViewModel.title) 29 | view?.setChartView(chartView) 30 | } 31 | } 32 | 33 | extension ChartPresenter: AnalyticsChartSpriteKitModuleOutput { 34 | func didChangeRangeValue( 35 | rangeValue: RangeValue 36 | ) { 37 | print(#function, rangeValue) 38 | } 39 | 40 | func didHandleLongPress() { 41 | print(#function) 42 | } 43 | 44 | func didHandlePan( 45 | deltaLocation: CGFloat 46 | ) { 47 | print(#function, deltaLocation) 48 | } 49 | 50 | func didHandlePinch( 51 | scale: CGFloat 52 | ) { 53 | print(#function, scale) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/Chart/View/ChartViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ChartViewController: UIViewController { 4 | 5 | // MARK: - VIPER 6 | 7 | var output: ChartViewOutput! 8 | 9 | var chartView: UIView? 10 | 11 | // MARK: - Initializing 12 | 13 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 14 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | super.init(coder: aDecoder) 19 | } 20 | 21 | // MARK: - Managing the View 22 | 23 | override func loadView() { 24 | view = UIView() 25 | view.backgroundColor = .cararra 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | output.setupView() 31 | } 32 | 33 | override func viewDidAppear(_ animated: Bool) { 34 | super.viewDidAppear(animated) 35 | } 36 | } 37 | 38 | // MARK: - ChartViewInput 39 | 40 | extension ChartViewController: ChartViewInput { 41 | func setTitle(_ title: String) { 42 | navigationItem.title = title 43 | } 44 | 45 | func setChartView(_ chartView: UIView) { 46 | chartView.translatesAutoresizingMaskIntoConstraints = false 47 | view.addSubview(chartView) 48 | let constraints = [ 49 | chartView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide 50 | .topAnchor), 51 | chartView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 52 | chartView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 53 | chartView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 54 | ] 55 | NSLayoutConstraint.activate(constraints) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/CustomChart/Assembly/CustomChartAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum CustomChartAssembly { 4 | static func makeModule( 5 | inputData: AnalyticsChartViewModel 6 | ) -> UIViewController { 7 | let view = CustomChartViewController() 8 | 9 | let presenter = CustomChartPresenter(analyticsChartViewModel: inputData) 10 | 11 | view.output = presenter 12 | 13 | presenter.view = view 14 | 15 | return view 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/CustomChart/CustomChartViewIO.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// CustomChart view input 4 | protocol CustomChartViewInput: class { 5 | func setTitle(_ title: String) 6 | func setChartView(_ chartView: UIView) 7 | } 8 | 9 | /// CustomChart view output 10 | protocol CustomChartViewOutput: class { 11 | func setupView() 12 | func didPressSetChartButton() 13 | func didPressSetChartSilentButton() 14 | func didPressSetLoadingStateButton() 15 | func didPressSetIdleStateButton() 16 | } 17 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/CustomChart/Presenter/CustomChartPresenter.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import MobileAnalyticsChartSwift 3 | 4 | final class CustomChartPresenter { 5 | 6 | // MARK: - VIPER 7 | 8 | weak var view: CustomChartViewInput? 9 | 10 | weak var analyticsChartSpriteKitModuleInput: AnalyticsChartSpriteKitModuleInput? 11 | 12 | private let analyticsChartViewModel: AnalyticsChartViewModel 13 | 14 | init(analyticsChartViewModel: AnalyticsChartViewModel) { 15 | self.analyticsChartViewModel = analyticsChartViewModel 16 | } 17 | 18 | // MARK: - Stored properties 19 | 20 | private var currentChartIndex = 0 21 | } 22 | 23 | extension CustomChartPresenter: CustomChartViewOutput { 24 | func setupView() { 25 | let (chartView, moduleInput) = AnalyticsChartSpriteKitAssembly.makeModule( 26 | inputData: analyticsChartViewModel.data, 27 | moduleOutput: self 28 | ) 29 | 30 | analyticsChartSpriteKitModuleInput = moduleInput 31 | 32 | view?.setTitle(analyticsChartViewModel.title) 33 | view?.setChartView(chartView) 34 | } 35 | 36 | func didPressSetChartButton() { 37 | currentChartIndex = abs(currentChartIndex - 1) 38 | analyticsChartSpriteKitModuleInput?.setChartViewModels( 39 | viewModels: [ 40 | analyticsChartViewModel.data.viewModels[currentChartIndex], 41 | ], 42 | silent: false 43 | ) 44 | } 45 | 46 | func didPressSetChartSilentButton() { 47 | currentChartIndex = abs(currentChartIndex - 1) 48 | analyticsChartSpriteKitModuleInput?.setChartViewModels( 49 | viewModels: [ 50 | analyticsChartViewModel.data.viewModels[currentChartIndex], 51 | ], 52 | silent: true 53 | ) 54 | } 55 | 56 | func didPressSetLoadingStateButton() { 57 | analyticsChartSpriteKitModuleInput?.setChartLoadingState() 58 | } 59 | 60 | func didPressSetIdleStateButton() { 61 | analyticsChartSpriteKitModuleInput?.setChartIdleState() 62 | } 63 | } 64 | 65 | extension CustomChartPresenter: AnalyticsChartSpriteKitModuleOutput { 66 | func didChangeRangeValue( 67 | rangeValue: RangeValue 68 | ) { 69 | print(#function, rangeValue) 70 | } 71 | 72 | func didHandleLongPress() { 73 | print(#function) 74 | } 75 | 76 | func didHandlePan( 77 | deltaLocation: CGFloat 78 | ) { 79 | print(#function, deltaLocation) 80 | } 81 | 82 | func didHandlePinch( 83 | scale: CGFloat 84 | ) { 85 | print(#function, scale) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/CustomChart/View/CustomChartViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class CustomChartViewController: UIViewController { 4 | 5 | // MARK: - VIPER 6 | 7 | var output: CustomChartViewOutput! 8 | 9 | // MARK: - UI properties 10 | 11 | private lazy var chartView: UIView = { 12 | let view = UIView() 13 | return view 14 | }() 15 | 16 | private lazy var mainStackView: UIStackView = { 17 | let view = UIStackView() 18 | view.axis = .vertical 19 | view.spacing = Space.double 20 | return view 21 | }() 22 | 23 | private lazy var setChartStackView: UIStackView = { 24 | let view = UIStackView() 25 | view.axis = .horizontal 26 | view.distribution = .fillEqually 27 | view.spacing = Space.double 28 | return view 29 | }() 30 | 31 | private lazy var setStateStackView: UIStackView = { 32 | let view = UIStackView() 33 | view.axis = .horizontal 34 | view.distribution = .fillEqually 35 | view.spacing = Space.double 36 | return view 37 | }() 38 | 39 | private lazy var setChartButton: UIButton = { 40 | let view = UIButton() 41 | view.setTitle("Set chart", for: .normal) 42 | view.setTitleColor(.dodgerBlue, for: .normal) 43 | view.addTarget(self, action: #selector(didPressSetChartButton), for: .touchUpInside) 44 | view.layer.cornerRadius = 5 45 | view.layer.borderWidth = 1 46 | view.layer.borderColor = UIColor.dodgerBlue.cgColor 47 | return view 48 | }() 49 | 50 | private lazy var setChartSilentButton: UIButton = { 51 | let view = UIButton() 52 | view.setTitle("Set chart silent", for: .normal) 53 | view.setTitleColor(.dodgerBlue, for: .normal) 54 | view.addTarget(self, action: #selector(didPressSetChartSilentButton), for: .touchUpInside) 55 | view.layer.cornerRadius = 5 56 | view.layer.borderWidth = 1 57 | view.layer.borderColor = UIColor.dodgerBlue.cgColor 58 | return view 59 | }() 60 | 61 | private lazy var setLoadingStateButton: UIButton = { 62 | let view = UIButton() 63 | view.setTitle("Set loading state", for: .normal) 64 | view.setTitleColor(.dodgerBlue, for: .normal) 65 | view.addTarget(self, action: #selector(didPressSetLoadingStateButton), for: .touchUpInside) 66 | view.layer.cornerRadius = 5 67 | view.layer.borderWidth = 1 68 | view.layer.borderColor = UIColor.dodgerBlue.cgColor 69 | return view 70 | }() 71 | 72 | private lazy var setIdleStateButton: UIButton = { 73 | let view = UIButton() 74 | view.setTitle("Set idle state", for: .normal) 75 | view.setTitleColor(.dodgerBlue, for: .normal) 76 | view.addTarget(self, action: #selector(didPressSetIdleStateButton), for: .touchUpInside) 77 | view.layer.cornerRadius = 5 78 | view.layer.borderWidth = 1 79 | view.layer.borderColor = UIColor.dodgerBlue.cgColor 80 | return view 81 | }() 82 | 83 | // MARK: - Managing the View 84 | 85 | override func loadView() { 86 | view = UIView() 87 | view.backgroundColor = .cararra 88 | setupView() 89 | setupConstraints() 90 | } 91 | 92 | override func viewDidLoad() { 93 | super.viewDidLoad() 94 | output.setupView() 95 | } 96 | 97 | // MARK: - Setup 98 | 99 | private func setupView() { 100 | [ 101 | chartView, 102 | mainStackView, 103 | ].forEach { 104 | $0.translatesAutoresizingMaskIntoConstraints = false 105 | view.addSubview($0) 106 | } 107 | 108 | [ 109 | setChartStackView, 110 | setStateStackView, 111 | ].forEach(mainStackView.addArrangedSubview) 112 | 113 | [ 114 | setChartButton, 115 | setChartSilentButton, 116 | ].forEach(setChartStackView.addArrangedSubview) 117 | 118 | [ 119 | setLoadingStateButton, 120 | setIdleStateButton, 121 | ].forEach(setStateStackView.addArrangedSubview) 122 | } 123 | 124 | private func setupConstraints() { 125 | let constraints = [ 126 | chartView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 127 | chartView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 128 | chartView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 129 | 130 | mainStackView.topAnchor.constraint(equalTo: chartView.bottomAnchor, 131 | constant: Space.double), 132 | mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, 133 | constant: Space.double), 134 | mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, 135 | constant: -Space.double), 136 | mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, 137 | constant: -Space.double), 138 | ] 139 | NSLayoutConstraint.activate(constraints) 140 | } 141 | 142 | // MARK: - Actions 143 | 144 | @objc 145 | private func didPressSetChartButton() { 146 | output.didPressSetChartButton() 147 | } 148 | 149 | @objc 150 | private func didPressSetChartSilentButton() { 151 | output.didPressSetChartSilentButton() 152 | } 153 | 154 | @objc 155 | private func didPressSetLoadingStateButton() { 156 | output.didPressSetLoadingStateButton() 157 | } 158 | 159 | @objc 160 | private func didPressSetIdleStateButton() { 161 | output.didPressSetIdleStateButton() 162 | } 163 | } 164 | 165 | // MARK: - CustomChartViewInput 166 | 167 | extension CustomChartViewController: CustomChartViewInput { 168 | func setTitle(_ title: String) { 169 | navigationItem.title = title 170 | } 171 | 172 | func setChartView(_ chartView: UIView) { 173 | chartView.translatesAutoresizingMaskIntoConstraints = false 174 | self.chartView.addSubview(chartView) 175 | let constraints = [ 176 | chartView.topAnchor.constraint(equalTo: self.chartView.topAnchor), 177 | chartView.leadingAnchor.constraint(equalTo: self.chartView.leadingAnchor), 178 | chartView.trailingAnchor.constraint(equalTo: self.chartView.trailingAnchor), 179 | chartView.bottomAnchor.constraint(equalTo: self.chartView.bottomAnchor), 180 | ] 181 | NSLayoutConstraint.activate(constraints) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/Assembly/ListChartsAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum ListChartsAssembly { 4 | static func makeModule() -> UIViewController { 5 | let view = ListChartsViewController() 6 | 7 | let analyticsChartFactory = AnalyticsChartFactoryImpl() 8 | let presenter = ListChartsPresenter( 9 | analyticsChartFactory: analyticsChartFactory 10 | ) 11 | 12 | let router = ListChartsRouter() 13 | 14 | view.output = presenter 15 | 16 | presenter.view = view 17 | presenter.router = router 18 | 19 | router.transitionHandler = view 20 | 21 | return view 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/ListChartsRouterIO.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol ListChartsRouterInput: class { 4 | func openChartModule( 5 | inputData: AnalyticsChartViewModel 6 | ) 7 | 8 | func openCustomChartModule( 9 | inputData: AnalyticsChartViewModel 10 | ) 11 | 12 | func openMultiselectChartsModule( 13 | inputData: AnalyticsChartViewModel 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/ListChartsViewIO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// ListCharts view input 4 | protocol ListChartsViewInput: class { 5 | func reloadData() 6 | } 7 | 8 | /// ListCharts view output 9 | protocol ListChartsViewOutput: class { 10 | func setupView() 11 | func numberOfRows() -> Int 12 | func titleForRow(at indexPath: IndexPath) -> String 13 | func didSelect(at indexPath: IndexPath) 14 | } 15 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/Presenter/ListChartsPresenter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MobileAnalyticsChartSwift 3 | 4 | final class ListChartsPresenter { 5 | 6 | // MARK: - VIPER 7 | 8 | var router: ListChartsRouterInput! 9 | 10 | weak var view: ListChartsViewInput? 11 | 12 | private let analyticsChartFactory: AnalyticsChartFactory 13 | 14 | init(analyticsChartFactory: AnalyticsChartFactory) { 15 | self.analyticsChartFactory = analyticsChartFactory 16 | } 17 | 18 | // MARK: - Stored data 19 | 20 | private var viewModels: [AnalyticsChartViewModel] = [] 21 | } 22 | 23 | extension ListChartsPresenter: ListChartsViewOutput { 24 | func setupView() { 25 | viewModels = analyticsChartFactory.makeViewModels() 26 | view?.reloadData() 27 | } 28 | 29 | func numberOfRows() -> Int { 30 | viewModels.count 31 | } 32 | 33 | func titleForRow(at indexPath: IndexPath) -> String { 34 | viewModels[indexPath.row].title 35 | } 36 | 37 | func didSelect(at indexPath: IndexPath) { 38 | let viewModel = viewModels[indexPath.row] 39 | switch viewModel.type { 40 | case .default: 41 | router.openChartModule(inputData: viewModel) 42 | case .custom: 43 | router.openCustomChartModule(inputData: viewModel) 44 | case .multiselect: 45 | router.openMultiselectChartsModule(inputData: viewModel) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/Router/ListChartsRouter.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ListChartsRouter { 4 | weak var transitionHandler: UIViewController? 5 | } 6 | 7 | // MARK: - 8 | 9 | extension ListChartsRouter: ListChartsRouterInput { 10 | func openChartModule( 11 | inputData: AnalyticsChartViewModel 12 | ) { 13 | let viewController = ChartAssembly.makeModule( 14 | inputData: inputData 15 | ) 16 | transitionHandler?.navigationController?.pushViewController( 17 | viewController, 18 | animated: true 19 | ) 20 | } 21 | 22 | func openCustomChartModule( 23 | inputData: AnalyticsChartViewModel 24 | ) { 25 | let viewController = CustomChartAssembly.makeModule( 26 | inputData: inputData 27 | ) 28 | transitionHandler?.navigationController?.pushViewController( 29 | viewController, 30 | animated: true 31 | ) 32 | } 33 | 34 | func openMultiselectChartsModule( 35 | inputData: AnalyticsChartViewModel 36 | ) { 37 | let viewController = MultiselectChartsAssembly.makeModule( 38 | inputData: inputData 39 | ) 40 | transitionHandler?.navigationController?.pushViewController( 41 | viewController, 42 | animated: true 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/View/ListChartsViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ListChartsViewController: UIViewController { 4 | 5 | // MARK: - VIPER 6 | 7 | var output: ListChartsViewOutput! 8 | 9 | // MARK: - Initializing 10 | 11 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 12 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 13 | setup() 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | super.init(coder: aDecoder) 18 | setup() 19 | } 20 | 21 | // MARK: - UI properties 22 | 23 | private lazy var tableView: UITableView = { 24 | let view = UITableView() 25 | view.translatesAutoresizingMaskIntoConstraints = false 26 | view.dataSource = self 27 | view.delegate = self 28 | view.register( 29 | UITableViewCell.self, 30 | forCellReuseIdentifier: Constants.defaultCellIdentifier 31 | ) 32 | view.tableFooterView = UIView() 33 | return view 34 | }() 35 | 36 | // MARK: - Managing the View 37 | 38 | override func loadView() { 39 | view = UIView() 40 | setupView() 41 | setupConstraints() 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | output.setupView() 47 | } 48 | 49 | // MARK: - Setup 50 | 51 | private func setup() { 52 | navigationItem.title = "Chart list" 53 | } 54 | 55 | private func setupView() { 56 | [ 57 | tableView, 58 | ].forEach(view.addSubview) 59 | } 60 | 61 | private func setupConstraints() { 62 | let constraints = [ 63 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 64 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 65 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 66 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 67 | ] 68 | NSLayoutConstraint.activate(constraints) 69 | } 70 | } 71 | 72 | // MARK: - ListChartsViewInput 73 | 74 | extension ListChartsViewController: ListChartsViewInput { 75 | func reloadData() { 76 | tableView.reloadData() 77 | } 78 | } 79 | 80 | // MARK: - UITableViewDataSource 81 | 82 | extension ListChartsViewController: UITableViewDataSource { 83 | func tableView( 84 | _ tableView: UITableView, 85 | numberOfRowsInSection section: Int 86 | ) -> Int { 87 | output.numberOfRows() 88 | } 89 | 90 | func tableView( 91 | _ tableView: UITableView, 92 | cellForRowAt indexPath: IndexPath 93 | ) -> UITableViewCell { 94 | let cell = tableView.dequeueReusableCell( 95 | withIdentifier: Constants.defaultCellIdentifier, 96 | for: indexPath 97 | ) 98 | cell.textLabel?.text = output.titleForRow(at: indexPath) 99 | return cell 100 | } 101 | } 102 | 103 | // MARK: - UITableViewDelegate 104 | 105 | extension ListChartsViewController: UITableViewDelegate { 106 | func tableView( 107 | _ tableView: UITableView, 108 | didSelectRowAt indexPath: IndexPath 109 | ) { 110 | tableView.deselectRow(at: indexPath, animated: true) 111 | output.didSelect(at: indexPath) 112 | } 113 | } 114 | 115 | private extension ListChartsViewController { 116 | enum Constants { 117 | static let defaultCellIdentifier = "defaultCellIdentifier" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/ListCharts/View/ViewModel/ListChartsViewModel.swift: -------------------------------------------------------------------------------- 1 | import MobileAnalyticsChartSwift 2 | import UIKit 3 | 4 | enum AnalyticsChartType { 5 | case `default` 6 | case custom 7 | case multiselect 8 | } 9 | 10 | // swiftlint:disable missing_docs 11 | struct AnalyticsChartViewModel { 12 | public let title: String 13 | public let type: AnalyticsChartType 14 | public let data: AnalyticsChartSpriteKitModuleInputData 15 | } 16 | // swiftlint:enable missing_docs 17 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/MultiselectCharts/Assembly/MultiselectChartsAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum MultiselectChartsAssembly { 4 | static func makeModule( 5 | inputData: AnalyticsChartViewModel 6 | ) -> UIViewController { 7 | let view = MultiselectChartsViewController() 8 | 9 | let presenter = MultiselectChartsPresenter(analyticsChartViewModel: inputData) 10 | 11 | view.output = presenter 12 | 13 | presenter.view = view 14 | 15 | return view 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/MultiselectCharts/MultiselectChartsViewIO.swift: -------------------------------------------------------------------------------- 1 | import MobileAnalyticsChartSwift 2 | import UIKit 3 | 4 | /// MultiselectCharts view input 5 | protocol MultiselectChartsViewInput: class { 6 | func reloadData() 7 | func setTitle(_ title: String) 8 | func setChartView(_ chartView: UIView) 9 | } 10 | 11 | /// MultiselectCharts view output 12 | protocol MultiselectChartsViewOutput: class { 13 | func setupView() 14 | func numberOfRows() -> Int 15 | func colorForRow(at indexPath: IndexPath) -> UIColor 16 | func isSelectedRow(at indexPath: IndexPath) -> Bool 17 | func didSelect(at indexPath: IndexPath) 18 | func didDeselect(at indexPath: IndexPath) 19 | } 20 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/MultiselectCharts/Presenter/MultiselectChartsPresenter.swift: -------------------------------------------------------------------------------- 1 | import MobileAnalyticsChartSwift 2 | import UIKit 3 | 4 | final class MultiselectChartsPresenter { 5 | 6 | // MARK: - VIPER 7 | 8 | weak var view: MultiselectChartsViewInput? 9 | 10 | weak var analyticsChartSpriteKitModuleInput: AnalyticsChartSpriteKitModuleInput? 11 | 12 | private let analyticsChartViewModel: AnalyticsChartViewModel 13 | 14 | init(analyticsChartViewModel: AnalyticsChartViewModel) { 15 | self.analyticsChartViewModel = analyticsChartViewModel 16 | self.selectedRows = Array(repeating: true, count: analyticsChartViewModel.data.viewModels.count) 17 | } 18 | 19 | // MARK: - Stored properties 20 | 21 | private var selectedRows: [Bool] = [] 22 | } 23 | 24 | extension MultiselectChartsPresenter: MultiselectChartsViewOutput { 25 | func setupView() { 26 | let (chartView, moduleInput) = AnalyticsChartSpriteKitAssembly.makeModule( 27 | inputData: analyticsChartViewModel.data 28 | ) 29 | 30 | analyticsChartSpriteKitModuleInput = moduleInput 31 | 32 | view?.setTitle(analyticsChartViewModel.title) 33 | view?.reloadData() 34 | view?.setChartView(chartView) 35 | } 36 | 37 | func numberOfRows() -> Int { 38 | analyticsChartViewModel.data.viewModels.count 39 | } 40 | 41 | func colorForRow(at indexPath: IndexPath) -> UIColor { 42 | analyticsChartViewModel.data.viewModels[indexPath.row].configuration.path.color 43 | } 44 | 45 | func isSelectedRow(at indexPath: IndexPath) -> Bool { 46 | selectedRows[indexPath.row] 47 | } 48 | 49 | func didSelect(at indexPath: IndexPath) { 50 | selectedRows[indexPath.row].toggle() 51 | redrawCharts() 52 | } 53 | 54 | func didDeselect(at indexPath: IndexPath) { 55 | selectedRows[indexPath.row].toggle() 56 | redrawCharts() 57 | } 58 | 59 | private func redrawCharts() { 60 | let onlySelectedRows = selectedRows.enumerated().filter { $0.element } 61 | var viewModels: [AnalyticsChartSpriteKitViewModel] = [] 62 | for item in analyticsChartViewModel.data.viewModels.enumerated() { 63 | if onlySelectedRows.contains(where: { $0.offset == item.offset }) { 64 | viewModels.append(item.element) 65 | } 66 | } 67 | analyticsChartSpriteKitModuleInput?.setChartViewModels( 68 | viewModels: viewModels, 69 | silent: true 70 | ) 71 | } 72 | } 73 | 74 | extension MultiselectChartsPresenter: AnalyticsChartSpriteKitModuleOutput { 75 | func didChangeRangeValue( 76 | rangeValue: RangeValue 77 | ) { 78 | print(#function, rangeValue) 79 | } 80 | 81 | func didHandleLongPress() { 82 | print(#function) 83 | } 84 | 85 | func didHandlePan( 86 | deltaLocation: CGFloat 87 | ) { 88 | print(#function, deltaLocation) 89 | } 90 | 91 | func didHandlePinch( 92 | scale: CGFloat 93 | ) { 94 | print(#function, scale) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Modules/MultiselectCharts/View/MultiselectChartsViewController.swift: -------------------------------------------------------------------------------- 1 | import MobileAnalyticsChartSwift 2 | import UIKit 3 | 4 | final class MultiselectChartsViewController: UIViewController { 5 | 6 | // MARK: - VIPER 7 | 8 | var output: MultiselectChartsViewOutput! 9 | 10 | // MARK: - UI properties 11 | 12 | private lazy var chartView: UIView = { 13 | let view = UIView() 14 | return view 15 | }() 16 | 17 | private lazy var tableView: UITableView = { 18 | let view = UITableView() 19 | view.dataSource = self 20 | view.delegate = self 21 | view.register( 22 | UITableViewCell.self, 23 | forCellReuseIdentifier: Constants.defaultCellIdentifier 24 | ) 25 | view.allowsMultipleSelection = true 26 | view.tableFooterView = UIView() 27 | return view 28 | }() 29 | 30 | // MARK: - Managing the View 31 | 32 | override func loadView() { 33 | view = UIView() 34 | view.backgroundColor = .cararra 35 | setupView() 36 | setupConstraints() 37 | } 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | output.setupView() 42 | } 43 | 44 | // MARK: - Setup 45 | 46 | private func setupView() { 47 | [ 48 | chartView, 49 | tableView, 50 | ].forEach { 51 | $0.translatesAutoresizingMaskIntoConstraints = false 52 | view.addSubview($0) 53 | } 54 | } 55 | 56 | private func setupConstraints() { 57 | let constraints = [ 58 | chartView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 59 | chartView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 60 | chartView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 61 | 62 | tableView.topAnchor.constraint(equalTo: chartView.bottomAnchor, 63 | constant: Space.double), 64 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 65 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 66 | tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, 67 | constant: -Space.double), 68 | tableView.heightAnchor.constraint(equalToConstant: 200), 69 | ] 70 | NSLayoutConstraint.activate(constraints) 71 | } 72 | } 73 | 74 | // MARK: - MultiselectChartsViewInput 75 | 76 | extension MultiselectChartsViewController: MultiselectChartsViewInput { 77 | func reloadData() { 78 | tableView.reloadData() 79 | for i in 0 ..< output.numberOfRows() { 80 | let indexPath = IndexPath(row: i, section: 0) 81 | if output.isSelectedRow(at: indexPath) { 82 | tableView.selectRow( 83 | at: indexPath, 84 | animated: true, 85 | scrollPosition: .none 86 | ) 87 | } 88 | } 89 | } 90 | 91 | func setTitle(_ title: String) { 92 | navigationItem.title = title 93 | } 94 | 95 | func setChartView(_ chartView: UIView) { 96 | chartView.translatesAutoresizingMaskIntoConstraints = false 97 | self.chartView.addSubview(chartView) 98 | let constraints = [ 99 | chartView.topAnchor.constraint(equalTo: self.chartView.topAnchor), 100 | chartView.leadingAnchor.constraint(equalTo: self.chartView.leadingAnchor), 101 | chartView.trailingAnchor.constraint(equalTo: self.chartView.trailingAnchor), 102 | chartView.bottomAnchor.constraint(equalTo: self.chartView.bottomAnchor), 103 | ] 104 | NSLayoutConstraint.activate(constraints) 105 | } 106 | } 107 | 108 | // MARK: - UITableViewDataSource 109 | 110 | extension MultiselectChartsViewController: UITableViewDataSource { 111 | func tableView( 112 | _ tableView: UITableView, 113 | numberOfRowsInSection section: Int 114 | ) -> Int { 115 | output.numberOfRows() 116 | } 117 | 118 | func tableView( 119 | _ tableView: UITableView, 120 | cellForRowAt indexPath: IndexPath 121 | ) -> UITableViewCell { 122 | let cell = tableView.dequeueReusableCell( 123 | withIdentifier: Constants.defaultCellIdentifier, 124 | for: indexPath 125 | ) 126 | cell.textLabel?.text = "Chart №\(indexPath.row)" 127 | cell.textLabel?.textColor = output.colorForRow(at: indexPath) 128 | cell.tintColor = output.colorForRow(at: indexPath) 129 | cell.selectionStyle = .none 130 | if output.isSelectedRow(at: indexPath) { 131 | cell.accessoryType = .checkmark 132 | } else { 133 | cell.accessoryType = .none 134 | } 135 | return cell 136 | } 137 | } 138 | 139 | // MARK: - UITableViewDelegate 140 | 141 | extension MultiselectChartsViewController: UITableViewDelegate { 142 | func tableView( 143 | _ tableView: UITableView, 144 | didSelectRowAt indexPath: IndexPath 145 | ) { 146 | output.didSelect(at: indexPath) 147 | tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark 148 | } 149 | 150 | func tableView( 151 | _ tableView: UITableView, 152 | didDeselectRowAt indexPath: IndexPath 153 | ) { 154 | output.didDeselect(at: indexPath) 155 | tableView.cellForRow(at: indexPath)?.accessoryType = .none 156 | } 157 | } 158 | 159 | private extension MultiselectChartsViewController { 160 | enum Constants { 161 | static let defaultCellIdentifier = "defaultCellIdentifier" 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /MobileAnalyticsChartSwiftExamplePods/Podfile-example: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '11.0' 4 | use_frameworks! 5 | 6 | target 'MobileAnalyticsChartSwiftExamplePods' do 7 | pod 'MobileAnalyticsChartSwift', 8 | :git => 'https://github.com/yoomoney/mobile-analytics-chart-swift.git', 9 | :tag => '1.0.0' 10 | end -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MobileAnalyticsChartSwift", 8 | platforms: [ 9 | .iOS(.v11), 10 | ], 11 | products: [ 12 | .library( 13 | name: "MobileAnalyticsChartSwift", 14 | targets: ["MobileAnalyticsChartSwift"] 15 | ), 16 | ], 17 | dependencies: [], 18 | targets: [ 19 | .target( 20 | name: "MobileAnalyticsChartSwift", 21 | dependencies: [], 22 | path: "MobileAnalyticsChartSwift" 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | workspace 'MobileAnalyticsChartSwift.xcworkspace' 4 | platform :ios, '11.0' 5 | use_frameworks! 6 | 7 | target 'MobileAnalyticsChartSwiftExamplePods' do 8 | pod 'SwiftLint', '~> 0.27.0' 9 | pod 'MobileAnalyticsChartSwift', :path => './' 10 | end 11 | 12 | post_install do |installer| 13 | puts "Turn off build_settings 'Require Only App-Extension-Safe API' on all pods targets" 14 | puts "Turn on build_settings 'Supress swift warnings' on all pods targets" 15 | puts "Turn off build_settings 'Documentation comments' on all pods targets" 16 | installer.pods_project.targets.each do |target| 17 | target.build_configurations.each do |config| 18 | config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = '' 19 | config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' 20 | config.build_settings['SWIFT_SUPPRESS_WARNINGS'] = 'YES' 21 | config.build_settings['CLANG_WARN_DOCUMENTATION_COMMENTS'] = 'NO' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MobileAnalyticsChartSwift (1.3.0) 3 | - SwiftLint (0.27.0) 4 | 5 | DEPENDENCIES: 6 | - MobileAnalyticsChartSwift (from `./`) 7 | - SwiftLint (~> 0.27.0) 8 | 9 | SPEC REPOS: 10 | https://github.com/CocoaPods/Specs.git: 11 | - SwiftLint 12 | 13 | EXTERNAL SOURCES: 14 | MobileAnalyticsChartSwift: 15 | :path: "./" 16 | 17 | SPEC CHECKSUMS: 18 | MobileAnalyticsChartSwift: 2efcd20eb80da20ca77b5c65b31cdfb6a6f5f2f0 19 | SwiftLint: 3207c1faa2240bf8973b191820a116113cd11073 20 | 21 | PODFILE CHECKSUM: 31d319b88d1ce01558e41645a4f93f2a9519396b 22 | 23 | COCOAPODS: 1.9.3 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile Analytics Chart 2 | 3 | [![Platform](https://img.shields.io/badge/Support-iOS%2011.0+-brightgreen.svg)](https://img.shields.io/badge/Support-iOS%2011.0+-brightgreen.svg)[![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-4BC51D.svg?style=flat)](https://cocoapods.org/)[![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://cocoapods.org/)[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | ------ 6 | 7 | - [Feature](#feature) 8 | * [Animation redrawing](#animation-redrawing) 9 | * [Gestures](#gestures) 10 | * [Loading state](#loading-state) 11 | * [An intermediate value](#an-intermediate-value) 12 | * [Supported](#supported) 13 | * [Chart configuration](#chart-configuration) 14 | * [Render configuration](#render-configuration) 15 | * [Calculator configuration](#calculator-configuration) 16 | - [Changelog](#changelog) 17 | - [Connecting dependencies](#connecting-dependencies) 18 | * [Swift Package Manager](#swift-package-manager) 19 | * [CocoaPods](#cocoapods) 20 | * [Carthage](#carthage) 21 | - [Fast integration](#fast-integration) 22 | - [Run Example](#run-example) 23 | - [License](#license) 24 | 25 | ## Feature 26 | 27 | ------ 28 | 29 | #### Animation redrawing 30 | 31 | ![Animation redrawing](Resources/redraw.gif) 32 | 33 | #### Gestures 34 | 35 | ![Gestures](Resources/gesture.gif) 36 | 37 | #### Loading state 38 | 39 | ![Loading state](Resources/loading.gif) 40 | 41 | #### An intermediate value 42 | 43 | An intermediate value 44 | 45 | #### Supported 46 | 47 | - Multiple chart 48 | - Dragging and panning 49 | - Single point display 50 | - Animation redrawing 51 | - Customization 52 | - Dark Theme 53 | 54 | #### Chart configuration 55 | 56 | - Quantity and currency units 57 | - Linear, quadratic and horizontalQuadratic path type 58 | - Gradient 59 | 60 | #### Render configuration 61 | 62 | - Customize range label, XAxis, YAxis, zero line, definition view 63 | - Toggle gesture (Swipe, pinch and handle) 64 | - Setup redraw animation duration 65 | - Configure insets and margins 66 | 67 | #### Calculator configuration 68 | 69 | - Setup minimum and maximum like custom value or these values will be recalculated automatically 70 | - Customize automatically recalculated line width 71 | 72 | ------ 73 | 74 | ## Changelog 75 | 76 | [Link to Changelog](https://github.com/yoomoney/mobile-analytics-chart-swift/tree/master/CHANGELOG.md) 77 | 78 | ## Connecting dependencies 79 | 80 | ### Swift Package Manager 81 | 82 | ```swift 83 | dependencies: [ 84 | .package(url: "https://github.com/yoomoney/mobile-analytics-chart-swift") 85 | ], 86 | targets: [ 87 | .target( 88 | name: "MyProject", 89 | dependencies: [..., "MobileAnalyticsChartSwift"] 90 | ) 91 | ... 92 | ] 93 | ``` 94 | ### CocoaPods 95 | 96 | 1. Install CocoaPods 97 | 98 | ```shell 99 | gem install cocoapods 100 | ``` 101 | 102 | 2. Create file Podfile\ 103 | 104 | > CocoaPods provides ```pod init``` command to create a Podfile with default settings. 105 | 106 | 3. Add dependencie in `Podfile`.\ 107 | [Example](https://github.com/yoomoney/mobile-analytics-chart-swift/tree/master/MobileAnalyticsChartSwiftExamplePods/Podfile-example) `Podfile`from demo-app. 108 | 109 | ```shell 110 | source 'https://github.com/CocoaPods/Specs.git' 111 | platform :ios, '11.0' 112 | use_frameworks! 113 | 114 | target 'Your Target Name' do 115 | pod 'MobileAnalyticsChartSwift', 116 | :git => 'https://github.com/yoomoney/mobile-analytics-chart-swift.git', 117 | :tag => 'tag' 118 | end 119 | ``` 120 | 121 | > `Your Target Name` - the target name in Xcode for your application.\ 122 | > `tag` - SDK version. The current version can be found on github in the [releases](https://github.com/yoomoney/mobile-analytics-chart-swift/releases). 123 | 124 | 4. Run the command ```pod install``` 125 | 126 | ### Carthage 127 | 128 | Carthage is not currently supported. 129 | 130 | ## Fast integration 131 | 132 | 1. Create `AnalyticsChartSpriteKitModuleInputData`. 133 | 134 | > To work with MobileAnalyticsChartSwift entities, import the dependencies into the source file 135 | 136 | ```swift 137 | import MobileAnalyticsChartSwift 138 | ``` 139 | 140 | An example of creating a `AnalyticsChartSpriteKitModuleInputData`: 141 | 142 | ```swift 143 | // Creating a fade animation configuration 144 | let fadeAnimation = ChartFadeAnimation( 145 | fadeOutColor: UIColor(white: 219 / 255, alpha: 1), 146 | fadeInColor: UIColor(white: 236 / 255, alpha: 1), 147 | startDuration: 0.2, 148 | fadeOutDuration: 0.6, 149 | fadeInDuration: 0.6 150 | ) 151 | 152 | // Creating date formatters 153 | let dmmmyyyyDateFormatter = DateFormatter() 154 | dmmmyyyyDateFormatter.dateFormat = "d MMM yyyy" 155 | let dmmmDateFormatter = DateFormatter() 156 | dmmmDateFormatter.dateFormat = "d MMM" 157 | 158 | // Creating a chart data 159 | let calendar = Calendar.current 160 | let dates = (0 ..< 6).compactMap { 161 | calendar.date(byAdding: DateComponents(day: $0), to: Date()) 162 | } 163 | let values: [CGFloat] = [4, 8, 15, 16, 23, 42] 164 | let chartData = ChartData( 165 | values: values, 166 | dates: dates 167 | ) 168 | let path = ChartPath( 169 | type: .horizontalQuadratic, 170 | color: UIColor(red: 51 / 255, green: 102 / 255, blue: 1, alpha: 1), 171 | minWidth: 1.0, 172 | maxWidth: 5.0, 173 | fadeAnimation: fadeAnimation 174 | ) 175 | let chartRenderConfiguration = ChartRenderConfiguration( 176 | unit: .quantity, 177 | path: path, 178 | gradient: nil 179 | ) 180 | let analyticsChartSpriteKitViewModel = AnalyticsChartSpriteKitViewModel( 181 | data: chartData, 182 | configuration: chartRenderConfiguration 183 | ) 184 | 185 | // Creating a range label configuration 186 | let rangeLabel = ChartRangeLabel( 187 | color: UIColor(white: 102 / 255, alpha: 1), 188 | font: .systemFont(ofSize: 13, weight: .regular), 189 | dateFormatter: dmmmyyyyDateFormatter, 190 | insets: UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) 191 | ) 192 | 193 | // Creating a xAxis configuration 194 | let xAxis = ChartXAxis( 195 | labelColor: UIColor(white: 179 / 255, alpha: 1), 196 | labelFont: .systemFont(ofSize: 11, weight: .regular), 197 | dateFormatter: dmmmDateFormatter, 198 | insets: UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0), 199 | margins: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16), 200 | zoomFactorLabels: 1.5 201 | ) 202 | 203 | // Creating a yAxis configuration 204 | let yAxis = ChartYAxis( 205 | labelColor: UIColor(white: 179 / 255, alpha: 1), 206 | labelFont: .systemFont(ofSize: 11, weight: .regular), 207 | labelInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 0), 208 | lineColor: UIColor(white: 0, alpha: 0.12), 209 | lineWidth: 1 210 | ) 211 | 212 | // Creating a zero line 213 | let zeroLine = ChartZeroLine( 214 | color: UIColor(white: 236 / 255, alpha: 1), 215 | width: 1 216 | ) 217 | 218 | // Creating a gesture state configuration 219 | let gestureState = ChartGestureState( 220 | swipeIsActive: true, 221 | pinchIsActive: true, 222 | handleIsActive: true 223 | ) 224 | 225 | // Creating a animation configuration 226 | let animation = ChartAnimation( 227 | redrawDuration: 0.2 228 | ) 229 | 230 | // Creating a definition configuration 231 | let definitionView = ChartDefinitionView( 232 | backgroundColor: UIColor(red: 0.043, green: 0.09, blue: 0.204, alpha: 1), 233 | valueLabelFont: .systemFont(ofSize: 13), 234 | valueLabelColor: UIColor(white: 0.95, alpha: 1.0), 235 | dateLabelFont: .systemFont(ofSize: 11), 236 | dateLabelColor: UIColor(white: 0.7, alpha: 1.0), 237 | dateFormatter: dmmmyyyyDateFormatter 238 | ) 239 | let definition = ChartDefinition( 240 | line: ChartDefinitionLine(color: UIColor(white: 236 / 255, alpha: 1), width: 1), 241 | point: ChartDefinitionPoint(minRadius: 4, maxRadius: 8), 242 | view: definitionView, 243 | fadeAnimation: fadeAnimation 244 | ) 245 | 246 | // Creating a chart sprite kit module input data 247 | let chartViewModel = AnalyticsChartSpriteKitModuleInputData( 248 | viewModels: [analyticsChartSpriteKitViewModel], 249 | renderConfiguration: RenderConfiguration( 250 | rangeLabel: rangeLabel, 251 | xAxis: xAxis, 252 | yAxis: yAxis, 253 | zeroLine: zeroLine, 254 | gestureState: gestureState, 255 | animation: animation, 256 | definition: definition, 257 | backgroundColor: UIColor(white: 247 / 255, alpha: 1), 258 | chartInsets: .zero, 259 | chartMargins: UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0), 260 | fadeInDuration: 0.3, 261 | fadeOutDuration: 0.3 262 | ), 263 | calculatorConfiguration: CalculatorConfiguration( 264 | minStaticValue: nil, 265 | maxStaticValue: nil 266 | ) 267 | ) 268 | ``` 269 | 270 | 2. Create `ChartView` and `AnalyticsChartSpriteKitModuleInput` using `AnalyticsChartSpriteKitAssembly`. 271 | 272 | An example of creating: 273 | 274 | ```swift 275 | let (chartView, moduleInput) = AnalyticsChartSpriteKitAssembly.makeModule( 276 | inputData: chartViewModel 277 | ) 278 | ``` 279 | 280 | 3. ChartView is a UIView and you can use it wherever you want. 281 | 4. With AnalyticsChartSpriteKitModuleInput you can set charts, set visibility range and enable or disable charts loading state. 282 | 283 | ## Run Example 284 | 285 | To run the Example application, you need to: 286 | 287 | 1. Make a `git clone` of the repository. 288 | 289 | ```shell 290 | git clone https://github.com/yoomoney/mobile-analytics-chart-swift.git 291 | ``` 292 | 293 | 2. Go to the project folder in the console and execute the following commands: 294 | 295 | ```shell 296 | gem install bundler 297 | bundle 298 | pod install 299 | ``` 300 | 301 | 4. Open `MobileAnalyticsChartSwift.xcworkspace`. 302 | 5. Select and run a scheme `MobileAnalyticsChartSwiftExamplePods`. 303 | 304 | ## License 305 | 306 | Mobile Analytics Chart Swift available under the MIT license. See the [LICENSE](https://github.com/yoomoney/mobile-analytics-chart-swift/blob/master/LICENSE) file for more information. 307 | -------------------------------------------------------------------------------- /Resources/definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoomoney/mobile-analytics-chart-swift/bf8f3e1534d5fdcb0e4e480f5b9f139709c4f8ea/Resources/definition.gif -------------------------------------------------------------------------------- /Resources/gesture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoomoney/mobile-analytics-chart-swift/bf8f3e1534d5fdcb0e4e480f5b9f139709c4f8ea/Resources/gesture.gif -------------------------------------------------------------------------------- /Resources/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoomoney/mobile-analytics-chart-swift/bf8f3e1534d5fdcb0e4e480f5b9f139709c4f8ea/Resources/loading.gif -------------------------------------------------------------------------------- /Resources/redraw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoomoney/mobile-analytics-chart-swift/bf8f3e1534d5fdcb0e4e480f5b9f139709c4f8ea/Resources/redraw.gif --------------------------------------------------------------------------------