diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 6fc33a114..e1586f23f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -348,6 +348,7 @@ DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CA271D5A0300BE3819 /* LineChartView.swift */; }; + DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; }; DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; }; @@ -1143,6 +1144,7 @@ DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = ""; }; DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = ""; }; DB71C7CA271D5A0300BE3819 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = ""; }; + DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurveAlgorithm.swift; sourceTree = ""; }; DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = ""; }; DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; }; @@ -1752,6 +1754,7 @@ 2D5A3D0125CF8640002347D6 /* Vender */ = { isa = PBXGroup; children = ( + DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */, 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB51D170262832380062B7A1 /* BlurHashDecode.swift */, DB51D171262832380062B7A1 /* BlurHashEncode.swift */, @@ -4234,6 +4237,7 @@ DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, + DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */, diff --git a/Mastodon/Scene/Search/Search/View/LineChartView.swift b/Mastodon/Scene/Search/Search/View/LineChartView.swift index 5b35bbc4f..51164034e 100644 --- a/Mastodon/Scene/Search/Search/View/LineChartView.swift +++ b/Mastodon/Scene/Search/Search/View/LineChartView.swift @@ -66,61 +66,33 @@ extension LineChartView { gradientLayer.isHidden = false // Draw smooth chart - // use vDSP scale the data with line interpolation method - var data = data.map { Float($0) } - // duplicate first and last value to prevent interpolation at edge data - data.insert(data[0], at: 0) - if let last = data.last { - data.append(last) - } - - let n = vDSP_Length(128) - let stride = vDSP_Stride(1) - - // generate fine control with smoothing (simd_smoothstep(_:_:_:)) - let denominator = Float(n) / Float(data.count - 1) - let control: [Float] = (0...n).map { - let x = Float($0) / denominator - return floor(x) + simd_smoothstep(0, 1, simd_fract(x)) - } - - var points = [Float](repeating: 0, count: Int(n)) - vDSP_vlint(data, - control, stride, - &points, stride, - n, - vDSP_Length(data.count)) - guard let maxDataPoint = data.max() else { return } - func calculateY(for point: Float, in frame: CGRect) -> CGFloat { + func calculateY(for point: CGFloat, in frame: CGRect) -> CGFloat { guard maxDataPoint > 0 else { return .zero } - return (1 - CGFloat(point / maxDataPoint)) * frame.height + return (1 - point / maxDataPoint) * frame.height } - let segmentCount = points.count - 1 + let segmentCount = data.count - 1 let segmentWidth = bounds.width / CGFloat(segmentCount) - let linePath = UIBezierPath() + let points: [CGPoint] = { + var points: [CGPoint] = [] + var x: CGFloat = 0 + for value in data { + let point = CGPoint(x: x, y: calculateY(for: value, in: bounds)) + points.append(point) + x += segmentWidth + } + return points + }() + + guard let linePath = CurveAlgorithm.shared.createCurvedPath(points) else { return } let dotPath = UIBezierPath() - // move to first data point - var x: CGFloat = 0 - let y = calculateY(for: points[0], in: bounds) - linePath.move(to: CGPoint(x: x, y: y)) - for point in points.dropFirst() { - x += segmentWidth - linePath.addLine(to: CGPoint( - x: x, - y: calculateY(for: point, in: bounds) - )) - } - if let last = points.last { - let y = calculateY(for: last, in: bounds) - let center = CGPoint(x: bounds.maxX, y: y) - dotPath.addArc(withCenter: center, radius: 3, startAngle: 0, endAngle: 2 * .pi, clockwise: true) + dotPath.addArc(withCenter: last, radius: 3, startAngle: 0, endAngle: 2 * .pi, clockwise: true) } // this not works diff --git a/Mastodon/Vender/CurveAlgorithm.swift b/Mastodon/Vender/CurveAlgorithm.swift new file mode 100644 index 000000000..00db06048 --- /dev/null +++ b/Mastodon/Vender/CurveAlgorithm.swift @@ -0,0 +1,47 @@ +// +// CurveAlgorithm.swift +// +// Ref: https://github.com/nhatminh12369/LineChart/blob/master/LineChart/CurveAlgorithm.swift + +import UIKit + +struct CurvedSegment { + var controlPoint1: CGPoint + var controlPoint2: CGPoint +} + +class CurveAlgorithm { + static let shared = CurveAlgorithm() + + private func controlPointsFrom(points: [CGPoint]) -> [CurvedSegment] { + var result: [CurvedSegment] = [] + + let delta: CGFloat = 0.4 + + // only use horizontal control point + for i in 1.. UIBezierPath? { + let path = UIBezierPath() + path.move(to: dataPoints[0]) + + var curveSegments: [CurvedSegment] = [] + curveSegments = controlPointsFrom(points: dataPoints) + + for i in 1..