ChartUtils.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. //
  2. // Utils.swift
  3. // Charts
  4. //
  5. // Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda
  6. // A port of MPAndroidChart for iOS
  7. // Licensed under Apache License 2.0
  8. //
  9. // https://github.com/danielgindi/Charts
  10. //
  11. import Foundation
  12. import CoreGraphics
  13. extension Comparable
  14. {
  15. func clamped(to range: ClosedRange<Self>) -> Self
  16. {
  17. if self > range.upperBound
  18. {
  19. return range.upperBound
  20. }
  21. else if self < range.lowerBound
  22. {
  23. return range.lowerBound
  24. }
  25. else
  26. {
  27. return self
  28. }
  29. }
  30. }
  31. extension FloatingPoint
  32. {
  33. var DEG2RAD: Self
  34. {
  35. return self * .pi / 180
  36. }
  37. var RAD2DEG: Self
  38. {
  39. return self * 180 / .pi
  40. }
  41. /// - Note: Value must be in degrees
  42. /// - Returns: An angle between 0.0 < 360.0 (not less than zero, less than 360)
  43. var normalizedAngle: Self
  44. {
  45. let angle = truncatingRemainder(dividingBy: 360)
  46. return (sign == .minus) ? angle + 360 : angle
  47. }
  48. }
  49. extension CGSize
  50. {
  51. func rotatedBy(degrees: CGFloat) -> CGSize
  52. {
  53. let radians = degrees.DEG2RAD
  54. return rotatedBy(radians: radians)
  55. }
  56. func rotatedBy(radians: CGFloat) -> CGSize
  57. {
  58. return CGSize(
  59. width: abs(width * cos(radians)) + abs(height * sin(radians)),
  60. height: abs(width * sin(radians)) + abs(height * cos(radians))
  61. )
  62. }
  63. }
  64. extension Double
  65. {
  66. /// Rounds the number to the nearest multiple of it's order of magnitude, rounding away from zero if halfway.
  67. func roundedToNextSignificant() -> Double
  68. {
  69. guard
  70. !isInfinite,
  71. !isNaN,
  72. self != 0
  73. else { return self }
  74. let d = ceil(log10(self < 0 ? -self : self))
  75. let pw = 1 - Int(d)
  76. let magnitude = pow(10.0, Double(pw))
  77. let shifted = (self * magnitude).rounded()
  78. return shifted / magnitude
  79. }
  80. var decimalPlaces: Int
  81. {
  82. guard
  83. !isNaN,
  84. !isInfinite,
  85. self != 0.0
  86. else { return 0 }
  87. let i = roundedToNextSignificant()
  88. guard
  89. !i.isInfinite,
  90. !i.isNaN
  91. else { return 0 }
  92. return Int(ceil(-log10(i))) + 2
  93. }
  94. }
  95. extension CGPoint
  96. {
  97. /// Calculates the position around a center point, depending on the distance from the center, and the angle of the position around the center.
  98. func moving(distance: CGFloat, atAngle angle: CGFloat) -> CGPoint
  99. {
  100. return CGPoint(x: x + distance * cos(angle.DEG2RAD),
  101. y: y + distance * sin(angle.DEG2RAD))
  102. }
  103. }
  104. extension CGContext
  105. {
  106. public func drawImage(_ image: NSUIImage, atCenter center: CGPoint, size: CGSize)
  107. {
  108. var drawOffset = CGPoint()
  109. drawOffset.x = center.x - (size.width / 2)
  110. drawOffset.y = center.y - (size.height / 2)
  111. NSUIGraphicsPushContext(self)
  112. if image.size.width != size.width && image.size.height != size.height
  113. {
  114. let key = "resized_\(size.width)_\(size.height)"
  115. // Try to take scaled image from cache of this image
  116. var scaledImage = objc_getAssociatedObject(image, key) as? NSUIImage
  117. if scaledImage == nil
  118. {
  119. // Scale the image
  120. NSUIGraphicsBeginImageContextWithOptions(size, false, 0.0)
  121. image.draw(in: CGRect(origin: .zero, size: size))
  122. scaledImage = NSUIGraphicsGetImageFromCurrentImageContext()
  123. NSUIGraphicsEndImageContext()
  124. // Put the scaled image in a cache owned by the original image
  125. objc_setAssociatedObject(image, key, scaledImage, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  126. }
  127. scaledImage?.draw(in: CGRect(origin: drawOffset, size: size))
  128. }
  129. else
  130. {
  131. image.draw(in: CGRect(origin: drawOffset, size: size))
  132. }
  133. NSUIGraphicsPopContext()
  134. }
  135. public func drawText(_ text: String, at point: CGPoint, align: TextAlignment, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5), angleRadians: CGFloat = 0.0, attributes: [NSAttributedString.Key : Any]?)
  136. {
  137. let drawPoint = getDrawPoint(text: text, point: point, align: align, attributes: attributes)
  138. if (angleRadians == 0.0)
  139. {
  140. NSUIGraphicsPushContext(self)
  141. (text as NSString).draw(at: drawPoint, withAttributes: attributes)
  142. NSUIGraphicsPopContext()
  143. }
  144. else
  145. {
  146. drawText(text, at: drawPoint, anchor: anchor, angleRadians: angleRadians, attributes: attributes)
  147. }
  148. }
  149. public func drawText(_ text: String, at point: CGPoint, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5), angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?)
  150. {
  151. var drawOffset = CGPoint()
  152. NSUIGraphicsPushContext(self)
  153. if angleRadians != 0.0
  154. {
  155. let size = text.size(withAttributes: attributes)
  156. // Move the text drawing rect in a way that it always rotates around its center
  157. drawOffset.x = -size.width * 0.5
  158. drawOffset.y = -size.height * 0.5
  159. var translate = point
  160. // Move the "outer" rect relative to the anchor, assuming its centered
  161. if anchor.x != 0.5 || anchor.y != 0.5
  162. {
  163. let rotatedSize = size.rotatedBy(radians: angleRadians)
  164. translate.x -= rotatedSize.width * (anchor.x - 0.5)
  165. translate.y -= rotatedSize.height * (anchor.y - 0.5)
  166. }
  167. saveGState()
  168. translateBy(x: translate.x, y: translate.y)
  169. rotate(by: angleRadians)
  170. (text as NSString).draw(at: drawOffset, withAttributes: attributes)
  171. restoreGState()
  172. }
  173. else
  174. {
  175. if anchor.x != 0.0 || anchor.y != 0.0
  176. {
  177. let size = text.size(withAttributes: attributes)
  178. drawOffset.x = -size.width * anchor.x
  179. drawOffset.y = -size.height * anchor.y
  180. }
  181. drawOffset.x += point.x
  182. drawOffset.y += point.y
  183. (text as NSString).draw(at: drawOffset, withAttributes: attributes)
  184. }
  185. NSUIGraphicsPopContext()
  186. }
  187. private func getDrawPoint(text: String, point: CGPoint, align: TextAlignment, attributes: [NSAttributedString.Key : Any]?) -> CGPoint
  188. {
  189. var point = point
  190. if align == .center
  191. {
  192. point.x -= text.size(withAttributes: attributes).width / 2.0
  193. }
  194. else if align == .right
  195. {
  196. point.x -= text.size(withAttributes: attributes).width
  197. }
  198. return point
  199. }
  200. func drawMultilineText(_ text: String, at point: CGPoint, constrainedTo size: CGSize, anchor: CGPoint, knownTextSize: CGSize, angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?)
  201. {
  202. var rect = CGRect(origin: .zero, size: knownTextSize)
  203. NSUIGraphicsPushContext(self)
  204. if angleRadians != 0.0
  205. {
  206. // Move the text drawing rect in a way that it always rotates around its center
  207. rect.origin.x = -knownTextSize.width * 0.5
  208. rect.origin.y = -knownTextSize.height * 0.5
  209. var translate = point
  210. // Move the "outer" rect relative to the anchor, assuming its centered
  211. if anchor.x != 0.5 || anchor.y != 0.5
  212. {
  213. let rotatedSize = knownTextSize.rotatedBy(radians: angleRadians)
  214. translate.x -= rotatedSize.width * (anchor.x - 0.5)
  215. translate.y -= rotatedSize.height * (anchor.y - 0.5)
  216. }
  217. saveGState()
  218. translateBy(x: translate.x, y: translate.y)
  219. rotate(by: angleRadians)
  220. (text as NSString).draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
  221. restoreGState()
  222. }
  223. else
  224. {
  225. if anchor.x != 0.0 || anchor.y != 0.0
  226. {
  227. rect.origin.x = -knownTextSize.width * anchor.x
  228. rect.origin.y = -knownTextSize.height * anchor.y
  229. }
  230. rect.origin.x += point.x
  231. rect.origin.y += point.y
  232. (text as NSString).draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
  233. }
  234. NSUIGraphicsPopContext()
  235. }
  236. func drawMultilineText(_ text: String, at point: CGPoint, constrainedTo size: CGSize, anchor: CGPoint, angleRadians: CGFloat, attributes: [NSAttributedString.Key : Any]?)
  237. {
  238. let rect = text.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
  239. drawMultilineText(text, at: point, constrainedTo: size, anchor: anchor, knownTextSize: rect.size, angleRadians: angleRadians, attributes: attributes)
  240. }
  241. }