├── .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 |
--------------------------------------------------------------------------------