├── .github └── FUNDING.yml ├── .gitignore ├── Resources ├── areaChart.png ├── lineChart.png ├── columnChart.png └── customChart.png ├── Sources └── Charts │ └── Chart │ ├── Styles │ ├── Style │ │ ├── LineType.swift │ │ ├── ChartStyleConfiguration.swift │ │ ├── ChartStyleKey.swift │ │ ├── View+ChartStyle.swift │ │ ├── Environment+ChartStyle.swift │ │ ├── AnyChartStyle.swift │ │ └── ChartStyle.swift │ ├── Area │ │ ├── AreaChartStyle.swift │ │ ├── StackedAreaChartStyle.swift │ │ └── AreaChart.swift │ ├── Line │ │ └── LineChartStyle.swift │ ├── Bar │ │ └── BarChartStyle.swift │ └── Column │ │ ├── StackedColumnChartStyle.swift │ │ └── ColumnChartStyle.swift │ ├── AxisLabels.swift │ └── Chart.swift ├── Package.resolved ├── Package.swift ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [spacenation] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm/ 7 | -------------------------------------------------------------------------------- /Resources/areaChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-charts/HEAD/Resources/areaChart.png -------------------------------------------------------------------------------- /Resources/lineChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-charts/HEAD/Resources/lineChart.png -------------------------------------------------------------------------------- /Resources/columnChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-charts/HEAD/Resources/columnChart.png -------------------------------------------------------------------------------- /Resources/customChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-charts/HEAD/Resources/customChart.png -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/LineType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum LineType { 4 | case line 5 | case quadCurve 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/ChartStyleConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct ChartStyleConfiguration { 4 | public let dataMatrix: [[CGFloat]] 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/ChartStyleKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ChartStyleKey: EnvironmentKey { 4 | static let defaultValue: AnyChartStyle = AnyChartStyle(LineChartStyle()) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/View+ChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | /// Sets the style for `Chart` within the environment of `self`. 5 | public func chartStyle(_ style: S) -> some View where S : ChartStyle { 6 | self.environment(\.chartStyle, AnyChartStyle(style)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/Environment+ChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension EnvironmentValues { 4 | var chartStyle: AnyChartStyle { 5 | get { 6 | return self[ChartStyleKey.self] 7 | } 8 | set { 9 | self[ChartStyleKey.self] = newValue 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Shapes", 6 | "repositoryURL": "https://github.com/spacenation/swiftui-shapes.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c58b15c37eae9bd20525c6daa93a06a689ca75cb", 10 | "version": "1.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Area/AreaChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Shapes 3 | 4 | public struct AreaChartStyle: ChartStyle { 5 | private let lineType: LineType 6 | private let fill: Fill 7 | 8 | public func makeBody(configuration: Self.Configuration) -> some View { 9 | fill 10 | .clipShape( 11 | AreaChart(unitData: configuration.dataMatrix.map { $0.reduce(0, +) }, lineType: self.lineType) 12 | ) 13 | } 14 | 15 | public init(_ lineType: LineType = .quadCurve, fill: Fill) { 16 | self.lineType = lineType 17 | self.fill = fill 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/AnyChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AnyChartStyle: ChartStyle { 4 | private let styleMakeBody: (ChartStyle.Configuration) -> AnyView 5 | 6 | init(_ style: S) { 7 | self.styleMakeBody = style.makeTypeErasedBody 8 | } 9 | 10 | func makeBody(configuration: ChartStyle.Configuration) -> AnyView { 11 | self.styleMakeBody(configuration) 12 | } 13 | } 14 | 15 | fileprivate extension ChartStyle { 16 | func makeTypeErasedBody(configuration: ChartStyle.Configuration) -> AnyView { 17 | AnyView(makeBody(configuration: configuration)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Charts", 7 | platforms: [ 8 | .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6) 9 | ], 10 | products: [ 11 | .library(name: "Charts", targets: ["Charts"]) 12 | ], 13 | dependencies: [ 14 | .package(name: "Shapes", url: "https://github.com/spacenation/swiftui-shapes.git", .upToNextMajor(from: "1.1.0")) 15 | ], 16 | targets: [ 17 | .target(name: "Charts",dependencies: ["Shapes"]) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Style/ChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Defines the implementation of all `Chart` instances within a view 4 | /// hierarchy. 5 | /// 6 | /// To configure the current `Chart` for a view hiearchy, use the 7 | /// `.chartStyle()` modifier. 8 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 9 | public protocol ChartStyle { 10 | /// A `View` representing the body of a `Chart`. 11 | associatedtype Body : View 12 | 13 | /// Creates a `View` representing the body of a `Chart`. 14 | /// 15 | /// - Parameter configuration: The properties of the value slider instance being 16 | /// created. 17 | /// 18 | /// This method will be called for each instance of `Chart` created within 19 | /// a view hierarchy where this style is the current `ChartStyle`. 20 | func makeBody(configuration: Self.Configuration) -> Self.Body 21 | 22 | /// The properties of a `Chart` instance being created. 23 | typealias Configuration = ChartStyleConfiguration 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SpaceNation Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Line/LineChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Shapes 3 | 4 | public struct LineChartStyle: ChartStyle { 5 | 6 | private let lineType: LineType 7 | private let lineColor: Color 8 | private let lineWidth: CGFloat 9 | 10 | @Binding private var trimFrom: CGFloat 11 | @Binding private var trimTo: CGFloat 12 | 13 | @ViewBuilder 14 | public func makeBody(configuration: Configuration) -> some View { 15 | switch lineType { 16 | case .line: 17 | Line(unitData: configuration.dataMatrix.map { $0.reduce(0, +) }) 18 | .trim(from: trimFrom, to: trimTo) 19 | .stroke(lineColor, style: .init(lineWidth: lineWidth, lineCap: .round)) 20 | case .quadCurve: 21 | QuadCurve(unitData: configuration.dataMatrix.map { $0.reduce(0, +) }) 22 | .trim(from: trimFrom, to: trimTo) 23 | .stroke(lineColor, style: .init(lineWidth: lineWidth, lineCap: .round)) 24 | } 25 | } 26 | 27 | public init(_ lineType: LineType = .quadCurve, lineColor: Color = .accentColor, lineWidth: CGFloat = 1, trimFrom: Binding = .constant(0), trimTo: Binding = .constant(1)) { 28 | self.lineType = lineType 29 | self.lineColor = lineColor 30 | self.lineWidth = lineWidth 31 | _trimFrom = trimFrom 32 | _trimTo = trimTo 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Bar/BarChartStyle.swift: -------------------------------------------------------------------------------- 1 | 2 | import Shapes 3 | import SwiftUI 4 | 5 | public struct BarChartStyle: ChartStyle { 6 | 7 | private let bar: Bar 8 | private let spacing: CGFloat 9 | 10 | public init(bar: Bar, spacing: CGFloat = 0) { 11 | self.bar = bar 12 | self.spacing = spacing 13 | } 14 | 15 | 16 | public func makeBody(configuration: Configuration) -> some View { 17 | let data: [ColumnData] = configuration.dataMatrix 18 | .map { $0.reduce(0, +) } 19 | .enumerated() 20 | .map { ColumnData(id: $0.offset, data: $0.element) } 21 | 22 | return GeometryReader { geometry in 23 | self.barChart(in: geometry, data: data) 24 | } 25 | } 26 | 27 | func barChart(in geometry: GeometryProxy, data: [ColumnData]) -> some View { 28 | let barHeight = (geometry.size.height - (CGFloat(data.count - 1) * spacing)) / CGFloat(data.count) 29 | 30 | return ZStack(alignment: .topLeading) { 31 | ForEach(data) { element in 32 | self.bar 33 | .alignmentGuide(.top, computeValue: { dimension in 34 | CGFloat(element.id) * (spacing + barHeight) 35 | }) 36 | .frame(width: element.data * geometry.size.width, height: barHeight) 37 | } 38 | } 39 | .frame(width: geometry.size.width, height: geometry.size.height, alignment: .bottom) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/Styles/Area/StackedAreaChartStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Shapes 3 | 4 | public struct StackedAreaChartStyle: ChartStyle { 5 | private let lineType: LineType 6 | private let colors: [Color] 7 | 8 | public func makeBody(configuration: Self.Configuration) -> some View { 9 | ZStack { 10 | ForEach(Array(configuration.dataMatrix.transpose().stacked().enumerated()), id: \.self.offset) { enumeratedData in 11 | colors[enumeratedData.offset % colors.count].clipShape( 12 | AreaChart( 13 | unitData: enumeratedData.element, 14 | lineType: self.lineType 15 | ) 16 | ) 17 | .zIndex(-Double(enumeratedData.offset)) 18 | } 19 | } 20 | .drawingGroup() 21 | } 22 | 23 | public init(_ lineType: LineType = .quadCurve, colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]) { 24 | self.lineType = lineType 25 | self.colors = colors 26 | } 27 | } 28 | 29 | extension Collection where Element == [CGFloat] { 30 | func stacked() -> [[CGFloat]] { 31 | self.reduce([]) { (result, element) -> [[CGFloat]] in 32 | if let lastElement = result.last { 33 | return result + [zip(lastElement, element).compactMap(+)] 34 | } else { 35 | return [element] 36 | } 37 | } 38 | } 39 | } 40 | 41 | extension Array where Element == [CGFloat] { 42 | func transpose() -> [[CGFloat]] { 43 | let columnsCount = self.first?.count ?? 0 44 | let rowCount = self.count 45 | 46 | return (0.. Path { 10 | Path { path in 11 | switch self.lineType { 12 | case .line: 13 | path.addLines(self.unitPoints.points(in: rect)) 14 | case .quadCurve: 15 | path.addQuadCurves(self.unitPoints.points(in: rect)) 16 | } 17 | 18 | if let bottomUnitPoints = bottomUnitPoints { 19 | switch self.lineType { 20 | case .line: 21 | bottomUnitPoints.reversed().forEach { 22 | path.addLine(to: CGPoint(unitPoint: $0, in: rect)) 23 | } 24 | case .quadCurve: 25 | path.addQuadCurves(bottomUnitPoints.reversed().points(in: rect)) 26 | } 27 | } else { 28 | path.addLine(to: CGPoint(unitPoint: .topTrailing, in: rect)) 29 | path.addLine(to: CGPoint(unitPoint: .topLeading, in: rect)) 30 | } 31 | path.closeSubpath() 32 | } 33 | } 34 | 35 | init(unitData: Data, bottomUnitData: Data? = nil, lineType: LineType) where Data.Element : BinaryFloatingPoint { 36 | self.lineType = lineType 37 | let step: CGFloat = unitData.count > 1 ? 1.0 / CGFloat(unitData.count - 1) : 1.0 38 | self.unitPoints = unitData.enumerated().map { (index, dataPoint) in UnitPoint(x: step * CGFloat(index), y: CGFloat(dataPoint)) } 39 | self.bottomUnitPoints = bottomUnitData?.enumerated().map { (index, dataPoint) in UnitPoint(x: step * CGFloat(index), y: CGFloat(dataPoint)) } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Charts/Chart/AxisLabels.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AxisLabels: View { 4 | let axis: Axis 5 | let labels: [AnyView] 6 | 7 | public var body: some View { 8 | GeometryReader { geometry in 9 | if self.axis == .horizontal { 10 | HStack(spacing: 0) { 11 | ForEach(0..(_ axis: Axis = .horizontal, data: Data, @ViewBuilder label: @escaping (Data.Element) -> Label) where Content == ForEach, Data : RandomAccessCollection, Label : View, Data.Element : Identifiable { 26 | self.axis = axis 27 | self.labels = data.map({ AnyView(label($0)) }) 28 | } 29 | 30 | public init(_ axis: Axis = .horizontal, data: Data, id: KeyPath, @ViewBuilder label: @escaping (Data.Element) -> Label) where Content == ForEach, Data : RandomAccessCollection, ID : Hashable, Label : View { 31 | self.axis = axis 32 | self.labels = data.map({ AnyView(label($0)) }) 33 | } 34 | 35 | public init