diff --git a/Charts/Charts.xcodeproj/project.pbxproj b/Charts/Charts.xcodeproj/project.pbxproj index 82aca392a5..f026061921 100644 --- a/Charts/Charts.xcodeproj/project.pbxproj +++ b/Charts/Charts.xcodeproj/project.pbxproj @@ -300,6 +300,8 @@ 65F06FAE1BE826010074498D /* IPieChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F06FAC1BE826010074498D /* IPieChartDataSet.swift */; }; 65F06FB01BE826090074498D /* IRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F06FAF1BE826090074498D /* IRadarChartDataSet.swift */; }; 65F06FB11BE826090074498D /* IRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F06FAF1BE826090074498D /* IRadarChartDataSet.swift */; }; + 909437471CA0CBC000BAFA36 /* TimeLineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909437461CA0CBC000BAFA36 /* TimeLineChartView.swift */; }; + 9094374B1CA0CBF000BAFA36 /* TimeLineChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 909437491CA0CBF000BAFA36 /* TimeLineChartRenderer.swift */; }; A52C5C3F1BAC5D1100594CDD /* ChartAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6A546D1AA5D2DC000F57C2 /* ChartAnimator.swift */; }; A52C5C401BAC5D1100594CDD /* ChartAnimationEasing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B378F161AD500A4009414A4 /* ChartAnimationEasing.swift */; }; A52C5C411BAC5D1100594CDD /* BarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6A54981AA66B14000F57C2 /* BarChartView.swift */; }; @@ -506,6 +508,8 @@ 65F06FA91BE818AA0074498D /* IBarChartDataSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IBarChartDataSet.swift; sourceTree = ""; }; 65F06FAC1BE826010074498D /* IPieChartDataSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPieChartDataSet.swift; sourceTree = ""; }; 65F06FAF1BE826090074498D /* IRadarChartDataSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IRadarChartDataSet.swift; sourceTree = ""; }; + 909437461CA0CBC000BAFA36 /* TimeLineChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeLineChartView.swift; sourceTree = ""; }; + 909437491CA0CBF000BAFA36 /* TimeLineChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeLineChartRenderer.swift; sourceTree = ""; }; A52C5C371BAC5CA400594CDD /* Charts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Charts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DDC77DC51C0A991A00C27BCF /* BarChartTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarChartTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -603,6 +607,7 @@ 5B759ED41A9F98A90039D97F /* Renderers */ = { isa = PBXGroup; children = ( + 909437491CA0CBF000BAFA36 /* TimeLineChartRenderer.swift */, 5B6A54961AA66AD2000F57C2 /* BarChartRenderer.swift */, 55E3565A1ADC63EB00A57971 /* BubbleChartRenderer.swift */, 5B6A54941AA66AC0000F57C2 /* CandleStickChartRenderer.swift */, @@ -681,6 +686,7 @@ 5BA8EC661A9D151C00CE82E1 /* Charts */ = { isa = PBXGroup; children = ( + 909437461CA0CBC000BAFA36 /* TimeLineChartView.swift */, 5B6A54981AA66B14000F57C2 /* BarChartView.swift */, 5B6A549A1AA66B2C000F57C2 /* BarLineChartViewBase.swift */, 55E356521ADC63BF00A57971 /* BubbleChartView.swift */, @@ -1047,6 +1053,7 @@ 659400D01BF463C2004F9C27 /* ScatterChartDataSet.swift in Sources */, 659400B21BF463C1004F9C27 /* CandleChartDataEntry.swift in Sources */, 659400B61BF463C1004F9C27 /* ChartData.swift in Sources */, + 909437471CA0CBC000BAFA36 /* TimeLineChartView.swift in Sources */, 5B680D221A9D17C30026A057 /* ChartXAxis.swift in Sources */, 5BA8EC891A9D151C00CE82E1 /* ChartDataBaseFilter.swift in Sources */, 659400A21BF463C1004F9C27 /* BarChartDataEntry.swift in Sources */, @@ -1072,6 +1079,7 @@ 5B00324D1B65351C00B6A2FE /* HorizontalBarChartHighlighter.swift in Sources */, 65F06FB01BE826090074498D /* IRadarChartDataSet.swift in Sources */, 659400C61BF463C1004F9C27 /* PieChartData.swift in Sources */, + 9094374B1CA0CBF000BAFA36 /* TimeLineChartRenderer.swift in Sources */, 5B6A54871AA669F4000F57C2 /* RadarChartRenderer.swift in Sources */, 5B6A548D1AA66A60000F57C2 /* ChartLegendRenderer.swift in Sources */, 65F06F941BE812210074498D /* IBarLineScatterCandleBubbleChartDataSet.swift in Sources */, diff --git a/Charts/Classes/Charts/TimeLineChartView.swift b/Charts/Classes/Charts/TimeLineChartView.swift new file mode 100644 index 0000000000..6ef75b37ff --- /dev/null +++ b/Charts/Classes/Charts/TimeLineChartView.swift @@ -0,0 +1,37 @@ +// +// TimeLineChartView.swift +// Charts +// +// Created by Daniel Cohen Gindi on 4/3/15. +// +// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda +// A port of MPAndroidChart for iOS +// Licensed under Apache License 2.0 +// +// https://github.com/danielgindi/ios-charts +// + +import Foundation +import CoreGraphics + +/// Chart that do all that line charts do and also support using the x axis to represent time +public class TimeLineChartView: LineChartView +{ + internal override func initialize() + { + super.initialize() + + renderer = TimeLineChartRenderer(dataProvider: self, animator: _animator, viewPortHandler: _viewPortHandler) + } + + internal override func calcMinMax() + { + super.calcMinMax() + guard let data = _data else { return } + + if (_deltaX == 0.0 && data.yValCount > 0) + { + _deltaX = 1.0 + } + } +} \ No newline at end of file diff --git a/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift b/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift index 8d1a64d0a7..b2dec92e83 100644 --- a/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift +++ b/Charts/Classes/Data/Implementations/ChartBaseDataSet.swift @@ -61,6 +61,18 @@ public class ChartBaseDataSet: NSObject, IChartDataSet fatalError("yMax is not implemented in ChartBaseDataSet") } + /// Note: xNumericVal is not supported by all chart types + public var xNumericValMin: Double + { + fatalError("xNumericValMin is not implemented in ChartBaseDataSet") + } + + /// Note: xNumericVal is not supported by all chart types + public var xNumericValMax: Double + { + fatalError("xNumericValMax is not implemented in ChartBaseDataSet") + } + public var entryCount: Int { fatalError("entryCount is not implemented in ChartBaseDataSet") @@ -71,6 +83,12 @@ public class ChartBaseDataSet: NSObject, IChartDataSet fatalError("yValForXIndex is not implemented in ChartBaseDataSet") } + /// Note: not all chart types support xNumericVal + public func xNumericValForXIndex(x: Int) -> Double + { + fatalError("xNumericValForXIndex is not implemented in ChartBaseDataSet") + } + public func entryForIndex(i: Int) -> ChartDataEntry? { fatalError("entryForIndex is not implemented in ChartBaseDataSet") diff --git a/Charts/Classes/Data/Implementations/Standard/ChartData.swift b/Charts/Classes/Data/Implementations/Standard/ChartData.swift index 63539d97fd..17eb612236 100644 --- a/Charts/Classes/Data/Implementations/Standard/ChartData.swift +++ b/Charts/Classes/Data/Implementations/Standard/ChartData.swift @@ -25,6 +25,10 @@ public class ChartData: NSObject internal var _rightAxisMin = Double(0.0) private var _yValCount = Int(0) + /// Note: xNumericVal is not supported by all chart types + internal var _xNumericValMax = Double(0.0) + internal var _xNumericValMin = Double(0.0) + /// the last start value used for calcMinMax internal var _lastStart: Int = 0 @@ -153,6 +157,8 @@ public class ChartData: NSObject { _yMax = 0.0 _yMin = 0.0 + _xNumericValMax = 0.0 + _xNumericValMin = 0.0 } else { @@ -161,6 +167,8 @@ public class ChartData: NSObject _yMin = DBL_MAX _yMax = -DBL_MAX + _xNumericValMin = DBL_MAX + _xNumericValMax = -DBL_MAX for (var i = 0; i < _dataSets.count; i++) { @@ -175,12 +183,24 @@ public class ChartData: NSObject { _yMax = _dataSets[i].yMax } + + if (_dataSets[i].xNumericValMin < _xNumericValMin) + { + _xNumericValMin = _dataSets[i].xNumericValMin + } + + if (_dataSets[i].xNumericValMax > _xNumericValMax) + { + _xNumericValMax = _dataSets[i].xNumericValMax + } } if (_yMin == DBL_MAX) { _yMin = 0.0 _yMax = 0.0 + _xNumericValMin = 0.0 + _xNumericValMax = 0.0 } // left axis @@ -314,6 +334,30 @@ public class ChartData: NSObject } } + /// - returns: the smallest xNumericVal the data object contains. + /// Note: xNumericVal is not supported by all chart types + public var xNumericValMin: Double + { + return _xNumericValMin + } + + public func getXNumericValMin() -> Double + { + return _xNumericValMin + } + + /// - returns: the greatest xNumericVal the data object contains. + /// Note: xNumericVal is not supported by all chart types + public var xNumericValMax: Double + { + return _xNumericValMax + } + + public func getXNumericValMax() -> Double + { + return _xNumericValMax + } + /// - returns: the average length (in characters) across all values in the x-vals array public var xValAverageLength: Double { @@ -478,6 +522,8 @@ public class ChartData: NSObject { _yMax = d.yMax _yMin = d.yMin + _xNumericValMax = d.xNumericValMax + _xNumericValMin = d.xNumericValMin if (d.axisDependency == .Left) { @@ -500,6 +546,14 @@ public class ChartData: NSObject { _yMin = d.yMin } + if (_xNumericValMax < d.xNumericValMax) + { + _xNumericValMax = d.xNumericValMax + } + if (_xNumericValMin > d.xNumericValMin) + { + _xNumericValMin = d.xNumericValMin + } if (d.axisDependency == .Left) { @@ -592,6 +646,7 @@ public class ChartData: NSObject if _dataSets != nil && _dataSets.count > dataSetIndex && dataSetIndex >= 0 { let val = e.value + let xNumericVal = e.xNumericVal let set = _dataSets[dataSetIndex] if !set.addEntry(e) { return } @@ -600,6 +655,8 @@ public class ChartData: NSObject { _yMin = val _yMax = val + _xNumericValMin = xNumericVal + _xNumericValMax = xNumericVal if (set.axisDependency == .Left) { @@ -622,6 +679,14 @@ public class ChartData: NSObject { _yMin = val } + if (_xNumericValMax < xNumericVal) + { + _xNumericValMax = xNumericVal + } + if (_xNumericValMin > xNumericVal) + { + _xNumericValMin = xNumericVal + } if (set.axisDependency == .Left) { diff --git a/Charts/Classes/Data/Implementations/Standard/ChartDataEntry.swift b/Charts/Classes/Data/Implementations/Standard/ChartDataEntry.swift index 84a57b8abc..87589e83bb 100644 --- a/Charts/Classes/Data/Implementations/Standard/ChartDataEntry.swift +++ b/Charts/Classes/Data/Implementations/Standard/ChartDataEntry.swift @@ -25,6 +25,10 @@ public class ChartDataEntry: NSObject /// optional spot for additional data this Entry represents public var data: AnyObject? + /// optional value to represent a more precise location on the x-axis than the standard xIndex + /// Not supported by all chart types + public var xNumericVal = Double(0.0) + public override required init() { super.init() @@ -38,6 +42,15 @@ public class ChartDataEntry: NSObject self.xIndex = xIndex } + public init(value: Double, xIndex: Int, xNumericVal: Double) + { + super.init() + + self.value = value + self.xIndex = xIndex + self.xNumericVal = xNumericVal + } + public init(value: Double, xIndex: Int, data: AnyObject?) { super.init() @@ -70,8 +83,13 @@ public class ChartDataEntry: NSObject { return false } + + if (!ChartUtils.Math.equalDoubles(object!.xNumericVal, doubleB: xNumericVal)) + { + return false + } - if (fabs(object!.value - value) > 0.00001) + if (!ChartUtils.Math.equalDoubles(object!.value, doubleB: value)) { return false } @@ -95,6 +113,7 @@ public class ChartDataEntry: NSObject copy.value = value copy.xIndex = xIndex copy.data = data + copy.xNumericVal = xNumericVal return copy } @@ -122,6 +141,11 @@ public func ==(lhs: ChartDataEntry, rhs: ChartDataEntry) -> Bool return false } + if (fabs(lhs.xNumericVal - rhs.xNumericVal) > 0.00001) + { + return false + } + if (fabs(lhs.value - rhs.value) > 0.00001) { return false diff --git a/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift b/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift index 1b14ee6381..2dfaac539c 100644 --- a/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift +++ b/Charts/Classes/Data/Implementations/Standard/ChartDataSet.swift @@ -58,6 +58,10 @@ public class ChartDataSet: ChartBaseDataSet internal var _yMax = Double(0.0) internal var _yMin = Double(0.0) + /// Note: xNumericVal is not supported by all chart types + internal var _xNumericValMax = Double(0.0) + internal var _xNumericValMin = Double(0.0) + /// the last start value used for calcMinMax internal var _lastStart: Int = 0 @@ -97,6 +101,8 @@ public class ChartDataSet: ChartBaseDataSet _yMin = DBL_MAX _yMax = -DBL_MAX + _xNumericValMin = DBL_MAX + _xNumericValMax = -DBL_MAX for (var i = start; i <= endValue; i++) { @@ -112,6 +118,14 @@ public class ChartDataSet: ChartBaseDataSet { _yMax = e.value } + if (e.xNumericVal < _xNumericValMin) + { + _xNumericValMin = e.xNumericVal + } + if (e.xNumericVal > _xNumericValMax) + { + _xNumericValMax = e.xNumericVal + } } } @@ -119,6 +133,8 @@ public class ChartDataSet: ChartBaseDataSet { _yMin = 0.0 _yMax = 0.0 + _xNumericValMin = 0.0 + _xNumericValMax = 0.0 } } @@ -128,6 +144,14 @@ public class ChartDataSet: ChartBaseDataSet /// - returns: the maximum y-value this DataSet holds public override var yMax: Double { return _yMax } + /// - returns: the minimum xNumericVal this DataSet holds + /// Note: xNumericVal is not supported by all chart types + public override var xNumericValMin: Double { return _xNumericValMin } + + /// - returns: the maximum xNumericVal this DataSet holds + /// Note: xNumericVal is not supported by all chart types + public override var xNumericValMax: Double { return _xNumericValMax } + /// - returns: the number of y-values this DataSet represents public override var entryCount: Int { return _yVals?.count ?? 0 } @@ -140,6 +164,16 @@ public class ChartDataSet: ChartBaseDataSet else { return Double.NaN } } + /// - returns: the xNumericValue of the Entry object at the given xIndex. Returns NaN if no xNumericValue is at the given x-index. + /// Note: not all chart types support xNumericVal + public override func xNumericValForXIndex(x: Int) -> Double + { + let e = self.entryForXIndex(x) + + if (e !== nil && e!.xIndex == x) { return e!.xNumericVal } + else { return Double.NaN } + } + /// - 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 @@ -300,6 +334,7 @@ public class ChartDataSet: ChartBaseDataSet public override func addEntry(e: ChartDataEntry) -> Bool { let val = e.value + let xNumericVal = e.xNumericVal if (_yVals == nil) { @@ -310,6 +345,8 @@ public class ChartDataSet: ChartBaseDataSet { _yMax = val _yMin = val + _xNumericValMax = xNumericVal + _xNumericValMin = xNumericVal } else { @@ -321,6 +358,14 @@ public class ChartDataSet: ChartBaseDataSet { _yMin = val } + if (_xNumericValMax < xNumericVal) + { + _xNumericValMax = xNumericVal + } + if (_xNumericValMin > xNumericVal) + { + _xNumericValMin = xNumericVal + } } _yVals.append(e) @@ -336,6 +381,7 @@ public class ChartDataSet: ChartBaseDataSet public override func addEntryOrdered(e: ChartDataEntry) -> Bool { let val = e.value + let xNumericVal = e.xNumericVal if (_yVals == nil) { @@ -346,6 +392,8 @@ public class ChartDataSet: ChartBaseDataSet { _yMax = val _yMin = val + _xNumericValMax = xNumericVal + _xNumericValMin = xNumericVal } else { @@ -357,6 +405,14 @@ public class ChartDataSet: ChartBaseDataSet { _yMin = val } + if (_xNumericValMax < xNumericVal) + { + _xNumericValMax = xNumericVal + } + if (_xNumericValMin > xNumericVal) + { + _xNumericValMin = xNumericVal + } } if _yVals.last?.xIndex > e.xIndex @@ -474,6 +530,8 @@ public class ChartDataSet: ChartBaseDataSet copy._yVals = _yVals copy._yMax = _yMax copy._yMin = _yMin + copy._xNumericValMax = _xNumericValMax + copy._xNumericValMin = _xNumericValMin copy._lastStart = _lastStart copy._lastEnd = _lastEnd diff --git a/Charts/Classes/Data/Interfaces/IChartDataSet.swift b/Charts/Classes/Data/Interfaces/IChartDataSet.swift index 36c0a12565..c0be3f73c9 100644 --- a/Charts/Classes/Data/Interfaces/IChartDataSet.swift +++ b/Charts/Classes/Data/Interfaces/IChartDataSet.swift @@ -34,12 +34,24 @@ public protocol IChartDataSet /// - returns: the maximum y-value this DataSet holds var yMax: Double { get } + /// - returns: the minimum xNumericVal this DataSet holds + /// Note: xNumericVal is not supported by all chart types + var xNumericValMin: Double { get } + + /// - returns: the maximum xNumericVal this DataSet holds + /// Note: xNumericVal is not supported by all chart types + var xNumericValMax: Double { get } + /// - returns: the number of y-values this DataSet represents var entryCount: Int { get } /// - 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: the xNumericValue of the Entry object at the given xIndex. Returns NaN if no xNumericValue is at the given x-index. + /// Note: xNumericVal is not supported by all chart types + func xNumericValForXIndex(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 diff --git a/Charts/Classes/Renderers/TimeLineChartRenderer.swift b/Charts/Classes/Renderers/TimeLineChartRenderer.swift new file mode 100644 index 0000000000..c6f4e32ca3 --- /dev/null +++ b/Charts/Classes/Renderers/TimeLineChartRenderer.swift @@ -0,0 +1,702 @@ +// +// TimeLineChartRenderer.swift +// Charts +// +// Created by Daniel Cohen Gindi on 4/3/15. +// +// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda +// A port of MPAndroidChart for iOS +// Licensed under Apache License 2.0 +// +// https://github.com/danielgindi/ios-charts +// + +import Foundation +import CoreGraphics + +#if !os(OSX) + import UIKit +#endif + +public class TimeLineChartRenderer: LineChartRenderer +{ + public override init(dataProvider: LineChartDataProvider?, animator: ChartAnimator?, viewPortHandler: ChartViewPortHandler) + { + super.init(dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler) + } + + public override func drawData(context context: CGContext) + { + guard let lineData = dataProvider?.lineData else { return } + + for (var i = 0; i < lineData.dataSetCount; i++) + { + guard let set = lineData.getDataSetByIndex(i) else { continue } + + if set.isVisible + { + if !(set is ILineChartDataSet) + { + fatalError("Datasets for TimeLineChartRenderer must conform to ILineChartDataSet") + } + + drawDataSet(context: context, dataSet: set as! ILineChartDataSet) + } + } + } + + public override func drawDataSet(context context: CGContext, dataSet: ILineChartDataSet) + { + let entryCount = dataSet.entryCount + + if (entryCount < 1) + { + return + } + + CGContextSaveGState(context) + + CGContextSetLineWidth(context, dataSet.lineWidth) + if (dataSet.lineDashLengths != nil) + { + CGContextSetLineDash(context, dataSet.lineDashPhase, dataSet.lineDashLengths!, dataSet.lineDashLengths!.count) + } + else + { + CGContextSetLineDash(context, 0.0, nil, 0) + } + + // if drawing cubic lines is enabled + if (dataSet.isDrawCubicEnabled) + { + drawCubic(context: context, dataSet: dataSet) + } + else + { // draw normal (straight) lines + drawLinear(context: context, dataSet: dataSet) + } + + CGContextRestoreGState(context) + } + + public override func drawCubic(context context: CGContext, dataSet: ILineChartDataSet) + { + guard let + trans = dataProvider?.getTransformer(dataSet.axisDependency), + animator = animator + else { return } + + let entryCount = dataSet.entryCount + + guard let + entryFrom = dataSet.entryForXIndex(self.minX < 0 ? self.minX : 0, rounding: .Down), + entryTo = dataSet.entryForXIndex(self.maxX, rounding: .Up) + else { return } + + let diff = (entryFrom == entryTo) ? 1 : 0 + let minx = max(dataSet.entryIndex(entry: entryFrom) - diff - 1, 0) + let maxx = min(max(minx + 2, dataSet.entryIndex(entry: entryTo) + 1), entryCount) + + let phaseX = animator.phaseX + let phaseY = animator.phaseY + + // get the color that is specified for this position from the DataSet + let drawingColor = dataSet.colors.first! + + let intensity = dataSet.cubicIntensity + + // the path for the cubic-spline + let cubicPath = CGPathCreateMutable() + + var valueToPixelMatrix = trans.valueToPixelMatrix + + let size = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx))) + + if (size - minx >= 2) + { + var prevDx: CGFloat = 0.0 + var prevDy: CGFloat = 0.0 + var curDx: CGFloat = 0.0 + var curDy: CGFloat = 0.0 + + var prevPrev: ChartDataEntry! = dataSet.entryForIndex(minx) + var prev: ChartDataEntry! = prevPrev + var cur: ChartDataEntry! = prev + var next: ChartDataEntry! = dataSet.entryForIndex(minx + 1) + + if cur == nil || next == nil { return } + + // let the spline start + CGPathMoveToPoint(cubicPath, &valueToPixelMatrix, CGFloat(cur.xIndex), CGFloat(cur.value) * phaseY) + + prevDx = CGFloat(cur.xIndex - prev.xIndex) * intensity + prevDy = CGFloat(cur.value - prev.value) * intensity + + curDx = CGFloat(next.xIndex - cur.xIndex) * intensity + curDy = CGFloat(next.value - cur.value) * intensity + + // the first cubic + CGPathAddCurveToPoint(cubicPath, &valueToPixelMatrix, + CGFloat(prev.xIndex) + prevDx, (CGFloat(prev.value) + prevDy) * phaseY, + CGFloat(cur.xIndex) - curDx, (CGFloat(cur.value) - curDy) * phaseY, + CGFloat(cur.xIndex), CGFloat(cur.value) * phaseY) + + for (var j = minx + 1, count = min(size, entryCount - 1); j < count; j++) + { + prevPrev = prev + prev = cur + cur = next + next = dataSet.entryForIndex(j + 1) + + if next == nil { break } + + prevDx = CGFloat(cur.xIndex - prevPrev.xIndex) * intensity + prevDy = CGFloat(cur.value - prevPrev.value) * intensity + curDx = CGFloat(next.xIndex - prev.xIndex) * intensity + curDy = CGFloat(next.value - prev.value) * intensity + + CGPathAddCurveToPoint(cubicPath, &valueToPixelMatrix, CGFloat(prev.xIndex) + prevDx, (CGFloat(prev.value) + prevDy) * phaseY, + CGFloat(cur.xIndex) - curDx, + (CGFloat(cur.value) - curDy) * phaseY, CGFloat(cur.xIndex), CGFloat(cur.value) * phaseY) + } + + if (size > entryCount - 1) + { + prevPrev = dataSet.entryForIndex(entryCount - (entryCount >= 3 ? 3 : 2)) + prev = dataSet.entryForIndex(entryCount - 2) + cur = dataSet.entryForIndex(entryCount - 1) + next = cur + + if prevPrev == nil || prev == nil || cur == nil { return } + + prevDx = CGFloat(cur.xIndex - prevPrev.xIndex) * intensity + prevDy = CGFloat(cur.value - prevPrev.value) * intensity + curDx = CGFloat(next.xIndex - prev.xIndex) * intensity + curDy = CGFloat(next.value - prev.value) * intensity + + // the last cubic + CGPathAddCurveToPoint(cubicPath, &valueToPixelMatrix, CGFloat(prev.xIndex) + prevDx, (CGFloat(prev.value) + prevDy) * phaseY, + CGFloat(cur.xIndex) - curDx, + (CGFloat(cur.value) - curDy) * phaseY, CGFloat(cur.xIndex), CGFloat(cur.value) * phaseY) + } + } + + CGContextSaveGState(context) + + if (dataSet.isDrawFilledEnabled) + { + // Copy this path because we make changes to it + let fillPath = CGPathCreateMutableCopy(cubicPath) + + drawCubicFill(context: context, dataSet: dataSet, spline: fillPath!, matrix: valueToPixelMatrix, from: minx, to: size) + } + + CGContextBeginPath(context) + CGContextAddPath(context, cubicPath) + CGContextSetStrokeColorWithColor(context, drawingColor.CGColor) + CGContextStrokePath(context) + + CGContextRestoreGState(context) + } + + public override func drawCubicFill(context context: CGContext, dataSet: ILineChartDataSet, spline: CGMutablePath, matrix: CGAffineTransform, from: Int, to: Int) + { + guard let dataProvider = dataProvider else { return } + + if to - from <= 1 + { + return + } + + let fillMin = dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0 + + // Take the from/to xIndex from the entries themselves, + // so missing entries won't screw up the filling. + // What we need to draw is line from points of the xIndexes - not arbitrary entry indexes! + let xTo = dataSet.entryForIndex(to - 1)?.xIndex ?? 0 + let xFrom = dataSet.entryForIndex(from)?.xIndex ?? 0 + + var pt1 = CGPoint(x: CGFloat(xTo), y: fillMin) + var pt2 = CGPoint(x: CGFloat(xFrom), y: fillMin) + pt1 = CGPointApplyAffineTransform(pt1, matrix) + pt2 = CGPointApplyAffineTransform(pt2, matrix) + + CGPathAddLineToPoint(spline, nil, pt1.x, pt1.y) + CGPathAddLineToPoint(spline, nil, pt2.x, pt2.y) + CGPathCloseSubpath(spline) + + if dataSet.fill != nil + { + drawFilledPath(context: context, path: spline, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) + } + else + { + drawFilledPath(context: context, path: spline, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) + } + } + + private var _lineSegments = [CGPoint](count: 2, repeatedValue: CGPoint()) + + public override func drawLinear(context context: CGContext, dataSet: ILineChartDataSet) + { + guard let + trans = dataProvider?.getTransformer(dataSet.axisDependency), + animator = animator + else { return } + + let valueToPixelMatrix = trans.valueToPixelMatrix + + let entryCount = dataSet.entryCount + let isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled + let pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2 + + let phaseX = animator.phaseX + let phaseY = animator.phaseY + + guard let + entryFrom = dataSet.entryForXIndex(self.minX < 0 ? self.minX : 0, rounding: .Down), + entryTo = dataSet.entryForXIndex(self.maxX, rounding: .Up) + else { return } + + let diff = (entryFrom == entryTo) ? 1 : 0 + let minx = max(dataSet.entryIndex(entry: entryFrom) - diff, 0) + let maxx = min(max(minx + 2, dataSet.entryIndex(entry: entryTo) + 1), entryCount) + + CGContextSaveGState(context) + + // more than 1 color + if (dataSet.colors.count > 1) + { + if (_lineSegments.count != pointsPerEntryPair) + { + _lineSegments = [CGPoint](count: pointsPerEntryPair, repeatedValue: CGPoint()) + } + + for (var j = minx, count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx))); j < count; j++) + { + if (count > 1 && j == count - 1) + { // Last point, we have already drawn a line to this point + break + } + + var e: ChartDataEntry! = dataSet.entryForIndex(j) + + if e == nil { continue } + + _lineSegments[0].x = CGFloat(e.xIndex) + _lineSegments[0].y = CGFloat(e.value) * phaseY + + if (j + 1 < count) + { + e = dataSet.entryForIndex(j + 1) + + if e == nil { break } + + if isDrawSteppedEnabled + { + _lineSegments[1] = CGPoint(x: CGFloat(e.xIndex), y: _lineSegments[0].y) + _lineSegments[2] = _lineSegments[1] + _lineSegments[3] = CGPoint(x: CGFloat(e.xIndex), y: CGFloat(e.value) * phaseY) + } + else + { + _lineSegments[1] = CGPoint(x: CGFloat(e.xIndex), y: CGFloat(e.value) * phaseY) + } + } + else + { + _lineSegments[1] = _lineSegments[0] + } + + for i in 0..<_lineSegments.count + { + _lineSegments[i] = CGPointApplyAffineTransform(_lineSegments[i], valueToPixelMatrix) + } + + if (!viewPortHandler.isInBoundsRight(_lineSegments[0].x)) + { + break + } + + // make sure the lines don't do shitty things outside bounds + if (!viewPortHandler.isInBoundsLeft(_lineSegments[1].x) + || (!viewPortHandler.isInBoundsTop(_lineSegments[0].y) && !viewPortHandler.isInBoundsBottom(_lineSegments[1].y)) + || (!viewPortHandler.isInBoundsTop(_lineSegments[0].y) && !viewPortHandler.isInBoundsBottom(_lineSegments[1].y))) + { + continue + } + + // get the color that is set for this line-segment + CGContextSetStrokeColorWithColor(context, dataSet.colorAt(j).CGColor) + CGContextStrokeLineSegments(context, _lineSegments, pointsPerEntryPair) + } + } + else + { // only one color per dataset + + var e1: ChartDataEntry! + var e2: ChartDataEntry! + + if (_lineSegments.count != max((entryCount - 1) * pointsPerEntryPair, pointsPerEntryPair)) + { + _lineSegments = [CGPoint](count: max((entryCount - 1) * pointsPerEntryPair, pointsPerEntryPair), repeatedValue: CGPoint()) + } + + e1 = dataSet.entryForIndex(minx) + + if e1 != nil + { + let count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx))) + + for (var x = count > 1 ? minx + 1 : minx, j = 0; x < count; x++) + { + e1 = dataSet.entryForIndex(x == 0 ? 0 : (x - 1)) + e2 = dataSet.entryForIndex(x) + + if e1 == nil || e2 == nil { continue } + + _lineSegments[j++] = CGPointApplyAffineTransform( + CGPoint( + x: CGFloat(e1.xIndex), + y: CGFloat(e1.value) * phaseY + ), valueToPixelMatrix) + + if isDrawSteppedEnabled + { + _lineSegments[j++] = CGPointApplyAffineTransform( + CGPoint( + x: CGFloat(e2.xIndex), + y: CGFloat(e1.value) * phaseY + ), valueToPixelMatrix) + _lineSegments[j++] = CGPointApplyAffineTransform( + CGPoint( + x: CGFloat(e2.xIndex), + y: CGFloat(e1.value) * phaseY + ), valueToPixelMatrix) + } + + _lineSegments[j++] = CGPointApplyAffineTransform( + CGPoint( + x: CGFloat(e2.xIndex), + y: CGFloat(e2.value) * phaseY + ), valueToPixelMatrix) + } + + let size = max((count - minx - 1) * pointsPerEntryPair, pointsPerEntryPair) + CGContextSetStrokeColorWithColor(context, dataSet.colorAt(0).CGColor) + CGContextStrokeLineSegments(context, _lineSegments, size) + } + } + + CGContextRestoreGState(context) + + // if drawing filled is enabled + if (dataSet.isDrawFilledEnabled && entryCount > 0) + { + drawLinearFill(context: context, dataSet: dataSet, minx: minx, maxx: maxx, trans: trans) + } + } + + public override func drawLinearFill(context context: CGContext, dataSet: ILineChartDataSet, minx: Int, maxx: Int, trans: ChartTransformer) + { + guard let dataProvider = dataProvider else { return } + + let filled = generateFilledPath( + dataSet: dataSet, + fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0, + from: minx, + to: maxx, + matrix: trans.valueToPixelMatrix) + + if dataSet.fill != nil + { + drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha) + } + else + { + drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha) + } + } + + /// Generates the path that is used for filled drawing. + private func generateFilledPath(dataSet dataSet: ILineChartDataSet, fillMin: CGFloat, from: Int, to: Int, var matrix: CGAffineTransform) -> CGPath + { + let phaseX = animator?.phaseX ?? 1.0 + let phaseY = animator?.phaseY ?? 1.0 + let isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled + + var e: ChartDataEntry! + + let filled = CGPathCreateMutable() + + e = dataSet.entryForIndex(from) + if e != nil + { + CGPathMoveToPoint(filled, &matrix, CGFloat(e.xIndex), fillMin) + CGPathAddLineToPoint(filled, &matrix, CGFloat(e.xIndex), CGFloat(e.value) * phaseY) + } + + // create a new path + for (var x = from + 1, count = Int(ceil(CGFloat(to - from) * phaseX + CGFloat(from))); x < count; x++) + { + guard let e = dataSet.entryForIndex(x) else { continue } + + if isDrawSteppedEnabled + { + guard let ePrev = dataSet.entryForIndex(x-1) else { continue } + CGPathAddLineToPoint(filled, &matrix, CGFloat(e.xIndex), CGFloat(ePrev.value) * phaseY) + } + + CGPathAddLineToPoint(filled, &matrix, CGFloat(e.xIndex), CGFloat(e.value) * phaseY) + } + + // close up + e = dataSet.entryForIndex(max(min(Int(ceil(CGFloat(to - from) * phaseX + CGFloat(from))) - 1, dataSet.entryCount - 1), 0)) + if e != nil + { + CGPathAddLineToPoint(filled, &matrix, CGFloat(e.xIndex), fillMin) + } + CGPathCloseSubpath(filled) + + return filled + } + + public override func drawValues(context context: CGContext) + { + guard let + dataProvider = dataProvider, + lineData = dataProvider.lineData, + animator = animator + else { return } + + if (CGFloat(lineData.yValCount) < CGFloat(dataProvider.maxVisibleValueCount) * viewPortHandler.scaleX) + { + var dataSets = lineData.dataSets + + let phaseX = animator.phaseX + let phaseY = animator.phaseY + + var pt = CGPoint() + + for (var i = 0; i < dataSets.count; i++) + { + guard let dataSet = dataSets[i] as? ILineChartDataSet else { continue } + + if !dataSet.isDrawValuesEnabled || dataSet.entryCount == 0 + { + continue + } + + let valueFont = dataSet.valueFont + + guard let formatter = dataSet.valueFormatter else { continue } + + let trans = dataProvider.getTransformer(dataSet.axisDependency) + let valueToPixelMatrix = trans.valueToPixelMatrix + + // make sure the values do not interfear with the circles + var valOffset = Int(dataSet.circleRadius * 1.75) + + if (!dataSet.isDrawCirclesEnabled) + { + valOffset = valOffset / 2 + } + + let entryCount = dataSet.entryCount + + guard let + entryFrom = dataSet.entryForXIndex(self.minX < 0 ? self.minX : 0, rounding: .Down), + entryTo = dataSet.entryForXIndex(self.maxX, rounding: .Up) + else { continue } + + let diff = (entryFrom == entryTo) ? 1 : 0 + let minx = max(dataSet.entryIndex(entry: entryFrom) - diff, 0) + let maxx = min(max(minx + 2, dataSet.entryIndex(entry: entryTo) + 1), entryCount) + + for (var j = minx, count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx))); j < count; j++) + { + guard let e = dataSet.entryForIndex(j) else { break } + + pt.x = CGFloat(e.xIndex) + pt.y = CGFloat(e.value) * phaseY + pt = CGPointApplyAffineTransform(pt, valueToPixelMatrix) + + if (!viewPortHandler.isInBoundsRight(pt.x)) + { + break + } + + if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) + { + continue + } + + ChartUtils.drawText(context: context, + text: formatter.stringFromNumber(e.value)!, + point: CGPoint( + x: pt.x, + y: pt.y - CGFloat(valOffset) - valueFont.lineHeight), + align: .Center, + attributes: [NSFontAttributeName: valueFont, NSForegroundColorAttributeName: dataSet.valueTextColorAt(j)]) + } + } + } + } + + public override func drawExtras(context context: CGContext) + { + drawCircles(context: context) + } + + private func drawCircles(context context: CGContext) + { + guard let + dataProvider = dataProvider, + lineData = dataProvider.lineData, + animator = animator + else { return } + + let phaseX = animator.phaseX + let phaseY = animator.phaseY + + let dataSets = lineData.dataSets + + var pt = CGPoint() + var rect = CGRect() + + CGContextSaveGState(context) + + for (var i = 0, count = dataSets.count; i < count; i++) + { + guard let dataSet = lineData.getDataSetByIndex(i) as? ILineChartDataSet else { continue } + + if !dataSet.isVisible || !dataSet.isDrawCirclesEnabled || dataSet.entryCount == 0 + { + continue + } + + let trans = dataProvider.getTransformer(dataSet.axisDependency) + let valueToPixelMatrix = trans.valueToPixelMatrix + + let entryCount = dataSet.entryCount + + let circleRadius = dataSet.circleRadius + let circleDiameter = circleRadius * 2.0 + let circleHoleDiameter = circleRadius + let circleHoleRadius = circleHoleDiameter / 2.0 + let isDrawCircleHoleEnabled = dataSet.isDrawCircleHoleEnabled + + guard let + entryFrom = dataSet.entryForXIndex(self.minX < 0 ? self.minX : 0, rounding: .Down), + entryTo = dataSet.entryForXIndex(self.maxX, rounding: .Up) + else { continue } + + let diff = (entryFrom == entryTo) ? 1 : 0 + let minx = max(dataSet.entryIndex(entry: entryFrom) - diff, 0) + let maxx = min(max(minx + 2, dataSet.entryIndex(entry: entryTo) + 1), entryCount) + + for (var j = minx, count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx))); j < count; j++) + { + guard let e = dataSet.entryForIndex(j) else { break } + + pt.x = CGFloat(e.xIndex) + pt.y = CGFloat(e.value) * phaseY + pt = CGPointApplyAffineTransform(pt, valueToPixelMatrix) + + if (!viewPortHandler.isInBoundsRight(pt.x)) + { + break + } + + // make sure the circles don't do shitty things outside bounds + if (!viewPortHandler.isInBoundsLeft(pt.x) || !viewPortHandler.isInBoundsY(pt.y)) + { + continue + } + + CGContextSetFillColorWithColor(context, dataSet.getCircleColor(j)!.CGColor) + + rect.origin.x = pt.x - circleRadius + rect.origin.y = pt.y - circleRadius + rect.size.width = circleDiameter + rect.size.height = circleDiameter + CGContextFillEllipseInRect(context, rect) + + if (isDrawCircleHoleEnabled) + { + CGContextSetFillColorWithColor(context, dataSet.circleHoleColor.CGColor) + + rect.origin.x = pt.x - circleHoleRadius + rect.origin.y = pt.y - circleHoleRadius + rect.size.width = circleHoleDiameter + rect.size.height = circleHoleDiameter + CGContextFillEllipseInRect(context, rect) + } + } + } + + CGContextRestoreGState(context) + } + + private var _highlightPointBuffer = CGPoint() + + public override func drawHighlighted(context context: CGContext, indices: [ChartHighlight]) + { + guard let + lineData = dataProvider?.lineData, + chartXMax = dataProvider?.chartXMax, + animator = animator + else { return } + + CGContextSaveGState(context) + + for (var i = 0; i < indices.count; i++) + { + guard let set = lineData.getDataSetByIndex(indices[i].dataSetIndex) as? ILineChartDataSet else { continue } + + if !set.isHighlightEnabled + { + continue + } + + CGContextSetStrokeColorWithColor(context, set.highlightColor.CGColor) + CGContextSetLineWidth(context, set.highlightLineWidth) + if (set.highlightLineDashLengths != nil) + { + CGContextSetLineDash(context, set.highlightLineDashPhase, set.highlightLineDashLengths!, set.highlightLineDashLengths!.count) + } + else + { + CGContextSetLineDash(context, 0.0, nil, 0) + } + + let xIndex = indices[i].xIndex; // get the x-position + + if (CGFloat(xIndex) > CGFloat(chartXMax) * animator.phaseX) + { + continue + } + + let yValue = set.yValForXIndex(xIndex) + if (yValue.isNaN) + { + continue + } + + let y = CGFloat(yValue) * animator.phaseY; // get the y-position + + _highlightPointBuffer.x = CGFloat(xIndex) + _highlightPointBuffer.y = y + + let trans = dataProvider?.getTransformer(set.axisDependency) + + trans?.pointValueToPixel(&_highlightPointBuffer) + + // draw the lines + drawHighlightLines(context: context, point: _highlightPointBuffer, set: set) + } + + CGContextRestoreGState(context) + } + +} \ No newline at end of file diff --git a/Charts/Classes/Utils/ChartUtils.swift b/Charts/Classes/Utils/ChartUtils.swift index 31c0adc175..8734ebab08 100755 --- a/Charts/Classes/Utils/ChartUtils.swift +++ b/Charts/Classes/Utils/ChartUtils.swift @@ -29,6 +29,17 @@ public class ChartUtils internal static let FRAD2DEG = CGFloat(180.0 / M_PI) internal static let DEG2RAD = M_PI / 180.0 internal static let RAD2DEG = 180.0 / M_PI + internal static let EPSILON = 0.00001 + + internal static func equalDoubles(doubleA: Double, doubleB: Double) -> Bool { + + if (fabs(doubleA - doubleB) > ChartUtils.Math.EPSILON) + { + return false + } + + return true + } } internal class func roundToNextSignificant(number number: Double) -> Double diff --git a/ChartsDemo/ChartsDemo.xcodeproj/project.pbxproj b/ChartsDemo/ChartsDemo.xcodeproj/project.pbxproj index ee3e08d2a9..d86947b2c7 100644 --- a/ChartsDemo/ChartsDemo.xcodeproj/project.pbxproj +++ b/ChartsDemo/ChartsDemo.xcodeproj/project.pbxproj @@ -95,6 +95,8 @@ 65BF12B11BFC9B00005C28D9 /* RealmLineChartViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65BF12AF1BFC9B00005C28D9 /* RealmLineChartViewController.xib */; }; 65BF12B61BFC9B50005C28D9 /* RealmBarChartViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65BF12B41BFC9B50005C28D9 /* RealmBarChartViewController.m */; }; 65BF12B71BFC9B50005C28D9 /* RealmBarChartViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65BF12B51BFC9B50005C28D9 /* RealmBarChartViewController.xib */; }; + 909437401CA0CB8F00BAFA36 /* TimeLineChartViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9094373E1CA0CB8F00BAFA36 /* TimeLineChartViewController.m */; }; + 909437411CA0CB8F00BAFA36 /* TimeLineChartViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9094373F1CA0CB8F00BAFA36 /* TimeLineChartViewController.xib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -298,6 +300,9 @@ 65BF12B31BFC9B50005C28D9 /* RealmBarChartViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RealmBarChartViewController.h; sourceTree = ""; }; 65BF12B41BFC9B50005C28D9 /* RealmBarChartViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RealmBarChartViewController.m; sourceTree = ""; }; 65BF12B51BFC9B50005C28D9 /* RealmBarChartViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RealmBarChartViewController.xib; sourceTree = ""; }; + 9094373D1CA0CB8F00BAFA36 /* TimeLineChartViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TimeLineChartViewController.h; sourceTree = ""; }; + 9094373E1CA0CB8F00BAFA36 /* TimeLineChartViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TimeLineChartViewController.m; sourceTree = ""; }; + 9094373F1CA0CB8F00BAFA36 /* TimeLineChartViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TimeLineChartViewController.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -397,8 +402,8 @@ children = ( 5B8E087E1C635B5200438BAF /* Charts.framework */, 5B8E08801C635B5200438BAF /* Charts.framework */, - 5B8E08821C635B5200438BAF /* ChartsTests.xctest */, 65B3F6571C73B4F6000983D0 /* Charts.framework */, + 5B8E08821C635B5200438BAF /* ChartsTests.xctest */, ); name = Products; sourceTree = ""; @@ -435,6 +440,9 @@ 5BD8F06F1AB89C7100566E05 /* Demos */ = { isa = PBXGroup; children = ( + 9094373D1CA0CB8F00BAFA36 /* TimeLineChartViewController.h */, + 9094373E1CA0CB8F00BAFA36 /* TimeLineChartViewController.m */, + 9094373F1CA0CB8F00BAFA36 /* TimeLineChartViewController.xib */, 5BEAED0F1ABBFB2B0013F194 /* AnotherBarChartViewController.h */, 5BEAED101ABBFB2B0013F194 /* AnotherBarChartViewController.m */, 5BEAED111ABBFB2B0013F194 /* AnotherBarChartViewController.xib */, @@ -702,6 +710,7 @@ 5BD47E661ABB424E008FCEC6 /* BarChartViewController.xib in Resources */, 5BDEDC421ABB7F73007D3A60 /* HorizontalBarChartViewController.xib in Resources */, 5B4AC19F1C4C19340028D1A6 /* RealmWikiExampleChartViewController.xib in Resources */, + 909437411CA0CB8F00BAFA36 /* TimeLineChartViewController.xib in Resources */, 5BDEDC481ABB871E007D3A60 /* CombinedChartViewController.xib in Resources */, 5B7B3AD91C437CBC001C109B /* RealmDemoListViewController.xib in Resources */, 5B4316371AB8D8B70009FCAA /* Default-736h@3x.png in Resources */, @@ -739,6 +748,7 @@ 5B4AC14C1C47E7250028D1A6 /* RealmBubbleChartViewController.m in Sources */, 65BF125A1BFBC6EE005C28D9 /* RealmDemoBaseViewController.m in Sources */, 5BEAED251ABC0BE20013F194 /* MultipleBarChartViewController.m in Sources */, + 909437401CA0CB8F00BAFA36 /* TimeLineChartViewController.m in Sources */, 5B57BBB81A9B26AA0036A6CC /* AppDelegate.m in Sources */, 5B7B3AD81C437CBC001C109B /* RealmDemoListViewController.m in Sources */, 55E356511ADC638F00A57971 /* BubbleChartViewController.m in Sources */, diff --git a/ChartsDemo/Classes/DemoListViewController.m b/ChartsDemo/Classes/DemoListViewController.m index cc5df68012..4f1114f24f 100644 --- a/ChartsDemo/Classes/DemoListViewController.m +++ b/ChartsDemo/Classes/DemoListViewController.m @@ -14,6 +14,7 @@ #import "DemoListViewController.h" #import "LineChart1ViewController.h" #import "LineChart2ViewController.h" +#import "TimeLineChartViewController.h" #import "BarChartViewController.h" #import "HorizontalBarChartViewController.h" #import "CombinedChartViewController.h" @@ -53,6 +54,11 @@ - (void)viewDidLoad @"subtitle": @"A simple demonstration of the linechart.", @"class": LineChart1ViewController.class }, + @{ + @"title": @"Time Line Chart", + @"subtitle": @"A demonstration of the linechart using x-axis to represent time.", + @"class": TimeLineChartViewController.class + }, @{ @"title": @"Line Chart (Dual YAxis)", @"subtitle": @"Demonstration of the linechart with dual y-axis.", diff --git a/ChartsDemo/Classes/Demos/TimeLineChartViewController.h b/ChartsDemo/Classes/Demos/TimeLineChartViewController.h new file mode 100644 index 0000000000..6475046c8e --- /dev/null +++ b/ChartsDemo/Classes/Demos/TimeLineChartViewController.h @@ -0,0 +1,20 @@ +// +// TimeLineChartViewController.h +// ChartsDemo +// +// Created by Daniel Cohen Gindi on 17/3/15. +// +// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda +// A port of MPAndroidChart for iOS +// Licensed under Apache License 2.0 +// +// https://github.com/danielgindi/ios-charts +// + +#import +#import "DemoBaseViewController.h" +#import + +@interface TimeLineChartViewController : DemoBaseViewController + +@end diff --git a/ChartsDemo/Classes/Demos/TimeLineChartViewController.m b/ChartsDemo/Classes/Demos/TimeLineChartViewController.m new file mode 100644 index 0000000000..fa8d100b39 --- /dev/null +++ b/ChartsDemo/Classes/Demos/TimeLineChartViewController.m @@ -0,0 +1,250 @@ +// +// TimeLineChartViewController.m +// ChartsDemo +// +// Created by Daniel Cohen Gindi on 17/3/15. +// +// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda +// A port of MPAndroidChart for iOS +// Licensed under Apache License 2.0 +// +// https://github.com/danielgindi/ios-charts +// + +#import "TimeLineChartViewController.h" +#import "ChartsDemo-Swift.h" + +@interface TimeLineChartViewController () + +@property (nonatomic, strong) IBOutlet TimeLineChartView *chartView; +@property (nonatomic, strong) IBOutlet UISlider *sliderX; +@property (nonatomic, strong) IBOutlet UISlider *sliderY; +@property (nonatomic, strong) IBOutlet UITextField *sliderTextX; +@property (nonatomic, strong) IBOutlet UITextField *sliderTextY; + +@end + +@implementation TimeLineChartViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = @"Line Chart 1 Chart"; + + self.options = @[ + @{@"key": @"toggleValues", @"label": @"Toggle Values"}, + @{@"key": @"toggleFilled", @"label": @"Toggle Filled"}, + @{@"key": @"toggleCircles", @"label": @"Toggle Circles"}, + @{@"key": @"toggleCubic", @"label": @"Toggle Cubic"}, + @{@"key": @"toggleStepped", @"label": @"Toggle Stepped"}, + @{@"key": @"toggleHighlight", @"label": @"Toggle Highlight"}, + @{@"key": @"animateX", @"label": @"Animate X"}, + @{@"key": @"animateY", @"label": @"Animate Y"}, + @{@"key": @"animateXY", @"label": @"Animate XY"}, + @{@"key": @"saveToGallery", @"label": @"Save to Camera Roll"}, + @{@"key": @"togglePinchZoom", @"label": @"Toggle PinchZoom"}, + @{@"key": @"toggleAutoScaleMinMax", @"label": @"Toggle auto scale min/max"}, + @{@"key": @"toggleData", @"label": @"Toggle Data"}, + ]; + + _chartView.delegate = self; + + _chartView.descriptionText = @""; + _chartView.noDataTextDescription = @"You need to provide data for the chart."; + + _chartView.dragEnabled = YES; + [_chartView setScaleEnabled:YES]; + _chartView.pinchZoomEnabled = YES; + _chartView.drawGridBackgroundEnabled = NO; + + // x-axis limit line + ChartLimitLine *llXAxis = [[ChartLimitLine alloc] initWithLimit:10.0 label:@"Index 10"]; + llXAxis.lineWidth = 4.0; + llXAxis.lineDashLengths = @[@(10.f), @(10.f), @(0.f)]; + llXAxis.labelPosition = ChartLimitLabelPositionRightBottom; + llXAxis.valueFont = [UIFont systemFontOfSize:10.f]; + + //[_chartView.xAxis addLimitLine:llXAxis]; + + ChartLimitLine *ll1 = [[ChartLimitLine alloc] initWithLimit:130.0 label:@"Upper Limit"]; + ll1.lineWidth = 4.0; + ll1.lineDashLengths = @[@5.f, @5.f]; + ll1.labelPosition = ChartLimitLabelPositionRightTop; + ll1.valueFont = [UIFont systemFontOfSize:10.0]; + + ChartLimitLine *ll2 = [[ChartLimitLine alloc] initWithLimit:-30.0 label:@"Lower Limit"]; + ll2.lineWidth = 4.0; + ll2.lineDashLengths = @[@5.f, @5.f]; + ll2.labelPosition = ChartLimitLabelPositionRightBottom; + ll2.valueFont = [UIFont systemFontOfSize:10.0]; + + ChartYAxis *leftAxis = _chartView.leftAxis; + [leftAxis removeAllLimitLines]; + [leftAxis addLimitLine:ll1]; + [leftAxis addLimitLine:ll2]; + leftAxis.customAxisMax = 220.0; + leftAxis.customAxisMin = -50.0; + leftAxis.gridLineDashLengths = @[@5.f, @5.f]; + leftAxis.drawZeroLineEnabled = NO; + leftAxis.drawLimitLinesBehindDataEnabled = YES; + + _chartView.rightAxis.enabled = NO; + + [_chartView.viewPortHandler setMaximumScaleY: 2.f]; + [_chartView.viewPortHandler setMaximumScaleX: 2.f]; + + BalloonMarker *marker = [[BalloonMarker alloc] initWithColor:[UIColor colorWithWhite:180/255. alpha:1.0] font:[UIFont systemFontOfSize:12.0] insets: UIEdgeInsetsMake(8.0, 8.0, 20.0, 8.0)]; + marker.minimumSize = CGSizeMake(80.f, 40.f); + _chartView.marker = marker; + + _chartView.legend.form = ChartLegendFormLine; + + _sliderX.value = 44.0; + _sliderY.value = 100.0; + [self slidersValueChanged:nil]; + + [_chartView animateWithXAxisDuration:2.5 easingOption:ChartEasingOptionEaseInOutQuart]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)updateChartData +{ + if (self.shouldHideData) + { + _chartView.data = nil; + return; + } + + [self setDataCount:(_sliderX.value + 1) range:_sliderY.value]; +} + +- (void)setDataCount:(int)count range:(double)range +{ + NSMutableArray *xVals = [[NSMutableArray alloc] init]; + + for (int i = 0; i < count; i++) + { + [xVals addObject:[@(i) stringValue]]; + } + + NSMutableArray *yVals = [[NSMutableArray alloc] init]; + + for (int i = 0; i < count; i++) + { + double mult = (range + 1); + double val = (double) (arc4random_uniform(mult)) + 3; + + // TODO: populate this with more realistic data + double xNumericVal = (double) i; + + [yVals addObject:[[ChartDataEntry alloc] initWithValue:val xIndex:i xNumericVal: xNumericVal]]; + } + + LineChartDataSet *set1 = [[LineChartDataSet alloc] initWithYVals:yVals label:@"Time DataSet"]; + + set1.lineDashLengths = @[@5.f, @2.5f]; + set1.highlightLineDashLengths = @[@5.f, @2.5f]; + [set1 setColor:UIColor.blackColor]; + [set1 setCircleColor:UIColor.blackColor]; + set1.lineWidth = 1.0; + set1.circleRadius = 3.0; + set1.drawCircleHoleEnabled = NO; + set1.valueFont = [UIFont systemFontOfSize:9.f]; + //set1.fillAlpha = 65/255.0; + //set1.fillColor = UIColor.blackColor; + + NSArray *gradientColors = @[ + (id)[ChartColorTemplates colorFromString:@"#00ff0000"].CGColor, + (id)[ChartColorTemplates colorFromString:@"#ffff0000"].CGColor + ]; + CGGradientRef gradient = CGGradientCreateWithColors(nil, (CFArrayRef)gradientColors, nil); + + set1.fillAlpha = 1.f; + set1.fill = [ChartFill fillWithLinearGradient:gradient angle:90.f]; + set1.drawFilledEnabled = YES; + + NSMutableArray *dataSets = [[NSMutableArray alloc] init]; + [dataSets addObject:set1]; + + LineChartData *data = [[LineChartData alloc] initWithXVals:xVals dataSets:dataSets]; + + _chartView.data = data; +} + +- (void)optionTapped:(NSString *)key +{ + if ([key isEqualToString:@"toggleFilled"]) + { + for (id set in _chartView.data.dataSets) + { + set.drawFilledEnabled = !set.isDrawFilledEnabled; + } + + [_chartView setNeedsDisplay]; + return; + } + + if ([key isEqualToString:@"toggleCircles"]) + { + for (id set in _chartView.data.dataSets) + { + set.drawCirclesEnabled = !set.isDrawCirclesEnabled; + } + + [_chartView setNeedsDisplay]; + return; + } + + if ([key isEqualToString:@"toggleCubic"]) + { + for (id set in _chartView.data.dataSets) + { + set.drawCubicEnabled = !set.isDrawCubicEnabled; + } + + [_chartView setNeedsDisplay]; + return; + } + + if ([key isEqualToString:@"toggleStepped"]) + { + for (id set in _chartView.data.dataSets) + { + set.drawSteppedEnabled = !set.isDrawSteppedEnabled; + } + + [_chartView setNeedsDisplay]; + } + + [super handleOption:key forChartView:_chartView]; +} + +#pragma mark - Actions + +- (IBAction)slidersValueChanged:(id)sender +{ + _sliderTextX.text = [@((int)_sliderX.value + 1) stringValue]; + _sliderTextY.text = [@((int)_sliderY.value) stringValue]; + + [self updateChartData]; +} + +#pragma mark - ChartViewDelegate + +- (void)chartValueSelected:(ChartViewBase * __nonnull)chartView entry:(ChartDataEntry * __nonnull)entry dataSetIndex:(NSInteger)dataSetIndex highlight:(ChartHighlight * __nonnull)highlight +{ + NSLog(@"chartValueSelected"); +} + +- (void)chartValueNothingSelected:(ChartViewBase * __nonnull)chartView +{ + NSLog(@"chartValueNothingSelected"); +} + +@end diff --git a/ChartsDemo/Classes/Demos/TimeLineChartViewController.xib b/ChartsDemo/Classes/Demos/TimeLineChartViewController.xib new file mode 100644 index 0000000000..4e09b18492 --- /dev/null +++ b/ChartsDemo/Classes/Demos/TimeLineChartViewController.xib @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChartsRealm/ChartsRealm.xcodeproj/project.pbxproj b/ChartsRealm/ChartsRealm.xcodeproj/project.pbxproj index d39231d6aa..93548f75f2 100644 --- a/ChartsRealm/ChartsRealm.xcodeproj/project.pbxproj +++ b/ChartsRealm/ChartsRealm.xcodeproj/project.pbxproj @@ -557,6 +557,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -582,6 +583,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1;