ChartDataSet.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. //
  2. // ChartDataSet.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 Algorithms
  12. import Foundation
  13. /// Determines how to round DataSet index values for `ChartDataSet.entryIndex(x, rounding)` when an exact x-value is not found.
  14. @objc
  15. public enum ChartDataSetRounding: Int
  16. {
  17. case up = 0
  18. case down = 1
  19. case closest = 2
  20. }
  21. /// The DataSet class represents one group or type of entries (Entry) in the Chart that belong together.
  22. /// It is designed to logically separate different groups of values inside the Chart (e.g. the values for a specific line in the LineChart, or the values of a specific group of bars in the BarChart).
  23. open class ChartDataSet: ChartBaseDataSet
  24. {
  25. public required init()
  26. {
  27. entries = []
  28. super.init()
  29. }
  30. public override convenience init(label: String)
  31. {
  32. self.init(entries: [], label: label)
  33. }
  34. @objc public init(entries: [ChartDataEntry], label: String)
  35. {
  36. self.entries = entries
  37. super.init(label: label)
  38. self.calcMinMax()
  39. }
  40. @objc public convenience init(entries: [ChartDataEntry])
  41. {
  42. self.init(entries: entries, label: "DataSet")
  43. }
  44. // MARK: - Data functions and accessors
  45. /// - Note: Calls `notifyDataSetChanged()` after setting a new value.
  46. /// - Returns: The array of y-values that this DataSet represents.
  47. /// the entries that this dataset represents / holds together
  48. @objc
  49. open private(set) var entries: [ChartDataEntry]
  50. /// Used to replace all entries of a data set while retaining styling properties.
  51. /// This is a separate method from a setter on `entries` to encourage usage
  52. /// of `Collection` conformances.
  53. ///
  54. /// - Parameter entries: new entries to replace existing entries in the dataset
  55. @objc
  56. public func replaceEntries(_ entries: [ChartDataEntry]) {
  57. self.entries = entries
  58. notifyDataSetChanged()
  59. }
  60. /// maximum y-value in the value array
  61. internal var _yMax: Double = -Double.greatestFiniteMagnitude
  62. /// minimum y-value in the value array
  63. internal var _yMin: Double = Double.greatestFiniteMagnitude
  64. /// maximum x-value in the value array
  65. internal var _xMax: Double = -Double.greatestFiniteMagnitude
  66. /// minimum x-value in the value array
  67. internal var _xMin: Double = Double.greatestFiniteMagnitude
  68. open override func calcMinMax()
  69. {
  70. _yMax = -Double.greatestFiniteMagnitude
  71. _yMin = Double.greatestFiniteMagnitude
  72. _xMax = -Double.greatestFiniteMagnitude
  73. _xMin = Double.greatestFiniteMagnitude
  74. guard !isEmpty else { return }
  75. forEach(calcMinMax)
  76. }
  77. open override func calcMinMaxY(fromX: Double, toX: Double)
  78. {
  79. _yMax = -Double.greatestFiniteMagnitude
  80. _yMin = Double.greatestFiniteMagnitude
  81. guard !isEmpty else { return }
  82. let indexFrom = entryIndex(x: fromX, closestToY: .nan, rounding: .closest)
  83. var indexTo = entryIndex(x: toX, closestToY: .nan, rounding: .up)
  84. if indexTo == -1 { indexTo = entryIndex(x: toX, closestToY: .nan, rounding: .closest) }
  85. guard indexTo >= indexFrom else { return }
  86. // only recalculate y
  87. self[indexFrom...indexTo].forEach(calcMinMaxY)
  88. }
  89. @objc open func calcMinMaxX(entry e: ChartDataEntry)
  90. {
  91. _xMin = Swift.min(e.x, _xMin)
  92. _xMax = Swift.max(e.x, _xMax)
  93. }
  94. @objc open func calcMinMaxY(entry e: ChartDataEntry)
  95. {
  96. _yMin = Swift.min(e.y, _yMin)
  97. _yMax = Swift.max(e.y, _yMax)
  98. }
  99. /// Updates the min and max x and y value of this DataSet based on the given Entry.
  100. ///
  101. /// - Parameters:
  102. /// - e:
  103. internal func calcMinMax(entry e: ChartDataEntry)
  104. {
  105. calcMinMaxX(entry: e)
  106. calcMinMaxY(entry: e)
  107. }
  108. /// The minimum y-value this DataSet holds
  109. @objc open override var yMin: Double { return _yMin }
  110. /// The maximum y-value this DataSet holds
  111. @objc open override var yMax: Double { return _yMax }
  112. /// The minimum x-value this DataSet holds
  113. @objc open override var xMin: Double { return _xMin }
  114. /// The maximum x-value this DataSet holds
  115. @objc open override var xMax: Double { return _xMax }
  116. /// The number of y-values this DataSet represents
  117. @available(*, deprecated, message: "Use `count` instead")
  118. open override var entryCount: Int { return count }
  119. /// - Throws: out of bounds
  120. /// if `i` is out of bounds, it may throw an out-of-bounds exception
  121. /// - Returns: The entry object found at the given index (not x-value!)
  122. @available(*, deprecated, message: "Use `subscript(index:)` instead.")
  123. open override func entryForIndex(_ i: Int) -> ChartDataEntry?
  124. {
  125. guard indices.contains(i) else {
  126. return nil
  127. }
  128. return self[i]
  129. }
  130. /// - Parameters:
  131. /// - xValue: the x-value
  132. /// - closestToY: If there are multiple y-values for the specified x-value,
  133. /// - rounding: determine whether to round up/down/closest if there is no Entry matching the provided x-value
  134. /// - Returns: The first Entry object found at the given x-value with binary search.
  135. /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value according to the rounding.
  136. /// nil if no Entry object at that x-value.
  137. open override func entryForXValue(
  138. _ xValue: Double,
  139. closestToY yValue: Double,
  140. rounding: ChartDataSetRounding) -> ChartDataEntry?
  141. {
  142. let index = entryIndex(x: xValue, closestToY: yValue, rounding: rounding)
  143. if index > -1
  144. {
  145. return self[index]
  146. }
  147. return nil
  148. }
  149. /// - Parameters:
  150. /// - xValue: the x-value
  151. /// - closestToY: If there are multiple y-values for the specified x-value,
  152. /// - Returns: The first Entry object found at the given x-value with binary search.
  153. /// If the no Entry at the specified x-value is found, this method returns the Entry at the closest x-value.
  154. /// nil if no Entry object at that x-value.
  155. open override func entryForXValue(
  156. _ xValue: Double,
  157. closestToY yValue: Double) -> ChartDataEntry?
  158. {
  159. return entryForXValue(xValue, closestToY: yValue, rounding: .closest)
  160. }
  161. /// - Returns: All Entry objects found at the given xIndex with binary search.
  162. /// An empty array if no Entry object at that index.
  163. open override func entriesForXValue(_ xValue: Double) -> [ChartDataEntry]
  164. {
  165. let match: (ChartDataEntry) -> Bool = { $0.x == xValue }
  166. var partitioned = self.entries
  167. _ = partitioned.partition(by: match)
  168. let i = partitioned.partitioningIndex(where: match)
  169. guard i < endIndex else { return [] }
  170. return partitioned[i...].prefix(while: match)
  171. }
  172. /// - Parameters:
  173. /// - xValue: x-value of the entry to search for
  174. /// - closestToY: If there are multiple y-values for the specified x-value,
  175. /// - rounding: Rounding method if exact value was not found
  176. /// - Returns: The array-index of the specified entry.
  177. /// If the no Entry at the specified x-value is found, this method returns the index of the Entry at the closest x-value according to the rounding.
  178. open override func entryIndex(
  179. x xValue: Double,
  180. closestToY yValue: Double,
  181. rounding: ChartDataSetRounding) -> Int
  182. {
  183. var closest = partitioningIndex { $0.x >= xValue }
  184. guard closest < endIndex else { return index(before: endIndex) }
  185. var closestXValue = self[closest].x
  186. switch rounding {
  187. case .up:
  188. // If rounding up, and found x-value is lower than specified x, and we can go upper...
  189. if closestXValue < xValue && closest < index(before: endIndex)
  190. {
  191. formIndex(after: &closest)
  192. }
  193. case .down:
  194. // If rounding down, and found x-value is upper than specified x, and we can go lower...
  195. if closestXValue > xValue && closest > startIndex
  196. {
  197. formIndex(before: &closest)
  198. }
  199. case .closest:
  200. // The closest value in the beginning of this function
  201. // `var closest = partitioningIndex { $0.x >= xValue }`
  202. // doesn't guarantee closest rounding method
  203. if closest > startIndex {
  204. let distanceAfter = abs(self[closest].x - xValue)
  205. let distanceBefore = abs(self[index(before: closest)].x - xValue)
  206. if distanceBefore < distanceAfter
  207. {
  208. closest = index(before: closest)
  209. }
  210. closestXValue = self[closest].x
  211. }
  212. }
  213. // Search by closest to y-value
  214. if !yValue.isNaN
  215. {
  216. while closest > startIndex && self[index(before: closest)].x == closestXValue
  217. {
  218. formIndex(before: &closest)
  219. }
  220. var closestYValue = self[closest].y
  221. var closestYIndex = closest
  222. while closest < index(before: endIndex)
  223. {
  224. formIndex(after: &closest)
  225. let value = self[closest]
  226. if value.x != closestXValue { break }
  227. if abs(value.y - yValue) <= abs(closestYValue - yValue)
  228. {
  229. closestYValue = yValue
  230. closestYIndex = closest
  231. }
  232. }
  233. closest = closestYIndex
  234. }
  235. return closest
  236. }
  237. /// - Parameters:
  238. /// - e: the entry to search for
  239. /// - Returns: The array-index of the specified entry
  240. // TODO: Should be returning `nil` to follow Swift convention
  241. @available(*, deprecated, message: "Use `firstIndex(of:)` or `lastIndex(of:)`")
  242. open override func entryIndex(entry e: ChartDataEntry) -> Int
  243. {
  244. return firstIndex(of: e) ?? -1
  245. }
  246. /// Adds an Entry to the DataSet dynamically.
  247. /// Entries are added to the end of the list.
  248. /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum.
  249. ///
  250. /// - Parameters:
  251. /// - e: the entry to add
  252. /// - Returns: True
  253. // TODO: This should return `Void` to follow Swift convention
  254. @available(*, deprecated, message: "Use `append(_:)` instead", renamed: "append(_:)")
  255. open override func addEntry(_ e: ChartDataEntry) -> Bool
  256. {
  257. append(e)
  258. return true
  259. }
  260. /// Adds an Entry to the DataSet dynamically.
  261. /// Entries are added to their appropriate index respective to it's x-index.
  262. /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum.
  263. ///
  264. /// - Parameters:
  265. /// - e: the entry to add
  266. /// - Returns: True
  267. // TODO: This should return `Void` to follow Swift convention
  268. open override func addEntryOrdered(_ e: ChartDataEntry) -> Bool
  269. {
  270. if let last = last, last.x > e.x
  271. {
  272. let startIndex = entryIndex(x: e.x, closestToY: e.y, rounding: .up)
  273. let closestIndex = self[startIndex...].lastIndex { $0.x < e.x }
  274. ?? startIndex
  275. calcMinMax(entry: e)
  276. entries.insert(e, at: closestIndex)
  277. }
  278. else
  279. {
  280. append(e)
  281. }
  282. return true
  283. }
  284. @available(*, renamed: "remove(_:)")
  285. open override func removeEntry(_ entry: ChartDataEntry) -> Bool
  286. {
  287. remove(entry)
  288. }
  289. /// Removes an Entry from the DataSet dynamically.
  290. /// This will also recalculate the current minimum and maximum values of the DataSet and the value-sum.
  291. ///
  292. /// - Parameters:
  293. /// - entry: the entry to remove
  294. /// - Returns: `true` if the entry was removed successfully, else if the entry does not exist
  295. open func remove(_ entry: ChartDataEntry) -> Bool
  296. {
  297. guard let index = firstIndex(of: entry) else { return false }
  298. _ = remove(at: index)
  299. return true
  300. }
  301. /// Removes the first Entry (at index 0) of this DataSet from the entries array.
  302. ///
  303. /// - Returns: `true` if successful, `false` if not.
  304. // TODO: This should return the removed entry to follow Swift convention.
  305. @available(*, deprecated, message: "Use `func removeFirst() -> ChartDataEntry` instead.")
  306. open override func removeFirst() -> Bool
  307. {
  308. let entry: ChartDataEntry? = isEmpty ? nil : removeFirst()
  309. return entry != nil
  310. }
  311. /// Removes the last Entry (at index size-1) of this DataSet from the entries array.
  312. ///
  313. /// - Returns: `true` if successful, `false` if not.
  314. // TODO: This should return the removed entry to follow Swift convention.
  315. @available(*, deprecated, message: "Use `func removeLast() -> ChartDataEntry` instead.")
  316. open override func removeLast() -> Bool
  317. {
  318. let entry: ChartDataEntry? = isEmpty ? nil : removeLast()
  319. return entry != nil
  320. }
  321. /// Removes all values from this DataSet and recalculates min and max value.
  322. @available(*, deprecated, message: "Use `removeAll(keepingCapacity:)` instead.")
  323. open override func clear()
  324. {
  325. removeAll(keepingCapacity: true)
  326. }
  327. // MARK: - Data functions and accessors
  328. // MARK: - NSCopying
  329. open override func copy(with zone: NSZone? = nil) -> Any
  330. {
  331. let copy = super.copy(with: zone) as! ChartDataSet
  332. copy.entries = entries
  333. copy._yMax = _yMax
  334. copy._yMin = _yMin
  335. copy._xMax = _xMax
  336. copy._xMin = _xMin
  337. return copy
  338. }
  339. }
  340. // MARK: MutableCollection
  341. extension ChartDataSet: MutableCollection {
  342. public typealias Index = Int
  343. public typealias Element = ChartDataEntry
  344. public var startIndex: Index {
  345. return entries.startIndex
  346. }
  347. public var endIndex: Index {
  348. return entries.endIndex
  349. }
  350. public func index(after: Index) -> Index {
  351. return entries.index(after: after)
  352. }
  353. @objc
  354. public subscript(position: Index) -> Element {
  355. get {
  356. // This is intentionally not a safe subscript to mirror
  357. // the behaviour of the built in Swift Collection Types
  358. return entries[position]
  359. }
  360. set {
  361. calcMinMax(entry: newValue)
  362. entries[position] = newValue
  363. }
  364. }
  365. }
  366. // MARK: RandomAccessCollection
  367. extension ChartDataSet: RandomAccessCollection {
  368. public func index(before: Index) -> Index {
  369. return entries.index(before: before)
  370. }
  371. }
  372. // MARK: RangeReplaceableCollection
  373. extension ChartDataSet: RangeReplaceableCollection {
  374. public func replaceSubrange<C>(_ subrange: Swift.Range<Index>, with newElements: C) where C : Collection, Element == C.Element {
  375. entries.replaceSubrange(subrange, with: newElements)
  376. notifyDataSetChanged()
  377. }
  378. public func append(_ newElement: Element) {
  379. calcMinMax(entry: newElement)
  380. entries.append(newElement)
  381. }
  382. public func remove(at position: Index) -> Element {
  383. let element = entries.remove(at: position)
  384. notifyDataSetChanged()
  385. return element
  386. }
  387. public func removeFirst() -> Element {
  388. let element = entries.removeFirst()
  389. notifyDataSetChanged()
  390. return element
  391. }
  392. public func removeFirst(_ n: Int) {
  393. entries.removeFirst(n)
  394. notifyDataSetChanged()
  395. }
  396. public func removeLast() -> Element {
  397. let element = entries.removeLast()
  398. notifyDataSetChanged()
  399. return element
  400. }
  401. public func removeLast(_ n: Int) {
  402. entries.removeLast(n)
  403. notifyDataSetChanged()
  404. }
  405. public func removeSubrange<R>(_ bounds: R) where R : RangeExpression, Index == R.Bound {
  406. entries.removeSubrange(bounds)
  407. notifyDataSetChanged()
  408. }
  409. @objc
  410. public func removeAll(keepingCapacity keepCapacity: Bool) {
  411. entries.removeAll(keepingCapacity: keepCapacity)
  412. notifyDataSetChanged()
  413. }
  414. }