ChartAxisValuesStaticGenerator.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. //
  2. // ChartAxisValuesStaticGenerator.swift
  3. // LoopUI
  4. //
  5. // Created by Nathaniel Hamming on 2020-09-08.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftCharts
  9. import UIKit
  10. extension ChartAxisValuesStaticGenerator {
  11. // This is the same as SwiftChart ChartAxisValuesStaticGenerator.generateAxisValuesWithChartPoints(...) with the exception that the `currentMultiple` is calculated linearly instead of quadratically
  12. static func generateYAxisValuesUsingLinearSegmentStep(chartPoints: [ChartPoint],
  13. minSegmentCount: Double,
  14. maxSegmentCount: Double,
  15. multiple: Double,
  16. axisValueGenerator: ChartAxisValueStaticGenerator,
  17. addPaddingSegmentIfEdge: Bool) -> [ChartAxisValue]
  18. {
  19. precondition(multiple > 0, "Invalid multiple: \(multiple)")
  20. let sortedChartPoints = chartPoints.sorted {(obj1, obj2) in
  21. return obj1.y.scalar < obj2.y.scalar
  22. }
  23. if let firstChartPoint = sortedChartPoints.first, let lastChartPoint = sortedChartPoints.last {
  24. let first = firstChartPoint.y.scalar
  25. let lastPar = lastChartPoint.y.scalar
  26. guard lastPar >=~ first else {fatalError("Invalid range generating axis values")}
  27. let last = lastPar =~ first ? lastPar + 1 : lastPar
  28. /// The first axis value will be less than or equal to the first scalar value, aligned with the desired multiple
  29. var firstValue = first - (first.truncatingRemainder(dividingBy: multiple))
  30. /// The last axis value will be greater than or equal to the last scalar value, aligned with the desired multiple
  31. let remainder = last.truncatingRemainder(dividingBy: multiple)
  32. var lastValue = remainder == 0 ? last : last + (multiple - remainder)
  33. var segmentSize = multiple
  34. /// If there should be a padding segment added when a scalar value falls on the first or last axis value, adjust the first and last axis values
  35. if firstValue =~ first && addPaddingSegmentIfEdge {
  36. firstValue = firstValue - segmentSize
  37. }
  38. // do not allow the first label to be displayed as -0
  39. while firstValue < 0 && firstValue.rounded() == -0 {
  40. firstValue = firstValue - segmentSize
  41. }
  42. if lastValue =~ last && addPaddingSegmentIfEdge {
  43. lastValue = lastValue + segmentSize
  44. }
  45. let distance = lastValue - firstValue
  46. var currentMultiple = multiple
  47. var segmentCount = distance / currentMultiple
  48. var potentialSegmentValues = stride(from: firstValue, to: lastValue, by: currentMultiple)
  49. /// Find the optimal number of segments and segment width
  50. /// If the number of segments is greater than desired, make each segment wider
  51. /// ensure no label of -0 will be displayed on the axis
  52. while segmentCount > maxSegmentCount ||
  53. !potentialSegmentValues.filter({ $0 < 0 && $0.rounded() == -0 }).isEmpty
  54. {
  55. currentMultiple += multiple
  56. segmentCount = distance / currentMultiple
  57. potentialSegmentValues = stride(from: firstValue, to: lastValue, by: currentMultiple)
  58. }
  59. segmentCount = ceil(segmentCount)
  60. /// Increase the number of segments until there are enough as desired
  61. while segmentCount < minSegmentCount {
  62. segmentCount += 1
  63. }
  64. segmentSize = currentMultiple
  65. /// Generate axis values from the first value, segment size and number of segments
  66. let offset = firstValue
  67. return (0...Int(segmentCount)).map {segment in
  68. var scalar = offset + (Double(segment) * segmentSize)
  69. // a value that could be displayed as 0 should truly be 0 to have the zero-line drawn correctly.
  70. if scalar != 0,
  71. scalar.rounded() == 0
  72. {
  73. scalar = 0
  74. }
  75. return axisValueGenerator(scalar)
  76. }
  77. } else {
  78. print("Trying to generate Y axis without datapoints, returning empty array")
  79. return []
  80. }
  81. }
  82. }
  83. fileprivate func =~ (a: Double, b: Double) -> Bool {
  84. return fabs(a - b) < Double.ulpOfOne
  85. }
  86. fileprivate func >=~ (a: Double, b: Double) -> Bool {
  87. return a =~ b || a > b
  88. }