├── .github ├── FUNDING.yml └── workflows │ ├── docs.yml │ └── ci.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── SourcePackages │ ├── workspace-state.json │ ├── manifest.db │ └── ManifestLoading │ │ └── swiftuicharts.dia │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ ├── xcbaselines │ └── SwiftUIChartsTests.xcbaseline │ │ ├── 4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist │ │ └── Info.plist │ └── xcschemes │ └── SwiftUICharts.xcscheme ├── Resources └── images │ ├── BarCharts │ ├── BarChart.png │ ├── RangeBarChart.png │ ├── GroupedBarChart.png │ └── StackedBarChart.png │ ├── PieCharts │ ├── PieChart.png │ └── DoughnutChart.png │ └── LineCharts │ ├── LineChart.png │ ├── FilledLineChart.png │ ├── MultiLineChart.png │ └── RangedLineChart.png ├── Tests ├── LinuxMain.swift └── SwiftUIChartsTests │ └── LineCharts │ └── LineChartPathTests.swift ├── Sources └── SwiftUICharts │ ├── Shared │ ├── Models │ │ ├── SubscriptionSet.swift │ │ ├── Protocols │ │ │ └── PublishableProtocol.swift │ │ ├── LegendData.swift │ │ ├── ChartMetadata.swift │ │ ├── ChartImageController.swift │ │ ├── InfoViewData.swift │ │ └── ColourStyle.swift │ ├── Extras │ │ ├── Extension+NSNotificationName.swift │ │ └── SharedEnums.swift │ ├── ViewModifiers │ │ ├── disableAnimation.swift │ │ ├── Legends.swift │ │ ├── HeaderBox.swift │ │ └── TouchOverlay.swift │ ├── Views │ │ ├── CustomNoDataView.swift │ │ └── LegendView.swift │ ├── Types │ │ ├── GradientStop.swift │ │ └── Stroke.swift │ └── Shapes │ │ └── AccessibilityRectangle.swift │ ├── SharedLineAndBar │ ├── Shapes │ │ ├── VerticalGridShape.swift │ │ ├── HorizontalGridShape.swift │ │ ├── DiamondShape.swift │ │ ├── LinearTrendLineShape.swift │ │ ├── LabelShape.swift │ │ └── Marker.swift │ ├── Models │ │ ├── ExtraLineDataPoint.swift │ │ ├── Protocols │ │ │ └── DataFunctionsProtocol.swift │ │ ├── GridStyle.swift │ │ └── ChartViewData.swift │ ├── Views │ │ ├── VerticalGridView.swift │ │ ├── HorizontalGridView.swift │ │ ├── PositionedPOILabel.swift │ │ └── TouchOverlayBox.swift │ ├── ViewModifiers │ │ ├── XAxisGrid.swift │ │ ├── YAxisGrid.swift │ │ ├── ExtraYAxisLabels.swift │ │ ├── XAxisLabels.swift │ │ ├── YAxisLabels.swift │ │ ├── AxisBorders.swift │ │ ├── FloatingInfoBox.swift │ │ └── LinearTrendLine.swift │ ├── Style │ │ └── ExtraLineStyle.swift │ └── Extras │ │ └── LineAndBarEnums.swift │ ├── LineChart │ ├── Shapes │ │ ├── LegendLine.swift │ │ ├── PointShape.swift │ │ └── LineShape.swift │ ├── Models │ │ ├── DataSet │ │ │ ├── MultiLineDataSet.swift │ │ │ ├── LineDataSet.swift │ │ │ └── RangedLineDataSet.swift │ │ ├── Style │ │ │ ├── LineStyle.swift │ │ │ ├── RangedLineStyle.swift │ │ │ └── PointStyle.swift │ │ ├── DataPoints │ │ │ ├── LineChartDataPoint.swift │ │ │ └── RangedLineChartDataPoint.swift │ │ └── Protocols │ │ │ └── LineChartProtocols.swift │ ├── ViewModifiers │ │ └── PointMarkers.swift │ ├── Views │ │ └── SubViews │ │ │ └── PosistionIndicator.swift │ └── Extras │ │ └── LineChartEnums.swift │ ├── BarChart │ ├── Models │ │ ├── GroupingData.swift │ │ ├── DataSet │ │ │ ├── BarDataSet.swift │ │ │ ├── RangedBarDataSet.swift │ │ │ ├── GroupedBarDataSets.swift │ │ │ └── StackedBarDataSet.swift │ │ ├── Style │ │ │ └── BarStyle.swift │ │ ├── Datapoints │ │ │ ├── StackedBarDataPoint.swift │ │ │ ├── GroupedBarDataPoint.swift │ │ │ ├── BarChartDataPoint.swift │ │ │ └── RangedBarDataPoint.swift │ │ ├── CornerRadius.swift │ │ └── Protocols │ │ │ └── BarChartProtocols.swift │ ├── Extras │ │ ├── BarLayout.swift │ │ └── BarChartEnums.swift │ ├── Views │ │ ├── HorizontalBarChart.swift │ │ ├── RangedBarChart.swift │ │ ├── BarChart.swift │ │ └── StackedBarChart.swift │ └── Shapes │ │ └── RoundedRectangleBarShape.swift │ └── PieChart │ ├── Shapes │ ├── PieSegmentShape.swift │ └── DoughnutSegmentShape.swift │ ├── Models │ ├── DataSets │ │ └── PieDataSet.swift │ ├── DataPoints │ │ └── PieChartDataPoint.swift │ ├── Protocols │ │ └── PieChartProtocols.swift │ ├── Style │ │ ├── PieChartStyle.swift │ │ └── DoughnutChartStyle.swift │ └── ChartData │ │ ├── PieChartData.swift │ │ └── DoughnutChartData.swift │ ├── Extras │ └── PieChartEnums.swift │ └── Views │ ├── PieChart.swift │ └── DoughnutChart.swift ├── SwiftUICharts.podspec ├── LICENSE ├── Package.swift └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: willdale 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/SourcePackages/workspace-state.json: -------------------------------------------------------------------------------- 1 | {"object": {"artifacts": [], "dependencies": []}, "version": 4} -------------------------------------------------------------------------------- /Resources/images/BarCharts/BarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/BarCharts/BarChart.png -------------------------------------------------------------------------------- /Resources/images/PieCharts/PieChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/PieCharts/PieChart.png -------------------------------------------------------------------------------- /.swiftpm/xcode/SourcePackages/manifest.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/.swiftpm/xcode/SourcePackages/manifest.db -------------------------------------------------------------------------------- /Resources/images/LineCharts/LineChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/LineCharts/LineChart.png -------------------------------------------------------------------------------- /Resources/images/BarCharts/RangeBarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/BarCharts/RangeBarChart.png -------------------------------------------------------------------------------- /Resources/images/PieCharts/DoughnutChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/PieCharts/DoughnutChart.png -------------------------------------------------------------------------------- /Resources/images/BarCharts/GroupedBarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/BarCharts/GroupedBarChart.png -------------------------------------------------------------------------------- /Resources/images/BarCharts/StackedBarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/BarCharts/StackedBarChart.png -------------------------------------------------------------------------------- /Resources/images/LineCharts/FilledLineChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/LineCharts/FilledLineChart.png -------------------------------------------------------------------------------- /Resources/images/LineCharts/MultiLineChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/LineCharts/MultiLineChart.png -------------------------------------------------------------------------------- /Resources/images/LineCharts/RangedLineChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/Resources/images/LineCharts/RangedLineChart.png -------------------------------------------------------------------------------- /.swiftpm/xcode/SourcePackages/ManifestLoading/swiftuicharts.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willdale/SwiftUICharts/HEAD/.swiftpm/xcode/SourcePackages/ManifestLoading/swiftuicharts.dia -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftUIChartsTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftUIChartsTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/SubscriptionSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubscriptionSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/06/2021. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | internal class SubscriptionSet { 12 | internal var subscription = Set() 13 | } 14 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Extras/Extension+NSNotificationName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension+NSNotificationName.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/05/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSNotification.Name { 11 | public static var updateLayoutDidFinish = NSNotification.Name(rawValue: "com.swiftuicharts.updateLayoutDidFinish") 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/ViewModifiers/disableAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisableAnimation.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/05/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | public func disableAnimation(chartData: ChartData, _ value: Bool = true) -> some View where ChartData: CTChartData { 12 | chartData.disableAnimation = value 13 | return self 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalGridShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Basic Vertical line shape. 12 | 13 | Used in Grid 14 | */ 15 | internal struct VerticalGridShape: Shape { 16 | internal func path(in rect: CGRect) -> Path { 17 | var path = Path() 18 | path.move(to: CGPoint(x: 0, y: rect.height)) 19 | path.addLine(to: CGPoint(x: 0, y: 0)) 20 | return path 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalGridShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Basic horizontal line shape. 12 | 13 | Used in Grid 14 | */ 15 | internal struct HorizontalGridShape: Shape { 16 | internal func path(in rect: CGRect) -> Path { 17 | var path = Path() 18 | path.move(to: CGPoint(x: 0, y: 0)) 19 | path.addLine(to: CGPoint(x: rect.width, y: 0)) 20 | return path 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomNoDataView.swift 3 | // 4 | // 5 | // Created by Will Dale on 17/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View to display text if there is not enough data to draw the chart. 12 | */ 13 | public struct CustomNoDataView: View where T: CTChartData { 14 | 15 | private let chartData: T 16 | 17 | init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | public var body: some View { 22 | chartData.noDataText 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegendLine.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 05/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Draw line in legend view 11 | internal struct LegendLine: Shape { 12 | 13 | private let width: CGFloat 14 | 15 | internal init(width: CGFloat) { 16 | self.width = width 17 | } 18 | 19 | internal func path(in rect: CGRect) -> Path { 20 | var path = Path() 21 | path.move(to: CGPoint(x: 0, y: 0)) 22 | path.addLine(to: CGPoint(x: width, y: 0)) 23 | return path 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | release: 5 | types: published 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: GetJazzy 16 | run: gem install jazzy 17 | 18 | - name: RunJazzy 19 | run: | 20 | jazzy --min-acl public --theme fullwidth 21 | touch docs/.nojekyll 22 | 23 | - name: Push 24 | run: | 25 | git add . 26 | git config user.email "nil" 27 | git config user.name "bob" 28 | git commit -m "make docs" 29 | git push -f origin HEAD:gh-pages 30 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiLineDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set containing multiple data sets for multiple lines 12 | 13 | Contains information about each of lines within the chart. 14 | */ 15 | public struct MultiLineDataSet: CTMultiLineChartDataSet, DataFunctionsProtocol { 16 | 17 | public let id: UUID = UUID() 18 | public var dataSets: [LineDataSet] 19 | 20 | /// Initialises a new data set for multi-line Line Charts. 21 | public init(dataSets: [LineDataSet]) { 22 | self.dataSets = dataSets 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/Protocols/PublishableProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublishableProtocol.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/06/2021. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | /** 12 | Protocol to enable publishing data streams over the Combine framework 13 | */ 14 | public protocol Publishable { 15 | 16 | associatedtype DataPoint: CTDataPointBaseProtocol 17 | 18 | 19 | var subscription: Set { get set } 20 | 21 | /** 22 | Streams the data points from touch overlay. 23 | 24 | Uses Combine 25 | */ 26 | var touchedDataPointPublisher: PassthroughSubject { get } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiamondShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 07/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Shape used in POI Markers when displaying value in the center. 12 | */ 13 | public struct DiamondShape: Shape { 14 | public func path(in rect: CGRect) -> Path { 15 | var path = Path() 16 | path.move(to: CGPoint(x: rect.midX, y: rect.maxY)) 17 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) 18 | path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) 19 | path.addLine(to: CGPoint(x: rect.minX, y: rect.midY)) 20 | path.closeSubpath() 21 | return path 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | LineChartPathTests 8 | 9 | testGetIndicatorLocationPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.000206 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Models/ExtraLineDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtraLineDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data point for Extra line View Modifier. 12 | */ 13 | public struct ExtraLineDataPoint: Hashable, Identifiable { 14 | 15 | public let id: UUID = UUID() 16 | public var value: Double 17 | public var pointColour: PointColour? 18 | public var pointDescription: String? 19 | 20 | public init( 21 | value: Double, 22 | pointColour: PointColour? = nil, 23 | pointDescription: String? = nil 24 | ) { 25 | self.value = value 26 | self.pointColour = pointColour 27 | self.pointDescription = pointDescription 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/GroupingData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupingData.swift 3 | // 4 | // 5 | // Created by Will Dale on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for grouping data points together so they can be drawn in the correct groupings. 12 | */ 13 | public struct GroupingData: CTBarColourProtocol, Hashable, Identifiable { 14 | 15 | public let id: UUID = UUID() 16 | public var title: String 17 | public var colour: ColourStyle 18 | 19 | /// Group with single colour 20 | /// - Parameters: 21 | /// - title: Title for legends 22 | /// - colour: Colour styling for the bars. 23 | public init( 24 | title: String, 25 | colour: ColourStyle 26 | ) { 27 | self.title = title 28 | self.colour = colour 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieSegmentShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 24/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Draws a pie segment. 12 | */ 13 | internal struct PieSegmentShape: Shape, Identifiable { 14 | 15 | var id: UUID 16 | var startAngle: Double 17 | var amount: Double 18 | 19 | internal func path(in rect: CGRect) -> Path { 20 | let radius = min(rect.width, rect.height) / 2 21 | let center = CGPoint(x: rect.width / 2, y: rect.height / 2) 22 | var path = Path() 23 | path.move(to: center) 24 | path.addRelativeArc(center: center, 25 | radius: radius, 26 | startAngle: Angle(radians: startAngle), 27 | delta: Angle(radians: amount)) 28 | return path 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftUICharts.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftUICharts' 3 | s.version = '2.10.2' 4 | s.summary = 'A charts / plotting library for SwiftUI.' 5 | s.description = 'A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS and has accessibility features built in.' 6 | s.homepage = 'https://github.com/willdale/SwiftUICharts' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'willdale' => 'www.linkedin.com/in/willdale-dev' } 9 | s.source = { :git => 'https://github.com/willdale/SwiftUICharts.git', :tag => s.version.to_s } 10 | s.ios.deployment_target = '14.0' 11 | s.tvos.deployment_target = '14.0' 12 | s.watchos.deployment_target = '7.0' 13 | s.macos.deployment_target = '11.0' 14 | s.swift_version = '5.0' 15 | s.source_files = 'Sources/SwiftUICharts/**/*' 16 | end 17 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 01/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set for a pie chart. 12 | */ 13 | public struct PieDataSet: CTSingleDataSetProtocol { 14 | 15 | public var id: UUID = UUID() 16 | public var dataPoints: [PieChartDataPoint] 17 | public var legendTitle: String 18 | 19 | /// Initialises a new data set for a standard pie chart. 20 | /// - Parameters: 21 | /// - dataPoints: Array of elements. 22 | /// - legendTitle: Label for the data in legend. 23 | public init( 24 | dataPoints: [PieChartDataPoint], 25 | legendTitle: String 26 | ) { 27 | self.dataPoints = dataPoints 28 | self.legendTitle = legendTitle 29 | } 30 | 31 | public typealias ID = UUID 32 | public typealias DataPoint = PieChartDataPoint 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Will Dale on 23/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set for a bar chart. 12 | */ 13 | public struct BarDataSet: CTStandardBarChartDataSet, DataFunctionsProtocol { 14 | 15 | public let id: UUID = UUID() 16 | public var dataPoints: [BarChartDataPoint] 17 | public var legendTitle: String 18 | 19 | /// Initialises a new data set for standard Bar Charts. 20 | /// - Parameters: 21 | /// - dataPoints: Array of elements. 22 | /// - legendTitle: label for the data in legend. 23 | public init( 24 | dataPoints: [BarChartDataPoint], 25 | legendTitle: String = "" 26 | ) { 27 | self.dataPoints = dataPoints 28 | self.legendTitle = legendTitle 29 | } 30 | 31 | public typealias ID = UUID 32 | public typealias DataPoint = BarChartDataPoint 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedBarDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set for ranged bar charts. 12 | */ 13 | public struct RangedBarDataSet: CTRangedBarChartDataSet, DataFunctionsProtocol { 14 | 15 | public var id: UUID = UUID() 16 | public var dataPoints: [RangedBarDataPoint] 17 | public var legendTitle: String 18 | 19 | /// Initialises a new data set for ranged bar chart. 20 | /// - Parameters: 21 | /// - dataPoints: Array of elements. 22 | /// - legendTitle: Label for the data in legend. 23 | public init( 24 | dataPoints: [RangedBarDataPoint], 25 | legendTitle: String = "" 26 | ) { 27 | self.dataPoints = dataPoints 28 | self.legendTitle = legendTitle 29 | } 30 | 31 | public typealias ID = UUID 32 | public typealias DataPoint = RangedBarDataPoint 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Types/GradientStop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientStop.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 09/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A mediator for `Gradient.Stop` to allow it to be stored in `LegendData`. 12 | 13 | Gradient.Stop doesn't conform to Hashable. 14 | */ 15 | public struct GradientStop: Hashable { 16 | 17 | public var color: Color 18 | public var location: CGFloat 19 | 20 | public init( 21 | color: Color, 22 | location: CGFloat 23 | ) { 24 | self.color = color 25 | self.location = location 26 | } 27 | } 28 | 29 | extension GradientStop { 30 | /// Convert an array of GradientStop into and array of Gradient.Stop 31 | /// - Parameter stops: Array of GradientStop 32 | /// - Returns: Array of Gradient.Stop 33 | static func convertToGradientStopsArray(stops: [GradientStop]) -> [Gradient.Stop] { 34 | stops.map { Gradient.Stop(color: $0.color, location: $0.location) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Shapes/DoughnutSegmentShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoughnutSegmentShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Draws a doughnut segment. 12 | */ 13 | internal struct DoughnutSegmentShape: InsettableShape, Identifiable { 14 | 15 | var id: UUID 16 | var startAngle: Double 17 | var amount: Double 18 | var insetAmount: CGFloat = 0 19 | 20 | func inset(by amount: CGFloat) -> some InsettableShape { 21 | var arc = self 22 | arc.insetAmount += amount 23 | return arc 24 | } 25 | 26 | internal func path(in rect: CGRect) -> Path { 27 | let radius = min(rect.width, rect.height) / 2 28 | let center = CGPoint(x: rect.width / 2, y: rect.height / 2) 29 | var path = Path() 30 | path.addRelativeArc(center: center, 31 | radius: radius - insetAmount, 32 | startAngle: Angle(radians: startAngle), 33 | delta: Angle(radians: amount)) 34 | return path 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Will Dale 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/SwiftUICharts/Shared/Shapes/AccessibilityRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityRectangle.swift 3 | // 4 | // 5 | // Created by Will Dale on 31/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Add rectangle overlay when in Voice Reader mode. 12 | */ 13 | internal struct AccessibilityRectangle: Shape { 14 | 15 | private let dataPointCount: Int 16 | private let dataPointNo: Int 17 | 18 | internal init( 19 | dataPointCount: Int, 20 | dataPointNo: Int 21 | ) { 22 | self.dataPointCount = dataPointCount 23 | self.dataPointNo = dataPointNo 24 | } 25 | 26 | internal func path(in rect: CGRect) -> Path { 27 | var path = Path() 28 | let x = rect.width / CGFloat(dataPointCount-1) 29 | let pointX: CGFloat = (CGFloat(dataPointNo) * x) - x / CGFloat(2) 30 | let point: CGRect = CGRect(x: pointX, 31 | y: 0, 32 | width: x, 33 | height: rect.height) 34 | path.addRoundedRect(in: point, cornerSize: CGSize(width: 10, height: 10)) 35 | return path 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineStyle.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 31/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for controlling the styling for individual lines. 12 | */ 13 | public struct LineStyle: CTLineStyle, Hashable { 14 | 15 | public var lineColour: ColourStyle 16 | public var lineType: LineType 17 | public var strokeStyle: Stroke 18 | public var ignoreZero: Bool 19 | 20 | /// Style of the line. 21 | /// 22 | /// - Parameters: 23 | /// - lineColour: Colour styling of the line. 24 | /// - lineType: Drawing style of the line 25 | /// - strokeStyle: Stroke Style 26 | /// - ignoreZero: Whether the chart should skip data points who's value is 0. 27 | public init( 28 | lineColour: ColourStyle = ColourStyle(colour: .red), 29 | lineType: LineType = .curvedLine, 30 | strokeStyle: Stroke = Stroke(), 31 | ignoreZero: Bool = false 32 | ) { 33 | self.lineColour = lineColour 34 | self.lineType = lineType 35 | self.strokeStyle = strokeStyle 36 | self.ignoreZero = ignoreZero 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 12/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for controlling the aesthetic of the bars. 12 | */ 13 | public struct BarStyle: CTBarStyle { 14 | 15 | public var barWidth: CGFloat 16 | public var cornerRadius: CornerRadius 17 | public var colourFrom: ColourFrom 18 | public var colour: ColourStyle 19 | 20 | // MARK: - Single colour 21 | /// Bar Chart with single colour 22 | /// - Parameters: 23 | /// - barWidth: How much of the available width to use. 0...1 24 | /// - cornerRadius: Corner radius of the bar shape. 25 | /// - colourFrom: Where to get the colour data from. 26 | /// - colour: Set up colour styling. 27 | public init( 28 | barWidth: CGFloat = 1, 29 | cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0), 30 | colourFrom: ColourFrom = .barStyle, 31 | colour: ColourStyle = ColourStyle(colour: .red) 32 | ) { 33 | self.barWidth = barWidth 34 | self.cornerRadius = cornerRadius 35 | self.colourFrom = colourFrom 36 | self.colour = colour 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/DataSet/GroupedBarDataSets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupedBarDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Main data set for a grouped bar charts. 12 | */ 13 | public struct GroupedBarDataSets: CTMultiDataSetProtocol, DataFunctionsProtocol { 14 | 15 | public let id: UUID = UUID() 16 | public var dataSets: [GroupedBarDataSet] 17 | 18 | /// Initialises a new data set for Grouped Bar Chart. 19 | public init(dataSets: [GroupedBarDataSet]) { 20 | self.dataSets = dataSets 21 | } 22 | } 23 | 24 | /** 25 | Individual data sets for grouped bars charts. 26 | */ 27 | public struct GroupedBarDataSet: CTMultiBarChartDataSet { 28 | 29 | public let id: UUID = UUID() 30 | public var dataPoints: [GroupedBarDataPoint] 31 | public var setTitle: String 32 | 33 | /// Initialises a new data set for a Bar Chart. 34 | public init( 35 | dataPoints: [GroupedBarDataPoint], 36 | setTitle: String = "" 37 | ) { 38 | self.dataPoints = dataPoints 39 | self.setTitle = setTitle 40 | } 41 | 42 | public typealias ID = UUID 43 | public typealias DataPoint = GroupedBarDataPoint 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Extras/BarLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarLayout.swift 3 | // 4 | // 5 | // Created by Will Dale on 20/05/2022. 6 | // 7 | 8 | import CoreGraphics.CGGeometry 9 | 10 | internal struct BarLayout { 11 | internal static func barWidth(_ totalWidth: CGFloat, _ widthFactor: CGFloat) -> CGFloat { 12 | return totalWidth * widthFactor 13 | } 14 | 15 | internal static func barHeight(_ totalHeight: CGFloat, _ value: Double, _ maxValue: Double) -> CGFloat { 16 | return totalHeight * divideByZeroProtection(CGFloat.self, value, maxValue) 17 | } 18 | 19 | internal static func barOffset(_ section: CGSize, _ widthFactor: CGFloat, _ value: Double, _ maxValue: Double) -> CGSize { 20 | return CGSize(width: barXOffset(section.width, widthFactor), 21 | height: barYOffset(section.height, value, maxValue)) 22 | } 23 | 24 | internal static func barXOffset(_ total: CGFloat, _ factor: CGFloat) -> CGFloat { 25 | return (-(total * factor) / 2) + (total / 2) 26 | } 27 | 28 | internal static func barYOffset(_ total: CGFloat, _ value: Double, _ maxValue: Double) -> CGFloat { 29 | return total - (total * divideByZeroProtection(CGFloat.self, value, maxValue)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/DataSet/StackedBarDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackedBarDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 18/04/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Main data set for a stacked bar chart. 12 | */ 13 | public struct StackedBarDataSets: CTMultiDataSetProtocol, DataFunctionsProtocol { 14 | 15 | public let id: UUID = UUID() 16 | public var dataSets: [StackedBarDataSet] 17 | 18 | /// Initialises a new data set for a Stacked Bar Chart. 19 | public init(dataSets: [StackedBarDataSet]) { 20 | self.dataSets = dataSets 21 | } 22 | } 23 | 24 | /** 25 | Individual data sets for stacked bars charts. 26 | */ 27 | public struct StackedBarDataSet: CTMultiBarChartDataSet { 28 | 29 | public let id: UUID = UUID() 30 | public var dataPoints: [StackedBarDataPoint] 31 | public var setTitle: String 32 | 33 | /// Initialises a new data set for a Stacked Bar Chart. 34 | public init( 35 | dataPoints: [StackedBarDataPoint], 36 | setTitle: String = "" 37 | ) { 38 | self.dataPoints = dataPoints 39 | self.setTitle = setTitle 40 | } 41 | 42 | public typealias ID = UUID 43 | public typealias DataPoint = StackedBarDataPoint 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/LinearTrendLineShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearTrendLineShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 26/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A line across the chart to show the trend in the data. 12 | */ 13 | internal struct LinearTrendLineShape: Shape { 14 | 15 | private let firstValue: Double 16 | private let lastValue: Double 17 | private let range: Double 18 | private let minValue: Double 19 | 20 | internal init( 21 | firstValue: Double, 22 | lastValue: Double, 23 | range: Double, 24 | minValue: Double 25 | ) { 26 | self.firstValue = firstValue 27 | self.lastValue = lastValue 28 | self.range = range 29 | self.minValue = minValue 30 | } 31 | 32 | internal func path(in rect: CGRect) -> Path { 33 | let y: CGFloat = rect.height / CGFloat(range) 34 | var path = Path() 35 | path.move(to: CGPoint(x: 0, 36 | y: (CGFloat(firstValue - minValue) * -y) + rect.height)) 37 | path.addLine(to: CGPoint(x: rect.width, 38 | y: (CGFloat(lastValue - minValue) * -y) + rect.height)) 39 | return path 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUICharts", 8 | defaultLocalization: "en", 9 | platforms: [ 10 | .macOS(.v11), .iOS(.v14), .watchOS(.v7), .tvOS(.v14) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "SwiftUICharts", 16 | targets: ["SwiftUICharts"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "SwiftUICharts", 27 | dependencies: []), 28 | .testTarget( 29 | name: "SwiftUIChartsTests", 30 | dependencies: ["SwiftUICharts"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 4224B4FE-4B1F-4DC7-9D83-A7B7F9824962 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Quad-Core Intel Core i7 17 | cpuSpeedInMHz 18 | 2800 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,5 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone13,3 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Extras/PieChartEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChartEnums.swift 3 | // 4 | // 5 | // Created by Will Dale on 21/04/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Option to add overlays on top of the segment. 12 | 13 | ``` 14 | case none // No overlay 15 | case barStyle // Text overlay 16 | case dataPoints // System icon overlay 17 | ``` 18 | */ 19 | public enum OverlayType: Hashable { 20 | /// No overlay 21 | case none 22 | 23 | /** 24 | Text overlay 25 | 26 | # Parameters: 27 | - text: Text the use as label. 28 | - colour: Foreground colour. 29 | - font: System font. 30 | - rFactor: Distance the from center of chart. 31 | 0 is center, 1 is perimeter. It can go beyond 1 to 32 | place it outside. 33 | */ 34 | case label(text: String, colour: Color = .primary, font: Font = .caption, rFactor: CGFloat = 0.75) 35 | 36 | /** 37 | System icon overlay 38 | 39 | # Parameters: 40 | - systemName: SF Symbols name. 41 | - colour: Foreground colour. 42 | - size: Image frame size. 43 | - rFactor: Distance the from center of chart. 44 | 0 is center, 1 is perimeter. It can go beyond 1 to 45 | place it outside. 46 | */ 47 | case icon(systemName: String, colour: Color = .primary, size: CGFloat = 30, rFactor: CGFloat = 0.75) 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/DataFunctionsProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataFunctionsProtocol.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol DataFunctionsProtocol { 11 | /** 12 | Returns the highest value in the data set. 13 | - Returns: Highest value in data set. 14 | */ 15 | func maxValue() -> Double 16 | 17 | /** 18 | Returns the lowest value in the data set. 19 | - Returns: Lowest value in data set. 20 | */ 21 | func minValue() -> Double 22 | 23 | /** 24 | Returns the average value from the data set. 25 | - Returns: Average of values in data set. 26 | */ 27 | func average() -> Double 28 | } 29 | 30 | public protocol GetDataProtocol { 31 | /** 32 | Returns the difference between the highest and lowest numbers in the data set or data sets. 33 | */ 34 | var range: Double { get } 35 | 36 | /** 37 | Returns the lowest value in the data set or data sets. 38 | */ 39 | var minValue: Double { get } 40 | 41 | /** 42 | Returns the highest value in the data set or data sets 43 | */ 44 | var maxValue: Double { get } 45 | 46 | /** 47 | Returns the average value from the data set or data sets. 48 | */ 49 | var average: Double { get } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Datapoints/StackedBarDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackedBarDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 19/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single stacked chart data point. 12 | */ 13 | public struct StackedBarDataPoint: CTMultiBarDataPoint { 14 | 15 | public let id: UUID = UUID() 16 | public var value: Double 17 | public var xAxisLabel: String? = nil 18 | public var description: String? 19 | public var date: Date? 20 | public var group: GroupingData 21 | public var legendTag: String = "" 22 | 23 | /// Data model for a single data point with colour info for use with a stacked bar chart. 24 | /// - Parameters: 25 | /// - value: Value of the data point. 26 | /// - description: A longer label that can be shown on touch input. 27 | /// - date: Date of the data point if any data based calculations are required. 28 | /// - group: Group data informs the data models how the data point are linked. 29 | public init( 30 | value: Double, 31 | description: String? = nil, 32 | date: Date? = nil, 33 | group: GroupingData 34 | ) { 35 | self.value = value 36 | self.description = description 37 | self.date = date 38 | self.group = group 39 | } 40 | 41 | public typealias ID = UUID 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedLineStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | /** 10 | Model for controlling the aesthetic of the ranged line chart. 11 | */ 12 | public struct RangedLineStyle: CTRangedLineStyle, Hashable { 13 | 14 | public var lineColour: ColourStyle 15 | public var fillColour: ColourStyle 16 | public var lineType: LineType 17 | public var strokeStyle: Stroke 18 | public var ignoreZero: Bool 19 | 20 | // MARK: Initializer 21 | /// Initialize the styling for ranged line chart. 22 | /// 23 | /// - Parameters: 24 | /// - colour: Single Colour 25 | /// - lineType: Drawing style of the line 26 | /// - strokeStyle: Stroke Style 27 | /// - ignoreZero: Whether the chart should skip data points who's value is 0. 28 | public init(lineColour: ColourStyle = ColourStyle(), 29 | fillColour: ColourStyle = ColourStyle(), 30 | lineType: LineType = .curvedLine, 31 | strokeStyle: Stroke = Stroke(), 32 | ignoreZero: Bool = false 33 | ) { 34 | self.lineColour = lineColour 35 | self.fillColour = fillColour 36 | self.lineType = lineType 37 | self.strokeStyle = strokeStyle 38 | self.ignoreZero = ignoreZero 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 23/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set for a single line 12 | 13 | Contains information specific to each line within the chart . 14 | */ 15 | public struct LineDataSet: CTLineChartDataSet, DataFunctionsProtocol { 16 | 17 | public let id: UUID = UUID() 18 | public var dataPoints: [LineChartDataPoint] 19 | public var legendTitle: String 20 | public var pointStyle: PointStyle 21 | public var style: LineStyle 22 | 23 | /// Initialises a data set for a line in a Line Chart. 24 | /// - Parameters: 25 | /// - dataPoints: Array of elements. 26 | /// - legendTitle: Label for the data in legend. 27 | /// - pointStyle: Styling information for the data point markers. 28 | /// - style: Styling for how the line will be draw in. 29 | public init( 30 | dataPoints: [LineChartDataPoint], 31 | legendTitle: String = "", 32 | pointStyle: PointStyle = PointStyle(), 33 | style: LineStyle = LineStyle() 34 | ) { 35 | self.dataPoints = dataPoints 36 | self.legendTitle = legendTitle 37 | self.pointStyle = pointStyle 38 | self.style = style 39 | } 40 | 41 | public typealias ID = UUID 42 | public typealias Styling = LineStyle 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupedBarDataPoint.swift 3 | // SwiftUICharts 4 | // 5 | // Created by Ataias Pereira Reis on 18/04/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single grouped bar chart data point. 12 | */ 13 | public struct GroupedBarDataPoint: CTMultiBarDataPoint { 14 | 15 | public let id: UUID = UUID() 16 | public var value: Double 17 | public var xAxisLabel: String? = nil 18 | public var description: String? 19 | public var date: Date? 20 | public var group: GroupingData 21 | public var legendTag: String = "" 22 | 23 | /// Data model for a single data point with colour info for use with a grouped bar chart. 24 | /// - Parameters: 25 | /// - value: Value of the data point. 26 | /// - description: A longer label that can be shown on touch input. 27 | /// - date: Date of the data point if any data based calculations are required. 28 | /// - group: Group data informs the data models how the data point are linked. 29 | public init( 30 | value: Double, 31 | description: String? = nil, 32 | date: Date? = nil, 33 | group: GroupingData 34 | ) { 35 | self.value = value 36 | self.description = description 37 | self.date = date 38 | self.group = group 39 | } 40 | 41 | public typealias ID = UUID 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 13/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Control for the look of the Grid 12 | */ 13 | public struct GridStyle { 14 | 15 | /** 16 | Number of lines to break up the axis 17 | */ 18 | public var numberOfLines: Int 19 | 20 | /** 21 | Line Colour 22 | */ 23 | public var lineColour: Color 24 | 25 | /** 26 | Line Width 27 | */ 28 | public var lineWidth: CGFloat 29 | 30 | /** 31 | Dash 32 | */ 33 | public var dash: [CGFloat] 34 | 35 | /** 36 | Dash Phase 37 | */ 38 | public var dashPhase: CGFloat 39 | 40 | /// Model for controlling the look of the Grid 41 | /// - Parameters: 42 | /// - numberOfLines: Number of lines to break up the axis 43 | /// - lineColour: Line Colour 44 | /// - lineWidth: Line Width 45 | /// - dash: Dash 46 | /// - dashPhase: Dash Phase 47 | public init( 48 | numberOfLines: Int = 10, 49 | lineColour: Color = Color(.gray).opacity(0.25), 50 | lineWidth: CGFloat = 1, 51 | dash: [CGFloat] = [10], 52 | dashPhase: CGFloat = 0 53 | ) { 54 | self.numberOfLines = numberOfLines 55 | self.lineColour = lineColour 56 | self.lineWidth = lineWidth 57 | self.dash = dash 58 | self.dashPhase = dashPhase 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRadius.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Corner radius of the bar shape. 12 | */ 13 | public struct CornerRadius: Hashable { 14 | 15 | public let topLeft: CGFloat 16 | public let topRight: CGFloat 17 | public let bottomLeft: CGFloat 18 | public let bottomRight: CGFloat 19 | 20 | /// Set the coner radius for the bar shapes for top and bottom 21 | public init( 22 | top: CGFloat = 15.0, 23 | bottom: CGFloat = 0.0 24 | ) { 25 | self.topLeft = top 26 | self.topRight = top 27 | self.bottomLeft = bottom 28 | self.bottomRight = bottom 29 | } 30 | 31 | /// Set the coner radius for the bar shapes for left and right 32 | public init( 33 | left: CGFloat = 0.0, 34 | right: CGFloat = 0.0 35 | ) { 36 | self.topLeft = left 37 | self.topRight = right 38 | self.bottomLeft = left 39 | self.bottomRight = right 40 | } 41 | 42 | /// Set the coner radius for the bar shapes for all corners 43 | public init( 44 | topLeft: CGFloat = 0, 45 | topRight: CGFloat = 0, 46 | bottomLeft: CGFloat = 0, 47 | bottomRight: CGFloat = 0 48 | ) { 49 | self.topLeft = topLeft 50 | self.topRight = topRight 51 | self.bottomLeft = bottomLeft 52 | self.bottomRight = bottomRight 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/LegendData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegendData.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 02/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data model to hold data for Legends 12 | */ 13 | public struct LegendData: Hashable, Identifiable { 14 | 15 | public var id: UUID 16 | 17 | /// The type of chart being used. 18 | public var chartType: ChartType 19 | 20 | /// Text to be displayed 21 | public var legend: String 22 | 23 | /// Style of the stroke 24 | public var strokeStyle: Stroke? 25 | 26 | /// Used to make sure the charts data legend is first 27 | public let prioity: Int 28 | 29 | public var colour: ColourStyle 30 | 31 | /// Legend. 32 | /// - Parameters: 33 | /// - legend: Text to be displayed. 34 | /// - colour: Colour styling. 35 | /// - strokeStyle: Stroke Style. 36 | /// - prioity: Used to make sure the charts data legend is first. 37 | /// - chartType: Type of chart being used. 38 | public init(id: UUID, 39 | legend: String, 40 | colour: ColourStyle, 41 | strokeStyle: Stroke?, 42 | prioity: Int, 43 | chartType: ChartType 44 | ) { 45 | self.id = id 46 | self.legend = legend 47 | self.colour = colour 48 | self.strokeStyle = strokeStyle 49 | self.prioity = prioity 50 | self.chartType = chartType 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineChartDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 24/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single data point. 12 | */ 13 | public struct LineChartDataPoint: CTStandardLineDataPoint, IgnoreMe { 14 | 15 | public let id: UUID = UUID() 16 | public var value: Double 17 | public var xAxisLabel: String? 18 | public var description: String? 19 | public var date: Date? 20 | public var pointColour: PointColour? 21 | 22 | public var ignoreMe: Bool = false 23 | 24 | public var legendTag: String = "" 25 | 26 | /// Data model for a single data point with colour for use with a line chart. 27 | /// - Parameters: 28 | /// - value: Value of the data point 29 | /// - xAxisLabel: Label that can be shown on the X axis. 30 | /// - description: A longer label that can be shown on touch input. 31 | /// - date: Date of the data point if any data based calculations are required. 32 | /// - pointColour: Colour of the point markers. 33 | public init( 34 | value: Double, 35 | xAxisLabel: String? = nil, 36 | description: String? = nil, 37 | date: Date? = nil, 38 | pointColour: PointColour? = nil 39 | ) { 40 | self.value = value 41 | self.xAxisLabel = xAxisLabel 42 | self.description = description 43 | self.date = date 44 | self.pointColour = pointColour 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarChartDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 24/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single bar chart data point. 12 | 13 | Colour can be solid or gradient. 14 | */ 15 | public struct BarChartDataPoint: CTStandardBarDataPoint { 16 | 17 | public let id = UUID() 18 | public var value: Double 19 | public var xAxisLabel: String? 20 | public var description: String? 21 | public var date: Date? 22 | public var colour: ColourStyle 23 | public var legendTag: String = "" 24 | 25 | // MARK: - Single colour 26 | /// Data model for a single data point with colour for use with a bar chart. 27 | /// - Parameters: 28 | /// - value: Value of the data point. 29 | /// - xAxisLabel: Label that can be shown on the X axis. 30 | /// - description: A longer label that can be shown on touch input. 31 | /// - date: Date of the data point if any data based calculations are required. 32 | /// - colour: Colour styling for the fill. 33 | public init( 34 | value: Double, 35 | xAxisLabel: String? = nil, 36 | description: String? = nil, 37 | date: Date? = nil, 38 | colour: ColourStyle = ColourStyle(colour: .red) 39 | ) { 40 | self.value = value 41 | self.xAxisLabel = xAxisLabel 42 | self.description = description 43 | self.date = date 44 | self.colour = colour 45 | } 46 | 47 | public typealias ID = UUID 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedLineDataSet.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data set for a ranged line. 12 | 13 | Contains information specific to the line and range fill. 14 | */ 15 | public struct RangedLineDataSet: CTRangedLineChartDataSet, DataFunctionsProtocol { 16 | 17 | public let id: UUID = UUID() 18 | public var dataPoints: [RangedLineChartDataPoint] 19 | public var legendTitle: String 20 | public var legendFillTitle: String 21 | public var pointStyle: PointStyle 22 | public var style: RangedLineStyle 23 | 24 | /// Initialises a data set for a line in a ranged line chart. 25 | /// - Parameters: 26 | /// - dataPoints: Array of elements. 27 | /// - legendTitle: Label for the data in legend. 28 | /// - legendFillTitle: Label for the range data in legend. 29 | /// - pointStyle: Styling information for the data point markers. 30 | /// - style: Styling for how the line will be draw in. 31 | public init( 32 | dataPoints: [RangedLineChartDataPoint], 33 | legendTitle: String = "", 34 | legendFillTitle: String = "", 35 | pointStyle: PointStyle = PointStyle(), 36 | style: RangedLineStyle = RangedLineStyle() 37 | ) { 38 | self.dataPoints = dataPoints 39 | self.legendTitle = legendTitle 40 | self.legendFillTitle = legendFillTitle 41 | self.pointStyle = pointStyle 42 | self.style = style 43 | } 44 | 45 | public typealias ID = UUID 46 | public typealias Styling = RangedLineStyle 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartMetadata.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 03/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data model for the chart's metadata 12 | 13 | Contains the Title, Subtitle and colour information for them. 14 | */ 15 | public struct ChartMetadata { 16 | /// The charts title 17 | public var title: String 18 | /// Font of the title 19 | public var titleFont: Font 20 | /// Color of the title 21 | public var titleColour: Color 22 | 23 | /// The charts subtitle 24 | public var subtitle: String 25 | /// Font of the subtitle 26 | public var subtitleFont: Font 27 | /// Color of the subtitle 28 | public var subtitleColour: Color 29 | 30 | /// Model to hold the metadata for the chart. 31 | /// - Parameters: 32 | /// - title: The charts title. 33 | /// - subtitle: The charts subtitle. 34 | /// - titleFont: Font of the title. 35 | /// - titleColour: Color of the title. 36 | /// - subtitleFont: Font of the subtitle. 37 | /// - subtitleColour: Color of the subtitle. 38 | public init( 39 | title: String = "", 40 | subtitle: String = "", 41 | titleFont: Font = .title3, 42 | titleColour: Color = Color.primary, 43 | subtitleFont: Font = .subheadline, 44 | subtitleColour: Color = Color.primary 45 | ) { 46 | self.title = title 47 | self.subtitle = subtitle 48 | self.titleFont = titleFont 49 | self.titleColour = titleColour 50 | self.subtitleFont = subtitleFont 51 | self.subtitleColour = subtitleColour 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalGridView.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Sub view of the X axis grid view modifier. 12 | */ 13 | internal struct VerticalGridView: View where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | @State private var startAnimation: Bool = false 22 | 23 | var body: some View { 24 | VerticalGridShape() 25 | .trim(to: animationValue) 26 | .stroke(chartData.chartStyle.xAxisGridStyle.lineColour, 27 | style: StrokeStyle(lineWidth: chartData.chartStyle.xAxisGridStyle.lineWidth, 28 | dash: chartData.chartStyle.xAxisGridStyle.dash, 29 | dashPhase: chartData.chartStyle.xAxisGridStyle.dashPhase)) 30 | .frame(width: chartData.chartStyle.xAxisGridStyle.lineWidth) 31 | .animateOnAppear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 32 | self.startAnimation = true 33 | } 34 | .animateOnDisappear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 35 | self.startAnimation = false 36 | } 37 | } 38 | 39 | var animationValue: CGFloat { 40 | if chartData.disableAnimation { 41 | return 1 42 | } else { 43 | return startAnimation ? 1 : 0 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalGridView.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Sub view of the Y axis grid view modifier. 12 | */ 13 | internal struct HorizontalGridView: View where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | @State private var startAnimation: Bool = false 22 | 23 | var body: some View { 24 | HorizontalGridShape() 25 | .trim(to: animationValue) 26 | .stroke(chartData.chartStyle.yAxisGridStyle.lineColour, 27 | style: StrokeStyle(lineWidth: chartData.chartStyle.yAxisGridStyle.lineWidth, 28 | dash: chartData.chartStyle.yAxisGridStyle.dash, 29 | dashPhase: chartData.chartStyle.yAxisGridStyle.dashPhase)) 30 | .frame(height: chartData.chartStyle.yAxisGridStyle.lineWidth) 31 | .animateOnAppear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 32 | self.startAnimation = true 33 | } 34 | .animateOnDisappear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 35 | self.startAnimation = false 36 | } 37 | } 38 | 39 | var animationValue: CGFloat { 40 | if chartData.disableAnimation { 41 | return 1 42 | } else { 43 | return startAnimation ? 1 : 0 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineChartPoints.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 24/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | ViewModifier for for laying out point markers. 12 | */ 13 | internal struct PointMarkers: ViewModifier where T: CTLineChartDataProtocol & GetDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | internal func body(content: Content) -> some View { 21 | ZStack { 22 | if chartData.isGreaterThanTwo() { 23 | content 24 | chartData.getPointMarker() 25 | } else { content } 26 | } 27 | } 28 | } 29 | 30 | extension View { 31 | /** 32 | Lays out markers over each of the data point. 33 | 34 | The style of the markers is set in the PointStyle data model as parameter in the Chart Data. 35 | 36 | - Requires: 37 | Chart Data to conform to CTLineChartDataProtocol. 38 | - LineChartData 39 | - MultiLineChartData 40 | 41 | # Available for: 42 | - Line Chart 43 | - Multi Line Chart 44 | - Filled Line Chart 45 | - Ranged Line Chart 46 | 47 | # Unavailable for: 48 | - Bar Chart 49 | - Grouped Bar Chart 50 | - Stacked Bar Chart 51 | - Ranged Bar Chart 52 | - Pie Chart 53 | - Doughnut Chart 54 | 55 | - Parameter chartData: Chart data model. 56 | - Returns: A new view containing the chart with point markers. 57 | 58 | */ 59 | public func pointMarkers(chartData: T) -> some View { 60 | self.modifier(PointMarkers(chartData: chartData)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedBarDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single ranged bar chart data point. 12 | */ 13 | public struct RangedBarDataPoint: CTRangedBarDataPoint { 14 | 15 | public let id: UUID = UUID() 16 | public var upperValue: Double 17 | public var lowerValue: Double 18 | public var xAxisLabel: String? 19 | public var description: String? 20 | public var date: Date? 21 | public var colour: ColourStyle 22 | public var legendTag: String = "" 23 | 24 | internal var _value: Double = 0 25 | internal var _valueOnly: Bool = false 26 | 27 | /// Data model for a single data point with colour for use with a ranged bar chart. 28 | /// - Parameters: 29 | /// - lowerValue: Value of the lower range of the data point. 30 | /// - upperValue: Value of the upper range of the data point. 31 | /// - xAxisLabel: Label that can be shown on the X axis. 32 | /// - description: A longer label that can be shown on touch input. 33 | /// - date: Date of the data point if any data based calculations are required. 34 | /// - colour: Colour styling for the fill. 35 | public init( 36 | lowerValue: Double, 37 | upperValue: Double, 38 | xAxisLabel: String? = nil, 39 | description: String? = nil, 40 | date: Date? = nil, 41 | colour: ColourStyle = ColourStyle(colour: .red) 42 | ) { 43 | self.upperValue = upperValue 44 | self.lowerValue = lowerValue 45 | self.xAxisLabel = xAxisLabel 46 | self.description = description 47 | self.date = date 48 | self.colour = colour 49 | } 50 | 51 | public typealias ID = UUID 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Views/HorizontalBarChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalBarChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 26/04/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct HorizontalBarChart: View where ChartData: HorizontalBarChartData { 11 | 12 | @ObservedObject private var chartData: ChartData 13 | @State private var timer: Timer? 14 | 15 | /// Initialises a bar chart view. 16 | /// - Parameter chartData: Must be BarChartData model. 17 | public init(chartData: ChartData) { 18 | self.chartData = chartData 19 | } 20 | 21 | public var body: some View { 22 | GeometryReader { geo in 23 | if chartData.isGreaterThanTwo() { 24 | VStack(spacing: 0) { 25 | switch chartData.barStyle.colourFrom { 26 | case .barStyle: 27 | HorizontalBarChartBarStyleSubView(chartData: chartData) 28 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 29 | case .dataPoints: 30 | HorizontalBarChartDataPointSubView(chartData: chartData) 31 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 32 | } 33 | } 34 | // Needed for axes label frames 35 | .onChange(of: geo.frame(in: .local)) { value in 36 | self.chartData.viewData.chartSize = value 37 | } 38 | .layoutNotifier(timer) 39 | } else { CustomNoDataView(chartData: chartData) } 40 | } 41 | .if(chartData.minValue.isLess(than: 0)) { 42 | $0.scaleEffect(x: CGFloat(chartData.maxValue/(chartData.maxValue - chartData.minValue)), anchor: .trailing) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Views/SubViews/PosistionIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PosistionIndicator.swift 3 | // 4 | // 5 | // Created by Will Dale on 19/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A dot that follows the line on touch events. 12 | */ 13 | internal struct PosistionIndicator: View { 14 | 15 | private let fillColour: Color 16 | private let lineColour: Color 17 | private let lineWidth: CGFloat 18 | 19 | internal init( 20 | fillColour: Color = Color.primary, 21 | lineColour: Color = Color.blue, 22 | lineWidth: CGFloat = 3 23 | ) { 24 | self.fillColour = fillColour 25 | self.lineColour = lineColour 26 | self.lineWidth = lineWidth 27 | } 28 | 29 | internal var body: some View { 30 | ZStack { 31 | Circle() 32 | .foregroundColor(lineColour) 33 | Circle() 34 | .foregroundColor(fillColour) 35 | .padding(EdgeInsets(top: lineWidth, leading: lineWidth, bottom: lineWidth, trailing: lineWidth)) 36 | } 37 | } 38 | } 39 | 40 | /** 41 | Styling of the dot that follows the line on touch events. 42 | */ 43 | public struct DotStyle { 44 | 45 | let size: CGFloat 46 | let fillColour: Color 47 | let lineColour: Color 48 | let lineWidth: CGFloat 49 | 50 | /// Sets the style of the Posistion Indicator 51 | /// - Parameters: 52 | /// - size: Size of the Indicator. 53 | /// - fillColour: Fill colour. 54 | /// - lineColour: Border colour. 55 | /// - lineWidth: Border width. 56 | public init( 57 | size: CGFloat = 15, 58 | fillColour: Color = Color.primary, 59 | lineColour: Color = Color.blue, 60 | lineWidth: CGFloat = 3 61 | ) { 62 | self.size = size 63 | self.fillColour = fillColour 64 | self.lineColour = lineColour 65 | self.lineWidth = lineWidth 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedLineChartDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single ranged data point. 12 | */ 13 | public struct RangedLineChartDataPoint: CTRangedLineDataPoint, IgnoreMe { 14 | 15 | public let id: UUID = UUID() 16 | public var value: Double 17 | public var upperValue: Double 18 | public var lowerValue: Double 19 | public var xAxisLabel: String? 20 | public var description: String? 21 | public var date: Date? 22 | public var pointColour: PointColour? 23 | 24 | public var ignoreMe: Bool = false 25 | 26 | public var legendTag: String = "" 27 | 28 | internal var _valueOnly: Bool = false 29 | 30 | /// Data model for a single data point with colour for use with a ranged line chart. 31 | /// - Parameters: 32 | /// - value: Value of the data point. 33 | /// - upperValue: Value of the upper range of the data point. 34 | /// - lowerValue: Value of the lower range of the data point. 35 | /// - xAxisLabel: Label that can be shown on the X axis. 36 | /// - description: A longer label that can be shown on touch input. 37 | /// - date: Date of the data point if any data based calculations are required. 38 | /// - pointColour: Colour of the point markers. 39 | public init( 40 | value: Double, 41 | upperValue: Double, 42 | lowerValue: Double, 43 | xAxisLabel: String? = nil, 44 | description: String? = nil, 45 | date: Date? = nil, 46 | pointColour: PointColour? = nil 47 | ) { 48 | self.value = value 49 | self.upperValue = upperValue 50 | self.lowerValue = lowerValue 51 | self.xAxisLabel = xAxisLabel 52 | self.description = description 53 | self.date = date 54 | self.pointColour = pointColour 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChartDataPoint.swift 3 | // 4 | // 5 | // Created by Will Dale on 01/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data for a single segement of a pie chart. 12 | */ 13 | public struct PieChartDataPoint: CTPieDataPoint { 14 | 15 | public var id: UUID = UUID() 16 | public var value: Double 17 | public var description: String? 18 | public var date: Date? 19 | public var colour: Color 20 | public var label: OverlayType 21 | public var startAngle: Double = 0 22 | public var amount: Double = 0 23 | public var legendTag: String = "" 24 | 25 | /// Data model for a single data point for a pie chart. 26 | /// - Parameters: 27 | /// - value: Value of the data point 28 | /// - description: A longer label that can be shown on touch input. 29 | /// - date: Date of the data point if any data based calculations are required. 30 | /// - colour: Colour of the segment. 31 | /// - label: Option to add overlays on top of the segment. 32 | public init( 33 | value: Double, 34 | description: String? = nil, 35 | date: Date? = nil, 36 | colour: Color = Color.red, 37 | label: OverlayType = .none 38 | ) { 39 | self.value = value 40 | self.description = description 41 | self.date = date 42 | self.colour = colour 43 | self.label = label 44 | } 45 | } 46 | 47 | extension PieChartDataPoint { 48 | // Remove legend tag from compare 49 | public static func == (left: PieChartDataPoint, right: PieChartDataPoint) -> Bool { 50 | return (left.id == right.id) && 51 | (left.amount == right.amount) && 52 | (left.startAngle == right.startAngle) && 53 | (left.value == right.value) && 54 | (left.date == right.date) && 55 | (left.description == right.description) && 56 | (left.label == right.label) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Types/Stroke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stroke.swift 3 | // 4 | // 5 | // Created by Will Dale on 14/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A hashable version of StrokeStyle 12 | 13 | StrokeStyle doesn't conform to Hashable. 14 | */ 15 | public struct Stroke: Hashable, Identifiable { 16 | 17 | public let id: UUID = UUID() 18 | 19 | private let lineWidth: CGFloat 20 | private let lineCap: CGLineCap 21 | private let lineJoin: CGLineJoin 22 | private let miterLimit: CGFloat 23 | private let dash: [CGFloat] 24 | private let dashPhase: CGFloat 25 | 26 | public init( 27 | lineWidth: CGFloat = 3, 28 | lineCap: CGLineCap = .round, 29 | lineJoin: CGLineJoin = .round, 30 | miterLimit: CGFloat = 10, 31 | dash: [CGFloat] = [CGFloat](), 32 | dashPhase: CGFloat = 0 33 | ) { 34 | self.lineWidth = lineWidth 35 | self.lineCap = lineCap 36 | self.lineJoin = lineJoin 37 | self.miterLimit = miterLimit 38 | self.dash = dash 39 | self.dashPhase = dashPhase 40 | } 41 | 42 | static let `default` = Stroke() 43 | } 44 | 45 | extension Stroke { 46 | /// Convert `Stroke` to `StrokeStyle` 47 | internal func strokeToStrokeStyle() -> StrokeStyle { 48 | StrokeStyle(lineWidth: self.lineWidth, 49 | lineCap: self.lineCap, 50 | lineJoin: self.lineJoin, 51 | miterLimit: self.miterLimit, 52 | dash: self.dash, 53 | dashPhase: self.dashPhase) 54 | } 55 | } 56 | 57 | extension StrokeStyle { 58 | /// Convert `StrokeStyle` to `Stroke` 59 | internal func toStroke() -> Stroke { 60 | Stroke(lineWidth: self.lineWidth, 61 | lineCap: self.lineCap, 62 | lineJoin: self.lineJoin, 63 | miterLimit: self.miterLimit, 64 | dash: self.dash, 65 | dashPhase: self.dashPhase) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/ChartImageController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartImageController.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/05/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | #if os(iOS) 11 | import Combine 12 | import SwiftUI 13 | import UIKit 14 | 15 | public final class ChartImageController { 16 | public var controller: UIViewController? 17 | 18 | public init() {} 19 | } 20 | 21 | public final class ChartImageHostingController: UIHostingController { 22 | 23 | public var finalImage = PassthroughSubject() 24 | private var cancellable: AnyCancellable? 25 | 26 | public override init(rootView: Content) { 27 | super.init(rootView: rootView) 28 | } 29 | 30 | required dynamic init?(coder aDecoder: NSCoder) { 31 | return nil 32 | } 33 | 34 | override public func viewDidLoad() { 35 | super.viewDidLoad() 36 | cancellable = NotificationCenter.default 37 | .publisher(for: .updateLayoutDidFinish) 38 | .sink { [weak self] _ in self?.drawView() } 39 | } 40 | 41 | public func start() { 42 | let targetSize = view.intrinsicContentSize 43 | view?.bounds = CGRect(origin: .zero, size: targetSize) 44 | 45 | let renderer = UIGraphicsImageRenderer(size: targetSize) 46 | _ = renderer.image { context in 47 | view?.drawHierarchy(in: view.bounds, afterScreenUpdates: true) 48 | } 49 | } 50 | 51 | private func drawView() { 52 | let targetSize = view.intrinsicContentSize 53 | view?.bounds = CGRect(origin: .zero, size: targetSize) 54 | 55 | let renderer = UIGraphicsImageRenderer(size: targetSize) 56 | let image = renderer.image { context in 57 | view?.drawHierarchy(in: view.bounds, afterScreenUpdates: true) 58 | } 59 | finalImage.send(image) 60 | cancellable?.cancel() 61 | finalImage.send(completion: .finished) 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/InfoViewData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoViewData.swift 3 | // 4 | // 5 | // Created by Will Dale on 04/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Data model to pass view information internally for the `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 12 | */ 13 | public struct InfoViewData { 14 | 15 | /** 16 | Is there currently input (touch or click) on the chart. 17 | 18 | Set from TouchOverlay via the relevant protocol. 19 | 20 | Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 21 | */ 22 | var isTouchCurrent: Bool = false 23 | 24 | /** 25 | Closest data points to input. 26 | 27 | Set from TouchOverlay via the relevant protocol. 28 | 29 | Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 30 | */ 31 | var touchOverlayInfo: [DP] = [] 32 | 33 | /** 34 | Set specifier of data point readout. 35 | 36 | Set from TouchOverlay via the relevant protocol. 37 | 38 | Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 39 | */ 40 | var touchSpecifier: String = "%.0f" 41 | 42 | /// Optional number formatter for the touch overlay. 43 | var touchFormatter: NumberFormatter? = nil 44 | 45 | /** 46 | X axis posistion of the overlay box. 47 | 48 | Used to set the location of the data point readout View. 49 | 50 | Set from TouchOverlay via the relevant protocol. 51 | 52 | Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 53 | */ 54 | var touchLocation: CGPoint = .zero 55 | 56 | 57 | /** 58 | Size of the chart. 59 | 60 | Used to set the location of the data point readout View. 61 | 62 | Set from TouchOverlay via the relevant protocol. 63 | 64 | Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. 65 | */ 66 | var chartSize: CGRect = .zero 67 | 68 | /** 69 | Option to display units before or after values. 70 | */ 71 | var touchUnit: TouchUnit = .none 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointStyle.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 04/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for controlling the aesthetic of the point markers. 12 | 13 | Point markers are placed on top of the line, marking where the data points are. 14 | */ 15 | public struct PointStyle: Hashable { 16 | 17 | /// Overall size of the mark 18 | public var pointSize: CGFloat 19 | 20 | /// Outter ring colour 21 | public var borderColour: Color 22 | 23 | /// Center fill colour 24 | public var fillColour: Color 25 | 26 | /// Outter ring line width 27 | public var lineWidth: CGFloat 28 | 29 | /// Style of the point marks 30 | public var pointType: PointType 31 | 32 | /// Shape of the points 33 | public var pointShape: PointShape 34 | 35 | /// Styling for the point markers. 36 | /// - Parameters: 37 | /// - pointSize: Overall size of the mark 38 | /// - borderColour: Outter ring colour 39 | /// - fillColour: Center fill colour 40 | /// - lineWidth: Outter ring line width 41 | /// - pointType: Style of the point marks 42 | /// - pointShape: Shape of the points 43 | public init( 44 | pointSize: CGFloat = 9, 45 | borderColour: Color = .primary, 46 | fillColour: Color = Color(.gray), 47 | lineWidth: CGFloat = 3, 48 | pointType: PointType = .outline, 49 | pointShape: PointShape = .circle 50 | ) { 51 | self.pointSize = pointSize 52 | self.borderColour = borderColour 53 | self.fillColour = fillColour 54 | self.lineWidth = lineWidth 55 | self.pointType = pointType 56 | self.pointShape = pointShape 57 | } 58 | } 59 | 60 | public struct PointColour: Hashable { 61 | public let border: Color 62 | public let fill: Color 63 | 64 | public init( 65 | border: Color = .primary, 66 | fill: Color = .primary 67 | ) { 68 | self.border = border 69 | self.fill = fill 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XAxisGrid.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 26/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Adds vertical lines along the X axis. 12 | */ 13 | internal struct XAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | internal func body(content: Content) -> some View { 22 | ZStack { 23 | if chartData.isGreaterThanTwo() { 24 | HStack { 25 | ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines-1), id: \.self) { index in 26 | if index != 0 { 27 | VerticalGridView(chartData: chartData) 28 | Spacer() 29 | .frame(minWidth: 0, maxWidth: 500) 30 | } 31 | } 32 | VerticalGridView(chartData: chartData) 33 | } 34 | content 35 | } else { content } 36 | } 37 | } 38 | } 39 | 40 | extension View { 41 | /** 42 | Adds vertical lines along the X axis. 43 | 44 | The style is set in ChartData --> ChartStyle --> xAxisGridStyle 45 | 46 | - Requires: 47 | Chart Data to conform to CTLineBarChartDataProtocol. 48 | 49 | # Available for: 50 | - Line Chart 51 | - Multi Line Chart 52 | - Filled Line Chart 53 | - Ranged Line Chart 54 | - Bar Chart 55 | - Grouped Bar Chart 56 | - Stacked Bar Chart 57 | - Ranged Bar Chart 58 | 59 | # Unavailable for: 60 | - Pie Chart 61 | - Doughnut Chart 62 | 63 | - Parameter chartData: Chart data model. 64 | - Returns: A new view containing the chart with vertical lines under it. 65 | */ 66 | public func xAxisGrid(chartData: T) -> some View { 67 | self.modifier(XAxisGrid(chartData: chartData)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarChartEnums.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Where to get the colour data from. 12 | ``` 13 | case barStyle // From BarStyle data model 14 | case dataPoints // From each data point 15 | ``` 16 | */ 17 | public enum ColourFrom { 18 | /// From BarStyle data model 19 | case barStyle 20 | /// From each data point 21 | case dataPoints 22 | } 23 | 24 | 25 | /** 26 | Where the marker lines come from to meet at a specified point. 27 | ``` 28 | case none // No overlay markers. 29 | case vertical // Vertical line from top to bottom. 30 | case full // Full width and height of view intersecting at a specified point. 31 | case bottomLeading // From bottom and leading edges meeting at a specified point. 32 | case bottomTrailing // From bottom and trailing edges meeting at a specified point. 33 | case topLeading // From top and leading edges meeting at a specified point. 34 | case topTrailing // From top and trailing edges meeting at a specified point. 35 | ``` 36 | */ 37 | public enum BarMarkerType: MarkerType { 38 | /// No overlay markers. 39 | case none 40 | /// Vertical line from top to bottom. 41 | case vertical(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 42 | /// Full width and height of view intersecting at a specified point. 43 | case full(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 44 | /// From bottom and leading edges meeting at a specified point. 45 | case bottomLeading(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 46 | /// From bottom and trailing edges meeting at a specified point. 47 | case bottomTrailing(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 48 | /// From top and leading edges meeting at a specified point. 49 | case topLeading(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 50 | /// From top and trailing edges meeting at a specified point. 51 | case topTrailing(colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YAxisGrid.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 24/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Adds horizontal lines along the X axis. 12 | */ 13 | internal struct YAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | internal func body(content: Content) -> some View { 22 | ZStack { 23 | if chartData.isGreaterThanTwo() { 24 | VStack { 25 | ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines-1), id: \.self) { index in 26 | if index != 0 { 27 | HorizontalGridView(chartData: chartData) 28 | Spacer() 29 | .frame(minHeight: 0, maxHeight: 500) 30 | } 31 | } 32 | HorizontalGridView(chartData: chartData) 33 | } 34 | content 35 | } else { content } 36 | } 37 | } 38 | } 39 | 40 | extension View { 41 | /** 42 | Adds horizontal lines along the X axis. 43 | 44 | The style is set in ChartData --> LineChartStyle --> yAxisGridStyle 45 | 46 | - Requires: 47 | Chart Data to conform to CTLineBarChartDataProtocol. 48 | 49 | # Available for: 50 | - Line Chart 51 | - Multi Line Chart 52 | - Filled Line Chart 53 | - Ranged Line Chart 54 | - Bar Chart 55 | - Grouped Bar Chart 56 | - Stacked Bar Chart 57 | - Ranged Bar Chart 58 | 59 | # Unavailable for: 60 | - Pie Chart 61 | - Doughnut Chart 62 | 63 | - Parameter chartData: Chart data model. 64 | - Returns: A new view containing the chart with horizontal lines under it. 65 | */ 66 | public func yAxisGrid(chartData: T) -> some View { 67 | self.modifier(YAxisGrid(chartData: chartData)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Models/ColourStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColourStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for setting up colour styling. 12 | */ 13 | public struct ColourStyle: CTColourStyle, Hashable { 14 | 15 | public var colourType: ColourType 16 | public var colour: Color? 17 | public var colours: [Color]? 18 | public var stops: [GradientStop]? 19 | public var startPoint: UnitPoint? 20 | public var endPoint: UnitPoint? 21 | 22 | // MARK: Single colour 23 | /// Single Colour 24 | /// - Parameters: 25 | /// - colour: Single Colour 26 | public init(colour: Color = Color(.red)) { 27 | self.colourType = .colour 28 | self.colour = colour 29 | self.colours = nil 30 | self.stops = nil 31 | self.startPoint = nil 32 | self.endPoint = nil 33 | } 34 | 35 | // MARK: Gradient colour 36 | /// Gradient Colour Line 37 | /// - Parameters: 38 | /// - colours: Colours for Gradient. 39 | /// - startPoint: Start point for Gradient. 40 | /// - endPoint: End point for Gradient. 41 | public init( 42 | colours: [Color] = [Color(.red), Color(.blue)], 43 | startPoint: UnitPoint = .leading, 44 | endPoint: UnitPoint = .trailing 45 | ) { 46 | self.colourType = .gradientColour 47 | self.colour = nil 48 | self.colours = colours 49 | self.stops = nil 50 | self.startPoint = startPoint 51 | self.endPoint = endPoint 52 | } 53 | 54 | // MARK: Gradient with stops 55 | /// Gradient with Stops Line 56 | /// - Parameters: 57 | /// - stops: Colours and Stops for Gradient with stop control. 58 | /// - startPoint: Start point for Gradient. 59 | /// - endPoint: End point for Gradient. 60 | public init( 61 | stops: [GradientStop] = [GradientStop(color: Color(.red), location: 0.0)], 62 | startPoint: UnitPoint = .leading, 63 | endPoint: UnitPoint = .trailing 64 | ) { 65 | self.colourType = .gradientStops 66 | self.colour = nil 67 | self.colours = nil 68 | self.stops = stops 69 | self.startPoint = startPoint 70 | self.endPoint = endPoint 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChartProtocols.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Chart Data 11 | /** 12 | A protocol to extend functionality of `CTChartData` specifically for Pie and Doughnut Charts. 13 | */ 14 | public protocol CTPieDoughnutChartDataProtocol: CTChartData {} 15 | 16 | /** 17 | A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Pie Charts. 18 | */ 19 | public protocol CTPieChartDataProtocol: CTPieDoughnutChartDataProtocol {} 20 | 21 | /** 22 | A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Doughnut Charts. 23 | */ 24 | public protocol CTDoughnutChartDataProtocol: CTPieDoughnutChartDataProtocol {} 25 | 26 | 27 | // MARK: - DataPoints 28 | /** 29 | A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Pie and Doughnut Charts. 30 | */ 31 | public protocol CTPieDataPoint: CTStandardDataPointProtocol, CTnotRanged { 32 | 33 | /** 34 | Colour of the segment 35 | */ 36 | var colour: Color { get set } 37 | 38 | /** 39 | Where the data point should start drawing from 40 | based on where the prvious one finished. 41 | 42 | In radians. 43 | */ 44 | var startAngle: Double { get set } 45 | 46 | /** 47 | The data points value in radians. 48 | */ 49 | var amount: Double { get set } 50 | 51 | /** 52 | Option to add overlays on top of the segment. 53 | */ 54 | var label: OverlayType { get set } 55 | } 56 | 57 | 58 | 59 | 60 | 61 | 62 | // MARK: - Style 63 | /** 64 | A protocol to extend functionality of `CTChartStyle` specifically for Pie and Doughnut Charts. 65 | */ 66 | public protocol CTPieAndDoughnutChartStyle: CTChartStyle {} 67 | 68 | 69 | /** 70 | A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Pie Charts. 71 | */ 72 | public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {} 73 | 74 | 75 | /** 76 | A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Doughnut Charts. 77 | */ 78 | public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { 79 | 80 | /** 81 | Width / Delta of the Doughnut Chart 82 | */ 83 | var strokeWidth: CGFloat { get set } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/ExtraYAxisLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtraYAxisLabels.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct ExtraYAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol { 11 | 12 | @ObservedObject private var chartData: T 13 | private let specifier: String 14 | private let colourIndicator: AxisColour 15 | 16 | internal init( 17 | chartData: T, 18 | specifier: String, 19 | colourIndicator: AxisColour 20 | ) { 21 | self.chartData = chartData 22 | self.specifier = specifier 23 | self.colourIndicator = colourIndicator 24 | } 25 | 26 | internal func body(content: Content) -> some View { 27 | Group { 28 | if chartData.isGreaterThanTwo() { 29 | switch chartData.chartStyle.yAxisLabelPosition { 30 | case .leading: 31 | HStack(spacing: 0) { 32 | content 33 | chartData.getExtraYAxisLabels().padding(.leading, 4) 34 | chartData.getExtraYAxisTitle(colour: colourIndicator) 35 | } 36 | case .trailing: 37 | HStack(spacing: 0) { 38 | chartData.getExtraYAxisTitle(colour: colourIndicator) 39 | chartData.getExtraYAxisLabels().padding(.trailing, 4) 40 | content 41 | } 42 | } 43 | } else { content } 44 | } 45 | .onAppear { 46 | chartData.viewData.hasYAxisLabels = true 47 | } 48 | } 49 | } 50 | 51 | extension View { 52 | /** 53 | Adds a second set of Y axis labels. 54 | 55 | - Parameters: 56 | - chartData: Data that conforms to CTLineBarChartDataProtocol. 57 | - specifier: Decimal precision for labels. 58 | - colourIndicator: Second Y Axis style. 59 | - Returns: A View with second set of Y axis labels. 60 | */ 61 | public func extraYAxisLabels( 62 | chartData: T, 63 | specifier: String = "%.0f", 64 | colourIndicator: AxisColour = .none 65 | ) -> some View { 66 | self.modifier(ExtraYAxisLabels(chartData: chartData, specifier: specifier, colourIndicator: colourIndicator)) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XAxisLabels.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 26/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Labels for the X axis. 12 | */ 13 | internal struct XAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | internal func body(content: Content) -> some View { 22 | Group { 23 | switch chartData.chartStyle.xAxisLabelPosition { 24 | case .bottom: 25 | if chartData.isGreaterThanTwo() { 26 | VStack { 27 | content 28 | chartData.getXAxisLabels().padding(.top, 2) 29 | chartData.getXAxisTitle() 30 | } 31 | } else { content } 32 | case .top: 33 | if chartData.isGreaterThanTwo() { 34 | VStack { 35 | chartData.getXAxisTitle() 36 | chartData.getXAxisLabels().padding(.bottom, 2) 37 | content 38 | } 39 | } else { content } 40 | } 41 | } 42 | .onAppear { 43 | self.chartData.viewData.hasXAxisLabels = true 44 | } 45 | } 46 | } 47 | 48 | extension View { 49 | /** 50 | Labels for the X axis. 51 | 52 | The labels can either come from ChartData --> xAxisLabels 53 | or ChartData --> DataSets --> DataPoints 54 | 55 | - Requires: 56 | Chart Data to conform to CTLineBarChartDataProtocol. 57 | 58 | - Requires: 59 | Chart Data to conform to CTLineBarChartDataProtocol. 60 | 61 | # Available for: 62 | - Line Chart 63 | - Multi Line Chart 64 | - Filled Line Chart 65 | - Ranged Line Chart 66 | - Bar Chart 67 | - Grouped Bar Chart 68 | - Stacked Bar Chart 69 | - Ranged Bar Chart 70 | 71 | # Unavailable for: 72 | - Pie Chart 73 | - Doughnut Chart 74 | 75 | - Parameter chartData: Chart data model. 76 | - Returns: A new view containing the chart with labels marking the x axis. 77 | */ 78 | public func xAxisLabels(chartData: T) -> some View { 79 | self.modifier(XAxisLabels(chartData: chartData)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedRectangleBarShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 12/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Round rectange used for the bar shapes 12 | 13 | [SO](https://stackoverflow.com/a/56763282) 14 | */ 15 | internal struct RoundedRectangleBarShape: Shape { 16 | 17 | private let tl: CGFloat 18 | private let tr: CGFloat 19 | private let bl: CGFloat 20 | private let br: CGFloat 21 | 22 | internal init( 23 | tl: CGFloat, 24 | tr: CGFloat, 25 | bl: CGFloat, 26 | br: CGFloat 27 | ) { 28 | self.tl = tl 29 | self.tr = tr 30 | self.bl = bl 31 | self.br = br 32 | } 33 | 34 | internal init(_ cornerRadius: CornerRadius) { 35 | self.tl = cornerRadius.topLeft 36 | self.tr = cornerRadius.topRight 37 | self.bl = cornerRadius.bottomLeft 38 | self.br = cornerRadius.bottomRight 39 | } 40 | 41 | internal func path(in rect: CGRect) -> Path { 42 | var path = Path() 43 | 44 | let w = rect.size.width 45 | let h = rect.size.height 46 | 47 | // Make sure we do not exceed the size of the rectangle 48 | let tr = min(min(self.tr, h/2), w/2) 49 | let tl = min(min(self.tl, h/2), w/2) 50 | let bl = min(min(self.bl, h/2), w/2) 51 | let br = min(min(self.br, h/2), w/2) 52 | 53 | path.move(to: CGPoint(x: tl, y: 0)) 54 | path.addLine(to: CGPoint(x: w - tr, y: 0)) 55 | path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, 56 | startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) 57 | 58 | path.addLine(to: CGPoint(x: w, y: h - br)) 59 | path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, 60 | startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) 61 | 62 | path.addLine(to: CGPoint(x: bl, y: h)) 63 | path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, 64 | startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) 65 | 66 | path.addLine(to: CGPoint(x: 0, y: tl)) 67 | path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, 68 | startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) 69 | 70 | return path 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointShape.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 24/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Draws point markers over the data point locations. 12 | */ 13 | internal struct Point: Shape { 14 | 15 | private let value: Double 16 | private let index: Int 17 | private let minValue: Double 18 | private let range: Double 19 | private let datapointCount: Int 20 | private let pointSize: CGFloat 21 | private let ignoreZero: Bool 22 | private let pointStyle: PointShape 23 | 24 | internal init( 25 | value: Double, 26 | index: Int, 27 | minValue: Double, 28 | range: Double, 29 | datapointCount: Int, 30 | pointSize: CGFloat, 31 | ignoreZero: Bool, 32 | pointStyle: PointShape 33 | ) { 34 | self.value = value 35 | self.index = index 36 | self.minValue = minValue 37 | self.range = range 38 | self.datapointCount = datapointCount 39 | self.pointSize = pointSize 40 | self.ignoreZero = ignoreZero 41 | self.pointStyle = pointStyle 42 | } 43 | 44 | internal func path(in rect: CGRect) -> Path { 45 | var path = Path() 46 | 47 | let x: CGFloat = rect.width / CGFloat(datapointCount-1) 48 | let y: CGFloat = rect.height / CGFloat(range) 49 | let offset: CGFloat = pointSize / CGFloat(2) 50 | 51 | let pointX: CGFloat = (CGFloat(index) * x) - offset 52 | let pointY: CGFloat = ((CGFloat(value - minValue) * -y) + rect.height) - offset 53 | let point: CGRect = CGRect(x: pointX, y: pointY, width: pointSize, height: pointSize) 54 | if !ignoreZero { 55 | pointSwitch(&path, point) 56 | } else { 57 | if value != 0 { 58 | pointSwitch(&path, point) 59 | } 60 | } 61 | return path 62 | } 63 | 64 | /// Draws the points based on chosen parameters. 65 | /// - Parameters: 66 | /// - path: Path to draw on. 67 | /// - point: Position to draw the point. 68 | internal func pointSwitch(_ path: inout Path, _ point: CGRect) { 69 | switch pointStyle { 70 | case .circle: 71 | path.addEllipse(in: point) 72 | case .square: 73 | path.addRect(point) 74 | case .roundSquare: 75 | path.addRoundedRect(in: point, cornerSize: CGSize(width: 3, height: 3)) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Legends.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 02/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Displays legends under the chart. 12 | */ 13 | internal struct Legends: ViewModifier where T: CTChartData { 14 | 15 | @ObservedObject private var chartData: T 16 | private let columns: [GridItem] 17 | private let width: CGFloat 18 | private let font: Font 19 | private let textColor: Color 20 | private let topPadding: CGFloat 21 | 22 | internal init( 23 | chartData: T, 24 | columns: [GridItem], 25 | width: CGFloat, 26 | font: Font, 27 | textColor: Color, 28 | topPadding: CGFloat 29 | ) { 30 | self.chartData = chartData 31 | self.columns = columns 32 | self.width = width 33 | self.font = font 34 | self.textColor = textColor 35 | self.topPadding = topPadding 36 | } 37 | 38 | internal func body(content: Content) -> some View { 39 | Group { 40 | if chartData.isGreaterThanTwo() { 41 | VStack { 42 | content 43 | LegendView(chartData: chartData, 44 | columns: columns, 45 | width: width, 46 | font: font, 47 | textColor: textColor) 48 | .padding(.top, topPadding) 49 | } 50 | } else { content } 51 | } 52 | } 53 | } 54 | 55 | extension View { 56 | /** 57 | Displays legends under the chart. 58 | 59 | - Parameters: 60 | - chartData: Chart data model. 61 | - columns: How to layout the legends. 62 | - textColor: Colour of the text. 63 | - Returns: A new view containing the chart with chart legends under. 64 | */ 65 | public func legends( 66 | chartData: T, 67 | columns: [GridItem] = [GridItem(.flexible())], 68 | iconWidth: CGFloat = 40, 69 | font: Font = .caption, 70 | textColor: Color = Color.primary, 71 | topPadding: CGFloat = 18 72 | ) -> some View { 73 | self.modifier(Legends(chartData: chartData, 74 | columns: columns, 75 | width: iconWidth, 76 | font: font, 77 | textColor: textColor, 78 | topPadding: topPadding)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangedBarChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View for creating a grouped bar chart. 12 | 13 | Uses `RangedBarChartData` data model. 14 | 15 | # Declaration 16 | 17 | ``` 18 | RangedBarChart(chartData: data) 19 | ``` 20 | 21 | # View Modifiers 22 | 23 | The order of the view modifiers is some what important 24 | as the modifiers are various types for stacks that wrap 25 | around the previous views. 26 | ``` 27 | .touchOverlay(chartData: data) 28 | .averageLine(chartData: data, 29 | strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10])) 30 | .yAxisPOI(chartData: data, 31 | markerName: "50", 32 | markerValue: 50, 33 | lineColour: Color.blue, 34 | strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10])) 35 | .xAxisGrid(chartData: data) 36 | .yAxisGrid(chartData: data) 37 | .xAxisLabels(chartData: data) 38 | .yAxisLabels(chartData: data) 39 | .infoBox(chartData: data) 40 | .floatingInfoBox(chartData: data) 41 | .headerBox(chartData: data) 42 | .legends(chartData: data) 43 | ``` 44 | */ 45 | public struct RangedBarChart: View where ChartData: RangedBarChartData { 46 | 47 | @ObservedObject private var chartData: ChartData 48 | @State private var timer: Timer? 49 | 50 | /// Initialises a bar chart view. 51 | /// - Parameter chartData: Must be RangedBarChartData model. 52 | public init(chartData: ChartData) { 53 | self.chartData = chartData 54 | } 55 | 56 | public var body: some View { 57 | GeometryReader { geo in 58 | if chartData.isGreaterThanTwo() { 59 | HStack(spacing: 0) { 60 | switch chartData.barStyle.colourFrom { 61 | case .barStyle: 62 | RangedBarChartBarStyleSubView(chartData: chartData) 63 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 64 | case .dataPoints: 65 | RangedBarChartDataPointSubView(chartData: chartData) 66 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 67 | } 68 | } 69 | // Needed for axes label frames 70 | .onAppear { 71 | self.chartData.viewData.chartSize = geo.frame(in: .local) 72 | } 73 | .layoutNotifier(timer) 74 | } else { CustomNoDataView(chartData: chartData) } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // 4 | // 5 | // Created by Will Dale on 10/01/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - ChartViewData 11 | /** 12 | The type of `DataSet` being used 13 | ``` 14 | case single // Single data set - i.e LineDataSet 15 | case multi // Multi data set - i.e MultiLineDataSet 16 | ``` 17 | */ 18 | public enum DataSetType { 19 | case single 20 | case multi 21 | } 22 | 23 | /** 24 | The type of chart being used. 25 | ``` 26 | case line // Line Chart Type 27 | case bar // Bar Chart Type 28 | case pie // Pie Chart Type 29 | ``` 30 | */ 31 | public enum ChartType { 32 | /// Line Chart Type 33 | case line 34 | /// Bar Chart Type 35 | case bar 36 | /// Pie Chart Type 37 | case pie 38 | } 39 | 40 | // MARK: - Style 41 | /** 42 | Type of colour styling. 43 | ``` 44 | case colour // Single Colour 45 | case gradientColour // Colour Gradient 46 | case gradientStops // Colour Gradient with stop control 47 | ``` 48 | */ 49 | public enum ColourType { 50 | /// Single Colour 51 | case colour 52 | /// Colour Gradient 53 | case gradientColour 54 | /// Colour Gradient with stop control 55 | case gradientStops 56 | } 57 | 58 | // MARK: - TouchOverlay 59 | /** 60 | Placement of the data point information panel when touch overlay modifier is applied. 61 | ``` 62 | case floating // Follows input across the chart. 63 | case infoBox(isStatic: Bool) // Display in the InfoBox. Must have .infoBox() 64 | case header // Fix in the Header box. Must have .headerBox(). 65 | ``` 66 | */ 67 | public enum InfoBoxPlacement { 68 | /// Follows input across the chart. 69 | case floating 70 | /// Display in the InfoBox. Must have .infoBox() 71 | case infoBox(isStatic: Bool = false) 72 | /// Display in the Header box. Must have .headerBox(). 73 | case header 74 | } 75 | 76 | 77 | // MARK: - TouchOverlay 78 | /** 79 | Alignment of the content inside of the information box 80 | ``` 81 | case vertical // Puts the legend, value and description verticaly 82 | case horizontal // Puts the legend, value and description horizontaly 83 | ``` 84 | */ 85 | public enum InfoBoxAlignment { 86 | case vertical 87 | case horizontal 88 | } 89 | 90 | 91 | /** 92 | Option to display units before or after values. 93 | 94 | ``` 95 | case none // No unit 96 | case prefix(of: String) // Before value 97 | case suffix(of: String) // After value 98 | ``` 99 | */ 100 | public enum TouchUnit { 101 | /// No units 102 | case none 103 | /// Before value 104 | case prefix(of: String) 105 | /// After value 106 | case suffix(of: String) 107 | } 108 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Views/BarChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 11/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View for creating a bar chart. 12 | 13 | Uses `BarChartData` data model. 14 | 15 | # Declaration 16 | 17 | ``` 18 | BarChart(chartData: data) 19 | ``` 20 | 21 | # View Modifiers 22 | 23 | The order of the view modifiers is some what important 24 | as the modifiers are various types for stacks that wrap 25 | around the previous views. 26 | ``` 27 | .touchOverlay(chartData: data) 28 | .averageLine(chartData: data, 29 | strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10])) 30 | .yAxisPOI(chartData: data, 31 | markerName: "50", 32 | markerValue: 50, 33 | lineColour: Color.blue, 34 | strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10])) 35 | .xAxisGrid(chartData: data) 36 | .yAxisGrid(chartData: data) 37 | .xAxisLabels(chartData: data) 38 | .yAxisLabels(chartData: data) 39 | .infoBox(chartData: data) 40 | .floatingInfoBox(chartData: data) 41 | .headerBox(chartData: data) 42 | .legends(chartData: data) 43 | ``` 44 | */ 45 | public struct BarChart: View where ChartData: BarChartData { 46 | 47 | @ObservedObject private var chartData: ChartData 48 | @State private var timer: Timer? 49 | 50 | /// Initialises a bar chart view. 51 | /// - Parameter chartData: Must be BarChartData model. 52 | public init(chartData: ChartData) { 53 | self.chartData = chartData 54 | } 55 | 56 | public var body: some View { 57 | GeometryReader { geo in 58 | if chartData.isGreaterThanTwo() { 59 | HStack(spacing: 0) { 60 | switch chartData.barStyle.colourFrom { 61 | case .barStyle: 62 | BarChartBarStyleSubView(chartData: chartData) 63 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 64 | case .dataPoints: 65 | BarChartDataPointSubView(chartData: chartData) 66 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 67 | } 68 | } 69 | // Needed for axes label frames 70 | .onChange(of: geo.frame(in: .local)) { value in 71 | self.chartData.viewData.chartSize = value 72 | } 73 | .layoutNotifier(timer) 74 | } else { CustomNoDataView(chartData: chartData) } 75 | } 76 | .if(chartData.minValue.isLess(than: 0)) { 77 | $0.scaleEffect(y: CGFloat(chartData.maxValue/(chartData.maxValue - chartData.minValue)), anchor: .top) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartViewData.swift 3 | // 4 | // Created by Will Dale on 03/01/2021. 5 | // 6 | 7 | import SwiftUI 8 | 9 | /** 10 | Data model to pass view information internally so the layout can configure its self. 11 | */ 12 | public struct ChartViewData { 13 | 14 | // MARK: Chart 15 | /** 16 | Size of the main chart. 17 | 18 | This does not include any view 19 | modifiers such as axis labels. 20 | */ 21 | var chartSize: CGRect = .zero 22 | 23 | // MARK: X Axis 24 | /** 25 | If the chart has labels on the X 26 | axis, the Y axis needs a different layout 27 | */ 28 | var hasXAxisLabels: Bool = false 29 | 30 | /** 31 | The hieght of X Axis Title if it is there. 32 | 33 | Needed to set the position of the Y Axis labels. 34 | */ 35 | var xAxisTitleHeight: CGFloat = 0 36 | 37 | /** 38 | The hieght of X Axis labels if they are there. 39 | 40 | Needed to set the position of the Y Axis labels. 41 | */ 42 | var xAxisLabelHeights: [CGFloat] = [] 43 | 44 | /** 45 | Width of the x axis title label once 46 | it has been rotated. 47 | 48 | Needed for calculating other parts 49 | of the layout system. 50 | */ 51 | var xAxislabelWidths: [CGFloat] = [] 52 | 53 | 54 | // MARK: Y Axis 55 | /** 56 | If the chart has labels on the Y axis, 57 | the X axis needs a different layout. 58 | */ 59 | var hasYAxisLabels: Bool = false 60 | 61 | /** 62 | Specifier for the values in the y axis labels. 63 | 64 | This gets passed in from the view modifier. 65 | */ 66 | var yAxisSpecifier: String = "%.0f" 67 | 68 | /// Optional number formatter for the y axis labels when they are `.numeric`. 69 | var yAxisNumberFormatter: NumberFormatter? = nil 70 | 71 | /** 72 | Width of the y axis title label once 73 | it has been rotated. 74 | 75 | Needed for calculating other parts 76 | of the layout system. 77 | */ 78 | var yAxisTitleWidth: CGFloat = 0 79 | /** 80 | Experimental 81 | */ 82 | var yAxisTitleHeight: CGFloat = 0 83 | 84 | /** 85 | Experimental 86 | */ 87 | var extraYAxisTitleWidth: CGFloat = 0 88 | /** 89 | Experimental 90 | */ 91 | var extraYAxisTitleHeight: CGFloat = 0 92 | 93 | /** 94 | Width of the y axis labels once 95 | they have been rotated. 96 | 97 | Needed for calculating other parts 98 | of the layout system. 99 | 100 | --- 101 | 102 | Current width of the `yAxisLabels` 103 | 104 | Needed line up the touch overlay to compensate for 105 | the loss of width. 106 | 107 | */ 108 | var yAxisLabelWidth: [CGFloat] = [] 109 | } 110 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/Views/LegendView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegendView.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 09/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Sub view to setup and display the legends. 12 | */ 13 | internal struct LegendView: View where T: CTChartData { 14 | 15 | @ObservedObject private var chartData: T 16 | private let columns: [GridItem] 17 | private let width: CGFloat 18 | private let font: Font 19 | private let textColor: Color 20 | 21 | internal init(chartData: T, 22 | columns: [GridItem], 23 | width: CGFloat, 24 | font: Font, 25 | textColor: Color 26 | ) { 27 | self.chartData = chartData 28 | self.columns = columns 29 | self.width = width 30 | self.font = font 31 | self.textColor = textColor 32 | } 33 | 34 | internal var body: some View { 35 | LazyVGrid(columns: columns, alignment: .leading) { 36 | ForEach(chartData.legends, id: \.id) { legend in 37 | legend.getLegend(width: width, font: font, textColor: textColor) 38 | .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } 39 | .if(scaleLegendPie(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } 40 | .accessibilityLabel(LocalizedStringKey(legend.accessibilityLegendLabel())) 41 | .accessibilityValue(LocalizedStringKey(legend.legend)) 42 | } 43 | } 44 | } 45 | 46 | /// Detects whether to run the scale effect on the legend. 47 | private func scaleLegendBar(legend: LegendData) -> Bool { 48 | if let chartData = chartData as? BarChartData, 49 | let datapoint = chartData.infoView.touchOverlayInfo.first { 50 | return chartData.infoView.isTouchCurrent && legend.id == datapoint.id 51 | } 52 | if let chartData = chartData as? GroupedBarChartData, 53 | let datapoint = chartData.infoView.touchOverlayInfo.first { 54 | return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour 55 | } 56 | if let chartData = chartData as? StackedBarChartData, 57 | let datapoint = chartData.infoView.touchOverlayInfo.first { 58 | return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour 59 | } 60 | return false 61 | } 62 | 63 | /// Detects whether to run the scale effect on the legend. 64 | private func scaleLegendPie(legend: LegendData) -> Bool { 65 | 66 | if chartData is PieChartData || chartData is DoughnutChartData { 67 | if let datapointID = chartData.infoView.touchOverlayInfo.first?.id as? UUID { 68 | return chartData.infoView.isTouchCurrent && legend.id == datapointID 69 | } else { 70 | return false 71 | } 72 | } else { 73 | return false 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Style/ExtraLineStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtraLineStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 05/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Control of the styling of the Extra Line. 12 | */ 13 | public struct ExtraLineStyle { 14 | 15 | public var lineColour: ColourStyle 16 | public var lineType: LineType 17 | public var lineSpacing: SpacingType 18 | 19 | public var markerType: LineMarkerType 20 | 21 | public var strokeStyle: Stroke 22 | 23 | public var pointStyle: PointStyle 24 | 25 | public var yAxisTitle: String? 26 | public var yAxisNumberOfLabels: Int 27 | 28 | public var animationType: AnimationType 29 | 30 | public var baseline: Baseline 31 | public var topLine: Topline 32 | 33 | public init( 34 | lineColour: ColourStyle = ColourStyle(colour: .red), 35 | lineType: LineType = .curvedLine, 36 | lineSpacing: SpacingType = .line, 37 | markerType: LineMarkerType = .indicator(style: DotStyle()), 38 | 39 | strokeStyle: Stroke = Stroke(), 40 | pointStyle: PointStyle = PointStyle(pointSize: 0, borderColour: .clear, fillColour: .clear), 41 | 42 | yAxisTitle: String? = nil, 43 | yAxisNumberOfLabels: Int = 7, 44 | 45 | animationType: AnimationType = .draw, 46 | 47 | baseline: Baseline = .minimumValue, 48 | topLine: Topline = .maximumValue 49 | ) { 50 | self.lineColour = lineColour 51 | self.lineType = lineType 52 | self.lineSpacing = lineSpacing 53 | 54 | self.markerType = markerType 55 | 56 | self.strokeStyle = strokeStyle 57 | self.pointStyle = pointStyle 58 | 59 | self.yAxisTitle = yAxisTitle 60 | self.yAxisNumberOfLabels = yAxisNumberOfLabels 61 | 62 | self.animationType = animationType 63 | 64 | self.baseline = baseline 65 | self.topLine = topLine 66 | } 67 | 68 | /** 69 | Controls which animations will be used. 70 | 71 | When using a line chart `.draw` is probably the 72 | right one to choose. 73 | 74 | When using on a filled line chart or on bar charts 75 | `.raise` is probably the right one to choose. 76 | 77 | ``` 78 | case draw // Draws the line using `.trim`. 79 | case raise // Animates using `.scale`. 80 | ``` 81 | */ 82 | public enum AnimationType: Hashable { 83 | /// Draws the line using `.trim`. 84 | case draw 85 | /// Animates using `.scale`. 86 | case raise 87 | } 88 | 89 | /** 90 | Sets what type of chart is being used. 91 | 92 | There is different spacing for line charts and bar charts, 93 | this sets that up. 94 | */ 95 | public enum SpacingType: Hashable { 96 | case line 97 | case bar 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChartStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 25/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for controlling the overall aesthetic of the chart. 12 | */ 13 | public struct PieChartStyle: CTPieChartStyle { 14 | 15 | public var infoBoxPlacement: InfoBoxPlacement 16 | public var infoBoxContentAlignment: InfoBoxAlignment 17 | 18 | public var infoBoxValueFont: Font 19 | public var infoBoxValueColour: Color 20 | 21 | public var infoBoxDescriptionFont: Font 22 | public var infoBoxDescriptionColour: Color 23 | 24 | public var infoBoxBackgroundColour: Color 25 | public var infoBoxBorderColour: Color 26 | public var infoBoxBorderStyle: StrokeStyle 27 | 28 | public var globalAnimation: Animation 29 | 30 | /// Model for controlling the overall aesthetic of the chart. 31 | /// - Parameters: 32 | /// - infoBoxPlacement: Placement of the information box that appears on touch input. 33 | /// - infoBoxContentAlignment: Alignment of the content inside of the information box 34 | /// 35 | /// - infoBoxValueFont: Font for the value part of the touch info. 36 | /// - infoBoxValueColour: Colour of the value part of the touch info. 37 | /// 38 | /// - infoBoxDescriptionFont: Font for the description part of the touch info. 39 | /// - infoBoxDescriptionColour: Colour of the description part of the touch info. 40 | /// 41 | /// - infoBoxBackgroundColour: Background colour of touch info. 42 | /// - infoBoxBorderColour: Border colour of the touch info. 43 | /// - infoBoxBorderStyle: Border style of the touch info. 44 | /// - globalAnimation: Global control of animations. 45 | public init( 46 | infoBoxPlacement: InfoBoxPlacement = .floating, 47 | infoBoxContentAlignment: InfoBoxAlignment = .vertical, 48 | 49 | infoBoxValueFont: Font = .title3, 50 | infoBoxValueColour: Color = Color.primary, 51 | 52 | infoBoxDescriptionFont: Font = .caption, 53 | infoBoxDescriptionColour: Color = Color.primary, 54 | 55 | infoBoxBackgroundColour: Color = Color.systemsBackground, 56 | infoBoxBorderColour: Color = Color.clear, 57 | infoBoxBorderStyle: StrokeStyle = StrokeStyle(lineWidth: 0), 58 | globalAnimation: Animation = Animation.linear(duration: 1) 59 | ) { 60 | self.infoBoxPlacement = infoBoxPlacement 61 | self.infoBoxContentAlignment = infoBoxContentAlignment 62 | 63 | self.infoBoxValueFont = infoBoxValueFont 64 | self.infoBoxValueColour = infoBoxValueColour 65 | 66 | self.infoBoxDescriptionFont = infoBoxDescriptionFont 67 | self.infoBoxDescriptionColour = infoBoxDescriptionColour 68 | 69 | self.infoBoxBackgroundColour = infoBoxBackgroundColour 70 | self.infoBoxBorderColour = infoBoxBorderColour 71 | self.infoBoxBorderStyle = infoBoxBorderStyle 72 | self.globalAnimation = globalAnimation 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Views/PositionedPOILabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionedPOILabel.swift 3 | // SwiftUICharts 4 | // 5 | // Created by Kunal Verma on 16/01/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct PositionedPOILabel: View where Content: View { 11 | 12 | enum Orientation { 13 | case horizontal 14 | case vertical 15 | } 16 | 17 | let content: () -> Content 18 | let orientation: Orientation 19 | let distanceFromLeading: CGFloat 20 | @State var contentSize: CGFloat = 0 21 | @State var containerSize: CGFloat = 0 22 | 23 | init(@ViewBuilder content: @escaping () -> Content, orientation: Orientation, distanceFromLeading: CGFloat) { 24 | self.content = content 25 | self.orientation = orientation 26 | self.distanceFromLeading = distanceFromLeading 27 | } 28 | 29 | var body: some View { 30 | if orientation == .horizontal { 31 | horizontalView() 32 | } else { 33 | verticalView() 34 | } 35 | } 36 | 37 | @ViewBuilder 38 | func horizontalView() -> some View { 39 | HStack(spacing: 0) { 40 | Spacer() 41 | .frame(width: (containerSize - contentSize) * distanceFromLeading) 42 | content() 43 | .background( 44 | GeometryReader { geo in 45 | Rectangle() 46 | .foregroundColor(Color.clear) 47 | .onAppear { 48 | self.contentSize = geo.size.width 49 | } 50 | } 51 | ) 52 | .fixedSize() 53 | Spacer() 54 | } 55 | .background( 56 | GeometryReader { stackGeo in 57 | Rectangle() 58 | .foregroundColor(Color.clear) 59 | .onAppear { 60 | self.containerSize = stackGeo.size.width 61 | } 62 | } 63 | ) 64 | } 65 | 66 | @ViewBuilder 67 | func verticalView() -> some View { 68 | VStack(spacing: 0) { 69 | Spacer() 70 | .frame(height: (containerSize - contentSize) * distanceFromLeading) 71 | content() 72 | .background( 73 | GeometryReader { geo in 74 | Rectangle() 75 | .foregroundColor(Color.clear) 76 | .onAppear { 77 | self.contentSize = geo.size.height 78 | } 79 | } 80 | ) 81 | .fixedSize() 82 | Spacer() 83 | } 84 | .background( 85 | GeometryReader { stackGeo in 86 | Rectangle() 87 | .foregroundColor(Color.clear) 88 | .onAppear { 89 | self.containerSize = stackGeo.size.height 90 | } 91 | } 92 | ) 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | 9 | concurrency: 10 | group: ci 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | macOS: 15 | name: Test macOS 12 16 | runs-on: macOS-12 17 | env: 18 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 19 | timeout-minutes: 10 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: macOS 12 23 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=macOS" clean test | xcpretty 24 | 25 | iOS_16: 26 | name: Test iOS 16 27 | runs-on: macOS-12 28 | env: 29 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 30 | timeout-minutes: 10 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: iOS 16 34 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=iOS Simulator,name=iPhone 14,OS=16.2" clean test | xcpretty 35 | 36 | iOS_15: 37 | name: Test iOS 15 38 | runs-on: macOS-12 39 | env: 40 | DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer 41 | timeout-minutes: 10 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: iOS 15 45 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=iOS Simulator,name=iPhone 13,OS=15.5" clean test | xcpretty 46 | 47 | iOS_14: 48 | name: Test iOS 14 49 | runs-on: macOS-11 50 | env: 51 | DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer 52 | timeout-minutes: 10 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: iOS 14 56 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" clean test | xcpretty 57 | 58 | tvOS: 59 | name: Test tvOS 60 | runs-on: macos-11 61 | env: 62 | DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer 63 | timeout-minutes: 10 64 | steps: 65 | - uses: actions/checkout@v3 66 | - name: tvOS 67 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=tvOS Simulator,name=Apple TV 4K (at 1080p) (2nd generation),OS=15.2" clean test | xcpretty 68 | 69 | watchOS: 70 | name: Test watchOS 71 | runs-on: macOS-12 72 | env: 73 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 74 | timeout-minutes: 10 75 | steps: 76 | - uses: actions/checkout@v3 77 | - name: watchOS 78 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme "SwiftUICharts" -destination "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm),OS=9.1" clean test | xcpretty 79 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YAxisLabels.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 24/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Automatically generated labels for the Y axis. 12 | */ 13 | internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | private let specifier: String 17 | private let colourIndicator: AxisColour 18 | private let formatter: NumberFormatter? 19 | 20 | internal init( 21 | chartData: T, 22 | specifier: String, 23 | formatter: NumberFormatter?, 24 | colourIndicator: AxisColour 25 | ) { 26 | self.chartData = chartData 27 | self.specifier = specifier 28 | self.colourIndicator = colourIndicator 29 | self.formatter = formatter 30 | } 31 | 32 | internal func body(content: Content) -> some View { 33 | Group { 34 | if chartData.isGreaterThanTwo() { 35 | switch chartData.chartStyle.yAxisLabelPosition { 36 | case .leading: 37 | HStack(spacing: 0) { 38 | chartData.getYAxisTitle(colour: colourIndicator) 39 | chartData.getYAxisLabels().padding(.trailing, 4) 40 | content 41 | } 42 | case .trailing: 43 | HStack(spacing: 0) { 44 | content 45 | chartData.getYAxisLabels().padding(.leading, 4) 46 | chartData.getYAxisTitle(colour: colourIndicator) 47 | } 48 | } 49 | } else { content } 50 | } 51 | .onAppear { 52 | chartData.viewData.hasYAxisLabels = true 53 | chartData.viewData.yAxisSpecifier = specifier 54 | chartData.viewData.yAxisNumberFormatter = formatter 55 | } 56 | } 57 | } 58 | 59 | extension View { 60 | /** 61 | Automatically generated labels for the Y axis. 62 | 63 | Controls are in ChartData --> ChartStyle 64 | 65 | - Requires: 66 | Chart Data to conform to CTLineBarChartDataProtocol. 67 | 68 | # Available for: 69 | - Line Chart 70 | - Multi Line Chart 71 | - Filled Line Chart 72 | - Ranged Line Chart 73 | - Bar Chart 74 | - Grouped Bar Chart 75 | - Stacked Bar Chart 76 | - Ranged Bar Chart 77 | 78 | # Unavailable for: 79 | - Pie Chart 80 | - Doughnut Chart 81 | 82 | - Parameters: 83 | - chartData: Data that conforms to CTLineBarChartDataProtocol 84 | - specifier: Decimal precision specifier 85 | - Returns: HStack of labels 86 | */ 87 | public func yAxisLabels( 88 | chartData: T, 89 | specifier: String = "%.0f", 90 | formatter: NumberFormatter? = nil, 91 | colourIndicator: AxisColour = .none 92 | ) -> some View { 93 | self.modifier(YAxisLabels(chartData: chartData, specifier: specifier, formatter: formatter, colourIndicator: colourIndicator)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoughnutChartStyle.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Model for controlling the overall aesthetic of the chart. 12 | */ 13 | public struct DoughnutChartStyle: CTDoughnutChartStyle { 14 | 15 | public var infoBoxPlacement: InfoBoxPlacement 16 | public var infoBoxContentAlignment: InfoBoxAlignment 17 | 18 | public var infoBoxValueFont: Font 19 | public var infoBoxValueColour: Color 20 | 21 | public var infoBoxDescriptionFont: Font 22 | public var infoBoxDescriptionColour: Color 23 | public var infoBoxBackgroundColour: Color 24 | public var infoBoxBorderColour: Color 25 | public var infoBoxBorderStyle: StrokeStyle 26 | 27 | public var globalAnimation: Animation 28 | 29 | public var strokeWidth: CGFloat 30 | 31 | /// Model for controlling the overall aesthetic of the chart. 32 | /// - Parameters: 33 | /// - infoBoxPlacement: Placement of the information box that appears on touch input. 34 | /// - infoBoxContentAlignment: Alignment of the content inside of the information box 35 | /// - infoBoxValueFont: Font for the value part of the touch info. 36 | /// - infoBoxValueColour: Colour of the value part of the touch info. 37 | /// - infoBoxDescriptionFont: Font for the description part of the touch info. 38 | /// - infoBoxDescriptionColour: Colour of the description part of the touch info. 39 | /// - infoBoxBackgroundColour: Background colour of touch info. 40 | /// - infoBoxBorderColour: Border colour of the touch info. 41 | /// - infoBoxBorderStyle: Border style of the touch info. 42 | /// - globalAnimation: Global control of animations. 43 | /// - strokeWidth: Width / Delta of the Doughnut Chart 44 | public init( 45 | infoBoxPlacement: InfoBoxPlacement = .floating, 46 | infoBoxContentAlignment: InfoBoxAlignment = .vertical, 47 | infoBoxValueFont: Font = .title3, 48 | infoBoxValueColour: Color = Color.primary, 49 | 50 | infoBoxDescriptionFont: Font = .caption, 51 | infoBoxDescriptionColour: Color = Color.primary, 52 | 53 | infoBoxBackgroundColour: Color = Color.systemsBackground, 54 | infoBoxBorderColour: Color = Color.clear, 55 | infoBoxBorderStyle: StrokeStyle = StrokeStyle(lineWidth: 0), 56 | 57 | globalAnimation: Animation = Animation.linear(duration: 1), 58 | strokeWidth: CGFloat = 30 59 | ) { 60 | self.infoBoxPlacement = infoBoxPlacement 61 | self.infoBoxContentAlignment = infoBoxContentAlignment 62 | 63 | self.infoBoxValueFont = infoBoxValueFont 64 | self.infoBoxValueColour = infoBoxValueColour 65 | 66 | self.infoBoxDescriptionFont = infoBoxDescriptionFont 67 | self.infoBoxDescriptionColour = infoBoxDescriptionColour 68 | 69 | self.infoBoxBackgroundColour = infoBoxBackgroundColour 70 | self.infoBoxBorderColour = infoBoxBorderColour 71 | self.infoBoxBorderStyle = infoBoxBorderStyle 72 | 73 | self.globalAnimation = globalAnimation 74 | self.strokeWidth = strokeWidth 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelShape.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Ordinate 11 | 12 | /** 13 | Custom Label Shape used in POI Markers when displaying POI values. 14 | */ 15 | public struct CustomLabelShape: Shape { 16 | public init(_ wrapped: S) { 17 | _path = { rect in 18 | let path = wrapped.path(in: rect) 19 | return path 20 | } 21 | } 22 | 23 | public func path(in rect: CGRect) -> Path { 24 | return _path(rect) 25 | } 26 | 27 | private let _path: (CGRect) -> Path 28 | } 29 | 30 | /** 31 | Shape used in POI Markers when displaying value in the Y axis labels on the leading edge. 32 | */ 33 | public struct LeadingLabelShape: Shape { 34 | public func path(in rect: CGRect) -> Path { 35 | var path = Path() 36 | path.move(to: CGPoint(x: rect.minX, y: rect.maxY)) 37 | path.addLine(to: CGPoint(x: rect.maxX - (rect.width / 5), y: rect.maxY)) 38 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) 39 | path.addLine(to: CGPoint(x: rect.maxX - (rect.width / 5), y: rect.minY)) 40 | path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) 41 | path.closeSubpath() 42 | return path 43 | } 44 | } 45 | 46 | /** 47 | Shape used in POI Markers when displaying value in the Y axis labels on the trailing edge. 48 | */ 49 | public struct TrailingLabelShape: Shape { 50 | public func path(in rect: CGRect) -> Path { 51 | var path = Path() 52 | path.move(to: CGPoint(x: rect.maxX, y: rect.maxY)) 53 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) 54 | path.addLine(to: CGPoint(x: rect.minX + (rect.width / 5), y: rect.minY)) 55 | path.addLine(to: CGPoint(x: rect.minX, y: rect.midY)) 56 | path.addLine(to: CGPoint(x: rect.minX + (rect.width / 5), y: rect.maxY)) 57 | path.closeSubpath() 58 | return path 59 | } 60 | } 61 | 62 | 63 | /** 64 | Shape used in POI Markers when displaying value in the X axis labels on the bottom edge. 65 | */ 66 | public struct BottomLabelShape: Shape { 67 | public func path(in rect: CGRect) -> Path { 68 | var path = Path() 69 | path.move(to: CGPoint(x: rect.minX, y: rect.maxY)) 70 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) 71 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY - (rect.height / 5))) 72 | path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) 73 | path.addLine(to: CGPoint(x: rect.minX, y: rect.midY - (rect.height / 5))) 74 | path.closeSubpath() 75 | return path 76 | } 77 | } 78 | 79 | /** 80 | Shape used in POI Markers when displaying value in the X axis labels on the top edge. 81 | */ 82 | public struct TopLabelShape: Shape { 83 | public func path(in rect: CGRect) -> Path { 84 | var path = Path() 85 | path.move(to: CGPoint(x: rect.minX, y: rect.minY)) 86 | path.addLine(to: CGPoint(x: rect.minX, y: rect.midY + (rect.height / 5))) 87 | path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) 88 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY + (rect.height / 5))) 89 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) 90 | path.closeSubpath() 91 | return path 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Views/PieChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 24/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View for creating a pie chart. 12 | 13 | Uses `PieChartData` data model. 14 | 15 | # Declaration 16 | ``` 17 | PieChart(chartData: data) 18 | ``` 19 | 20 | # View Modifiers 21 | The order of the view modifiers is some what important 22 | as the modifiers are various types for stacks that wrap 23 | around the previous views. 24 | ``` 25 | .touchOverlay(chartData: data) 26 | .infoBox(chartData: data) 27 | .floatingInfoBox(chartData: data) 28 | .headerBox(chartData: data) 29 | .legends(chartData: data) 30 | ``` 31 | */ 32 | public struct PieChart: View where ChartData: PieChartData { 33 | 34 | @ObservedObject private var chartData: ChartData 35 | @State private var timer: Timer? 36 | 37 | /// Initialises a bar chart view. 38 | /// - Parameter chartData: Must be PieChartData. 39 | public init(chartData: ChartData) { 40 | self.chartData = chartData 41 | } 42 | 43 | @State private var startAnimation: Bool = false 44 | 45 | public var body: some View { 46 | GeometryReader { geo in 47 | ZStack { 48 | ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in 49 | PieSegmentShape(id: chartData.dataSets.dataPoints[data].id, 50 | startAngle: chartData.dataSets.dataPoints[data].startAngle, 51 | amount: chartData.dataSets.dataPoints[data].amount) 52 | .fill(chartData.dataSets.dataPoints[data].colour) 53 | .overlay(dataPoint: chartData.dataSets.dataPoints[data], chartData: chartData, rect: geo.frame(in: .local)) 54 | .scaleEffect(animationValue) 55 | .opacity(Double(animationValue)) 56 | .animation(Animation.spring().delay(Double(data) * 0.06)) 57 | .if(chartData.infoView.touchOverlayInfo == [chartData.dataSets.dataPoints[data]]) { 58 | $0 59 | .scaleEffect(1.1) 60 | .zIndex(1) 61 | .shadow(color: Color.primary, radius: 10) 62 | } 63 | .accessibilityLabel(chartData.metadata.title) 64 | .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier, 65 | formatter: chartData.infoView.touchFormatter)) 66 | } 67 | } 68 | } 69 | .animateOnAppear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 70 | self.startAnimation = true 71 | } 72 | .animateOnDisappear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 73 | self.startAnimation = false 74 | } 75 | .layoutNotifier(timer) 76 | } 77 | 78 | var animationValue: CGFloat { 79 | if chartData.disableAnimation { 80 | return 1 81 | } else { 82 | return startAnimation ? 1 : 0.001 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Views/TouchOverlayBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchOverlayBox.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 09/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View that displays information from the touch events. 12 | */ 13 | internal struct TouchOverlayBox: View { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | @Binding private var boxFrame: CGRect 18 | 19 | internal init( 20 | chartData: T, 21 | boxFrame: Binding 22 | ) { 23 | self.chartData = chartData 24 | self._boxFrame = boxFrame 25 | } 26 | 27 | internal var body: some View { 28 | Group { 29 | if chartData.chartStyle.infoBoxContentAlignment == .vertical { 30 | VStack(alignment: .leading, spacing: 0) { 31 | ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in 32 | chartData.infoDescription(info: point) 33 | .font(chartData.chartStyle.infoBoxDescriptionFont) 34 | .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) 35 | chartData.infoValueUnit(info: point) 36 | .font(chartData.chartStyle.infoBoxValueFont) 37 | .foregroundColor(chartData.chartStyle.infoBoxValueColour) 38 | chartData.infoLegend(info: point) 39 | .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) 40 | } 41 | } 42 | } else { 43 | HStack { 44 | ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in 45 | chartData.infoLegend(info: point) 46 | .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) 47 | .layoutPriority(1) 48 | chartData.infoDescription(info: point) 49 | .font(chartData.chartStyle.infoBoxDescriptionFont) 50 | .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) 51 | chartData.infoValueUnit(info: point) 52 | .font(chartData.chartStyle.infoBoxValueFont) 53 | .foregroundColor(chartData.chartStyle.infoBoxValueColour) 54 | } 55 | } 56 | } 57 | } 58 | .padding(.all, 8) 59 | .background( 60 | GeometryReader { geo in 61 | if chartData.infoView.isTouchCurrent { 62 | RoundedRectangle(cornerRadius: 5.0, style: .continuous) 63 | .fill(chartData.chartStyle.infoBoxBackgroundColour) 64 | .overlay( 65 | RoundedRectangle(cornerRadius: 5.0, style: .continuous) 66 | .stroke(chartData.chartStyle.infoBoxBorderColour, style: chartData.chartStyle.infoBoxBorderStyle) 67 | ) 68 | .onAppear { 69 | self.boxFrame = geo.frame(in: .local) 70 | } 71 | .onChange(of: geo.frame(in: .local)) { frame in 72 | self.boxFrame = frame 73 | } 74 | } 75 | } 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieChartData.swift 3 | // 4 | // 5 | // Created by Will Dale on 24/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /** 12 | Data for drawing and styling a pie chart. 13 | 14 | This model contains the data and styling information for a pie chart. 15 | */ 16 | public final class PieChartData: CTPieChartDataProtocol, Publishable { 17 | 18 | // MARK: Properties 19 | public var id: UUID = UUID() 20 | @Published public final var dataSets: PieDataSet 21 | @Published public final var metadata: ChartMetadata 22 | @Published public final var chartStyle: PieChartStyle 23 | @Published public final var legends: [LegendData] 24 | @Published public final var infoView: InfoViewData 25 | 26 | // Publishable 27 | public var subscription = SubscriptionSet().subscription 28 | public let touchedDataPointPublisher = PassthroughSubject() 29 | 30 | public final var noDataText: Text 31 | public final var chartType: (chartType: ChartType, dataSetType: DataSetType) 32 | 33 | public var disableAnimation = false 34 | 35 | // MARK: Initializer 36 | /// Initialises Pie Chart data. 37 | /// 38 | /// - Parameters: 39 | /// - dataSets: Data to draw and style the chart. 40 | /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend. 41 | /// - chartStyle: The style data for the aesthetic of the chart. 42 | /// - noDataText: Customisable Text to display when where is not enough data to draw the chart. 43 | public init( 44 | dataSets: PieDataSet, 45 | metadata: ChartMetadata, 46 | chartStyle: PieChartStyle = PieChartStyle(), 47 | noDataText: Text = Text("No Data") 48 | ) { 49 | self.dataSets = dataSets 50 | self.metadata = metadata 51 | self.chartStyle = chartStyle 52 | self.legends = [LegendData]() 53 | self.infoView = InfoViewData() 54 | self.noDataText = noDataText 55 | self.chartType = (chartType: .pie, dataSetType: .single) 56 | 57 | self.setupLegends() 58 | self.makeDataPoints() 59 | } 60 | 61 | public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } 62 | 63 | public typealias SetType = PieDataSet 64 | public typealias DataPoint = PieChartDataPoint 65 | public typealias CTStyle = PieChartStyle 66 | } 67 | 68 | // MARK: - Touch 69 | extension PieChartData { 70 | 71 | public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { 72 | let touchDegree = degree(from: touchLocation, in: chartSize) 73 | let index = self.dataSets.dataPoints.firstIndex(where:) { 74 | let start = $0.startAngle * Double(180 / Double.pi) <= Double(touchDegree) 75 | let end = ($0.startAngle * Double(180 / Double.pi)) + ($0.amount * Double(180 / Double.pi)) >= Double(touchDegree) 76 | return start && end 77 | } 78 | guard let wrappedIndex = index else { return } 79 | self.dataSets.dataPoints[wrappedIndex].legendTag = dataSets.legendTitle 80 | self.infoView.touchOverlayInfo = [self.dataSets.dataPoints[wrappedIndex]] 81 | touchedDataPointPublisher.send(self.dataSets.dataPoints[wrappedIndex]) 82 | } 83 | 84 | public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { 85 | return nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoughnutChartData.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /** 12 | Data for drawing and styling a doughnut chart. 13 | 14 | This model contains the data and styling information for a doughnut chart. 15 | */ 16 | public final class DoughnutChartData: CTDoughnutChartDataProtocol, Publishable { 17 | 18 | // MARK: Properties 19 | public var id: UUID = UUID() 20 | @Published public final var dataSets: PieDataSet 21 | @Published public final var metadata: ChartMetadata 22 | @Published public final var chartStyle: DoughnutChartStyle 23 | @Published public final var legends: [LegendData] 24 | @Published public final var infoView: InfoViewData 25 | 26 | // Publishable 27 | public var subscription = SubscriptionSet().subscription 28 | public let touchedDataPointPublisher = PassthroughSubject() 29 | 30 | public final var noDataText: Text 31 | public final var chartType: (chartType: ChartType, dataSetType: DataSetType) 32 | 33 | public var disableAnimation = false 34 | 35 | // MARK: Initializer 36 | /// Initialises Doughnut Chart data. 37 | /// 38 | /// - Parameters: 39 | /// - dataSets: Data to draw and style the chart. 40 | /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend. 41 | /// - chartStyle: The style data for the aesthetic of the chart. 42 | /// - noDataText: Customisable Text to display when where is not enough data to draw the chart. 43 | public init( 44 | dataSets: PieDataSet, 45 | metadata: ChartMetadata, 46 | chartStyle: DoughnutChartStyle = DoughnutChartStyle(), 47 | noDataText: Text 48 | ) { 49 | self.dataSets = dataSets 50 | self.metadata = metadata 51 | self.chartStyle = chartStyle 52 | self.legends = [LegendData]() 53 | self.infoView = InfoViewData() 54 | self.noDataText = noDataText 55 | self.chartType = (chartType: .pie, dataSetType: .single) 56 | 57 | self.setupLegends() 58 | self.makeDataPoints() 59 | } 60 | 61 | public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } 62 | 63 | public typealias SetType = PieDataSet 64 | public typealias DataPoint = PieChartDataPoint 65 | public typealias CTStyle = DoughnutChartStyle 66 | } 67 | 68 | // MARK: - Touch 69 | extension DoughnutChartData { 70 | public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { 71 | let touchDegree = degree(from: touchLocation, in: chartSize) 72 | let index = self.dataSets.dataPoints.firstIndex(where:) { 73 | let start = $0.startAngle * Double(180 / Double.pi) <= Double(touchDegree) 74 | let end = ($0.startAngle * Double(180 / Double.pi)) + ($0.amount * Double(180 / Double.pi)) >= Double(touchDegree) 75 | return start && end 76 | } 77 | guard let wrappedIndex = index else { return } 78 | self.dataSets.dataPoints[wrappedIndex].legendTag = dataSets.legendTitle 79 | self.infoView.touchOverlayInfo = [self.dataSets.dataPoints[wrappedIndex]] 80 | touchedDataPointPublisher.send(self.dataSets.dataPoints[wrappedIndex]) 81 | } 82 | public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { 83 | return nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at w.dale@me.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoughnutChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 01/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View for creating a doughnut chart. 12 | 13 | Uses `DoughnutChartData` data model. 14 | 15 | # Declaration 16 | ``` 17 | DoughnutChart(chartData: data) 18 | ``` 19 | 20 | # View Modifiers 21 | The order of the view modifiers is some what important 22 | as the modifiers are various types for stacks that wrap 23 | around the previous views. 24 | ``` 25 | .touchOverlay(chartData: data) 26 | .infoBox(chartData: data) 27 | .floatingInfoBox(chartData: data) 28 | .headerBox(chartData: data) 29 | .legends(chartData: data) 30 | ``` 31 | */ 32 | public struct DoughnutChart: View where ChartData: DoughnutChartData { 33 | 34 | @ObservedObject private var chartData: ChartData 35 | @State private var timer: Timer? 36 | 37 | /// Initialises a bar chart view. 38 | /// - Parameter chartData: Must be DoughnutChartData. 39 | public init(chartData: ChartData) { 40 | self.chartData = chartData 41 | } 42 | 43 | @State private var startAnimation: Bool = false 44 | 45 | public var body: some View { 46 | GeometryReader { geo in 47 | ZStack { 48 | ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in 49 | DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id, 50 | startAngle: chartData.dataSets.dataPoints[data].startAngle, 51 | amount: chartData.dataSets.dataPoints[data].amount) 52 | .stroke(chartData.dataSets.dataPoints[data].colour, 53 | lineWidth: chartData.chartStyle.strokeWidth) 54 | .overlay(dataPoint: chartData.dataSets.dataPoints[data], 55 | chartData: chartData, 56 | rect: geo.frame(in: .local)) 57 | .scaleEffect(animationValue) 58 | .opacity(Double(animationValue)) 59 | .animation(Animation.spring().delay(Double(data) * 0.06)) 60 | .if(chartData.infoView.touchOverlayInfo == [chartData.dataSets.dataPoints[data]]) { 61 | $0 62 | .scaleEffect(1.1) 63 | .zIndex(1) 64 | .shadow(color: Color.primary, radius: 10) 65 | } 66 | .accessibilityLabel(chartData.metadata.title) 67 | .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier, 68 | formatter: chartData.infoView.touchFormatter)) 69 | } 70 | } 71 | } 72 | .animateOnAppear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 73 | self.startAnimation = true 74 | } 75 | .animateOnDisappear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 76 | self.startAnimation = false 77 | } 78 | .layoutNotifier(timer) 79 | } 80 | 81 | var animationValue: CGFloat { 82 | if chartData.disableAnimation { 83 | return 1 84 | } else { 85 | return startAnimation ? 1 : 0.001 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SwiftUICharts.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisDividers.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 02/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Dividing line drawn between the X axis labels and the chart. 12 | */ 13 | internal struct XAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | private let labelsAndTop: Bool 17 | private let labelsAndBottom: Bool 18 | 19 | init(chartData: T) { 20 | self.chartData = chartData 21 | self.labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top 22 | self.labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom 23 | } 24 | 25 | internal func body(content: Content) -> some View { 26 | Group { 27 | if chartData.isGreaterThanTwo() { 28 | if labelsAndBottom { 29 | VStack { 30 | ZStack(alignment: .bottom) { 31 | content 32 | Divider().if(chartData.chartStyle.xAxisBorderColour != nil) { $0.background(chartData.chartStyle.xAxisBorderColour) } 33 | } 34 | } 35 | } else if labelsAndTop { 36 | VStack { 37 | ZStack(alignment: .top) { 38 | content 39 | Divider().if(chartData.chartStyle.xAxisBorderColour != nil) { $0.background(chartData.chartStyle.xAxisBorderColour) } 40 | } 41 | } 42 | } else { 43 | content 44 | } 45 | } else { content } 46 | } 47 | } 48 | } 49 | 50 | /** 51 | Dividing line drawn between the Y axis labels and the chart. 52 | */ 53 | internal struct YAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol { 54 | 55 | @ObservedObject private var chartData: T 56 | private let labelsAndLeading: Bool 57 | private let labelsAndTrailing: Bool 58 | 59 | internal init(chartData: T) { 60 | self.chartData = chartData 61 | self.labelsAndLeading = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .leading 62 | self.labelsAndTrailing = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .trailing 63 | } 64 | 65 | internal func body(content: Content) -> some View { 66 | Group { 67 | if labelsAndLeading { 68 | HStack { 69 | ZStack(alignment: .leading) { 70 | content 71 | Divider().if(chartData.chartStyle.yAxisBorderColour != nil) { $0.background(chartData.chartStyle.yAxisBorderColour) } 72 | } 73 | } 74 | } else if labelsAndTrailing { 75 | HStack { 76 | ZStack(alignment: .trailing) { 77 | content 78 | Divider().if(chartData.chartStyle.yAxisBorderColour != nil) { $0.background(chartData.chartStyle.yAxisBorderColour) } 79 | } 80 | } 81 | } else { 82 | content 83 | } 84 | } 85 | } 86 | } 87 | 88 | extension View { 89 | internal func xAxisBorder(chartData: T) -> some View { 90 | self.modifier(XAxisBorder(chartData: chartData)) 91 | } 92 | 93 | internal func yAxisBorder(chartData: T) -> some View { 94 | self.modifier(YAxisBorder(chartData: chartData)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/FloatingInfoBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingInfoBox.swift 3 | // 4 | // 5 | // Created by Will Dale on 12/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A view that displays information from `TouchOverlay`. 12 | */ 13 | internal struct FloatingInfoBox: ViewModifier where T: CTLineBarChartDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | internal init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 0, height: 70) 22 | 23 | internal func body(content: Content) -> some View { 24 | Group { 25 | switch chartData.chartStyle.infoBoxPlacement { 26 | case .floating: 27 | ZStack { 28 | floating 29 | content 30 | } 31 | case .infoBox: 32 | content 33 | case .header: 34 | content 35 | } 36 | } 37 | } 38 | 39 | private var floating: some View { 40 | TouchOverlayBox(chartData: chartData, 41 | boxFrame: $boxFrame) 42 | .position(x: chartData.setBoxLocation(touchLocation: chartData.infoView.touchLocation.x, 43 | boxFrame: boxFrame, 44 | chartSize: chartData.infoView.chartSize) - 6, // -6 to compensate for `.padding(.horizontal, 6)` 45 | y: boxFrame.midY - 10) 46 | .padding(.horizontal, 6) 47 | .zIndex(1) 48 | } 49 | } 50 | 51 | /** 52 | A view that displays information from `TouchOverlay`. 53 | */ 54 | internal struct HorizontalFloatingInfoBox: ViewModifier where T: CTLineBarChartDataProtocol & isHorizontal { 55 | 56 | @ObservedObject private var chartData: T 57 | 58 | internal init(chartData: T) { 59 | self.chartData = chartData 60 | } 61 | 62 | @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 70, height: 70) 63 | 64 | internal func body(content: Content) -> some View { 65 | Group { 66 | switch chartData.chartStyle.infoBoxPlacement { 67 | case .floating: 68 | ZStack { 69 | floating 70 | content 71 | } 72 | case .infoBox: 73 | content 74 | case .header: 75 | content 76 | } 77 | } 78 | } 79 | 80 | private var floating: some View { 81 | TouchOverlayBox(chartData: chartData, 82 | boxFrame: $boxFrame) 83 | .position(x: chartData.infoView.chartSize.width, 84 | y: chartData.setBoxLocation(touchLocation: chartData.infoView.touchLocation.y, 85 | boxFrame: boxFrame, 86 | chartSize: chartData.infoView.chartSize)) 87 | .padding(.horizontal, 6) 88 | .zIndex(1) 89 | } 90 | } 91 | 92 | extension View { 93 | /** 94 | A view that displays information from `TouchOverlay`. 95 | 96 | Places the info box on top of the chart. 97 | 98 | - Parameter chartData: Chart data model. 99 | - Returns: A new view containing the chart with a view to 100 | display touch overlay information. 101 | */ 102 | public func floatingInfoBox(chartData: T) -> some View { 103 | self.modifier(FloatingInfoBox(chartData: chartData)) 104 | } 105 | 106 | /** 107 | A view that displays information from `TouchOverlay`. 108 | 109 | Places the info box on top of the chart. 110 | 111 | - Parameter chartData: Chart data model. 112 | - Returns: A new view containing the chart with a view to 113 | display touch overlay information. 114 | */ 115 | public func floatingInfoBox(chartData: T) -> some View { 116 | self.modifier(HorizontalFloatingInfoBox(chartData: chartData)) 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineChartEnums.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Drawing style of the line 12 | ``` 13 | case line // Straight line from point to point 14 | case curvedLine // Dual control point curved line 15 | case stepped // Stepped line from point to point 16 | ``` 17 | */ 18 | public enum LineType { 19 | /// Straight line from point to point 20 | case line 21 | /// Dual control point curved line 22 | case curvedLine 23 | /// Stepped line from point to point 24 | case stepped 25 | } 26 | 27 | /** 28 | Style of the point marks 29 | ``` 30 | case filled // Just fill 31 | case outline // Just stroke 32 | case filledOutLine // Both fill and stroke 33 | ``` 34 | */ 35 | public enum PointType { 36 | /// Just fill 37 | case filled 38 | /// Just stroke 39 | case outline 40 | /// Both fill and stroke 41 | case filledOutLine 42 | } 43 | 44 | /** 45 | Shape of the points 46 | ``` 47 | case circle 48 | case square 49 | case roundSquare 50 | ``` 51 | */ 52 | public enum PointShape { 53 | /// Circle Shape 54 | case circle 55 | /// Square Shape 56 | case square 57 | /// Rounded Square Shape 58 | case roundSquare 59 | } 60 | 61 | /** 62 | Where the Y and X touch markers should attach themselves to. 63 | ``` 64 | case line(dot: Dot) // Attached to the line. 65 | case point // Attached to the data points. 66 | ``` 67 | */ 68 | public enum MarkerAttachment { 69 | /// Attached to the line. 70 | case line(dot: Dot) 71 | /// Attached to the data points. 72 | case point 73 | } 74 | 75 | /** 76 | Where the marker lines come from to meet at a specified point. 77 | ``` 78 | case none // No overlay markers. 79 | case indicator(style: DotStyle) // Dot that follows the path. 80 | case vertical(attachment: MarkerAttachment) // Vertical line from top to bottom. 81 | case full(attachment: MarkerAttachment) // Full width and height of view intersecting at a specified point. 82 | case bottomLeading(attachment: MarkerAttachment) // From bottom and leading edges meeting at a specified point. 83 | case bottomTrailing(attachment: MarkerAttachment) // From bottom and trailing edges meeting at a specified point. 84 | case topLeading(attachment: MarkerAttachment) // From top and leading edges meeting at a specified point. 85 | case topTrailing(attachment: MarkerAttachment) // From top and trailing edges meeting at a specified point. 86 | ``` 87 | */ 88 | public enum LineMarkerType: MarkerType { 89 | /// No overlay markers. 90 | case none 91 | /// Dot that follows the path. 92 | case indicator(style: DotStyle) 93 | /// Vertical line from top to bottom. 94 | case vertical(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 95 | /// Full width and height of view intersecting at a specified point. 96 | case full(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 97 | /// From bottom and leading edges meeting at a specified point. 98 | case bottomLeading(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 99 | /// From bottom and trailing edges meeting at a specified point. 100 | case bottomTrailing(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 101 | /// From top and leading edges meeting at a specified point. 102 | case topLeading(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 103 | /// From top and trailing edges meeting at a specified point. 104 | case topTrailing(attachment: MarkerAttachment, colour: Color = Color.primary, style: StrokeStyle = StrokeStyle()) 105 | } 106 | 107 | /** 108 | Whether or not to show a dot on the line 109 | 110 | ``` 111 | case none // No Dot 112 | case style(_ style: DotStyle) // Adds a dot the line at point of touch. 113 | ``` 114 | */ 115 | public enum Dot { 116 | /// No Dot 117 | case none 118 | /// Adds a dot the line at point of touch. 119 | case style(_ style: DotStyle) 120 | } 121 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineAndBarEnums.swift 3 | // 4 | // 5 | // Created by Will Dale on 08/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - XAxisLabels 11 | /** 12 | Location of the X axis labels 13 | ``` 14 | case top 15 | case bottom 16 | ``` 17 | */ 18 | public enum XAxisLabelPosistion { 19 | case top 20 | case bottom 21 | } 22 | 23 | /** 24 | Where the label data come from. 25 | 26 | xAxisLabel comes from ChartData --> DataPoint model. 27 | 28 | xAxisLabels comes from ChartData --> xAxisLabels 29 | ``` 30 | case dataPoint // ChartData --> DataPoint --> xAxisLabel 31 | case chartData // ChartData --> xAxisLabels 32 | ``` 33 | */ 34 | public enum LabelsFrom { 35 | /// ChartData --> DataPoint --> xAxisLabel 36 | case dataPoint(rotation: Angle = Angle.degrees(0)) 37 | /// ChartData --> xAxisLabels 38 | case chartData(rotation: Angle = Angle.degrees(0)) 39 | } 40 | 41 | // MARK: - YAxisLabels 42 | /** 43 | Location of the Y axis labels 44 | ``` 45 | case leading 46 | case trailing 47 | ``` 48 | */ 49 | public enum YAxisLabelPosistion { 50 | case leading 51 | case trailing 52 | } 53 | 54 | /** 55 | Option to display the markers' value inline with the marker.. 56 | 57 | ``` 58 | case none // No label. 59 | case yAxis(specifier: String, formatter: NumberFormatter? = nil) // Places the label in the yAxis labels. 60 | case center(specifier: String, formatter: NumberFormatter? = nil) // Places the label in the center of chart. 61 | case position(location: CGFloat, specifier: String, formatter: NumberFormatter? = nil) // Places the label at a relative position from leading edge. 62 | ``` 63 | */ 64 | public enum DisplayValue { 65 | /// No label. 66 | case none 67 | /// Places the label in the yAxis labels. 68 | case yAxis(specifier: String, formatter: NumberFormatter? = nil) 69 | /// Places the label in the center of chart. 70 | case center(specifier: String, formatter: NumberFormatter? = nil) 71 | /// Places the label in between the graph at a certain distance from the axis, i.e. 0 places it on the leading edge and 1 places it on the trailing edge. Defaults to 0.5 if location >1 or <0 72 | case position(location: CGFloat, specifier: String, formatter: NumberFormatter? = nil) 73 | 74 | } 75 | 76 | /** 77 | Where to start drawing the line chart from. 78 | ``` 79 | case minimumValue // Lowest value in the data set(s) 80 | case minimumWithMaximum(of: Double) // Set a custom baseline 81 | case zero // Set 0 as the lowest value 82 | ``` 83 | */ 84 | public enum Baseline: Hashable { 85 | /// Lowest value in the data set(s) 86 | case minimumValue 87 | /// Set a custom baseline 88 | case minimumWithMaximum(of: Double) 89 | /// Set 0 as the lowest value 90 | case zero 91 | } 92 | 93 | /** 94 | Where to end drawing the chart. 95 | ``` 96 | case maximumValue // Highest value in the data set(s) 97 | case maximum(of: Double) // Set a custom topline 98 | ``` 99 | */ 100 | public enum Topline: Hashable { 101 | /// Highest value in the data set(s) 102 | case maximumValue 103 | /// Set a custom topline 104 | case maximum(of: Double) 105 | } 106 | 107 | /** 108 | Option to choose between auto generated, numeric labels 109 | or custum array of strings. 110 | 111 | Custom is set from `ChartData -> yAxisLabels` 112 | 113 | ``` 114 | case numeric // Auto generated, numeric labels. 115 | case custom // Custom labels array -- `ChartData -> yAxisLabels` 116 | ``` 117 | */ 118 | public enum YAxisLabelType { 119 | /// Auto generated, numeric labels. 120 | case numeric 121 | /// Custom labels array 122 | case custom 123 | } 124 | 125 | // MARK: - Extra Y Axis 126 | /** 127 | Controls how second Y Axis will be styled. 128 | 129 | ``` 130 | case none // No colour marker. 131 | case style(size: CGFloat) // Get style from data model. 132 | case custom(colour: ColourStyle, size: CGFloat) // Set custom style. 133 | ``` 134 | */ 135 | public enum AxisColour { 136 | /// No colour marker. 137 | case none 138 | /// Get style from data model. 139 | case style(size: CGFloat) 140 | /// Set custom style. 141 | case custom(colour: ColourStyle, size: CGFloat) 142 | } 143 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderBox.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 03/01/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Displays the metadata about the chart as well as optionally touch overlay information. 12 | */ 13 | internal struct HeaderBox: ViewModifier where T: CTChartData { 14 | 15 | @ObservedObject private var chartData: T 16 | 17 | init(chartData: T) { 18 | self.chartData = chartData 19 | } 20 | 21 | var titleBox: some View { 22 | VStack(alignment: .leading) { 23 | Text(LocalizedStringKey(chartData.metadata.title)) 24 | .font(chartData.metadata.titleFont) 25 | .foregroundColor(chartData.metadata.titleColour) 26 | Text(LocalizedStringKey(chartData.metadata.subtitle)) 27 | .font(chartData.metadata.subtitleFont) 28 | .foregroundColor(chartData.metadata.subtitleColour) 29 | } 30 | } 31 | var touchOverlay: some View { 32 | VStack(alignment: .trailing) { 33 | if chartData.infoView.isTouchCurrent { 34 | ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in 35 | chartData.infoValueUnit(info: point) 36 | .font(chartData.chartStyle.infoBoxValueFont) 37 | .foregroundColor(chartData.chartStyle.infoBoxValueColour) 38 | chartData.infoDescription(info: point) 39 | .font(chartData.chartStyle.infoBoxDescriptionFont) 40 | .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) 41 | } 42 | } else { 43 | Text("") 44 | .font(chartData.chartStyle.infoBoxValueFont) 45 | Text("") 46 | .font(chartData.chartStyle.infoBoxDescriptionFont) 47 | } 48 | } 49 | } 50 | 51 | internal func body(content: Content) -> some View { 52 | Group { 53 | #if !os(tvOS) 54 | if chartData.isGreaterThanTwo() { 55 | switch chartData.chartStyle.infoBoxPlacement { 56 | case .floating: 57 | VStack(alignment: .leading) { 58 | titleBox 59 | content 60 | } 61 | case .infoBox: 62 | VStack(alignment: .leading) { 63 | titleBox 64 | content 65 | } 66 | case .header: 67 | VStack(alignment: .leading) { 68 | HStack(spacing: 0) { 69 | HStack(spacing: 0) { 70 | titleBox 71 | Spacer() 72 | } 73 | .frame(minWidth: 0, maxWidth: .infinity) 74 | Spacer() 75 | HStack(spacing: 0) { 76 | Spacer() 77 | touchOverlay 78 | } 79 | .frame(minWidth: 0, maxWidth: .infinity) 80 | } 81 | content 82 | } 83 | } 84 | } else { content } 85 | #elseif os(tvOS) 86 | if chartData.isGreaterThanTwo() { 87 | VStack(alignment: .leading) { 88 | titleBox 89 | content 90 | } 91 | } else { content } 92 | #endif 93 | } 94 | } 95 | } 96 | 97 | extension View { 98 | /** 99 | Displays the metadata about the chart. 100 | 101 | Adds a view above the chart that displays the title and subtitle. 102 | If infoBoxPlacement is set to .header then the datapoint info will 103 | be displayed here as well. 104 | 105 | - Parameter chartData: Chart data model. 106 | - Returns: A new view containing the chart with a view above 107 | to display metadata. 108 | */ 109 | public func headerBox(chartData: T) -> some View { 110 | self.modifier(HeaderBox(chartData: chartData)) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineShape.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 24/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Main line shape 12 | */ 13 | internal struct LineShape: Shape where DP: CTStandardDataPointProtocol & IgnoreMe { 14 | 15 | private let dataPoints: [DP] 16 | private let lineType: LineType 17 | private let isFilled: Bool 18 | private let minValue: Double 19 | private let range: Double 20 | private let ignoreZero: Bool 21 | 22 | internal init( 23 | dataPoints: [DP], 24 | lineType: LineType, 25 | isFilled: Bool, 26 | minValue: Double, 27 | range: Double, 28 | ignoreZero: Bool 29 | ) { 30 | self.dataPoints = dataPoints 31 | self.lineType = lineType 32 | self.isFilled = isFilled 33 | self.minValue = minValue 34 | self.range = range 35 | self.ignoreZero = ignoreZero 36 | } 37 | 38 | internal func path(in rect: CGRect) -> Path { 39 | switch lineType { 40 | case .curvedLine: 41 | switch ignoreZero { 42 | case false: 43 | return Path.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 44 | case true: 45 | return Path.curvedLineIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 46 | } 47 | case .line: 48 | switch ignoreZero { 49 | case false: 50 | return Path.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 51 | case true: 52 | return Path.straightLineIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 53 | } 54 | case .stepped: 55 | switch ignoreZero { 56 | case false: 57 | return Path.steppedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 58 | case true: 59 | return Path.steppedLineIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) 60 | } 61 | } 62 | } 63 | } 64 | 65 | /** 66 | Background fill based on the upper and lower values 67 | for a Ranged Line Chart. 68 | */ 69 | internal struct RangedLineFillShape: Shape where DP: CTRangedLineDataPoint & IgnoreMe { 70 | 71 | private let dataPoints: [DP] 72 | private let lineType: LineType 73 | private let minValue: Double 74 | private let range: Double 75 | private let ignoreZero: Bool 76 | 77 | internal init( 78 | dataPoints: [DP], 79 | lineType: LineType, 80 | minValue: Double, 81 | range: Double, 82 | ignoreZero: Bool 83 | ) { 84 | self.dataPoints = dataPoints 85 | self.lineType = lineType 86 | self.minValue = minValue 87 | self.range = range 88 | self.ignoreZero = ignoreZero 89 | } 90 | 91 | internal func path(in rect: CGRect) -> Path { 92 | switch lineType { 93 | case .curvedLine: 94 | switch ignoreZero { 95 | case false: 96 | return Path.curvedLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 97 | case true: 98 | return Path.curvedLineBoxIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 99 | } 100 | case .line: 101 | switch ignoreZero { 102 | case false: 103 | return Path.straightLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 104 | case true: 105 | return Path.straightLineBoxIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 106 | } 107 | case .stepped: 108 | switch ignoreZero { 109 | case false: 110 | return Path.steppedLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 111 | case true: 112 | return Path.steppedLineBoxIgnoreZero(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) 113 | } 114 | } 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/LinearTrendLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearTrendLine.swift 3 | // 4 | // 5 | // Created by Will Dale on 26/03/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | Draws a line across the chart to show the the trend in the data. 12 | */ 13 | internal struct LinearTrendLine: ViewModifier where T: CTLineBarChartDataProtocol & GetDataProtocol { 14 | 15 | @ObservedObject private var chartData: T 16 | private let firstValue: Double 17 | private let lastValue: Double 18 | private let lineColour: ColourStyle 19 | private let strokeStyle: StrokeStyle 20 | 21 | init( 22 | chartData: T, 23 | firstValue: Double, 24 | lastValue: Double, 25 | lineColour: ColourStyle, 26 | strokeStyle: StrokeStyle 27 | ) { 28 | self.chartData = chartData 29 | self.firstValue = firstValue 30 | self.lastValue = lastValue 31 | self.lineColour = lineColour 32 | self.strokeStyle = strokeStyle 33 | } 34 | 35 | internal func body(content: Content) -> some View { 36 | ZStack { 37 | content 38 | if lineColour.colourType == .colour, 39 | let colour = lineColour.colour 40 | { 41 | LinearTrendLineShape(firstValue: firstValue, 42 | lastValue: lastValue, 43 | range: chartData.range, 44 | minValue: chartData.minValue) 45 | .stroke(colour, style: strokeStyle) 46 | } else if lineColour.colourType == .gradientColour, 47 | let colours = lineColour.colours, 48 | let startPoint = lineColour.startPoint, 49 | let endPoint = lineColour.endPoint 50 | { 51 | LinearTrendLineShape(firstValue: firstValue, 52 | lastValue: lastValue, 53 | range: chartData.range, 54 | minValue: chartData.minValue) 55 | .stroke(LinearGradient(gradient: Gradient(colors: colours), 56 | startPoint: startPoint, 57 | endPoint: endPoint), 58 | style: strokeStyle) 59 | } else if lineColour.colourType == .gradientStops, 60 | let stops = lineColour.stops, 61 | let startPoint = lineColour.startPoint, 62 | let endPoint = lineColour.endPoint 63 | { 64 | let stops = GradientStop.convertToGradientStopsArray(stops: stops) 65 | LinearTrendLineShape(firstValue: firstValue, 66 | lastValue: lastValue, 67 | range: chartData.range, 68 | minValue: chartData.minValue) 69 | .stroke(LinearGradient(gradient: Gradient(stops: stops), 70 | startPoint: startPoint, 71 | endPoint: endPoint), 72 | style: strokeStyle) 73 | } 74 | } 75 | } 76 | } 77 | 78 | extension View { 79 | /** 80 | Draws a line across the chart to show the the trend in the data. 81 | 82 | - Parameters: 83 | - chartData: Chart data model. 84 | - firstValue: The value of the leading data point. 85 | - lastValue: The value of the trailnig data point. 86 | - lineColour: Line Colour. 87 | - strokeStyle: Stroke Style. 88 | - Returns: A new view containing the chart with a trend line. 89 | */ 90 | public func linearTrendLine( 91 | chartData: T, 92 | firstValue: Double, 93 | lastValue: Double, 94 | lineColour: ColourStyle = ColourStyle(), 95 | strokeStyle: StrokeStyle = StrokeStyle() 96 | ) -> some View { 97 | self.modifier(LinearTrendLine(chartData: chartData, 98 | firstValue: firstValue, 99 | lastValue: lastValue, 100 | lineColour: lineColour, 101 | strokeStyle: strokeStyle)) 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarChartProtocols.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Chart Data 11 | /** 12 | A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Bar Charts. 13 | */ 14 | public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { 15 | 16 | associatedtype BarStyle: CTBarStyle 17 | 18 | /** 19 | Overall styling for the bars 20 | */ 21 | var barStyle: BarStyle { get set } 22 | } 23 | 24 | 25 | 26 | /** 27 | A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. 28 | */ 29 | public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { 30 | 31 | /** 32 | Grouping data to inform the chart about the relationship between the datapoints. 33 | */ 34 | var groups: [GroupingData] { get set } 35 | } 36 | 37 | /** 38 | A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. 39 | */ 40 | public protocol CTRangedBarChartDataProtocol: CTBarChartDataProtocol {} 41 | 42 | /** 43 | A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Horizontal Bar Charts. 44 | */ 45 | public protocol CTHorizontalBarChartDataProtocol: CTBarChartDataProtocol, isHorizontal {} 46 | 47 | public protocol isHorizontal {} 48 | 49 | // MARK: - Style 50 | /** 51 | A protocol to extend functionality of `CTLineBarChartStyle` specifically for Bar Charts. 52 | */ 53 | public protocol CTBarChartStyle: CTLineBarChartStyle {} 54 | 55 | public protocol CTBarStyle: CTBarColourProtocol, Hashable { 56 | /// How much of the available width to use. 0...1 57 | var barWidth: CGFloat { get set } 58 | /// Corner radius of the bar shape. 59 | var cornerRadius: CornerRadius { get set } 60 | /// Where to get the colour data from. 61 | var colourFrom: ColourFrom { get set } 62 | /// Drawing style of the fill. 63 | var colour: ColourStyle { get set } 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | // MARK: - DataSet 72 | /** 73 | A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Standard Bar Charts. 74 | */ 75 | public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol { 76 | /** 77 | Label to display in the legend. 78 | */ 79 | var legendTitle: String { get set } 80 | } 81 | 82 | /** 83 | A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Multi Part Bar Charts. 84 | */ 85 | public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol { 86 | /** 87 | Title of the data set. 88 | 89 | This is used as an x axis label. 90 | */ 91 | var setTitle: String { get set } 92 | } 93 | 94 | /** 95 | A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Ranged Bar Charts. 96 | */ 97 | public protocol CTRangedBarChartDataSet: CTStandardBarChartDataSet {} 98 | 99 | 100 | 101 | 102 | 103 | // MARK: - DataPoints 104 | /** 105 | A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. 106 | 107 | This is base to specify conformance for generics. 108 | */ 109 | public protocol CTBarDataPointBaseProtocol: CTLineBarDataPointProtocol {} 110 | 111 | /** 112 | A protocol to a standard colour scheme for bar charts. 113 | */ 114 | public protocol CTBarColourProtocol { 115 | /// Drawing style of the range fill. 116 | var colour: ColourStyle { get set } 117 | } 118 | 119 | /** 120 | A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for standard Bar Charts. 121 | */ 122 | public protocol CTStandardBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataPointProtocol, CTBarColourProtocol, CTnotRanged {} 123 | 124 | /** 125 | A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for standard Bar Charts. 126 | */ 127 | public protocol CTRangedBarDataPoint: CTBarDataPointBaseProtocol, CTRangeDataPointProtocol, CTBarColourProtocol, CTisRanged {} 128 | 129 | /** 130 | A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for multi part Bar Charts. 131 | i.e: Grouped or Stacked 132 | */ 133 | public protocol CTMultiBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataPointProtocol, CTnotRanged { 134 | 135 | /** 136 | For grouping data points together so they can be drawn in the correct groupings. 137 | */ 138 | var group: GroupingData { get set } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineChartProtocols.swift 3 | // 4 | // 5 | // Created by Will Dale on 02/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Chart Data 11 | /** 12 | A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Line Charts. 13 | */ 14 | public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { 15 | 16 | /// A type representing opaque View 17 | associatedtype Points: View 18 | 19 | /// A type representing opaque View 20 | associatedtype Access: View 21 | 22 | /** 23 | Displays Shapes over the data points. 24 | 25 | - Returns: Relevent view containing point markers based the chosen parameters. 26 | */ 27 | func getPointMarker() -> Points 28 | 29 | /** 30 | Ensures that line charts have an accessibility layer. 31 | 32 | - Returns: A view with invisible rectangles over the data point. 33 | */ 34 | func getAccessibility() -> Access 35 | } 36 | 37 | // MARK: - Style 38 | /** 39 | A protocol to extend functionality of `CTLineBarChartStyle` specifically for Line Charts. 40 | */ 41 | public protocol CTLineChartStyle: CTLineBarChartStyle {} 42 | 43 | /** 44 | Protocol to set up the styling for individual lines. 45 | */ 46 | public protocol CTLineStyle { 47 | /// Drawing style of the line. 48 | var lineType: LineType { get set } 49 | 50 | /// Colour styling of the line. 51 | var lineColour: ColourStyle { get set } 52 | 53 | /** 54 | Styling for stroke 55 | 56 | Replica of Apple’s StrokeStyle that conforms to Hashable 57 | */ 58 | var strokeStyle: Stroke { get set } 59 | 60 | /** 61 | Whether the chart should skip data points who's value is 0. 62 | 63 | This might be useful when showing trends over time but each day does not necessarily have data. 64 | 65 | The default is false. 66 | */ 67 | var ignoreZero: Bool { get set } 68 | } 69 | 70 | /** 71 | A protocol to extend functionality of `CTLineStyle` specifically for Ranged Line Charts. 72 | */ 73 | public protocol CTRangedLineStyle: CTLineStyle { 74 | /// Drawing style of the range fill. 75 | var fillColour: ColourStyle { get set } 76 | } 77 | 78 | 79 | 80 | // MARK: - DataSet 81 | /** 82 | A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. 83 | */ 84 | public protocol CTLineChartDataSet: CTSingleDataSetProtocol { 85 | 86 | /// A type representing colour styling 87 | associatedtype Styling: CTLineStyle 88 | 89 | /** 90 | Label to display in the legend. 91 | */ 92 | var legendTitle: String { get set } 93 | 94 | /** 95 | Sets the style for the Data Set (as opposed to Chart Data Style). 96 | */ 97 | var style: Styling { get set } 98 | 99 | /** 100 | Sets the look of the markers over the data points. 101 | 102 | The markers are layed out when the ViewModifier `PointMarkers` 103 | is applied. 104 | */ 105 | var pointStyle: PointStyle { get set } 106 | } 107 | 108 | /** 109 | A protocol to extend functionality of `CTLineChartDataSet` specifically for Ranged Line Charts. 110 | */ 111 | public protocol CTRangedLineChartDataSet: CTLineChartDataSet { 112 | 113 | /** 114 | Label to display in the legend for the range area.. 115 | */ 116 | var legendFillTitle: String { get set } 117 | } 118 | 119 | /** 120 | A protocol to extend functionality of `CTMultiDataSetProtocol` specifically for Multi Line Charts. 121 | */ 122 | public protocol CTMultiLineChartDataSet: CTMultiDataSetProtocol {} 123 | 124 | 125 | 126 | // MARK: - Data Point 127 | /** 128 | A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for Line and Bar Charts. 129 | */ 130 | public protocol CTLineDataPointProtocol: CTLineBarDataPointProtocol { 131 | var pointColour: PointColour? { get set } 132 | } 133 | 134 | /** 135 | A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Ranged Line Charts. 136 | */ 137 | public protocol CTStandardLineDataPoint: CTLineDataPointProtocol, CTStandardDataPointProtocol, CTnotRanged {} 138 | 139 | /** 140 | A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Ranged Line Charts. 141 | */ 142 | public protocol CTRangedLineDataPoint: CTLineDataPointProtocol, CTStandardDataPointProtocol, CTRangeDataPointProtocol, CTisRanged {} 143 | 144 | 145 | 146 | 147 | public protocol IgnoreMe { 148 | var ignoreMe: Bool { get set } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackedBarChart.swift 3 | // 4 | // 5 | // Created by Will Dale on 12/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | View for creating a stacked bar chart. 12 | 13 | Uses `StackedBarChartData` data model. 14 | 15 | # Declaration 16 | 17 | ``` 18 | StackedBarChart(chartData: data) 19 | ``` 20 | 21 | # View Modifiers 22 | 23 | The order of the view modifiers is some what important 24 | as the modifiers are various types for stacks that wrap 25 | around the previous views. 26 | ``` 27 | .touchOverlay(chartData: data) 28 | .averageLine(chartData: data, 29 | strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10])) 30 | .yAxisPOI(chartData: data, 31 | markerName: "50", 32 | markerValue: 50, 33 | lineColour: Color.blue, 34 | strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10])) 35 | .xAxisGrid(chartData: data) 36 | .yAxisGrid(chartData: data) 37 | .xAxisLabels(chartData: data) 38 | .yAxisLabels(chartData: data) 39 | .infoBox(chartData: data) 40 | .floatingInfoBox(chartData: data) 41 | .headerBox(chartData: data) 42 | .legends(chartData: data) 43 | ``` 44 | */ 45 | public struct StackedBarChart: View where ChartData: StackedBarChartData { 46 | 47 | @ObservedObject private var chartData: ChartData 48 | @State private var timer: Timer? 49 | 50 | /// Initialises a stacked bar chart view. 51 | /// - Parameters: 52 | /// - chartData: Must be StackedBarChartData model. 53 | public init(chartData: ChartData) { 54 | self.chartData = chartData 55 | } 56 | 57 | @State private var startAnimation: Bool = false 58 | 59 | public var body: some View { 60 | if chartData.isGreaterThanTwo() { 61 | HStack(alignment: .bottom, spacing: 0) { 62 | ForEach(chartData.dataSets.dataSets) { dataSet in 63 | GeometryReader { geo in 64 | StackElementSubView(dataSet: dataSet, 65 | specifier: chartData.infoView.touchSpecifier, 66 | formatter: chartData.infoView.touchFormatter) 67 | .clipShape(RoundedRectangleBarShape(chartData.barStyle.cornerRadius)) 68 | 69 | .frame(width: BarLayout.barWidth(geo.size.width, chartData.barStyle.barWidth)) 70 | .frame(height: frameAnimationValue(dataSet.maxValue(), height: geo.size.height)) 71 | .offset(offsetAnimationValue(dataSet.maxValue(), size: geo.size)) 72 | 73 | .animation(.default, value: chartData.dataSets) 74 | .background(Color(.gray).opacity(0.000000001)) 75 | .animateOnAppear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 76 | self.startAnimation = true 77 | } 78 | .animateOnDisappear(disabled: chartData.disableAnimation, using: chartData.chartStyle.globalAnimation) { 79 | self.startAnimation = false 80 | } 81 | .accessibilityLabel(LocalizedStringKey(chartData.metadata.title)) 82 | } 83 | } 84 | } 85 | .layoutNotifier(timer) 86 | } else { CustomNoDataView(chartData: chartData) } 87 | } 88 | 89 | func animationValue(_ dsMax: Double, _ dataMax: Double) -> CGFloat { 90 | let value = divideByZeroProtection(CGFloat.self, dsMax, dataMax) 91 | if chartData.disableAnimation { 92 | return value 93 | } else { 94 | return startAnimation ? value : 0 95 | } 96 | } 97 | 98 | func frameAnimationValue(_ value: Double, height: CGFloat) -> CGFloat { 99 | let value = BarLayout.barHeight(height, value, chartData.maxValue) 100 | if chartData.disableAnimation { 101 | return value 102 | } else { 103 | return startAnimation ? value : 0 104 | } 105 | } 106 | 107 | func offsetAnimationValue(_ value: Double, size: CGSize) -> CGSize { 108 | let value = BarLayout.barOffset(size, chartData.barStyle.barWidth, value, chartData.maxValue) 109 | let zero = BarLayout.barOffset(size, chartData.barStyle.barWidth, 0, 0) 110 | if chartData.disableAnimation { 111 | return value 112 | } else { 113 | return startAnimation ? value : zero 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Marker.swift 3 | // 4 | // 5 | // Created by Will Dale on 30/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Generic line, drawn horizontally across the chart. 11 | internal struct HorizontalMarker: Shape where ChartData: CTLineBarChartDataProtocol & PointOfInterestProtocol { 12 | 13 | @ObservedObject private var chartData: ChartData 14 | private let value: Double 15 | private let range: Double 16 | private let minValue: Double 17 | 18 | internal init( 19 | chartData: ChartData, 20 | value: Double, 21 | range: Double, 22 | minValue: Double 23 | ) { 24 | self.chartData = chartData 25 | self.value = value 26 | self.range = range 27 | self.minValue = minValue 28 | } 29 | 30 | internal func path(in rect: CGRect) -> Path { 31 | let pointY: CGFloat = chartData.poiValueLabelPositionCenter(frame: rect, markerValue: value, minValue: minValue, range: range).y 32 | 33 | let firstPoint = CGPoint(x: 0, y: pointY) 34 | let nextPoint = CGPoint(x: rect.width, y: pointY) 35 | 36 | var path = Path() 37 | path.move(to: firstPoint) 38 | path.addLine(to: nextPoint) 39 | return path 40 | } 41 | } 42 | 43 | /// Generic line, drawn vertically across the chart. 44 | internal struct VerticalMarker: Shape where ChartData: CTLineBarChartDataProtocol & PointOfInterestProtocol { 45 | 46 | @ObservedObject private var chartData: ChartData 47 | private let value: Double 48 | private let range: Double 49 | private let minValue: Double 50 | 51 | internal init( 52 | chartData: ChartData, 53 | value: Double, 54 | range: Double, 55 | minValue: Double 56 | ) { 57 | self.chartData = chartData 58 | self.value = value 59 | self.range = range 60 | self.minValue = minValue 61 | } 62 | 63 | internal func path(in rect: CGRect) -> Path { 64 | let pointX: CGFloat = chartData.poiValueLabelPositionCenter(frame: rect, markerValue: value, minValue: minValue, range: range).x 65 | 66 | let firstPoint = CGPoint(x: pointX, y: 0) 67 | let nextPoint = CGPoint(x: pointX, y: rect.height) 68 | 69 | var path = Path() 70 | path.move(to: firstPoint) 71 | path.addLine(to: nextPoint) 72 | return path 73 | } 74 | } 75 | 76 | 77 | /// Generic line, drawn vertically across the chart. 78 | internal struct VerticalAbscissaMarker: Shape where ChartData: CTLineBarChartDataProtocol & PointOfInterestProtocol { 79 | 80 | @ObservedObject private var chartData: ChartData 81 | private let markerValue: Int 82 | private let dataPointCount: Int 83 | 84 | internal init( 85 | chartData: ChartData, 86 | markerValue: Int, 87 | dataPointCount: Int 88 | ) { 89 | self.chartData = chartData 90 | self.markerValue = markerValue 91 | self.dataPointCount = dataPointCount 92 | } 93 | 94 | internal func path(in rect: CGRect) -> Path { 95 | let pointX: CGFloat = chartData.poiAbscissaValueLabelPositionCenter(frame: rect, markerValue: markerValue, count: dataPointCount).x 96 | let firstPoint = CGPoint(x: pointX, y: 0) 97 | let nextPoint = CGPoint(x: pointX, y: rect.height) 98 | 99 | var path = Path() 100 | path.move(to: firstPoint) 101 | path.addLine(to: nextPoint) 102 | return path 103 | } 104 | } 105 | 106 | /// Generic line, drawn horizontally across the chart. 107 | internal struct HorizontalAbscissaMarker: Shape where ChartData: CTLineBarChartDataProtocol & PointOfInterestProtocol { 108 | 109 | @ObservedObject private var chartData: ChartData 110 | private let markerValue: Int 111 | private let dataPointCount: Int 112 | 113 | internal init( 114 | chartData: ChartData, 115 | markerValue: Int, 116 | dataPointCount: Int 117 | ) { 118 | self.chartData = chartData 119 | self.markerValue = markerValue 120 | self.dataPointCount = dataPointCount 121 | } 122 | 123 | internal func path(in rect: CGRect) -> Path { 124 | let pointY: CGFloat = chartData.poiAbscissaValueLabelPositionCenter(frame: rect, markerValue: markerValue, count: dataPointCount).y 125 | let firstPoint = CGPoint(x: 0, y: pointY) 126 | let nextPoint = CGPoint(x: rect.width, y: pointY) 127 | 128 | var path = Path() 129 | path.move(to: firstPoint) 130 | path.addLine(to: nextPoint) 131 | return path 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchOverlay.swift 3 | // LineChart 4 | // 5 | // Created by Will Dale on 29/12/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if !os(tvOS) 11 | /** 12 | Finds the nearest data point and displays the relevent information. 13 | */ 14 | internal struct TouchOverlay: ViewModifier where T: CTChartData { 15 | 16 | @ObservedObject private var chartData: T 17 | let minDistance: CGFloat 18 | private let specifier: String 19 | private let formatter: NumberFormatter? 20 | private let unit: TouchUnit 21 | 22 | internal init( 23 | chartData: T, 24 | specifier: String, 25 | formatter: NumberFormatter?, 26 | unit: TouchUnit, 27 | minDistance: CGFloat 28 | ) { 29 | self.chartData = chartData 30 | self.minDistance = minDistance 31 | self.specifier = specifier 32 | self.formatter = formatter 33 | self.unit = unit 34 | } 35 | 36 | internal func body(content: Content) -> some View { 37 | Group { 38 | if chartData.isGreaterThanTwo() { 39 | GeometryReader { geo in 40 | ZStack { 41 | content 42 | .gesture( 43 | DragGesture(minimumDistance: minDistance, coordinateSpace: .local) 44 | .onChanged { (value) in 45 | chartData.setTouchInteraction(touchLocation: value.location, 46 | chartSize: geo.frame(in: .local)) 47 | } 48 | .onEnded { _ in 49 | chartData.infoView.isTouchCurrent = false 50 | chartData.infoView.touchOverlayInfo = [] 51 | } 52 | ) 53 | if chartData.infoView.isTouchCurrent { 54 | chartData.getTouchInteraction(touchLocation: chartData.infoView.touchLocation, 55 | chartSize: geo.frame(in: .local)) 56 | } 57 | } 58 | } 59 | } else { content } 60 | } 61 | .onAppear { 62 | self.chartData.infoView.touchSpecifier = specifier 63 | self.chartData.infoView.touchFormatter = formatter 64 | self.chartData.infoView.touchUnit = unit 65 | } 66 | } 67 | } 68 | #endif 69 | 70 | extension View { 71 | #if !os(tvOS) 72 | /** 73 | Adds touch interaction with the chart. 74 | 75 | Adds an overlay to detect touch and display the relivent information from the nearest data point. 76 | 77 | - Requires: 78 | If ChartStyle --> infoBoxPlacement is set to .header 79 | then `.headerBox` is required. 80 | 81 | If ChartStyle --> infoBoxPlacement is set to .infoBox 82 | then `.infoBox` is required. 83 | 84 | If ChartStyle --> infoBoxPlacement is set to .floating 85 | then `.floatingInfoBox` is required. 86 | 87 | - Attention: 88 | Unavailable in tvOS 89 | 90 | - Parameters: 91 | - chartData: Chart data model. 92 | - specifier: Decimal precision for labels. 93 | - unit: Unit to put before or after the value. 94 | - minDistance: The distance that the touch event needs to travel to register. 95 | - Returns: A new view containing the chart with a touch overlay. 96 | */ 97 | public func touchOverlay( 98 | chartData: T, 99 | specifier: String = "%.0f", 100 | formatter: NumberFormatter? = nil, 101 | unit: TouchUnit = .none, 102 | minDistance: CGFloat = 0 103 | ) -> some View { 104 | self.modifier(TouchOverlay(chartData: chartData, 105 | specifier: specifier, 106 | formatter: formatter, 107 | unit: unit, 108 | minDistance: minDistance)) 109 | } 110 | #elseif os(tvOS) 111 | /** 112 | Adds touch interaction with the chart. 113 | 114 | - Attention: 115 | Unavailable in tvOS 116 | */ 117 | public func touchOverlay( 118 | chartData: T, 119 | specifier: String = "%.0f", 120 | formatter: NumberFormatter? = nil, 121 | unit: TouchUnit = .none, 122 | minDistance: CGFloat = 0 123 | ) -> some View { 124 | self.modifier(EmptyModifier()) 125 | } 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | @testable import SwiftUICharts 4 | 5 | final class LineChartPathTests: XCTestCase { 6 | 7 | let chartData = LineChartData(dataSets: LineDataSet(dataPoints: [ 8 | LineChartDataPoint(value: 0), 9 | LineChartDataPoint(value: 25), 10 | LineChartDataPoint(value: 50), 11 | LineChartDataPoint(value: 75), 12 | LineChartDataPoint(value: 100) 13 | ])) 14 | 15 | let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100) 16 | let touchLocation: CGPoint = CGPoint(x: 25, y: 25) 17 | 18 | func testGetIndicatorLocation() { 19 | 20 | let test = LineChartData.getIndicatorLocation(rect: rect, 21 | dataPoints: chartData.dataSets.dataPoints, 22 | touchLocation: touchLocation, 23 | lineType: .line, 24 | minValue: chartData.minValue, 25 | range: chartData.range, 26 | ignoreZero: false) 27 | 28 | XCTAssertEqual(test.x, 25, accuracy: 0.1) 29 | XCTAssertEqual(test.y, 75, accuracy: 0.1) 30 | } 31 | 32 | 33 | func testGetPercentageOfPath() { 34 | 35 | let path = Path.straightLine(rect: rect, 36 | dataPoints: chartData.dataSets.dataPoints, 37 | minValue: chartData.minValue, 38 | range: chartData.range, 39 | isFilled: false) 40 | 41 | let test = LineChartData.getPercentageOfPath(path: path, touchLocation: touchLocation) 42 | 43 | XCTAssertEqual(test, 0.25, accuracy: 0.1) 44 | } 45 | 46 | func testGetTotalLength() { 47 | 48 | let path = Path.straightLine(rect: rect, 49 | dataPoints: chartData.dataSets.dataPoints, 50 | minValue: chartData.minValue, 51 | range: chartData.range, 52 | isFilled: false) 53 | 54 | let test = LineChartData.getTotalLength(of: path) 55 | 56 | XCTAssertEqual(test, 141.42, accuracy: 0.01) 57 | } 58 | 59 | func testGetLengthToTouch() { 60 | 61 | let path = Path.straightLine(rect: rect, 62 | dataPoints: chartData.dataSets.dataPoints, 63 | minValue: chartData.minValue, 64 | range: chartData.range, 65 | isFilled: false) 66 | 67 | let test = LineChartData.getLength(to: touchLocation, on: path) 68 | 69 | XCTAssertEqual(test, 35.35, accuracy: 0.01) 70 | } 71 | 72 | func testRelativePoint() { 73 | 74 | let pointOne = CGPoint(x: 0.0, y: 0.0) 75 | let pointTwo = CGPoint(x: 100, y: 100) 76 | 77 | let test = LineChartData.relativePoint(from: pointOne, to: pointTwo, touchX: touchLocation.x) 78 | 79 | XCTAssertEqual(test.x, 25, accuracy: 0.01) 80 | XCTAssertEqual(test.y, 25, accuracy: 0.01) 81 | } 82 | 83 | func testDistanceToTouch() { 84 | 85 | let pointOne = CGPoint(x: 0.0, y: 0.0) 86 | let pointTwo = CGPoint(x: 100, y: 100) 87 | 88 | let test = LineChartData.distanceToTouch(from: pointOne, to: pointTwo, touchX: touchLocation.x) 89 | 90 | XCTAssertEqual(test, 35.355, accuracy: 0.01) 91 | } 92 | 93 | func testDistance() { 94 | 95 | let pointOne = CGPoint(x: 0.0, y: 0.0) 96 | let pointTwo = CGPoint(x: 100, y: 100) 97 | 98 | let test = LineChartData.distance(from: pointOne, to: pointTwo) 99 | 100 | XCTAssertEqual(test, 141.421356237309, accuracy: 0.01) 101 | } 102 | 103 | func testGetLocationOnPath() { 104 | 105 | let path = Path.straightLine(rect: rect, 106 | dataPoints: chartData.dataSets.dataPoints, 107 | minValue: chartData.minValue, 108 | range: chartData.range, 109 | isFilled: false) 110 | 111 | let test = LineChartData.locationOnPath(0.5, path) 112 | 113 | XCTAssertEqual(test.x, 50, accuracy: 0.1) 114 | XCTAssertEqual(test.y, 50, accuracy: 0.1) 115 | } 116 | } 117 | --------------------------------------------------------------------------------