diff --git a/Charts/Classes/Charts/BarChartView.swift b/Charts/Classes/Charts/BarChartView.swift index 6e48b6f2d2..9afd35a3f5 100644 --- a/Charts/Classes/Charts/BarChartView.swift +++ b/Charts/Classes/Charts/BarChartView.swift @@ -66,7 +66,7 @@ public class BarChartView: BarLineChartViewBase, BarChartDataProvider return nil } - return self.highlighter?.getHighlight(x: Double(pt.x), y: Double(pt.y)) + return self.highlighter?.getHighlight(x: pt.x, y: pt.y) } /// - returns: the bounding box of the specified Entry in the specified DataSet. Returns null if the Entry could not be found in the charts data. diff --git a/Charts/Classes/Charts/BarLineChartViewBase.swift b/Charts/Classes/Charts/BarLineChartViewBase.swift index 803dfbc95d..6d92118779 100644 --- a/Charts/Classes/Charts/BarLineChartViewBase.swift +++ b/Charts/Classes/Charts/BarLineChartViewBase.swift @@ -1618,7 +1618,7 @@ public class BarLineChartViewBase: ChartViewBase, BarLineScatterCandleBubbleChar return nil } - return self.highlighter?.getHighlight(x: Double(pt.x), y: Double(pt.y)) + return self.highlighter?.getHighlight(x: pt.x, y: pt.y) } /// - returns: the x and y values in the chart at the given touch point diff --git a/Charts/Classes/Charts/ChartViewBase.swift b/Charts/Classes/Charts/ChartViewBase.swift index ad43c0e9d5..9b66050f28 100755 --- a/Charts/Classes/Charts/ChartViewBase.swift +++ b/Charts/Classes/Charts/ChartViewBase.swift @@ -448,6 +448,13 @@ public class ChartViewBase: NSUIView, ChartDataProvider, ChartAnimatorDelegate highlightValue(highlight: highlight, callDelegate: false) } + /// Highlights the value at the given x-index in the given DataSet. + /// Provide -1 as the x-index to undo all highlighting. + public func highlightValue(xIndex xIndex: Int, dataSetIndex: Int) + { + highlightValue(xIndex: xIndex, dataSetIndex: dataSetIndex, callDelegate: true) + } + /// Highlights the value at the given x-index in the given DataSet. /// Provide -1 as the x-index to undo all highlighting. public func highlightValue(xIndex xIndex: Int, dataSetIndex: Int, callDelegate: Bool) @@ -482,7 +489,9 @@ public class ChartViewBase: NSUIView, ChartDataProvider, ChartAnimatorDelegate { // set the indices to highlight entry = _data?.getEntryForHighlight(h!) - if (entry === nil || entry!.xIndex != h?.xIndex) + if (entry == nil || + entry?.xIndex != h?.xIndex || + (entry?.value != h!.value && !isnan(h!.value))) { h = nil entry = nil diff --git a/Charts/Classes/Charts/HorizontalBarChartView.swift b/Charts/Classes/Charts/HorizontalBarChartView.swift index 283d07f72c..56c868b8b2 100644 --- a/Charts/Classes/Charts/HorizontalBarChartView.swift +++ b/Charts/Classes/Charts/HorizontalBarChartView.swift @@ -157,7 +157,7 @@ public class HorizontalBarChartView: BarChartView return nil } - return self.highlighter?.getHighlight(x: Double(pt.y), y: Double(pt.x)) + return self.highlighter?.getHighlight(x: pt.y, y: pt.x) } public override var lowestVisibleXIndex: Int diff --git a/Charts/Classes/Charts/PieRadarChartViewBase.swift b/Charts/Classes/Charts/PieRadarChartViewBase.swift index 5eea92d73c..f0c13e3a92 100755 --- a/Charts/Classes/Charts/PieRadarChartViewBase.swift +++ b/Charts/Classes/Charts/PieRadarChartViewBase.swift @@ -897,7 +897,7 @@ public class PieRadarChartViewBase: ChartViewBase // get the dataset that is closest to the selection (PieChart only has one DataSet) if (self.isKindOfClass(RadarChartView)) { - dataSetIndex = ChartUtils.closestDataSetIndex(valsAtIndex, value: Double(distance / (self as! RadarChartView).factor), axis: nil) + dataSetIndex = ChartUtils.closestDataSetIndexByValue(valsAtIndex: valsAtIndex, value: Double(distance / (self as! RadarChartView).factor), axis: nil) ?? -1 } if (dataSetIndex < 0) diff --git a/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift b/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift index c5313e7c49..d6800dd98d 100644 --- a/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift +++ b/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift @@ -71,6 +71,11 @@ public class ChartBaseDataSet: NSObject, IChartDataSet fatalError("yValForXIndex is not implemented in ChartBaseDataSet") } + public func yValsForXIndex(x: Int) -> [Double] + { + fatalError("yValsForXIndex is not implemented in ChartBaseDataSet") + } + public func entryForIndex(i: Int) -> ChartDataEntry? { fatalError("entryForIndex is not implemented in ChartBaseDataSet") @@ -86,6 +91,11 @@ public class ChartBaseDataSet: NSObject, IChartDataSet fatalError("entryForXIndex is not implemented in ChartBaseDataSet") } + public func entriesForXIndex(x: Int) -> [ChartDataEntry] + { + fatalError("entriesForXIndex is not implemented in ChartBaseDataSet") + } + public func entryIndex(xIndex x: Int, rounding: ChartDataSetRounding) -> Int { fatalError("entryIndex is not implemented in ChartBaseDataSet") diff --git a/Charts/Classes/Data/Implementations/Standard/ChartData.swift b/Charts/Classes/Data/Implementations/Standard/ChartData.swift index 072437cf9b..bb08b8a56c 100644 --- a/Charts/Classes/Data/Implementations/Standard/ChartData.swift +++ b/Charts/Classes/Data/Implementations/Standard/ChartData.swift @@ -439,7 +439,18 @@ public class ChartData: NSObject } else { - return _dataSets[highlight.dataSetIndex].entryForXIndex(highlight.xIndex) + // The value of the highlighted entry could be NaN - if we are not interested in highlighting a specific value. + + let entries = _dataSets[highlight.dataSetIndex].entriesForXIndex(highlight.xIndex) + for e in entries + { + if e.value == highlight.value || isnan(highlight.value) + { + return e + } + } + + return nil } } diff --git a/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift b/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift index 03adda37f5..34e2aa3f1d 100644 --- a/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift +++ b/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift @@ -152,6 +152,20 @@ public class ChartDataSet: ChartBaseDataSet else { return Double.NaN } } + /// - returns: all of the y values of the Entry objects at the given xIndex. Returns NaN if no value is at the given x-index. + public override func yValsForXIndex(x: Int) -> [Double] + { + let entries = self.entriesForXIndex(x) + + var yVals = [Double]() + for e in entries + { + yVals.append(e.value) + } + + return yVals + } + /// - returns: the entry object found at the given index (not x-index!) /// - throws: out of bounds /// if `i` is out of bounds, it may throw an out-of-bounds exception @@ -181,7 +195,9 @@ public class ChartDataSet: ChartBaseDataSet return entryForXIndex(x, rounding: .Closest) } - public func entriesForXIndex(x: Int) -> [ChartDataEntry] + /// - returns: all Entry objects found at the given xIndex with binary search. + /// An empty array if no Entry object at that index. + public override func entriesForXIndex(x: Int) -> [ChartDataEntry] { var entries = [ChartDataEntry]() @@ -190,7 +206,7 @@ public class ChartDataSet: ChartBaseDataSet while (low <= high) { - var m = Int((high + low) / 2) + var m = (high + low) / 2 var entry = _yVals[m] if (x == entry.xIndex) @@ -215,15 +231,19 @@ public class ChartDataSet: ChartBaseDataSet m += 1 } - } - - if (x > _yVals[m].xIndex) - { - low = m + 1 + + break } else { - high = m - 1 + if (x > _yVals[m].xIndex) + { + low = m + 1 + } + else + { + high = m - 1 + } } } diff --git a/Charts/Classes/Data/Implementations/Standard/CombinedChartData.swift b/Charts/Classes/Data/Implementations/Standard/CombinedChartData.swift index 2b9723255b..de809172ce 100644 --- a/Charts/Classes/Data/Implementations/Standard/CombinedChartData.swift +++ b/Charts/Classes/Data/Implementations/Standard/CombinedChartData.swift @@ -177,7 +177,7 @@ public class CombinedChartData: BarLineScatterCandleBubbleChartData data.append(bubbleData) } - return data; + return data } public override func notifyDataChanged() @@ -205,4 +205,41 @@ public class CombinedChartData: BarLineScatterCandleBubbleChartData super.notifyDataChanged() // recalculate everything } + + + /// Get the Entry for a corresponding highlight object + /// + /// - parameter highlight: + /// - returns: the entry that is highlighted + public override func getEntryForHighlight(highlight: ChartHighlight) -> ChartDataEntry? + { + let dataObjects = allData + + if highlight.dataIndex >= dataObjects.count + { + return nil + } + + let data = dataObjects[highlight.dataIndex] + + if highlight.dataSetIndex >= data.dataSetCount + { + return nil + } + else + { + // The value of the highlighted entry could be NaN - if we are not interested in highlighting a specific value. + + let entries = data.getDataSetByIndex(highlight.dataSetIndex).entriesForXIndex(highlight.xIndex) + for e in entries + { + if e.value == highlight.value || isnan(highlight.value) + { + return e + } + } + + return nil + } + } } diff --git a/Charts/Classes/Data/Interfaces/IChartDataSet.swift b/Charts/Classes/Data/Interfaces/IChartDataSet.swift index c799c22dd9..b7f43f7d56 100644 --- a/Charts/Classes/Data/Interfaces/IChartDataSet.swift +++ b/Charts/Classes/Data/Interfaces/IChartDataSet.swift @@ -40,6 +40,9 @@ public protocol IChartDataSet /// - returns: the value of the Entry object at the given xIndex. Returns NaN if no value is at the given x-index. func yValForXIndex(x: Int) -> Double + /// - returns: all of the y values of the Entry objects at the given xIndex. Returns NaN if no value is at the given x-index. + func yValsForXIndex(x: Int) -> [Double] + /// - returns: the entry object found at the given index (not x-index!) /// - throws: out of bounds /// if `i` is out of bounds, it may throw an out-of-bounds exception @@ -55,6 +58,10 @@ public protocol IChartDataSet /// nil if no Entry object at that index. func entryForXIndex(x: Int) -> ChartDataEntry? + /// - returns: all Entry objects found at the given xIndex with binary search. + /// An empty array if no Entry object at that index. + func entriesForXIndex(x: Int) -> [ChartDataEntry] + /// - returns: the array-index of the specified entry /// /// - parameter x: x-index of the entry to search for diff --git a/Charts/Classes/Highlight/BarChartHighlighter.swift b/Charts/Classes/Highlight/BarChartHighlighter.swift index e4496bd375..84ee718a70 100644 --- a/Charts/Classes/Highlight/BarChartHighlighter.swift +++ b/Charts/Classes/Highlight/BarChartHighlighter.swift @@ -17,40 +17,55 @@ import CoreGraphics public class BarChartHighlighter: ChartHighlighter { - public override func getHighlight(x x: Double, y: Double) -> ChartHighlight? + public override func getHighlight(x x: CGFloat, y: CGFloat) -> ChartHighlight? { - let h = super.getHighlight(x: x, y: y) - - if h === nil - { - return h - } - else + if let barData = self.chart?.data as? BarChartData { - if let set = self.chart?.data?.getDataSetByIndex(h!.dataSetIndex) as? BarChartDataSet + let xIndex = getXIndex(x) + let baseNoSpace = getBase(x) + let setCount = barData.dataSetCount + var dataSetIndex = Int(baseNoSpace) % setCount + + if dataSetIndex < 0 { - if set.isStacked - { - // create an array of the touch-point - var pt = CGPoint() - pt.y = CGFloat(y) - - // take any transformer to determine the x-axis value - self.chart?.getTransformer(set.axisDependency).pixelToValue(&pt) - - return getStackedHighlight(old: h, set: set, xIndex: h!.xIndex, dataSetIndex: h!.dataSetIndex, yValue: Double(pt.y)) - } + dataSetIndex = 0 + } + else if dataSetIndex >= setCount + { + dataSetIndex = setCount - 1 + } + + guard let selectionDetail = getSelectionDetail(xIndex: xIndex, y: y, dataSetIndex: dataSetIndex) + else { return nil } + + if let set = barData.getDataSetByIndex(dataSetIndex) as? IBarChartDataSet + where set.isStacked + { + var pt = CGPoint(x: 0.0, y: y) + + // take any transformer to determine the x-axis value + self.chart?.getTransformer(set.axisDependency).pixelToValue(&pt) + + return getStackedHighlight(selectionDetail: selectionDetail, + set: set, + xIndex: xIndex, + yValue: Double(pt.y)) } - return h + return ChartHighlight(xIndex: xIndex, + value: selectionDetail.value, + dataIndex: selectionDetail.dataIndex, + dataSetIndex: selectionDetail.dataSetIndex, + stackIndex: -1) } + return nil } - public override func getXIndex(x: Double) -> Int + public override func getXIndex(x: CGFloat) -> Int { - if let barChartData = self.chart?.data as? BarChartData + if let barData = self.chart?.data as? BarChartData { - if !barChartData.isGrouped + if !barData.isGrouped { return super.getXIndex(x) } @@ -58,10 +73,10 @@ public class BarChartHighlighter: ChartHighlighter { let baseNoSpace = getBase(x) - let setCount = barChartData.dataSetCount + let setCount = barData.dataSetCount var xIndex = Int(baseNoSpace) / setCount - let valCount = barChartData.xValCount + let valCount = barData.xValCount if xIndex < 0 { @@ -81,62 +96,59 @@ public class BarChartHighlighter: ChartHighlighter } } - public override func getDataSetIndex(xIndex xIndex: Int, x: Double, y: Double) -> Int + public override func getSelectionDetail(xIndex xIndex: Int, y: CGFloat, dataSetIndex: Int?) -> ChartSelectionDetail? { - if let barChartData = self.chart?.data as? BarChartData + if let barData = self.chart?.data as? BarChartData { - if !barChartData.isGrouped - { - return 0 - } - else - { - let baseNoSpace = getBase(x) - - let setCount = barChartData.dataSetCount - var dataSetIndex = Int(baseNoSpace) % setCount - - if dataSetIndex < 0 - { - dataSetIndex = 0 - } - else if dataSetIndex >= setCount - { - dataSetIndex = setCount - 1 - } - - return dataSetIndex - } + let dataSetIndex = dataSetIndex ?? 0 + let dataSet = barData.dataSetCount > dataSetIndex ? barData.getDataSetByIndex(dataSetIndex) : nil + let yValue = dataSet.yValForXIndex(xIndex) + + if isnan(yValue) { return nil } + + return ChartSelectionDetail(value: yValue, dataSetIndex: dataSetIndex, dataSet: dataSet) } else { - return 0 + return nil } } /// This method creates the Highlight object that also indicates which value of a stacked BarEntry has been selected. - /// - parameter old: the old highlight object before looking for stacked values + /// - parameter selectionDetail: the selection detail to work with /// - parameter set: /// - parameter xIndex: - /// - parameter dataSetIndex: /// - parameter yValue: /// - returns: - public func getStackedHighlight(old old: ChartHighlight?, set: BarChartDataSet, xIndex: Int, dataSetIndex: Int, yValue: Double) -> ChartHighlight? + public func getStackedHighlight(selectionDetail selectionDetail: ChartSelectionDetail, + set: IBarChartDataSet, + xIndex: Int, + yValue: Double) -> ChartHighlight? { - let entry = set.entryForXIndex(xIndex) as? BarChartDataEntry + guard let entry = set.entryForXIndex(xIndex) as? BarChartDataEntry + else { return nil } - if entry?.values === nil + if entry.values == nil { - return old + return ChartHighlight(xIndex: xIndex, + value: entry.value, + dataIndex: selectionDetail.dataIndex, + dataSetIndex: selectionDetail.dataSetIndex, + stackIndex: -1) } - if let ranges = getRanges(entry: entry!) + if let ranges = getRanges(entry: entry) where ranges.count > 0 { let stackIndex = getClosestStackIndex(ranges: ranges, value: yValue) - let h = ChartHighlight(xIndex: xIndex, dataSetIndex: dataSetIndex, stackIndex: stackIndex, range: ranges[stackIndex]) - return h + return ChartHighlight(xIndex: xIndex, + value: entry.positiveSum - entry.negativeSum, + dataIndex: selectionDetail.dataIndex, + dataSetIndex: selectionDetail.dataSetIndex, + stackIndex: stackIndex, + range: ranges[stackIndex]) } + return nil } @@ -173,33 +185,29 @@ public class BarChartHighlighter: ChartHighlighter /// Returns the base x-value to the corresponding x-touch value in pixels. /// - parameter x: /// - returns: - public func getBase(x: Double) -> Double + public func getBase(x: CGFloat) -> Double { - if let barChartData = self.chart?.data as? BarChartData - { - // create an array of the touch-point - var pt = CGPoint() - pt.x = CGFloat(x) - - // take any transformer to determine the x-axis value - self.chart?.getTransformer(ChartYAxis.AxisDependency.Left).pixelToValue(&pt) - let xVal = Double(pt.x) - - let setCount = barChartData.dataSetCount ?? 0 - - // calculate how often the group-space appears - let steps = Int(xVal / (Double(setCount) + Double(barChartData.groupSpace))) - - let groupSpaceSum = Double(barChartData.groupSpace) * Double(steps) - - let baseNoSpace = xVal - groupSpaceSum - - return baseNoSpace - } - else - { - return 0.0 - } + guard let barData = self.chart?.data as? BarChartData + else { return 0.0 } + + // create an array of the touch-point + var pt = CGPoint() + pt.x = CGFloat(x) + + // take any transformer to determine the x-axis value + self.chart?.getTransformer(ChartYAxis.AxisDependency.Left).pixelToValue(&pt) + let xVal = Double(pt.x) + + let setCount = barData.dataSetCount ?? 0 + + // calculate how often the group-space appears + let steps = Int(xVal / (Double(setCount) + Double(barData.groupSpace))) + + let groupSpaceSum = Double(barData.groupSpace) * Double(steps) + + let baseNoSpace = xVal - groupSpaceSum + + return baseNoSpace } /// Splits up the stack-values of the given bar-entry into Range objects. diff --git a/Charts/Classes/Highlight/ChartHighlight.swift b/Charts/Classes/Highlight/ChartHighlight.swift index 1417d86939..9ea695d519 100644 --- a/Charts/Classes/Highlight/ChartHighlight.swift +++ b/Charts/Classes/Highlight/ChartHighlight.swift @@ -19,6 +19,12 @@ public class ChartHighlight: NSObject /// the x-index of the highlighted value private var _xIndex = Int(0) + /// the y-value of the highlighted value + private var _value = Double.NaN + + /// the index of the data object - in case it refers to more than one + private var _dataIndex = Int(0) + /// the index of the dataset the highlighted value is in private var _dataSetIndex = Int(0) @@ -35,39 +41,74 @@ public class ChartHighlight: NSObject super.init() } - public init(xIndex x: Int, dataSetIndex: Int) + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter value: the y-value of the highlighted value + /// - parameter dataIndex: the index of the Data the highlighted value belongs to + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to + /// - parameter stackIndex: references which value of a stacked-bar entry has been selected + /// - parameter range: the range the selected stack-value is in + public init(xIndex x: Int, value: Double, dataIndex: Int, dataSetIndex: Int, stackIndex: Int, range: ChartRange?) { super.init() _xIndex = x + _value = value + _dataIndex = dataIndex _dataSetIndex = dataSetIndex + _stackIndex = stackIndex + _range = range } - public init(xIndex x: Int, dataSetIndex: Int, stackIndex: Int) + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter value: the y-value of the highlighted value + /// - parameter dataIndex: the index of the Data the highlighted value belongs to + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to + /// - parameter stackIndex: references which value of a stacked-bar entry has been selected + public convenience init(xIndex x: Int, value: Double, dataIndex: Int, dataSetIndex: Int, stackIndex: Int) { - super.init() - - _xIndex = x - _dataSetIndex = dataSetIndex - _stackIndex = stackIndex + self.init(xIndex: x, value: value, dataIndex: dataIndex, dataSetIndex: dataSetIndex, stackIndex: stackIndex, range: nil) } - /// Constructor, only used for stacked-barchart. - /// - /// - parameter x: the index of the highlighted value on the x-axis - /// - parameter dataSet: the index of the DataSet the highlighted value belongs to + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter value: the y-value of the highlighted value + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to /// - parameter stackIndex: references which value of a stacked-bar entry has been selected /// - parameter range: the range the selected stack-value is in - public convenience init(xIndex x: Int, dataSetIndex: Int, stackIndex: Int, range: ChartRange) + public convenience init(xIndex x: Int, value: Double, dataSetIndex: Int, stackIndex: Int, range: ChartRange?) { - self.init(xIndex: x, dataSetIndex: dataSetIndex, stackIndex: stackIndex) - - _range = range + self.init(xIndex: x, value: value, dataIndex: 0, dataSetIndex: dataSetIndex, stackIndex: stackIndex, range: range) } - - public var dataSetIndex: Int { return _dataSetIndex; } - public var xIndex: Int { return _xIndex; } - public var stackIndex: Int { return _stackIndex; } + + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter value: the y-value of the highlighted value + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to + /// - parameter stackIndex: references which value of a stacked-bar entry has been selected + /// - parameter range: the range the selected stack-value is in + public convenience init(xIndex x: Int, value: Double, dataSetIndex: Int, stackIndex: Int) + { + self.init(xIndex: x, value: value, dataIndex: 0, dataSetIndex: dataSetIndex, stackIndex: stackIndex, range: nil) + } + + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to + /// - parameter stackIndex: references which value of a stacked-bar entry has been selected + public convenience init(xIndex x: Int, dataSetIndex: Int, stackIndex: Int) + { + self.init(xIndex: x, value: Double.NaN, dataSetIndex: dataSetIndex, stackIndex: stackIndex, range: nil) + } + + /// - parameter xIndex: the index of the highlighted value on the x-axis + /// - parameter dataSetIndex: the index of the DataSet the highlighted value belongs to + public convenience init(xIndex x: Int, dataSetIndex: Int) + { + self.init(xIndex: x, value: Double.NaN, dataSetIndex: dataSetIndex, stackIndex: -1, range: nil) + } + + public var xIndex: Int { return _xIndex } + public var value: Double { return _value } + public var dataIndex: Int { return _dataIndex } + public var dataSetIndex: Int { return _dataSetIndex } + public var stackIndex: Int { return _stackIndex } /// - returns: the range of values the selected value of a stacked bar is in. (this is only relevant for stacked-barchart) public var range: ChartRange? { return _range } @@ -76,7 +117,7 @@ public class ChartHighlight: NSObject public override var description: String { - return "Highlight, xIndex: \(_xIndex), dataSetIndex: \(_dataSetIndex), stackIndex (only stacked barentry): \(_stackIndex)" + return "Highlight, xIndex: \(_xIndex), dataIndex (combined charts): \(_dataIndex),dataSetIndex: \(_dataSetIndex), stackIndex (only stacked barentry): \(_stackIndex), value: \(_value)" } public override func isEqual(object: AnyObject?) -> Bool @@ -96,6 +137,11 @@ public class ChartHighlight: NSObject return false } + if (object!.dataIndex != dataIndex) + { + return false + } + if (object!.dataSetIndex != _dataSetIndex) { return false @@ -106,6 +152,11 @@ public class ChartHighlight: NSObject return false } + if (object!.value != value) + { + return false + } + return true } } @@ -127,6 +178,11 @@ func ==(lhs: ChartHighlight, rhs: ChartHighlight) -> Bool return false } + if (lhs._dataIndex != rhs._dataIndex) + { + return false + } + if (lhs._dataSetIndex != rhs._dataSetIndex) { return false @@ -137,5 +193,10 @@ func ==(lhs: ChartHighlight, rhs: ChartHighlight) -> Bool return false } + if (lhs._value != rhs._value) + { + return false + } + return true } \ No newline at end of file diff --git a/Charts/Classes/Highlight/ChartHighlighter.swift b/Charts/Classes/Highlight/ChartHighlighter.swift index e02c4c4da5..ae24e0d73b 100644 --- a/Charts/Classes/Highlight/ChartHighlighter.swift +++ b/Charts/Classes/Highlight/ChartHighlighter.swift @@ -29,27 +29,21 @@ public class ChartHighlighter : NSObject /// - parameter x: /// - parameter y: /// - returns: - public func getHighlight(x x: Double, y: Double) -> ChartHighlight? + public func getHighlight(x x: CGFloat, y: CGFloat) -> ChartHighlight? { let xIndex = getXIndex(x) - if (xIndex == -Int.max) - { - return nil - } - let dataSetIndex = getDataSetIndex(xIndex: xIndex, x: x, y: y) - if (dataSetIndex == -Int.max) - { - return nil - } + guard let + selectionDetail = getSelectionDetail(xIndex: xIndex, y: y, dataSetIndex: nil) + else { return nil } - return ChartHighlight(xIndex: xIndex, dataSetIndex: dataSetIndex) + return ChartHighlight(xIndex: xIndex, value: selectionDetail.value, dataIndex: selectionDetail.dataIndex, dataSetIndex: selectionDetail.dataSetIndex, stackIndex: -1) } /// Returns the corresponding x-index for a given touch-position in pixels. /// - parameter x: /// - returns: - public func getXIndex(x: Double) -> Int + public func getXIndex(x: CGFloat) -> Int { // create an array of the touch-point var pt = CGPoint(x: x, y: 0.0) @@ -60,57 +54,65 @@ public class ChartHighlighter : NSObject return Int(round(pt.x)) } - /// Returns the corresponding dataset-index for a given xIndex and xy-touch position in pixels. + /// Returns the corresponding ChartSelectionDetail for a given xIndex and y-touch position in pixels. /// - parameter xIndex: - /// - parameter x: /// - parameter y: + /// - parameter dataSetIndex: A dataset index to look at - or nil, to figure that out automatically /// - returns: - public func getDataSetIndex(xIndex xIndex: Int, x: Double, y: Double) -> Int + public func getSelectionDetail(xIndex xIndex: Int, y: CGFloat, dataSetIndex: Int?) -> ChartSelectionDetail? { - let valsAtIndex = getSelectionDetailsAtIndex(xIndex) + let valsAtIndex = getSelectionDetailsAtIndex(xIndex, dataSetIndex: dataSetIndex) - let leftdist = ChartUtils.getMinimumDistance(valsAtIndex, val: y, axis: ChartYAxis.AxisDependency.Left) - let rightdist = ChartUtils.getMinimumDistance(valsAtIndex, val: y, axis: ChartYAxis.AxisDependency.Right) + let leftdist = ChartUtils.getMinimumDistance(valsAtIndex, y: y, axis: ChartYAxis.AxisDependency.Left) + let rightdist = ChartUtils.getMinimumDistance(valsAtIndex, y: y, axis: ChartYAxis.AxisDependency.Right) let axis = leftdist < rightdist ? ChartYAxis.AxisDependency.Left : ChartYAxis.AxisDependency.Right - let dataSetIndex = ChartUtils.closestDataSetIndex(valsAtIndex, value: y, axis: axis) + let detail = ChartUtils.closestSelectionDetailByPixelY(valsAtIndex: valsAtIndex, y: y, axis: axis) - return dataSetIndex + return detail } /// Returns a list of SelectionDetail object corresponding to the given xIndex. /// - parameter xIndex: + /// - parameter dataSetIndex: A dataset index to look at - or nil, to figure that out automatically /// - returns: - public func getSelectionDetailsAtIndex(xIndex: Int) -> [ChartSelectionDetail] + public func getSelectionDetailsAtIndex(xIndex: Int, dataSetIndex: Int?) -> [ChartSelectionDetail] { var vals = [ChartSelectionDetail]() var pt = CGPoint() - for i in 0 ..< (self.chart?.data?.dataSetCount ?? 0) + guard let + data = self.chart?.data + else { return vals } + + for i in 0 ..< data.dataSetCount { - let dataSet = self.chart!.data!.getDataSetByIndex(i) - - // dont include datasets that cannot be highlighted - if !dataSet.isHighlightEnabled + if dataSetIndex != nil && dataSetIndex != i { continue } - // extract all y-values from all DataSets at the given x-index - let yVal: Double = dataSet.yValForXIndex(xIndex) - if yVal.isNaN + let dataSet = data.getDataSetByIndex(i) + + // dont include datasets that cannot be highlighted + if !dataSet.isHighlightEnabled { continue } - pt.y = CGFloat(yVal) - - self.chart!.getTransformer(dataSet.axisDependency).pointValueToPixel(&pt) - - if !pt.y.isNaN + // extract all y-values from all DataSets at the given x-index + let yVals: [Double] = dataSet.yValsForXIndex(xIndex) + for yVal in yVals { - vals.append(ChartSelectionDetail(value: Double(pt.y), dataSetIndex: i, dataSet: dataSet)) + pt.y = CGFloat(yVal) + + self.chart!.getTransformer(dataSet.axisDependency).pointValueToPixel(&pt) + + if !pt.y.isNaN + { + vals.append(ChartSelectionDetail(y: pt.y, value: yVal, dataSetIndex: i, dataSet: dataSet)) + } } } diff --git a/Charts/Classes/Highlight/CombinedHighlighter.swift b/Charts/Classes/Highlight/CombinedHighlighter.swift index 658e298cd5..68a3f9499f 100644 --- a/Charts/Classes/Highlight/CombinedHighlighter.swift +++ b/Charts/Classes/Highlight/CombinedHighlighter.swift @@ -20,43 +20,43 @@ public class CombinedHighlighter: ChartHighlighter /// Returns a list of SelectionDetail object corresponding to the given xIndex. /// - parameter xIndex: /// - returns: - public override func getSelectionDetailsAtIndex(xIndex: Int) -> [ChartSelectionDetail] + public override func getSelectionDetailsAtIndex(xIndex: Int, dataSetIndex: Int?) -> [ChartSelectionDetail] { var vals = [ChartSelectionDetail]() + var pt = CGPoint() - if let data = self.chart?.data as? CombinedChartData + guard let + data = self.chart?.data as? CombinedChartData + else { return vals } + + // get all chartdata objects + var dataObjects = data.allData + + for i in 0 ..< dataObjects.count { - // get all chartdata objects - var dataObjects = data.allData - - var pt = CGPoint() - - for i in 0 ..< dataObjects.count + for j in 0 ..< dataObjects[i].dataSetCount { - for j in 0 ..< dataObjects[i].dataSetCount + let dataSet = dataObjects[i].getDataSetByIndex(j) + + // dont include datasets that cannot be highlighted + if !dataSet.isHighlightEnabled + { + continue + } + + // extract all y-values from all DataSets at the given x-index + let yVals: [Double] = dataSet.yValsForXIndex(xIndex) + for yVal in yVals { - let dataSet = dataObjects[i].getDataSetByIndex(j) - - // dont include datasets that cannot be highlighted - if !dataSet.isHighlightEnabled - { - continue - } - - // extract all y-values from all DataSets at the given x-index - let yVal = dataSet.yValForXIndex(xIndex) - if yVal.isNaN - { - continue - } - pt.y = CGFloat(yVal) - self.chart!.getTransformer(dataSet.axisDependency).pointValueToPixel(&pt) + self.chart! + .getTransformer(dataSet.axisDependency) + .pointValueToPixel(&pt) if !pt.y.isNaN { - vals.append(ChartSelectionDetail(value: Double(pt.y), dataSetIndex: j, dataSet: dataSet)) + vals.append(ChartSelectionDetail(y: pt.y, value: yVal, dataIndex: i, dataSetIndex: j, dataSet: dataSet)) } } } diff --git a/Charts/Classes/Highlight/HorizontalBarChartHighlighter.swift b/Charts/Classes/Highlight/HorizontalBarChartHighlighter.swift index 5103ba32cf..7f63b29f24 100644 --- a/Charts/Classes/Highlight/HorizontalBarChartHighlighter.swift +++ b/Charts/Classes/Highlight/HorizontalBarChartHighlighter.swift @@ -17,82 +17,75 @@ import CoreGraphics public class HorizontalBarChartHighlighter: BarChartHighlighter { - public override func getHighlight(x x: Double, y: Double) -> ChartHighlight? + public override func getHighlight(x x: CGFloat, y: CGFloat) -> ChartHighlight? { - let h = super.getHighlight(x: x, y: y) - - if h === nil + if let barData = self.chart?.data as? BarChartData { - return h - } - else - { - if let set = self.chart?.data?.getDataSetByIndex(h!.dataSetIndex) as? BarChartDataSet + let xIndex = getXIndex(x) + let baseNoSpace = getBase(x) + let setCount = barData.dataSetCount + var dataSetIndex = Int(baseNoSpace) % setCount + + if dataSetIndex < 0 { - if set.isStacked - { - // create an array of the touch-point - var pt = CGPoint() - pt.x = CGFloat(y) - - // take any transformer to determine the x-axis value - self.chart?.getTransformer(set.axisDependency).pixelToValue(&pt) - - return getStackedHighlight(old: h, set: set, xIndex: h!.xIndex, dataSetIndex: h!.dataSetIndex, yValue: Double(pt.x)) - } + dataSetIndex = 0 } - - return h - } - } - - public override func getXIndex(x: Double) -> Int - { - if let barChartData = self.chart?.data as? BarChartData - { - if !barChartData.isGrouped + else if dataSetIndex >= setCount { - // create an array of the touch-point - var pt = CGPoint(x: 0.0, y: x) - - // take any transformer to determine the x-axis value - self.chart?.getTransformer(ChartYAxis.AxisDependency.Left).pixelToValue(&pt) - - return Int(round(pt.y)) + dataSetIndex = setCount - 1 } - else + + guard let selectionDetail = getSelectionDetail(xIndex: xIndex, y: y, dataSetIndex: dataSetIndex) + else { return nil } + + if let set = barData.getDataSetByIndex(dataSetIndex) as? IBarChartDataSet + where set.isStacked { - let baseNoSpace = getBase(x) + var pt = CGPoint(x: y, y: 0.0) - let setCount = barChartData.dataSetCount - var xIndex = Int(baseNoSpace) / setCount - - let valCount = barChartData.xValCount - - if xIndex < 0 - { - xIndex = 0 - } - else if xIndex >= valCount - { - xIndex = valCount - 1 - } + // take any transformer to determine the x-axis value + self.chart?.getTransformer(set.axisDependency).pixelToValue(&pt) - return xIndex + return getStackedHighlight(selectionDetail: selectionDetail, + set: set, + xIndex: xIndex, + yValue: Double(pt.x)) } + + return ChartHighlight(xIndex: xIndex, + value: selectionDetail.value, + dataIndex: selectionDetail.dataIndex, + dataSetIndex: selectionDetail.dataSetIndex, + stackIndex: -1) + } + return nil + } + + public override func getXIndex(x: CGFloat) -> Int + { + if let barData = self.chart?.data as? BarChartData + where !barData.isGrouped + { + // create an array of the touch-point + var pt = CGPoint(x: 0.0, y: x) + + // take any transformer to determine the x-axis value + self.chart?.getTransformer(ChartYAxis.AxisDependency.Left).pixelToValue(&pt) + + return Int(round(pt.y)) } else { - return 0 + return getXIndex(x) } } /// Returns the base y-value to the corresponding x-touch value in pixels. /// - parameter y: /// - returns: - public override func getBase(y: Double) -> Double + public override func getBase(y: CGFloat) -> Double { - if let barChartData = self.chart?.data as? BarChartData + if let barData = self.chart?.data as? BarChartData { // create an array of the touch-point var pt = CGPoint() @@ -102,12 +95,12 @@ public class HorizontalBarChartHighlighter: BarChartHighlighter self.chart?.getTransformer(ChartYAxis.AxisDependency.Left).pixelToValue(&pt) let yVal = Double(pt.y) - let setCount = barChartData.dataSetCount ?? 0 + let setCount = barData.dataSetCount ?? 0 // calculate how often the group-space appears - let steps = Int(yVal / (Double(setCount) + Double(barChartData.groupSpace))) + let steps = Int(yVal / (Double(setCount) + Double(barData.groupSpace))) - let groupSpaceSum = Double(barChartData.groupSpace) * Double(steps) + let groupSpaceSum = Double(barData.groupSpace) * Double(steps) let baseNoSpace = yVal - groupSpaceSum diff --git a/Charts/Classes/Renderers/BubbleChartRenderer.swift b/Charts/Classes/Renderers/BubbleChartRenderer.swift index 7013db9980..7679100661 100644 --- a/Charts/Classes/Renderers/BubbleChartRenderer.swift +++ b/Charts/Classes/Renderers/BubbleChartRenderer.swift @@ -241,12 +241,14 @@ public class BubbleChartRenderer: ChartDataRendererBase for indice in indices { - guard let dataSet = bubbleData.getDataSetByIndex(indice.dataSetIndex) as? IBubbleChartDataSet else { continue } + guard let dataSet = bubbleData.getDataSetByIndex(indice.dataSetIndex) as? IBubbleChartDataSet + where dataSet.isHighlightEnabled + else { continue } - if (!dataSet.isHighlightEnabled) - { - continue - } + guard let + entry = bubbleData.getEntryForHighlight(indice) as? BubbleChartDataEntry + where entry.xIndex == indice.xIndex + else { continue } let entryFrom = dataSet.entryForXIndex(self.minX) let entryTo = dataSet.entryForXIndex(self.maxX) @@ -254,12 +256,6 @@ public class BubbleChartRenderer: ChartDataRendererBase let minx = max(dataSet.entryIndex(entry: entryFrom!), 0) let maxx = min(dataSet.entryIndex(entry: entryTo!) + 1, dataSet.entryCount) - let entry: BubbleChartDataEntry! = bubbleData.getEntryForHighlight(indice) as! BubbleChartDataEntry - if (entry === nil || entry.xIndex != indice.xIndex) - { - continue - } - let trans = dataProvider.getTransformer(dataSet.axisDependency) _sizeBuffer[0].x = 0.0 diff --git a/Charts/Classes/Renderers/CombinedChartRenderer.swift b/Charts/Classes/Renderers/CombinedChartRenderer.swift index 6cf5416a51..ae8fd16817 100644 --- a/Charts/Classes/Renderers/CombinedChartRenderer.swift +++ b/Charts/Classes/Renderers/CombinedChartRenderer.swift @@ -121,7 +121,34 @@ public class CombinedChartRenderer: ChartDataRendererBase { for renderer in _renderers { - renderer.drawHighlighted(context: context, indices: indices) + var data: ChartData? + + if renderer is BarChartRenderer + { + data = (renderer as! BarChartRenderer).dataProvider?.barData + } + else if renderer is LineChartRenderer + { + data = (renderer as! LineChartRenderer).dataProvider?.lineData + } + else if renderer is CandleStickChartRenderer + { + data = (renderer as! CandleStickChartRenderer).dataProvider?.candleData + } + else if renderer is ScatterChartRenderer + { + data = (renderer as! ScatterChartRenderer).dataProvider?.scatterData + } + else if renderer is BubbleChartRenderer + { + data = (renderer as! BubbleChartRenderer).dataProvider?.bubbleData + } + + let dataIndex = data == nil ? nil : (chart?.data as? CombinedChartData)?.allData.indexOf(data!) + + let dataIndices = indices.filter{ $0.dataIndex == dataIndex } + + renderer.drawHighlighted(context: context, indices: dataIndices) } } diff --git a/Charts/Classes/Utils/ChartSelectionDetail.swift b/Charts/Classes/Utils/ChartSelectionDetail.swift index a2bd4832d2..4682468e51 100644 --- a/Charts/Classes/Utils/ChartSelectionDetail.swift +++ b/Charts/Classes/Utils/ChartSelectionDetail.swift @@ -16,7 +16,9 @@ import Foundation public class ChartSelectionDetail: NSObject { + private var _y = CGFloat.NaN private var _value = Double(0) + private var _dataIndex = Int(0) private var _dataSetIndex = Int(0) private var _dataSet: IChartDataSet! @@ -25,20 +27,42 @@ public class ChartSelectionDetail: NSObject super.init() } - public init(value: Double, dataSetIndex: Int, dataSet: IChartDataSet) + public init(y: CGFloat, value: Double, dataIndex: Int, dataSetIndex: Int, dataSet: IChartDataSet) { super.init() + _y = y _value = value + _dataIndex = dataIndex _dataSetIndex = dataSetIndex _dataSet = dataSet } + public convenience init(y: CGFloat, value: Double, dataSetIndex: Int, dataSet: IChartDataSet) + { + self.init(y: y, value: value, dataIndex: 0, dataSetIndex: dataSetIndex, dataSet: dataSet) + } + + public convenience init(value: Double, dataSetIndex: Int, dataSet: IChartDataSet) + { + self.init(y: CGFloat.NaN, value: value, dataIndex: 0, dataSetIndex: dataSetIndex, dataSet: dataSet) + } + + public var y: CGFloat + { + return _y + } + public var value: Double { return _value } + public var dataIndex: Int + { + return _dataIndex + } + public var dataSetIndex: Int { return _dataSetIndex diff --git a/Charts/Classes/Utils/ChartUtils.swift b/Charts/Classes/Utils/ChartUtils.swift index 7036ce4007..8cc191f0d9 100755 --- a/Charts/Classes/Utils/ChartUtils.swift +++ b/Charts/Classes/Utils/ChartUtils.swift @@ -67,12 +67,60 @@ public class ChartUtils return number + DBL_EPSILON } } - - /// - returns: the index of the DataSet that contains the closest value on the y-axis. This will return -Integer.MAX_VALUE if failure. - internal class func closestDataSetIndex(valsAtIndex: [ChartSelectionDetail], value: Double, axis: ChartYAxis.AxisDependency?) -> Int + + /// - returns: the index of the DataSet that contains the closest value on the y-axis + internal class func closestDataSetIndexByPixelY( + valsAtIndex valsAtIndex: [ChartSelectionDetail], + y: CGFloat, + axis: ChartYAxis.AxisDependency?) -> Int? + { + return closestSelectionDetailByPixelY(valsAtIndex: valsAtIndex, y: y, axis: axis)?.dataSetIndex + } + + /// - returns: the index of the DataSet that contains the closest value on the y-axis + internal class func closestDataSetIndexByValue( + valsAtIndex valsAtIndex: [ChartSelectionDetail], + value: Double, + axis: ChartYAxis.AxisDependency?) -> Int? + { + return closestSelectionDetailByValue(valsAtIndex: valsAtIndex, value: value, axis: axis)?.dataSetIndex + } + + /// - returns: the `ChartSelectionDetail` of the closest value on the y-axis + internal class func closestSelectionDetailByPixelY( + valsAtIndex valsAtIndex: [ChartSelectionDetail], + y: CGFloat, + axis: ChartYAxis.AxisDependency?) -> ChartSelectionDetail? + { + var distance = CGFloat.max + var detail: ChartSelectionDetail? + + for i in 0 ..< valsAtIndex.count + { + let sel = valsAtIndex[i] + + if (axis == nil || sel.dataSet?.axisDependency == axis) + { + let cdistance = abs(sel.y - y) + if (cdistance < distance) + { + detail = sel + distance = cdistance + } + } + } + + return detail + } + + /// - returns: the `ChartSelectionDetail` of the closest value on the y-axis + internal class func closestSelectionDetailByValue( + valsAtIndex valsAtIndex: [ChartSelectionDetail], + value: Double, + axis: ChartYAxis.AxisDependency?) -> ChartSelectionDetail? { - var index = -Int.max var distance = DBL_MAX + var detail: ChartSelectionDetail? for i in 0 ..< valsAtIndex.count { @@ -83,19 +131,22 @@ public class ChartUtils let cdistance = abs(sel.value - value) if (cdistance < distance) { - index = valsAtIndex[i].dataSetIndex + detail = sel distance = cdistance } } } - return index + return detail } /// - returns: the minimum distance from a touch-y-value (in pixels) to the closest y-value (in pixels) that is displayed in the chart. - internal class func getMinimumDistance(valsAtIndex: [ChartSelectionDetail], val: Double, axis: ChartYAxis.AxisDependency) -> Double + internal class func getMinimumDistance( + valsAtIndex: [ChartSelectionDetail], + y: CGFloat, + axis: ChartYAxis.AxisDependency) -> CGFloat { - var distance = DBL_MAX + var distance = CGFloat.max for i in 0 ..< valsAtIndex.count { @@ -103,7 +154,7 @@ public class ChartUtils if (sel.dataSet!.axisDependency == axis) { - let cdistance = abs(sel.value - val) + let cdistance = abs(sel.y - y) if (cdistance < distance) { distance = cdistance diff --git a/ChartsRealm/Classes/Data/RealmBaseDataSet.swift b/ChartsRealm/Classes/Data/RealmBaseDataSet.swift index abe7a3fab7..2357392365 100644 --- a/ChartsRealm/Classes/Data/RealmBaseDataSet.swift +++ b/ChartsRealm/Classes/Data/RealmBaseDataSet.swift @@ -294,6 +294,20 @@ public class RealmBaseDataSet: ChartBaseDataSet else { return Double.NaN } } + /// - returns: all of the y values of the Entry objects at the given xIndex. Returns NaN if no value is at the given x-index. + public override func yValsForXIndex(x: Int) -> [Double] + { + let entries = self.entriesForXIndex(x) + + var yVals = [Double]() + for e in entries + { + yVals.append(e.value) + } + + return yVals + } + /// - returns: the entry object found at the given index (not x-index!) /// - throws: out of bounds /// if `i` is out of bounds, it may throw an out-of-bounds exception @@ -327,6 +341,36 @@ public class RealmBaseDataSet: ChartBaseDataSet return entryForXIndex(x, rounding: .Closest) } + /// - returns: all Entry objects found at the given xIndex with binary search. + /// An empty array if no Entry object at that index. + public override func entriesForXIndex(x: Int) -> [ChartDataEntry] + { + var entries = [ChartDataEntry]() + + guard let results = _results else { return entries } + + if _xIndexField == nil + { + if results.count > UInt(x) + { + entries.append(buildEntryFromResultObject(results.objectAtIndex(UInt(x)), atIndex: UInt(x))) + } + } + else + { + let foundObjects = results.objectsWithPredicate( + NSPredicate(format: "%K == %d", _xIndexField!, x) + ) + + for e in foundObjects + { + entries.append(buildEntryFromResultObject(e as! RLMObject, atIndex: UInt(x))) + } + } + + return entries + } + /// - returns: the array-index of the specified entry /// /// - parameter x: x-index of the entry to search for