Compare commits

..

1 Commits

Author SHA1 Message Date
Andras Samu ac7680320f feat(core): refactoring chart dispalying
now it is possible to add background lines precisely as charts are displayed at correct size
also rewrote basics to conform with Shapes and Animatable protocol
2021-04-04 15:54:15 +02:00
44 changed files with 426 additions and 828 deletions
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>
@@ -1,98 +0,0 @@
import SwiftUI
public struct AxisLabels<Content: View>: View {
struct YAxisViewKey: ViewPreferenceKey { }
struct ChartViewKey: ViewPreferenceKey { }
var axisLabelsData = AxisLabelsData()
var axisLabelsStyle = AxisLabelsStyle()
@State private var yAxisWidth: CGFloat = 25
@State private var chartWidth: CGFloat = 0
@State private var chartHeight: CGFloat = 0
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var yAxis: some View {
VStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in
Text(axisYData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(height: getYHeight(index: index,
chartHeight: chartHeight,
count: axisLabelsData.axisYLabels.count),
alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count))
}
}
.padding([.leading, .trailing], 4.0)
.background(ViewGeometry<YAxisViewKey>())
.onPreferenceChange(YAxisViewKey.self) { value in
yAxisWidth = value.first?.size.width ?? 0.0
}
}
func xAxis(chartWidth: CGFloat) -> some View {
HStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in
Text(axisXData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1))
}
}
.frame(height: 24.0, alignment: .top)
}
var chart: some View {
self.content()
.background(ViewGeometry<ChartViewKey>())
.onPreferenceChange(ChartViewKey.self) { value in
chartWidth = value.first?.size.width ?? 0.0
chartHeight = value.first?.size.height ?? 0.0
}
}
public var body: some View {
VStack(spacing: 0.0) {
HStack {
if axisLabelsStyle.axisLabelsYPosition == .leading {
yAxis
} else {
Spacer(minLength: yAxisWidth)
}
chart
if axisLabelsStyle.axisLabelsYPosition == .leading {
Spacer(minLength: yAxisWidth)
} else {
yAxis
}
}
xAxis(chartWidth: chartWidth)
}
}
private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat {
if index == 0 || index == count - 1 {
return chartHeight / (CGFloat(count - 1) * 2) + 10
}
return chartHeight / CGFloat(count - 1)
}
private func getYAlignment(index: Int, count: Int) -> Alignment {
if index == 0 {
return .top
}
if index == count - 1 {
return .bottom
}
return .center
}
}
@@ -1,57 +0,0 @@
import SwiftUI
extension AxisLabels {
public func setAxisYLabels(_ labels: [String],
position: AxisLabelsYPosition = .leading) -> AxisLabels {
self.axisLabelsData.axisYLabels = labels
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [String]) -> AxisLabels {
self.axisLabelsData.axisXLabels = labels
return self
}
public func setAxisYLabels(_ labels: [(Double, String)],
range: ClosedRange<Int>,
position: AxisLabelsYPosition = .leading) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisYLabels = labelArray
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange<Int>) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisXLabels = labelArray
return self
}
public func setColor(_ color: Color) -> AxisLabels {
self.axisLabelsStyle.axisFontColor = color
return self
}
public func setFont(_ font: Font) -> AxisLabels {
self.axisLabelsStyle.axisFont = font
return self
}
}
@@ -1,11 +0,0 @@
import Foundation
public enum AxisLabelsYPosition {
case leading
case trailing
}
public enum AxisLabelsXPosition {
case top
case bottom
}
@@ -1,11 +0,0 @@
import SwiftUI
public final class AxisLabelsStyle: ObservableObject {
@Published public var axisFont: Font = .callout
@Published public var axisFontColor: Color = .primary
@Published var axisLabelsYPosition: AxisLabelsYPosition = .leading
@Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom
public init() {
// no-op
}
}
@@ -1,10 +0,0 @@
import SwiftUI
public final class AxisLabelsData: ObservableObject {
@Published public var axisYLabels: [String] = []
@Published public var axisXLabels: [String] = []
public init() {
// no-op
}
}
@@ -26,9 +26,9 @@ public struct CardView<Content: View>: View, ChartBase {
if showShadow {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.shadow(color: Color(white: 0.9, opacity: 1), radius: 8)
.shadow(color: Color.gray, radius: 8)
}
VStack (alignment: .leading) {
VStack {
self.content()
}
.clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0))
@@ -1,6 +1,6 @@
import SwiftUI
/// Protocol for any type of chart, to get access to underlying data
public protocol ChartBase: View {
public protocol ChartBase {
var chartData: ChartData { get }
}
@@ -2,78 +2,36 @@ import SwiftUI
/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [(Double, Double)] = []
public var rangeY: ClosedRange<Double>?
public var rangeX: ClosedRange<Double>?
@Published public var data: [(String, Double)] = []
var points: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
data.map { $0.1 }
}
var values: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
var values: [String] {
data.map { $0.0 }
}
var normalisedPoints: [Double] {
let absolutePoints = points.map { abs($0) }
var maxPoint = absolutePoints.max()
if let rangeY = rangeY {
maxPoint = Double(rangeY.overreach)
return points.map { ($0 - rangeY.lowerBound) / (maxPoint ?? 1.0) }
}
return points.map { $0 / (maxPoint ?? 1.0) }
points.map { $0 / (points.max() ?? 1.0) }
}
var normalisedValues: [Double] {
let absoluteValues = values.map { abs($0) }
var maxValue = absoluteValues.max()
if let rangeX = rangeX {
maxValue = Double(rangeX.overreach)
return values.map { ($0 - rangeX.lowerBound) / (maxValue ?? 1.0) }
}
return values.map { $0 / (maxValue ?? 1.0) }
}
var normalisedData: [(Double, Double)] {
Array(zip(normalisedValues, normalisedPoints))
}
var normalisedYRange: Double {
if let _ = rangeY {
return 1
}
return (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0)
}
var normalisedXRange: Double {
if let _ = rangeX {
return 1
}
return (normalisedValues.max() ?? 0.0) - (normalisedValues.min() ?? 0.0)
var normalisedRange: Double {
(normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0)
}
var isInNegativeDomain: Bool {
if let rangeY = rangeY {
return rangeY.lowerBound < 0
}
return (points.min() ?? 0.0) < 0
(points.min() ?? 0.0) < 0
}
/// Initialize with data array
/// - Parameter data: Array of `Double`
public init(_ data: [Double], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data.enumerated().map{ (index, value) in (Double(index), value) }
self.rangeY = rangeY
public init(_ data: [Double]) {
self.data = data.map { ("", $0) }
}
public init(_ data: [(Double, Double)], rangeY: ClosedRange<FloatLiteralType>? = nil) {
public init(_ data: [(String, Double)]) {
self.data = data
self.rangeY = rangeY
}
public init() {
@@ -1,10 +0,0 @@
import SwiftUI
public struct ViewGeometry<T>: View where T: PreferenceKey {
public var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value)
}
}
}
@@ -1,15 +0,0 @@
import SwiftUI
public protocol ViewPreferenceKey: PreferenceKey {
typealias Value = [ViewSizeData]
}
public extension ViewPreferenceKey {
static var defaultValue: [ViewSizeData] {
[]
}
static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {
value.append(contentsOf: nextValue())
}
}
@@ -1,14 +0,0 @@
import SwiftUI
public struct ViewSizeData: Identifiable, Equatable, Hashable {
public let id: UUID = UUID()
public let size: CGSize
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
@@ -17,10 +17,3 @@ extension Array where Element == ColorGradient {
return self[index]
}
}
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
@@ -38,10 +38,4 @@ extension CGPoint {
return CGPoint(x: stepWidth, y: stepHeight)
}
func denormalize(with geometry: GeometryProxy) -> CGPoint {
let width = geometry.frame(in: .local).width
let height = geometry.frame(in: .local).height
return CGPoint(x: self.x * width, y: self.y * height)
}
}
@@ -1,23 +1,21 @@
import SwiftUI
extension ChartBase {
public func data(_ data: [Double]) -> some ChartBase {
chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
extension View where Self: ChartBase {
/// Set data for a chart
/// - Parameter data: array of `Double`
/// - Returns: modified `View` with data attached
public func data(_ data: [Double]) -> some View {
chartData.data = data.map { ("", $0) }
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}
public func data(_ data: [(Double, Double)]) -> some ChartBase {
public func data(_ data: [(String, Double)]) -> some View {
chartData.data = data
return self
}
public func rangeY(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeY = range
return self
}
public func rangeX(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeX = range
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}
}
@@ -1,31 +1,47 @@
import SwiftUI
extension Path {
/// Returns a tiny segment of path based on percentage along the path
///
/// TODO: Explain why more than 1 gets 0 and why less than 0 gets 1
/// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled)
/// - Returns: tiny path right around the requested fraction
func trimmedPath(for percent: CGFloat) -> Path {
let boundsDistance: CGFloat = 0.001
let completion: CGFloat = 1 - boundsDistance
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
// Start/end points centered around given percentage, but capped if right at the very end
// Start/end points centered around given percentage, but capped if right at the very end
let start = pct > completion ? completion : pct - boundsDistance
let end = pct > completion ? 1 : pct + boundsDistance
return trimmedPath(from: start, to: end)
}
/// Find the `CGPoint` for the given fraction along the path.
///
/// This works by requesting a very tiny trimmed section of the path, then getting the center of the bounds rectangle
/// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled)
/// - Returns: a `CGPoint` representing the location of that section of the path
func point(for percent: CGFloat) -> CGPoint {
let path = trimmedPath(for: percent)
return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
}
/// <#Description#>
/// - Parameter maxX: <#maxX description#>
/// - Returns: <#description#>
func point(to maxX: CGFloat) -> CGPoint {
let total = length
let sub = length(to: maxX)
let percent = sub / total
return point(for: percent)
}
var length: CGFloat {
/// <#Description#>
/// - Returns: <#description#>
var length: CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
var point = CGPoint.zero
@@ -57,6 +73,9 @@ extension Path {
return ret
}
/// <#Description#>
/// - Parameter maxX: <#maxX description#>
/// - Returns: <#description#>
func length(to maxX: CGFloat) -> CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -108,13 +127,19 @@ extension Path {
return ret
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - globalOffset: <#globalOffset description#>
/// - Returns: <#description#>
static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if points.count < 2 {
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
// guard let offset = points.min() else { return path }
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
@@ -127,153 +152,117 @@ extension Path {
return path
}
static func quadCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - globalOffset: <#globalOffset description#>
/// - Returns: <#description#>
static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if data.count < 2 {
if points.count < 2 {
return path
}
let offset = globalOffset ?? points.min()!
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
return path
}
static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 }
if filteredData.count < 1 {
return path
}
let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width }
let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height }
let markerSize = CGSize(width: 8, height: 8)
for pointIndex in 0..<filteredData.count {
path.addRoundedRect(in: CGRect(origin: CGPoint(x: convertedXValues[pointIndex] - markerSize.width / 2,
y: convertedYPoints[pointIndex] - markerSize.height / 2),
size: markerSize),
cornerSize: CGSize(width: markerSize.width / 2,
height: markerSize.height / 2))
}
return path
}
static func drawGridLines(numberOfHorizontalLines: Int, numberOfVerticalLines: Int, in rect: CGRect) -> Path {
var path = Path()
for index in 0..<numberOfHorizontalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfHorizontalLines - 1)
let startPoint = CGPoint(x: 0, y: normalisedSpacing * CGFloat(index) * rect.height)
let endPoint = CGPoint(x: rect.width, y: normalisedSpacing * CGFloat(index) * rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
for index in 0..<numberOfVerticalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfVerticalLines - 1)
let startPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: 0)
let endPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
return path
}
static func quadClosedCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: CGPoint(x: convertedXValues[0], y: 0))
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
static func linePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
return path
}
static func closedLinePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
// guard let offset = points.min() else { return path }
path.move(to: .zero)
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
for pointIndex in 1..<points.count {
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - Returns: <#description#>
static func linePathWithPoints(points: [Double], step: CGPoint) -> Path {
var path = Path()
if points.count < 2 {
return path
}
guard let offset = points.min() else {
return path
}
let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
path.addLine(to: point2)
}
return path
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - Returns: <#description#>
static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path {
var path = Path()
if points.count < 2 {
return path
}
guard let offset = points.min() else {
return path
}
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
point1 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
path.addLine(to: point1)
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
}
extension CGPoint {
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - x: <#x description#>
/// - Returns: <#description#>
func point(to: CGPoint, x: CGFloat) -> CGPoint {
let a = (to.y - self.y) / (to.x - self.x)
let y = self.y + (x - self.x) * a
return CGPoint(x: x, y: y)
}
/// <#Description#>
/// - Parameter to: <#to description#>
/// - Returns: <#description#>
func line(to: CGPoint) -> CGFloat {
dist(to: to)
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - x: <#x description#>
/// - Returns: <#description#>
func line(to: CGPoint, x: CGFloat) -> CGFloat {
dist(to: point(to: to, x: x))
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control: <#control description#>
/// - Returns: <#description#>
func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -289,6 +278,12 @@ extension CGPoint {
return dist
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control: <#control description#>
/// - x: <#x description#>
/// - Returns: <#description#>
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -314,6 +309,12 @@ extension CGPoint {
return dist
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - t: <#t description#>
/// - control: <#control description#>
/// - Returns: <#description#>
func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
@@ -321,6 +322,12 @@ extension CGPoint {
return CGPoint(x: x, y: y)
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -338,6 +345,13 @@ extension CGPoint {
return dist
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - x: <#x description#>
/// - Returns: <#description#>
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -365,6 +379,13 @@ extension CGPoint {
return dist
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - t: <#t description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, control1: control1.x, control2: control2.x)
let y = CGPoint.value(x: self.y, y: to.y, t: t, control1: control1.y, control2: control2.x)
@@ -372,6 +393,13 @@ extension CGPoint {
return CGPoint(x: x, y: y)
}
/// <#Description#>
/// - Parameters:
/// - x: <#x description#>
/// - y: <#y description#>
/// - t: <#t description#>
/// - c: <#c description#>
/// - Returns: <#description#>
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
@@ -381,6 +409,14 @@ extension CGPoint {
return value
}
/// <#Description#>
/// - Parameters:
/// - x: <#x description#>
/// - y: <#y description#>
/// - t: <#t description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
static func value(x: CGFloat, y: CGFloat, t: CGFloat, control1: CGFloat, control2: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
@@ -391,6 +427,11 @@ extension CGPoint {
return value
}
/// <#Description#>
/// - Parameters:
/// - point1: <#point1 description#>
/// - point2: <#point2 description#>
/// - Returns: <#description#>
static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(
x: point1.x + (point2.x - point1.x) / 2,
@@ -398,16 +439,29 @@ extension CGPoint {
)
}
/// <#Description#>
/// - Parameter to: <#to description#>
/// - Returns: <#description#>
func dist(to: CGPoint) -> CGFloat {
return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
}
/// <#Description#>
/// - Parameters:
/// - firstPoint: <#firstPoint description#>
/// - secondPoint: <#secondPoint description#>
/// - Returns: <#description#>
static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
return CGPoint(
x: (firstPoint.x + secondPoint.x) / 2,
y: (firstPoint.y + secondPoint.y) / 2)
}
/// <#Description#>
/// - Parameters:
/// - firstPoint: <#firstPoint description#>
/// - secondPoint: <#secondPoint description#>
/// - Returns: <#description#>
static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint)
let diffY = abs(secondPoint.y - controlPoint.y)
@@ -1,7 +0,0 @@
import Foundation
public extension ClosedRange where Bound: AdditiveArithmetic {
var overreach: Bound {
self.upperBound - self.lowerBound
}
}
@@ -1,17 +0,0 @@
import SwiftUI
extension Shape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
extension InsettableShape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.strokeBorder(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
+40 -11
View File
@@ -1,24 +1,53 @@
import SwiftUI
public struct ChartGrid<Content: View>: View {
public struct ChartGrid<Content: View>: View, ChartBase {
public var chartData = ChartData()
let content: () -> Content
public var gridOptions = GridOptions()
let numberOfHorizontalLines = 4
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
ZStack {
ChartGridShape(numberOfHorizontalLines: gridOptions.numberOfHorizontalLines,
numberOfVerticalLines: gridOptions.numberOfVerticalLines)
.stroke(gridOptions.color, style: gridOptions.strokeStyle)
if gridOptions.showBaseLine {
ChartGridBaseShape()
.stroke(gridOptions.color, style: gridOptions.baseStrokeStyle)
.rotationEffect(.degrees(180), anchor: .center)
HStack {
ZStack {
VStack {
ForEach(0..<numberOfHorizontalLines) { _ in
GridElement()
Spacer()
}
}
self.content()
}
self.content()
}
}
}
struct GridElement: View {
var body: some View {
DashedLine()
.frame(maxHeight: 2, alignment: .center)
}
}
struct DashedLine: View {
func line(frame: CGRect) -> Path {
let baseLine: CGFloat = CGFloat(frame.height / 2)
var hLine = Path()
hLine.move(to: CGPoint(x:0, y: baseLine))
hLine.addLine(to: CGPoint(x: frame.width, y: baseLine))
return hLine
}
var body: some View {
GeometryReader { geometry in
line(frame: geometry.frame(in: .local))
.stroke(Color(white: 0.3), style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10]))
}
}
}
@@ -1,18 +0,0 @@
import SwiftUI
struct ChartGridBaseShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
struct ChartGridBaseShape_Previews: PreviewProvider {
static var previews: some View {
ChartGridBaseShape()
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
}
}
@@ -1,30 +0,0 @@
import SwiftUI
struct ChartGridShape: Shape {
var numberOfHorizontalLines: Int
var numberOfVerticalLines: Int
func path(in rect: CGRect) -> Path {
let path = Path.drawGridLines(numberOfHorizontalLines: numberOfHorizontalLines,
numberOfVerticalLines: numberOfVerticalLines,
in: rect)
return path
}
}
struct ChartGridShape_Previews: PreviewProvider {
static var previews: some View {
Group {
ChartGridShape(numberOfHorizontalLines: 5, numberOfVerticalLines: 0)
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
ChartGridShape(numberOfHorizontalLines: 4, numberOfVerticalLines: 4)
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
.padding()
}
}
@@ -1,31 +0,0 @@
import SwiftUI
extension ChartGrid {
public func setNumberOfHorizontalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfHorizontalLines = numberOfLines
return self
}
public func setNumberOfVerticalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfVerticalLines = numberOfLines
return self
}
public func setStoreStyle(_ strokeStyle: StrokeStyle) -> ChartGrid {
self.gridOptions.strokeStyle = strokeStyle
return self
}
public func setColor(_ color: Color) -> ChartGrid {
self.gridOptions.color = color
return self
}
public func showBaseLine(_ show: Bool, with style: StrokeStyle? = nil) -> ChartGrid {
self.gridOptions.showBaseLine = show
if let style = style {
self.gridOptions.baseStrokeStyle = style
}
return self
}
}
@@ -1,14 +0,0 @@
import SwiftUI
public final class GridOptions: ObservableObject {
@Published public var numberOfHorizontalLines: Int = 3
@Published public var numberOfVerticalLines: Int = 3
@Published public var strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])
@Published public var color: Color = Color(white: 0.85)
@Published public var showBaseLine: Bool = true
@Published public var baseStrokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5, 0])
public init() {
// no-op
}
}
@@ -39,13 +39,13 @@ public struct ChartLabel: View {
private var labelPadding: EdgeInsets {
switch labelType {
case .title:
return EdgeInsets(top: 16.0, leading: 0, bottom: 0.0, trailing: 8.0)
return EdgeInsets(top: 16.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .legend:
return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0)
return EdgeInsets(top: 4.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .subTitle:
return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0)
return EdgeInsets(top: 8.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .largeTitle:
return EdgeInsets(top: 24.0, leading: 0, bottom: 0.0, trailing: 8.0)
return EdgeInsets(top: 24.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .custom(_, let padding, _):
return padding
}
@@ -59,13 +59,13 @@ public struct ChartLabel: View {
private var labelColor: Color {
switch labelType {
case .title:
return Color.primary
return Color(UIColor.label)
case .legend:
return Color.secondary
return Color(UIColor.secondaryLabel)
case .subTitle:
return Color.primary
return Color(UIColor.label)
case .largeTitle:
return Color.primary
return Color(UIColor.label)
case .custom(_, _, let color):
return color
}
@@ -1,24 +1,44 @@
import SwiftUI
/// Descripton of colors/styles for any kind of chart
public class ChartStyle: ObservableObject {
/// colors for background are of chart
public let backgroundColor: ColorGradient
/// colors for foreground fill of chart
public let foregroundColor: [ColorGradient]
/// Initialize with a single background color and an array of `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `Color`
/// - foregroundColor: array of `ColorGradient`
public init(backgroundColor: Color, foregroundColor: [ColorGradient]) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = foregroundColor
}
/// Initialize with a single background color and a single `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `Color`
/// - foregroundColor: a `ColorGradient`
public init(backgroundColor: Color, foregroundColor: ColorGradient) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = [foregroundColor]
}
/// Initialize with a single background `ColorGradient` and a single `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `ColorGradient`
/// - foregroundColor: a `ColorGradient`
public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) {
self.backgroundColor = backgroundColor
self.foregroundColor = [foregroundColor]
}
/// Initialize with a single background `ColorGradient` and an array of `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `ColorGradient`
/// - foregroundColor: array of `ColorGradient`
public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
@@ -1,25 +1,39 @@
import SwiftUI
/// An encapsulation of a simple gradient between one color and another
public struct ColorGradient: Equatable {
public let startColor: Color
public let endColor: Color
/// Initialize as a solid color
/// - Parameter color: a single `Color` (no gradient effect visible)
public init(_ color: Color) {
self.startColor = color
self.endColor = color
}
/// Initialize a color gradient from two specified colors
/// - Parameters:
/// - startColor: starting color
/// - endColor: ending color
public init(_ startColor: Color, _ endColor: Color) {
self.startColor = startColor
self.endColor = endColor
}
/// Convert to a `Gradient` object (more complicated than just two colors)
/// - Returns: a `Gradient` between the specified start and end colors
public var gradient: Gradient {
return Gradient(colors: [startColor, endColor])
}
}
extension ColorGradient {
/// Convenience method to return a SwiftUI LinearGradient view from the ColorGradient
/// - Parameters:
/// - startPoint: starting point
/// - endPoint: ending point
/// - Returns: a Linear gradient
public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient {
return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint)
}
@@ -1,6 +1,8 @@
import SwiftUI
/// Some predefined colors, used for demos, defaults if color is missing, and data indicator point
public enum ChartColors {
// Orange
public static let orangeBright = Color(hexString: "#FF782C")
public static let orangeDark = Color(hexString: "#EC2301")
@@ -1,12 +1,17 @@
import SwiftUI
public struct BarChart: ChartBase {
/// A type of chart that displays vertical bars for each data point
public struct BarChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `BarChart`.
///
///
public var body: some View {
BarChartRow(chartData: chartData, style: style)
BarChartRow(chartData: data, style: style)
}
public init() {}
@@ -1,5 +1,6 @@
import SwiftUI
/// A single vertical bar in a `BarChart`
public struct BarChartCell: View {
var value: Double
var index: Int = 0
@@ -18,6 +19,9 @@ public struct BarChartCell: View {
self.touchLocation = touchLocation
}
/// The content and behavior of the `BarChartCell`.
///
/// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set.
public var body: some View {
BarChartCellShape(value: didCellAppear ? value : 0.0)
.fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear {
@@ -3,7 +3,6 @@ import SwiftUI
struct BarChartCellShape: Shape, Animatable {
var value: Double
var cornerRadius: CGFloat = 6.0
var animatableData: CGFloat {
get { CGFloat(value) }
set { value = Double(newValue) }
@@ -17,14 +16,14 @@ struct BarChartCellShape: Shape, Animatable {
path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: Double.pi),
endAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: value < 0 ? adjustedOriginY + 2 * cornerRadius : adjustedOriginY))
endAngle: Angle(radians: -Double.pi/2),
clockwise: false)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY))
path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
startAngle: Angle(radians: -Double.pi/2),
endAngle: Angle(radians: 0),
clockwise: value < 0 ? true : false)
clockwise: false)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
@@ -40,10 +39,6 @@ struct BarChartCellShape_Previews: PreviewProvider {
BarChartCellShape(value: 0.3)
.fill(Color.blue)
BarChartCellShape(value: -0.3)
.fill(Color.blue)
.offset(x: 0, y: -600)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
/// A single row of data, a view in a `BarChart`
public struct BarChartRow: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@@ -14,6 +15,11 @@ public struct BarChartRow: View {
return max != 0 ? max : 1
}
/// The content and behavior of the `BarChartRow`.
///
/// Shows each `BarChartCell` in an `HStack`; may be scaled up if it's the one currently being touched.
/// Not using a drawing group for optimizing animation.
/// As touched (dragged) the `touchLocation` is updated and the current value is highlighted.
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom,
@@ -46,6 +52,11 @@ public struct BarChartRow: View {
}
}
/// Size to scale the touch indicator
/// - Parameters:
/// - touchLocation: fraction of width where touch is happening
/// - index: index into data array
/// - Returns: a scale larger than 1.0 if in bounds; 1.0 (unscaled) if not in bounds
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) &&
touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) {
@@ -54,6 +65,9 @@ public struct BarChartRow: View {
return CGSize(width: 1, height: 1)
}
/// Get data value where touch happened
/// - Parameter width: width of chart
/// - Returns: value as `Double` if chart has data
func getCurrentValue(width: CGFloat) -> Double? {
guard self.chartData.data.count > 0 else { return nil}
let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count))))))
@@ -1,24 +0,0 @@
import SwiftUI
extension LineChart {
public func setLineWidth(width: CGFloat) -> LineChart {
self.chartProperties.lineWidth = width
return self
}
public func setBackground(colorGradient: ColorGradient) -> LineChart {
self.chartProperties.backgroundGradient = colorGradient
return self
}
public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart {
self.chartProperties.showChartMarks = show
self.chartProperties.customChartMarksColors = color
return self
}
public func setLineStyle(to style: LineStyle) -> LineChart {
self.chartProperties.lineStyle = style
return self
}
}
@@ -1,6 +1,10 @@
import SwiftUI
/// A dot representing a single data point as user moves finger over line in `LineChart`
struct IndicatorPoint: View {
/// The content and behavior of the `IndicatorPoint`.
///
/// A filled circle with a thick white outline and a shadow
public var body: some View {
ZStack {
Circle()
@@ -2,50 +2,41 @@ import SwiftUI
/// A single line of data, a view in a `LineChart`
public struct Line: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@ObservedObject var chartProperties: LineChartProperties
var curvedLines: Bool = true
var style: ChartStyle
@State private var showIndicator: Bool = false
@State private var touchLocation: CGPoint = .zero
@State private var showBackground: Bool = true
@State private var didCellAppear: Bool = false
var path: Path {
Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints,
step: CGPoint(x: 1.0, y: 1.0))
}
public init(chartData: ChartData,
style: ChartStyle,
chartProperties: LineChartProperties) {
self.chartData = chartData
self.style = style
self.chartProperties = chartProperties
}
var curvedLines: Bool = true
/// The content and behavior of the `Line`.
/// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched.
/// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point.
/// TODO: explain rotation
public var body: some View {
GeometryReader { geometry in
ZStack {
if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
if self.didCellAppear && self.showBackground {
LineBackgroundShapeView(chartData: chartData,
geometry: geometry,
backgroundColor: backgroundColor)
style: style)
}
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: didCellAppear ? 1.0 : 0.0)
.animation(Animation.easeIn(duration: 0.75))
if self.showIndicator {
IndicatorPoint()
.position(self.getClosestPointOnPath(geometry: geometry,
touchLocation: self.touchLocation))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
.animation(.easeIn)
// if self.showIndicator {
// IndicatorPoint()
// .position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
// .rotationEffect(.degrees(180), anchor: .center)
// .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
// }
}
.onAppear {
didCellAppear = true
@@ -53,47 +44,44 @@ public struct Line: View {
.onDisappear() {
didCellAppear = false
}
// .gesture(DragGesture()
// .onChanged({ value in
// self.touchLocation = value.location
// self.showIndicator = true
// self.getClosestDataPoint(geometry: geometry, touchLocation: value.location)
// })
// .onEnded({ value in
// self.touchLocation = .zero
// self.showIndicator = false
// })
// )
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicator = true
// self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location))
self.chartValue.interactionInProgress = true
})
.onEnded({ value in
self.touchLocation = .zero
self.showIndicator = false
self.chartValue.interactionInProgress = false
})
)
}
}
}
// MARK: - Private functions
extension Line {
/// Calculate point closest to where the user touched
/// - Parameter touchLocation: location in view where touched
/// - Returns: `CGPoint` of data point on chart
private func getClosestPointOnPath(geometry: GeometryProxy, touchLocation: CGPoint) -> CGPoint {
let geometryWidth = geometry.frame(in: .local).width
let normalisedTouchLocationX = (touchLocation.x / geometryWidth) * CGFloat(chartData.normalisedPoints.count - 1)
let closest = self.path.point(to: normalisedTouchLocationX)
var denormClosest = closest.denormalize(with: geometry)
denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1)
denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedYRange)
return denormClosest
}
//extension Line {
// /// Calculate point closest to where the user touched
// /// - Parameter touchLocation: location in view where touched
// /// - Returns: `CGPoint` of data point on chart
// private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
// let closest = self.path.point(to: touchLocation.x)
// return closest
// }
//
// /// Figure out where closest touch point was
// /// - Parameter point: location of data point on graph, near touch location
private func getClosestDataPoint(geometry: GeometryProxy, touchLocation: CGPoint) {
let geometryWidth = geometry.frame(in: .local).width
let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1)))
if (index >= 0 && index < self.chartData.data.count){
// private func getClosestDataPoint(point: CGPoint) {
// let index = Int(round((point.x)/step.x))
// if (index >= 0 && index < self.chartData.data.count){
// self.chartValue.currentValue = self.chartData.points[index]
}
}
}
// }
// }
//}
struct Line_Previews: PreviewProvider {
/// Predefined style, black over white, for preview
@@ -104,12 +92,8 @@ struct Line_Previews: PreviewProvider {
static var previews: some View {
Group {
Line(chartData: ChartData([8, 23, 32, 7, 23, -4]),
style: blackLineStyle,
chartProperties: LineChartProperties())
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: redLineStyle,
chartProperties: LineChartProperties())
Line(chartData: ChartData([8, 23, 32, 7, 23, -4]), style: blackLineStyle)
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle)
}
}
}
@@ -1,9 +1,9 @@
import SwiftUI
struct LineBackgroundShape: Shape {
var data: [(Double, Double)]
var data: [Double]
func path(in rect: CGRect) -> Path {
let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect)
let path = Path.quadClosedCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0))
return path
}
}
@@ -12,13 +12,15 @@ struct LineBackgroundShape_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
LineBackgroundShape(data: [0, 0.5, 0.8, 0.6, 1])
.transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height))
.fill(Color.red)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
GeometryReader { geometry in
LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
LineBackgroundShape(data: [0, -0.5, 0.8, -0.6, 1])
.transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6))
.fill(Color.blue)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
@@ -3,12 +3,14 @@ import SwiftUI
struct LineBackgroundShapeView: View {
var chartData: ChartData
var geometry: GeometryProxy
var backgroundColor: ColorGradient
var style: ChartStyle
var body: some View {
LineBackgroundShape(data: chartData.normalisedData)
.fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
backgroundColor.endColor]),
LineBackgroundShape(data: chartData.normalisedPoints)
.transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1),
y: geometry.size.height / CGFloat(chartData.normalisedRange)))
.fill(LinearGradient(gradient: Gradient(colors: [style.foregroundColor.first?.startColor ?? .white,
style.backgroundColor.startColor]),
startPoint: .bottom,
endPoint: .top))
.rotationEffect(.degrees(180), anchor: .center)
@@ -1,14 +1,17 @@
import SwiftUI
public struct LineChart: ChartBase {
/// A type of chart that displays a line connecting the data points
public struct LineChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var chartProperties = LineChartProperties()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `LineChart`.
///
///
public var body: some View {
Line(chartData: chartData,
style: style,
chartProperties: chartProperties)
Line(chartData: data, style: style)
}
public init() {}
@@ -1,16 +1,9 @@
import SwiftUI
struct LineShape: Shape {
var data: [(Double, Double)]
var lineStyle: LineStyle = .curved
var data: [Double]
func path(in rect: CGRect) -> Path {
var path = Path()
switch lineStyle {
case .curved:
path = Path.quadCurvedPathWithPoints(data: data, in: rect)
case .straight:
path = Path.linePathWithPoints(data: data, in: rect)
}
let path = Path.quadCurvedPathWithPoints(points: data, step: CGPoint(x: 1.0, y: 1.0))
return path
}
}
@@ -18,15 +11,20 @@ struct LineShape: Shape {
struct LineShape_Previews: PreviewProvider {
static var previews: some View {
Group {
LineShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
LineShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)], lineStyle: .straight)
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
GeometryReader { geometry in
LineShape(data: [0, 0.5, 0.8, 0.6, 1])
.transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height))
.stroke(Color.red)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
GeometryReader { geometry in
LineShape(data: [0, -0.5, 0.8, -0.6, 1])
.transform(CGAffineTransform(scaleX: geometry.size.width / 4.0, y: geometry.size.height / 1.6))
.stroke(Color.blue)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
}
}
@@ -2,8 +2,6 @@ import SwiftUI
struct LineShapeView: View, Animatable {
var chartData: ChartData
var chartProperties: LineChartProperties
var geometry: GeometryProxy
var style: ChartStyle
var trimTo: Double = 0
@@ -13,73 +11,16 @@ struct LineShapeView: View, Animatable {
set { trimTo = Double(newValue) }
}
var chartMarkColor: LinearGradient {
if let customColor = chartProperties.customChartMarksColors {
return customColor.linearGradient(from: .leading, to: .trailing)
}
return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing)
}
var body: some View {
ZStack {
LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle)
.trim(from: 0, to: CGFloat(trimTo))
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: chartProperties.lineWidth, lineJoin: .round))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.clipped()
if chartProperties.showChartMarks {
MarkerShape(data: chartData.normalisedData)
.trim(from: 0, to: CGFloat(trimTo))
.fill(.white,
strokeBorder: chartMarkColor,
lineWidth: chartProperties.lineWidth)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
LineShape(data: chartData.normalisedPoints)
.trim(from: 0, to: CGFloat(trimTo))
.transform(CGAffineTransform(scaleX: geometry.size.width / CGFloat(chartData.normalisedPoints.count - 1),
y: geometry.size.height / CGFloat(chartData.normalisedRange)))
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: 3, lineJoin: .round))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
struct LineShapeView_Previews: PreviewProvider {
static let chartData = ChartData([6, 8, 6], rangeY: 6...10)
static let chartDataOutOfRange = ChartData([-1, 8, 6, 12, 3], rangeY: -5...15)
static let chartDataOutOfRange2 = ChartData([6,6,8,5], rangeY: 5...10)
static let chartStyle = ChartStyle(backgroundColor: Color.white,
foregroundColor: [ColorGradient(Color.orange, Color.red)])
static var previews: some View {
Group {
GeometryReader { geometry in
LineShapeView(chartData: chartData,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange2,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
}
}
}
@@ -1,25 +0,0 @@
import SwiftUI
struct MarkerShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.drawChartMarkers(data: data, in: rect)
return path
}
}
struct MarkerShape_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkerShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
MarkerShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
}
@@ -1,13 +0,0 @@
import SwiftUI
public class LineChartProperties: ObservableObject {
@Published var lineWidth: CGFloat = 2.0
@Published var backgroundGradient: ColorGradient?
@Published var showChartMarks: Bool = true
@Published var customChartMarksColors: ColorGradient?
@Published var lineStyle: LineStyle = .curved
public init() {
// no-op
}
}
@@ -1,6 +0,0 @@
import Foundation
public enum LineStyle {
case curved
case straight
}
@@ -1,16 +1,17 @@
import SwiftUI
/// A type of chart that displays a slice of "pie" for each data point
public struct PieChart: ChartBase {
public struct PieChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `PieChart`.
///
///
public var body: some View {
PieChartRow(chartData: chartData, style: style)
PieChartRow(chartData: data, style: style)
}
public init() {}
@@ -1,14 +1,22 @@
//
// RingsChart.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChart: ChartBase {
public struct RingsChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
// TODO - should put background opacity, ring width & spacing as chart style values
public var body: some View {
RingsChartRow(width:10.0, spacing:5.0, chartData: chartData, style: style)
RingsChartRow(width:10.0, spacing:5.0, chartData: data, style: style)
}
public init() {}