diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist
new file mode 100644
index 00000000..a54214c7
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ classNames
+
+ LineChartPathTests
+
+ testGetIndicatorLocationPerformance()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.000206
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/Info.plist
new file mode 100644
index 00000000..da246a50
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/Info.plist
@@ -0,0 +1,40 @@
+
+
+
+
+ runDestinationsByUUID
+
+ 4224B4FE-4B1F-4DC7-9D83-A7B7F9824962
+
+ localComputer
+
+ busSpeedInMHz
+ 100
+ cpuCount
+ 1
+ cpuKind
+ Quad-Core Intel Core i7
+ cpuSpeedInMHz
+ 2800
+ logicalCPUCoresPerPackage
+ 8
+ modelCode
+ MacBookPro11,5
+ physicalCPUCoresPerPackage
+ 4
+ platformIdentifier
+ com.apple.platform.macosx
+
+ targetArchitecture
+ x86_64
+ targetDevice
+
+ modelCode
+ iPhone13,3
+ platformIdentifier
+ com.apple.platform.iphonesimulator
+
+
+
+
+
diff --git a/Package.swift b/Package.swift
index b2861104..35d37c1a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "SwiftUICharts",
+ defaultLocalization: "en",
platforms: [
.macOS(.v11), .iOS(.v14), .watchOS(.v7), .tvOS(.v14)
],
diff --git a/README.md b/README.md
index 5da61500..08de1a87 100644
--- a/README.md
+++ b/README.md
@@ -1,684 +1,500 @@
# SwiftUICharts
-A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS.
+A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS and has accessibility features built in.
[Demo Project](https://github.com/willdale/SwiftUICharts-Demo)
-## Examples
-### Line Chart
+## Chart Types
-![Example of Line Chart](Resources/LineOne.png)
+- [Line Chart](#Line-Chart)
+- [Filled Line Chart](#Filled-Line-Chart)
+- [Multi Line Chart](#Multi-Line-Chart)
+- [Ranged Line Chart](#Ranged-Line-Chart)
-#### View
-```swift
-LineChart()
- .touchOverlay()
- .pointMarkers()
- .averageLine(strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
- .yAxisPOI(markerName: "50", markerValue: 50, lineColour: Color(red: 0.25, green: 0.25, blue: 1.0), strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
- .xAxisGrid()
- .yAxisGrid()
- .xAxisLabels()
- .yAxisLabels()
- .headerBox()
- .legends()
- .environmentObject(data)
-```
+- [Bar Chart](#Bar-Chart)
+- [Ranged Bar Chart](#Ranged-Bar-Chart)
+- [Grouped Bar Chart](#Grouped-Bar-Chart)
+- [Stacked Bar Chart](#Stacked-Bar-Chart)
-#### Data Model
+- [Pie Chart](#Pie-Chart)
+- [Doughnut Chart](#Doughnut-Chart)
-```swift
-static func weekOfData() -> ChartData {
-
- let data : [ChartDataPoint] = [
- ChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"),
- ChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"),
- ChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"),
- ChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"),
- ChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"),
- ChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"),
- ChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday")
- ]
-
- let metadata : ChartMetadata = ChartMetadata(title : "Test Data",
- subtitle : "A weeks worth",
- lineLegend : "Data")
-
- let labels : [String] = ["Mon", "Thu", "Sun"]
- let gridStyle : GridStyle = GridStyle(lineColour : Color(.lightGray).opacity(0.25),
- lineWidth : 1,
- dash: [CGFloat]())
-
- let chartStyle : ChartStyle = ChartStyle(infoBoxPlacement: .header,
- yAxisGridStyle: GridStyle(lineColour: Color.primary.opacity(0.5)))
-
- let lineStyle : LineStyle = LineStyle(colours : [Color(red: 1.0, green: 0.15, blue: 0.15), Color(red: 1.0, green: 0.35, blue: 0.35)],
- startPoint : .leading,
- endPoint : .trailing,
- lineType : .curvedLine,
- strokeStyle : StrokeStyle(lineWidth: 3,
- lineCap: .round,
- lineJoin: .round))
-
- let pointStyle : PointStyle = PointStyle(pointSize: 9, borderColour: Color.primary, lineWidth: 2, pointType: .outline, pointShape: .circle)
-
- return ChartData(dataPoints : data,
- metadata : metadata,
- xAxisLabels : labels,
- chartStyle : chartStyle,
- lineStyle : lineStyle,
- pointStyle : pointStyle
- )
-}
+### Line Charts
-```
+#### Line Chart
+![Example of Line Chart](Resources/images/LineCharts/LineChart.png)
+Uses `LineChartData` data model.
-![Example of Line Chart](Resources/LineTwo.png)
+```swift
+LineChart(chartData: LineChartData)
+```
-#### View
+---
-```swift
-LineChart()
- .touchOverlay(specifier: "%.2f")
- .yAxisGrid()
- .xAxisLabels()
- .yAxisLabels()
- .headerBox()
- .legends()
- .environmentObject(data)
-```
+#### Filled Line Chart
+![Example of Filled Line Chart](Resources/images/LineCharts/FilledLineChart.png)
-#### Data Model
+Uses `LineChartData` data model.
```swift
-static func yearOfDataMonthlyAverage() -> ChartData {
-
- var data : [ChartDataPoint] = []
- let calendar = Calendar.current
- let date = Date()
-
- for index in 1...365 {
- let value: Double = Double(Int.random(in: -100...100))
- let date = calendar.date(byAdding: .day, value: index, to: date)
- data.append(ChartDataPoint(value: value, date: date))
- }
-
- let metadata : ChartMetadata = ChartMetadata(title : "Test Data",
- subtitle : "A years worth - Monthly Average",
- lineLegend : "Data")
-
- let labels : [String] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
-
- let chartStyle : ChartStyle = ChartStyle(infoBoxPlacement: .header,
- yAxisGridStyle: GridStyle(lineColour: Color.primary.opacity(0.5)))
-
- let lineStyle : LineStyle = LineStyle(colour: Color(red: 0.15, green: 0.15, blue: 1.0),
- lineType: .curvedLine,
- strokeStyle: StrokeStyle(lineWidth: 3,
- lineCap: .round,
- lineJoin: .round))
-
- return ChartData(dataPoints : data,
- metadata : metadata,
- xAxisLabels : labels,
- chartStyle : chartStyle,
- lineStyle : lineStyle,
- calculations : .averageMonth)
-}
+FilledLineChart(chartData: LineChartData)
```
-### Bar Chart
+---
-![Example of Line Chart](Resources/BarOne.png)
+#### Multi Line Chart
+![Example of Multi Line Chart](Resources/images/LineCharts/MultiLineChart.png)
-#### View
+Uses `MultiLineChartData` data model.
```swift
-BarChart()
- .touchOverlay()
- .averageLine(markerName: "Average", lineColour: Color.primary, strokeStyle: StrokeStyle(lineWidth: 2, dash: [5, 10]))
- .yAxisGrid()
- .xAxisLabels()
- .yAxisLabels()
- .headerBox()
- .legends()
- .environmentObject(data)
+MultiLineChart(chartData: MultiLineChartData)
```
-#### Data Model
-```swift
-static func weekOfData() -> ChartData {
-
- let data : [ChartDataPoint] = [
- ChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"),
- ChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"),
- ChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"),
- ChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"),
- ChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"),
- ChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"),
- ChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday")
- ]
-
- let metadata : ChartMetadata = ChartMetadata(title : "Test Data",
- subtitle : "A weeks worth",
- lineLegend : "Data")
-
- let gridStyle : GridStyle = GridStyle(lineColour : Color(.lightGray),
- lineWidth : 1)
-
- let chartStyle : ChartStyle = ChartStyle(infoBoxPlacement: .header,
- xAxisGridStyle : gridStyle,
- yAxisGridStyle : gridStyle)
-
- let barStyle : BarStyle = BarStyle(barWidth: 0.5,
- colourFrom: .barStyle,
- colours: [Color(red: 1.0, green: 0.15, blue: 0.15),
- Color(red: 1.0, green: 0.35, blue: 0.35)],
- startPoint: .bottom,
- endPoint: .top)
-
- return ChartData(dataPoints : data,
- metadata : metadata,
- chartStyle : chartStyle,
- barStyle : barStyle)
-}
+---
+
+#### Ranged Line Chart
+![Example of Ranged Line Chart](Resources/images/LineCharts/RangedLineChart.png)
+
+Uses `RangedLineChart` data model.
+
+```swift
+RangedLineChart(chartData: RangedLineChartData)
```
-![Example of Line Chart](Resources/BarTwo.png)
+---
-#### View
+### Bar Charts
-```swift
-BarChart()
- .touchOverlay()
- .averageLine(markerName: "Average", lineColour: Color.primary, strokeStyle: StrokeStyle(lineWidth: 2, dash: [5, 10]))
- .yAxisGrid()
- .xAxisLabels()
- .yAxisLabels()
- .headerBox()
- .legends()
- .environmentObject(data)
-```
+#### Bar Chart
+![Example of Bar Chart](Resources/images/BarCharts/BarChart.png)
-#### Data Model
+Uses `BarChartData` data model.
```swift
-static func weekOfData() -> ChartData {
-
- let data : [ChartDataPoint] = [
- ChartDataPoint(value: 70, xAxisLabel: "M", pointLabel: "Monday" , colour: Color(.systemRed)),
- ChartDataPoint(value: 40, xAxisLabel: "T", pointLabel: "Tuesday" , colour: Color(.systemBlue)),
- ChartDataPoint(value: 90, xAxisLabel: "W", pointLabel: "Wednesday", colour: Color(.systemGreen)),
- ChartDataPoint(value: 35, xAxisLabel: "T", pointLabel: "Thursday" , colour: Color(.systemOrange)),
- ChartDataPoint(value: 60, xAxisLabel: "F", pointLabel: "Friday" , colour: Color(.systemTeal)),
- ChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday" , colour: Color(.systemPurple)),
- ChartDataPoint(value: 40, xAxisLabel: "S", pointLabel: "Sunday" , colour: Color(.systemYellow))
- ]
-
- let metadata : ChartMetadata = ChartMetadata(title : "Test Data",
- subtitle : "A weeks worth",
- lineLegend : "Data")
-
-
- let gridStyle : GridStyle = GridStyle(lineColour : Color(.lightGray),
- lineWidth : 1)
-
- let chartStyle : ChartStyle = ChartStyle(infoBoxPlacement: .header,
- xAxisGridStyle : gridStyle,
- yAxisGridStyle : gridStyle,
- xAxisLabelsFrom: .dataPoint)
-
- let barStyle : BarStyle = BarStyle(barWidth: 1,
- colourFrom: .dataPoints,
- colours: [Color(red: 1.0, green: 0.15, blue: 0.15),
- Color(red: 1.0, green: 0.35, blue: 0.35)],
- startPoint: .bottom,
- endPoint: .top)
-
- return ChartData(dataPoints : data,
- metadata : metadata,
- chartStyle : chartStyle,
- barStyle : barStyle
- )
-}
+BarChart(chartData: BarChartData)
```
-
-## Documentation
+---
+#### Range Bar Chart
+![Example of Range Bar Chart](Resources/images/BarCharts/RangeBarChart.png)
-All data and most styling is passed into the view by an Environment Object. See [ChartData](#ChartData).
+Uses `RangedBarChartData` data model.
```swift
-.environmentObject(data)
+RangedBarChart(chartData: RangedBarChartData)
```
-[View Modifiers](#View-Modifiers)
-- [Touch Overlay](#Touch-Overlay)
-- [Point Markers](#Point-Markers)
-- [Average Line](#Average-Line)
-- [Y Axis Point Of Interest](#Y-Axis-Point-Of-Interest)
-- [X Axis Grid](#X-Axis-Grid)
-- [Y Axis Grid](#Y-Axis-Grid)
-- [X Axis Labels](#X-Axis-Labels)
-- [Y Axis Labels](#Y-Axis-Labels)
-- [Header Box](#Header-Box)
-- [Legends](#Legends)
-[Data Models](#Data-Models)
-- [Chart Data](#ChartData)
-- [Chart Data Point](#ChartDataPoint)
-- [Chart Metadata](#ChartMetadata)
-- [Chart Style](#ChartStyle)
- - [Grid Style](#GridStyle)
- - [XAxisLabelSetup](#XAxisLabelSetup)
- - [YAxisLabelSetup](#YAxisLabelSetup)
-- [Line Style](#LineStyle)
-- [Bar Style](#BarStyle)
-- [Point Style](#PointStyle)
+---
-## View Modifiers
-### Touch Overlay
-Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information.
+#### Grouped Bar Chart
+![Example of Grouped Bar Chart](Resources/images/BarCharts/GroupedBarChart.png)
-The location of the info box is set in [ChartStyle](#ChartStyle).
+Uses `GroupedBarChartData` data model.
```swift
-.touchOverlay(specifier: String)
+GroupedBarChart(chartData: GroupedBarChartData)
```
-- specifier: Decimal precision for labels
-Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle)
-### Point Markers
+---
-Lays out markers over each of the data point.
+
+#### Stacked Bar Chart
+![Example of Stacked Bar Chart](Resources/images/BarCharts/StackedBarChart.png)
+
+Uses `StackedBarChartData` data model.
```swift
-.pointMarkers()
+StackedBarChart(chartData: StackedBarChartData)
```
-Setup within [ChartData](#ChartData) --> [PointStyle](#PointStyle)
+---
-### Average Line
-Shows a marker line at the average of all the data points.
+### Pie Charts
+
+#### Pie Chart
+![Example of Pie Chart](Resources/images/PieCharts/PieChart.png)
+
+Uses `PieChartData` data model.
```swift
-.averageLine(markerName : String = "Average",
- lineColour : Color = Color.primary,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2,
- lineCap: .round,
- lineJoin: .round,
- miterLimit: 10,
- dash: [CGFloat](),
- dashPhase: 0)
+PieChart(chartData: PieChartData)
```
-- markerName: Title of marker, for the legend
-- lineColour: Line Colour
-- strokeStyle: Style of Stroke
-### Y Axis Point Of Interest
+---
-Configurable Point of interest
+
+#### Doughnut Chart
+![Example of Doughnut Chart](Resources/images/PieCharts/DoughnutChart.png)
+
+Uses `DoughnutChartData` data model.
```swift
-.yAxisPOI(markerName : String = "Average",
- lineColour : Color = Color.primary,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2,
- lineCap: .round,
- lineJoin: .round,
- miterLimit: 10,
- dash: [CGFloat](),
- dashPhase: 0)
+DoughnutChart(chartData: DoughnutChartData)
```
-- markerName: Title of marker, for the legend
-- markerValue : Chosen point.
-- lineColour: Line Colour
-- strokeStyle: Style of Stroke
-### X Axis Grid
+---
-Adds vertical lines along the X axis.
+## Documentation
+### Installation
+
+Swift Package Manager
+```
+File > Swift Packages > Add Package Dependency...
+```
```swift
-.xAxisGrid()
+import SwiftUICharts
```
-Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [GridStyle](#GridStyle).
+If you have trouble with views not updating correctly, add `.id()` to your View.
+```swift
+LineChart(chartData: LineChartData)
+ .id(LineChartData.id)
+```
-### Y Axis Grid
+---
-Adds horizontal lines along the Y axis.
-```swift
-.yAxisGrid()
-```
-Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [GridStyle](#GridStyle).
+## View Modifiers
+- [Touch Overlay](#Touch-Overlay)
+- [Info Box](#Info-Box)
+- [Floating Info Box](#Floating-Info-Box)
+- [Header Box](#Header-Box)
+- [Legends](#Legends)
-### X Axis Labels
+- [Average Line](#Average-Line)
+- [Y Axis Point Of Interest](#Y-Axis-Point-Of-Interest)
+- [X Axis Grid](#X-Axis-Grid)
+- [Y Axis Grid](#Y-Axis-Grid)
+- [X Axis Labels](#X-Axis-Labels)
+- [Y Axis Labels](#Y-Axis-Labels)
-Labels for the X axis.
+- [Point Markers](#Point-Markers)
-```swift
-.xAxisLabels()
-```
-Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [XAxisLabelSetup](#XAxisLabelSetup)
+The order of the view modifiers is some what important as the modifiers are various types of stacks that wrap around the previous views.
-### Y Axis Labels
-Automatically generated labels for the Y axis
+---
-```swift
-.yAxisLabels(specifier : String = "%.0f")
-```
-- specifier: Decimal precision specifier.
-Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [YAxisLabelSetup](#YAxisLabelSetup)
+### All Chart Types
+#### Touch Overlay
-### Header Box
+Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information where specified.
-Displays the metadata about the chart. See [ChartMetadata](#ChartMetadata)
+The location of the info box is set in `ChartStyle -> infoBoxPlacement`.
```swift
-.headerBox()
+.touchOverlay(chartData: CTChartData, specifier: String, unit: TouchUnit)
```
+- chartData: Chart data model.
+- specifier: Decimal precision for labels.
+- unit: Unit to put before or after the value.
+
+Setup within Chart Data --> Chart Style
+
+---
-### Legends
-Legends from the data being show on the chart (See [ChartMetadata](#ChartMetadata) ) and any markers (See [Average Line](#Average-Line) and [Y Axis Point Of Interest](#Y-Axis-Point-Of-Interest)).
+#### Info Box
+
+Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.infoBox`.
+
+The location of the info box is set in `ChartStyle -> infoBoxPlacement`.
```swift
-.legends()
+.infoBox(chartData: CTChartData)
```
-Lays out markers over each of the data point.
+- chartData: Chart data model.
+
+---
-## Data Models
+#### Floating Info Box
-### ChartData
+Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.floating`.
-The ChartData type is where the majority of the configuration is done. The only required initialiser is dataPoints.
+The location of the info box is set in `ChartStyle -> infoBoxPlacement`.
```swift
-ChartData(dataPoints : [ChartDataPoint],
- metadata : ChartMetadata?,
- xAxisLabels : [String]?,
- chartStyle : ChartStyle,
- lineStyle : LineStyle,
- barStyle : BarStyle,
- pointStyle : PointStyle,
- calculations : CalculationType)
+.floatingInfoBox(chartData: CTChartData)
```
-- dataPoints: Array of ChartDataPoints. See [ChartDataPoint](#ChartDataPoint).
-- metadata: Data to fill in the metadata box above the chart. See [ChartMetadata](#ChartMetadata).
-- xAxisLabels: Array of Strings for when there are too many data points to show all xAxisLabels.
-- chartStyle : The parameters for the aesthetic of the chart. See [ChartStyle](#ChartStyle).
-- lineStyle: The parameters for the aesthetic of the line chart. See [LineChartStyle](#LineLineChartStyle).
-- barStyle: The parameters for the aesthetic of the bar chart. See [BarStyle](#BarStyle).
-- pointStyle: The parameters for the aesthetic of the data point markers. See [PointStyle](#PointStyle).
-- calculations: Choose whether to perform calculations on the data points. If so, then by what means.
+- chartData: Chart data model.
-### ChartDataPoint
+---
-ChartDataPoint holds the information for each of the individual data points.
-Colours are only used in Bar Charts.
+#### Header Box
-__All__
-```swift
-ChartDataPoint(value: Double,
- xAxisLabel: String?,
- pointLabel: String?,
- date: Date?
- ...)
-```
-- value: Value of the data point.
-- xAxisLabel: Label that can be shown on the X axis.
-- pointLabel: A longer label that can be shown on touch input.
-- date: Date of the data point if any data based calculations are required.
+Displays the metadata about the chart, set in `Chart Data -> ChartMetadata`
-__Single Colour__
-```swift
-ChartDataPoint(...
- colour: Color)
-
-```
-- colour: Colour for use with a bar chart.
+Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.header`.
-__Colour Gradient__
-```swift
-ChartDataPoint(...
- colours : [Color]?,
- startPoint : UnitPoint?,
- endPoint : UnitPoint?)
-```
-- colours: Colours for Gradient
-- startPoint: Start point for Gradient
-- endPoint: End point for Gradient
+The location of the info box is set in `ChartStyle -> infoBoxPlacement`.
-__Colour Gradient with stop control__
```swift
-ChartDataPoint(...
- stops: [GradientStop],
- startPoint: UnitPoint?,
- endPoint: UnitPoint?)
-
+.headerBox(chartData: CTChartData)
```
-- stops: Colours and Stops for Gradient with stop control.
-- startPoint: Start point for Gradient.
-- endPoint: End point for Gradient.
-### ChartMetadata
+---
+
+
+#### Legends
-Data model for the chart's metadata
+Displays legends.
```swift
-ChartMetadata(title: String?,
- subtitle: String?,
- lineLegend: String?)
+.legends()
```
-- title: The charts Title
-- subtitle: The charts subtitle
-- lineLegend: The title for the legend
+Lays out markers over each of the data point.
-### ChartStyle
+---
-Model for controlling the overall aesthetic of the chart.
+
+### Line and Bar Charts
+
+#### Average Line
+
+Shows a marker line at the average of all the data points.
```swift
-ChartStyle(infoBoxPlacement : InfoBoxPlacement,
- xAxisGridStyle : GridStyle,
- yAxisGridStyle : GridStyle,
- xAxisLabelPosition : XAxisLabelPosistion,
- xAxisLabelsFrom : LabelsFrom,
- yAxisLabelPosition : YAxisLabelPosistion,
- yAxisNumberOfLabels : Int,
- globalAnimation : Animation
+.averageLine(chartData: CTLineBarChartDataProtocol,
+ markerName: "Average",
+ labelPosition: .yAxis(specifier: "%.0f"),
+ lineColour: .primary,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
```
-- infoBoxPlacement: Placement of the information box that appears on touch input.
-- xAxisGridStyle: Style of the vertical lines breaking up the chart. See [GridStyle](#GridStyle).
-- yAxisGridStyle: Style of the horizontal lines breaking up the chart. See [GridStyle](#GridStyle).
-- xAxisLabelPosition: Location of the X axis labels - Top or Bottom
-- xAxisLabelsFrom: Where the label data come from. DataPoint or xAxisLabels
-- yAxisLabelPosition: Location of the X axis labels - Leading or Trailing
-- yAxisNumberOfLabel: Number Of Labels on Y Axis
-- globalAnimation: Gobal control of animations.
+- chartData: Chart data model.
+- markerName: Title of marker, for the legend.
+- labelPosition: Option to display the markers’ value inline with the marker.
+- labelColour: Colour of the Text.
+- labelBackground: Colour of the background.
+- lineColour: Line Colour.
+- strokeStyle: Style of Stroke.
-### GridStyle
+---
-Model for controlling the look of the Grid
+
+#### Y Axis Point Of Interest
+
+Configurable Point of interest
```swift
-GridStyle(numberOfLines : Int,
- lineColour : Color,
- lineWidth : CGFloat,
- dash : [CGFloat],
- dashPhase : CGFloat)
+.yAxisPOI(chartData: CTLineBarChartDataProtocol,
+ markerName: "Marker",
+ markerValue: 123,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.black,
+ labelBackground: Color.orange,
+ lineColour: Color.orange,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
```
-- numberOfLines: Number of lines to break up the axis
-- lineColour: Line Colour
-- lineWidth: Line Width
-- dash: Dash
-- dashPhase: Dash Phase
+- chartData: Chart data model.
+- markerName: Title of marker, for the legend.
+- markerValue: Value to mark
+- labelPosition: Option to display the markers’ value inline with the marker.
+- labelColour: Colour of the Text.
+- labelBackground: Colour of the background.
+- lineColour: Line Colour.
+- strokeStyle: Style of Stroke.
+- Returns: A new view containing the chart with a marker line at a specified value.
+
+
+---
-### XAxisLabelSetup
+#### X Axis Grid
-Model for the styling of the labels on the X axis.
+Adds vertical lines along the X axis.
```swift
-XAxisLabelSetup(labelPosition: XAxisLabelPosistion,
- labelsFrom: LabelsFrom)
+.xAxisGrid(chartData: CTLineBarChartDataProtocol)
```
-- labelPosition: Location of the X axis labels - Top or Bottom
-- labelsFrom: Where the label data come from. DataPoint or xAxisLabels
+Setup within `ChartData -> ChartStyle`.
+
+---
-### YAxisLabelSetup
-Model for the styling of the labels on the Y axis.
+#### Y Axis Grid
+
+Adds horizontal lines along the Y axis.
```swift
-YAxisLabelSetup(labelPosition : YAxisLabelPosistion,
- numberOfLabels : Int)
+.yAxisGrid(chartData: CTLineBarChartDataProtocol)
```
-- labelPosition: Location of the Y axis labels - Leading or Trailing
-- numberOfLabels: Number Of Labels on Y Axis
+Setup within `ChartData -> ChartStyle`.
-### LineStyle
+---
-Model for controlling the overall aesthetic of the line chart.
-There are three possible initialisers: Single Colour, Colour Gradient or Colour Gradient with stop control.
+#### X Axis Labels
-__Single Colour__
-```swift
-LineChartStyle(colour: Color,
- ...
-```
-- colour: Single Colour
+Labels for the X axis.
-__Colour Gradient__
```swift
-LineChartStyle(colours: [Color]?,
- startPoint: UnitPoint?,
- endPoint: UnitPoint?,
- ...
+.xAxisLabels(chartData: CTLineBarChartDataProtocol)
```
-- colours: Colours for Gradient
-- startPoint: Start point for Gradient
-- endPoint: End point for Gradient
+Setup within `ChartData -> ChartStyle`.
-__Colour Gradient with stop control__
-```swift
-LineChartStyle(stops: [GradientStop],
- startPoint: UnitPoint?,
- endPoint: UnitPoint?,
- ...
-```
-- stops: Colours and Stops for Gradient with stop control.
-- startPoint: Start point for Gradient.
-- endPoint: End point for Gradient.
-__All__
+---
+
+
+#### Y Axis Labels
+
+Automatically generated labels for the Y axis
+
```swift
-LineChartStyle(...
- strokeStyle : StrokeStyle,
- ignoreZero: Bool)
+.yAxisLabels(chartData: CTLineBarChartDataProtocol, specifier: "%.0f")
```
-- lineType: Drawing style of the line.
-- strokeStyle: Stroke Style
-- ignoreZero: Whether the chart should skip data points who's value is 0 while keeping the spacing.
+- specifier: Decimal precision specifier.
+Setup within `ChartData -> ChartStyle`.
-### BarStyle
-Model for controlling the aesthetic of the bar chart.
+---
-There are three possible initialisers: Single Colour, Colour Gradient or Colour Gradient with stop control.
-__All__
-```swift
-BarStyle(barWidth : CGFloat,
- cornerRadius : CornerRadius,
- colourFrom : ColourFrom,
- ...)
-```
-- barWidth: How much of the available width to use. 0...1
-- cornerRadius: Corner radius of the bar shape.
-- colourFrom: Where to get the colour data from.
+### Line Charts
-__Single Colour__
-```swift
-BarStyle(...
- colour: Single Colour)
-```
-- colour: Single Colour
+#### Point Markers
-__Colour Gradient__
-```swift
-BarStyle(...
- colours : [Color]
- startPoint : UnitPoint
- endPoint : UnitPoint)
-```
-- colours: Colours for Gradient
-- startPoint: Start point for Gradient
-- endPoint: End point for Gradient
+Lays out markers over each of the data point.
-__Colour Gradient with stop control__
```swift
-BarStyle(...
- stops : [GradientStop]
- startPoint : UnitPoint
- endPoint : UnitPoint)
+.pointMarkers(chartData: CTLineChartDataProtocol)
```
-- stops: Colours and Stops for Gradient with stop control.
-- startPoint: Start point for Gradient.
-- endPoint: End point for Gradient.
+Setup within `Data Set -> PointStyle`.
-### PointStyle
+---
-Model for controlling the aesthetic of the point markers.
+## Examples
+
+### Line Chart
```swift
-PointStyle(pointSize : CGFloat,
- borderColour : Color,
- fillColour : Color,
- lineWidth : CGFloat,
- pointType : PointType,
- pointShape : PointShape)
+struct LineChartDemoView: View {
+
+ let data : LineChartData = weekOfData()
+
+ var body: some View {
+ VStack {
+ LineChart(chartData: data)
+ .pointMarkers(chartData: data)
+ .touchOverlay(chartData: data, specifier: "%.0f")
+ .yAxisPOI(chartData: data,
+ markerName: "Step Count Aim",
+ markerValue: 15_000,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.black,
+ labelBackground: Color(red: 1.0, green: 0.75, blue: 0.25),
+ lineColour: Color(red: 1.0, green: 0.75, blue: 0.25),
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "Minimum Recommended",
+ markerValue: 10_000,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.white,
+ labelBackground: Color(red: 0.25, green: 0.75, blue: 1.0),
+ lineColour: Color(red: 0.25, green: 0.75, blue: 1.0),
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data, columns: [GridItem(.flexible()), GridItem(.flexible())])
+ .id(data.id)
+ .frame(minWidth: 150, maxWidth: 900, minHeight: 150, idealHeight: 250, maxHeight: 400, alignment: .center)
+ }
+ .navigationTitle("Week of Data")
+ }
+
+ static func weekOfData() -> LineChartData {
+ let data = LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 12000, xAxisLabel: "M", description: "Monday"),
+ LineChartDataPoint(value: 10000, xAxisLabel: "T", description: "Tuesday"),
+ LineChartDataPoint(value: 8000, xAxisLabel: "W", description: "Wednesday"),
+ LineChartDataPoint(value: 17500, xAxisLabel: "T", description: "Thursday"),
+ LineChartDataPoint(value: 16000, xAxisLabel: "F", description: "Friday"),
+ LineChartDataPoint(value: 11000, xAxisLabel: "S", description: "Saturday"),
+ LineChartDataPoint(value: 9000, xAxisLabel: "S", description: "Sunday")
+ ],
+ legendTitle: "Steps",
+ pointStyle: PointStyle(),
+ style: LineStyle(lineColour: ColourStyle(colour: .red), lineType: .curvedLine))
+
+ let metadata = ChartMetadata(title: "Step Count", subtitle: "Over a Week")
+
+ let gridStyle = GridStyle(numberOfLines: 7,
+ lineColour : Color(.lightGray).opacity(0.5),
+ lineWidth : 1,
+ dash : [8],
+ dashPhase : 0)
+
+ let chartStyle = LineChartStyle(infoBoxPlacement : .infoBox(isStatic: false),
+ infoBoxBorderColour : Color.primary,
+ infoBoxBorderStyle : StrokeStyle(lineWidth: 1),
+
+ markerType : .vertical(attachment: .line(dot: .style(DotStyle()))),
+
+ xAxisGridStyle : gridStyle,
+ xAxisLabelPosition : .bottom,
+ xAxisLabelColour : Color.primary,
+ xAxisLabelsFrom : .dataPoint(rotation: .degrees(0)),
+
+ yAxisGridStyle : gridStyle,
+ yAxisLabelPosition : .leading,
+ yAxisLabelColour : Color.primary,
+ yAxisNumberOfLabels : 7,
+
+ baseline : .minimumWithMaximum(of: 5000),
+ topLine : .maximum(of: 20000),
+
+ globalAnimation : .easeOut(duration: 1))
+
+ return LineChartData(dataSets : data,
+ metadata : metadata,
+ chartStyle : chartStyle)
+
+ }
+}
```
-- pointSize: Overall size of the mark
-- borderColour: Outter ring colour
-- fillColour: Center fill colour
-- lineWidth: Outter ring line width
-- pointType: Style of the point marks.
-- pointShape: Shape of the points
+
+
+---
diff --git a/Resources/BarOne.png b/Resources/BarOne.png
deleted file mode 100644
index 9f5aa69a..00000000
Binary files a/Resources/BarOne.png and /dev/null differ
diff --git a/Resources/BarTwo.png b/Resources/BarTwo.png
deleted file mode 100644
index 7242e4ae..00000000
Binary files a/Resources/BarTwo.png and /dev/null differ
diff --git a/Resources/LineOne.png b/Resources/LineOne.png
deleted file mode 100644
index e17eef4d..00000000
Binary files a/Resources/LineOne.png and /dev/null differ
diff --git a/Resources/LineTwo.png b/Resources/LineTwo.png
deleted file mode 100644
index 43ba951e..00000000
Binary files a/Resources/LineTwo.png and /dev/null differ
diff --git a/Resources/images/BarCharts/BarChart.png b/Resources/images/BarCharts/BarChart.png
new file mode 100644
index 00000000..7fe2a08e
Binary files /dev/null and b/Resources/images/BarCharts/BarChart.png differ
diff --git a/Resources/images/BarCharts/GroupedBarChart.png b/Resources/images/BarCharts/GroupedBarChart.png
new file mode 100644
index 00000000..f3c726da
Binary files /dev/null and b/Resources/images/BarCharts/GroupedBarChart.png differ
diff --git a/Resources/images/BarCharts/RangeBarChart.png b/Resources/images/BarCharts/RangeBarChart.png
new file mode 100644
index 00000000..772df7f0
Binary files /dev/null and b/Resources/images/BarCharts/RangeBarChart.png differ
diff --git a/Resources/images/BarCharts/StackedBarChart.png b/Resources/images/BarCharts/StackedBarChart.png
new file mode 100644
index 00000000..b565c677
Binary files /dev/null and b/Resources/images/BarCharts/StackedBarChart.png differ
diff --git a/Resources/images/LineCharts/FilledLineChart.png b/Resources/images/LineCharts/FilledLineChart.png
new file mode 100644
index 00000000..72682d51
Binary files /dev/null and b/Resources/images/LineCharts/FilledLineChart.png differ
diff --git a/Resources/images/LineCharts/LineChart.png b/Resources/images/LineCharts/LineChart.png
new file mode 100644
index 00000000..32fdca4c
Binary files /dev/null and b/Resources/images/LineCharts/LineChart.png differ
diff --git a/Resources/images/LineCharts/MultiLineChart.png b/Resources/images/LineCharts/MultiLineChart.png
new file mode 100644
index 00000000..5cd09837
Binary files /dev/null and b/Resources/images/LineCharts/MultiLineChart.png differ
diff --git a/Resources/images/LineCharts/RangedLineChart.png b/Resources/images/LineCharts/RangedLineChart.png
new file mode 100644
index 00000000..9c9215af
Binary files /dev/null and b/Resources/images/LineCharts/RangedLineChart.png differ
diff --git a/Resources/images/PieCharts/DoughnutChart.png b/Resources/images/PieCharts/DoughnutChart.png
new file mode 100644
index 00000000..bb0fd14e
Binary files /dev/null and b/Resources/images/PieCharts/DoughnutChart.png differ
diff --git a/Resources/images/PieCharts/PieChart.png b/Resources/images/PieCharts/PieChart.png
new file mode 100644
index 00000000..cfefbeb8
Binary files /dev/null and b/Resources/images/PieCharts/PieChart.png differ
diff --git a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift
new file mode 100644
index 00000000..590fbe48
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift
@@ -0,0 +1,50 @@
+//
+// BarChartEnums.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import Foundation
+
+/**
+ Where to get the colour data from.
+ ```
+ case barStyle // From BarStyle data model
+ case dataPoints // From each data point
+ ```
+ */
+public enum ColourFrom {
+ case barStyle
+ case dataPoints
+}
+
+
+/**
+ Where the marker lines come from to meet at a specified point.
+ ```
+ case none // No overlay markers.
+ case vertical // Vertical line from top to bottom.
+ case full // Full width and height of view intersecting at a specified point.
+ case bottomLeading // From bottom and leading edges meeting at a specified point.
+ case bottomTrailing // From bottom and trailing edges meeting at a specified point.
+ case topLeading // From top and leading edges meeting at a specified point.
+ case topTrailing // From top and trailing edges meeting at a specified point.
+ ```
+ */
+public enum BarMarkerType: MarkerType {
+ /// No overlay markers.
+ case none
+ /// Vertical line from top to bottom.
+ case vertical
+ /// Full width and height of view intersecting at a specified point.
+ case full
+ /// From bottom and leading edges meeting at a specified point.
+ case bottomLeading
+ /// From bottom and trailing edges meeting at a specified point.
+ case bottomTrailing
+ /// From top and leading edges meeting at a specified point.
+ case topLeading
+ /// From top and trailing edges meeting at a specified point.
+ case topTrailing
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift
deleted file mode 100644
index 7bd7c083..00000000
--- a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift
+++ /dev/null
@@ -1,123 +0,0 @@
-//
-// BarStyle.swift
-//
-//
-// Created by Will Dale on 12/01/2021.
-//
-
-import SwiftUI
-
-/// Model for controlling the aesthetic of the bar chart.
-public struct BarStyle {
-
- /// How much of the available width to use. 0 ..1
- var barWidth : CGFloat
- /// Corner radius of the bar shape.
- var cornerRadius: CornerRadius
- /// Where to get the colour data from.
- var colourFrom : ColourFrom
- /// Type of colour styling for the chart.
- var colourType : ColourType
-
- /// Single Colour
- var colour : Color?
- /// Colours for Gradient
- var colours : [Color]?
- /// Colours and Stops for Gradient with stop control
- var stops : [GradientStop]?
-
- /// Start point for Gradient
- var startPoint : UnitPoint?
- /// End point for Gradient
- var endPoint : UnitPoint?
-
-
-
- /// Bar Chart with single colour
- /// - Parameters:
- /// - barWidth: How much of the available width to use. 0...1
- /// - cornerRadius: Corner radius of the bar shape.
- /// - colourFrom: Where to get the colour data from.
- /// - colour: Single Colour
- public init(barWidth : CGFloat = 1,
- cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0),
- colourFrom : ColourFrom = .barStyle,
- colour : Color? = Color(.red)
- ) {
- self.barWidth = barWidth
- self.cornerRadius = cornerRadius
- self.colourFrom = colourFrom
- self.colour = colour
- self.colours = nil
- self.stops = nil
- self.startPoint = nil
- self.endPoint = nil
- self.colourType = .colour
- }
-
- /// Bar Chart with Gradient Colour
- /// - Parameters:
- /// - barWidth: How much of the available width to use. 0...1
- /// - cornerRadius: Corner radius of the bar shape.
- /// - colourFrom: Where to get the colour data from.
- /// - colours: Colours for Gradient.
- /// - startPoint: Start point for Gradient.
- /// - endPoint: End point for Gradient.
- public init(barWidth : CGFloat = 1,
- cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0),
- colourFrom : ColourFrom = .barStyle,
- colours : [Color] = [Color(.red), Color(.blue)],
- startPoint : UnitPoint = .leading,
- endPoint : UnitPoint = .trailing
- ) {
- self.barWidth = barWidth
- self.cornerRadius = cornerRadius
- self.colourFrom = colourFrom
- self.colour = nil
- self.colours = colours
- self.stops = nil
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.colourType = .gradientColour
- }
-
- /// Bar Chart with Gradient with Stops
- /// - Parameters:
- /// - barWidth: How much of the available width to use. 0...1
- /// - cornerRadius: Corner radius of the bar shape.
- /// - colourFrom: Where to get the colour data from.
- /// - stops: Colours and Stops for Gradient with stop control.
- /// - startPoint: Start point for Gradient.
- /// - endPoint: End point for Gradient.
- public init(barWidth : CGFloat = 1,
- cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0),
- colourFrom : ColourFrom = .barStyle,
- stops : [GradientStop] = [GradientStop(color: Color(.red), location: 0.0)],
- startPoint : UnitPoint = .leading,
- endPoint : UnitPoint = .trailing
- ) {
- self.barWidth = barWidth
- self.cornerRadius = cornerRadius
- self.colourFrom = colourFrom
- self.colour = nil
- self.colours = nil
- self.stops = stops
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.colourType = .gradientStops
- }
-}
-
-/// Corner radius of the bar shape.
-public struct CornerRadius {
-
- var top : CGFloat
- var bottom : CGFloat
-
- public init(top: CGFloat, bottom: CGFloat) {
- self.top = top
- self.bottom = bottom
- }
-}
-
-
diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift
new file mode 100644
index 00000000..904b38e9
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift
@@ -0,0 +1,200 @@
+//
+// BarChartData.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling a standard Bar Chart.
+ */
+public final class BarChartData: CTBarChartDataProtocol {
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public final var dataSets : BarDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var barStyle : BarStyle
+ @Published public final var chartStyle : BarChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+
+ public final var noDataText : Text
+ public final let chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises a standard Bar Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the bars.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - barStyle: Control for the aesthetic of the bar chart.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : BarDataSet,
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ barStyle : BarStyle = BarStyle(),
+ chartStyle : BarChartStyle = BarChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.barStyle = barStyle
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (.bar, .single)
+ self.setupLegends()
+ }
+
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+
+ HStack(alignment: .top, spacing: 0) {
+ ForEach(dataSets.dataPoints) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.wrappedXAxisLabel)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+
+ case .chartData:
+
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Touch
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView()
+ }
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [BarChartDataPoint] = []
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count)
+ let index : Int = Int((touchLocation.x) / xSection)
+ if index >= 0 && index < dataSets.dataPoints.count {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+ public final func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count)
+ let ySection : CGFloat = chartSize.height / CGFloat(self.maxValue)
+ let index : Int = Int((touchLocation.x) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+ return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2),
+ y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))
+ }
+ return nil
+ }
+
+ public typealias Set = BarDataSet
+ public typealias DataPoint = BarChartDataPoint
+ public typealias CTStyle = BarChartStyle
+}
+
+
+
+internal struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData : ChartData
+
+ private let label : String
+ private let rotationAngle : Angle
+
+ internal init(chartData: ChartData, label: String, rotationAngle : Angle) {
+ self.chartData = chartData
+ self.label = label
+ self.rotationAngle = rotationAngle
+ }
+
+ @State private var width: CGFloat = 0
+
+ internal var body: some View {
+
+ Text(label)
+ .font(.caption)
+ .lineLimit(1)
+ .overlay(
+ GeometryReader { geo in
+ Color.clear
+ .onAppear {
+ self.width = geo.frame(in: .local).width
+ }
+ }
+ )
+ .fixedSize(horizontal: true, vertical: false)
+ .rotationEffect(rotationAngle, anchor: .center)
+ .frame(width: 10, height: width)
+ .onAppear {
+ chartData.viewData.xAxisLabelHeights.append(width)
+ }
+
+ }
+}
+
+internal struct YAxisChartDataCell: View where ChartData: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData : ChartData
+
+ private let label : String
+
+ internal init(chartData: ChartData, label: String) {
+ self.chartData = chartData
+ self.label = label
+ }
+
+ @State private var height: CGFloat = 0
+
+ internal var body: some View {
+
+ Text(label)
+ .font(.caption)
+ .lineLimit(1)
+ .overlay(
+ GeometryReader { geo in
+ Color.clear
+ .onAppear {
+ self.height = geo.frame(in: .local).height
+ }
+ }
+ )
+ .onAppear {
+ chartData.viewData.xAxisLabelHeights.append(height)
+ }
+
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift
new file mode 100644
index 00000000..8d2ec3ee
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift
@@ -0,0 +1,192 @@
+//
+// MultiBarChartData.swift
+//
+//
+// Created by Will Dale on 26/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data model for drawing and styling a Grouped Bar Chart.
+
+ The grouping data informs the model as to how the datapoints are linked.
+ ```
+ */
+public final class GroupedBarChartData: CTMultiBarChartDataProtocol {
+
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public final var dataSets : MultiBarDataSets
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var barStyle : BarStyle
+ @Published public final var chartStyle : BarChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+ @Published public final var groups : [GroupingData]
+
+ public final var noDataText : Text
+ public final var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ final var groupSpacing : CGFloat = 0
+
+ // MARK: Initializer
+ /// Initialises a Grouped Bar Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the bars.
+ /// - groups: Information for how to group the data points.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - barStyle: Control for the aesthetic of the bar chart.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : MultiBarDataSets,
+ groups : [GroupingData],
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ barStyle : BarStyle = BarStyle(),
+ chartStyle : BarChartStyle = BarChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.groups = groups
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.barStyle = barStyle
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (chartType: .bar, dataSetType: .multi)
+ self.setupLegends()
+ }
+
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ VStack {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+ HStack(spacing: self.groupSpacing) {
+ ForEach(dataSets.dataSets) { dataSet in
+ HStack(spacing: 0) {
+ ForEach(dataSet.dataPoints) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: data.group.title, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.group.title)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+
+ case .chartData:
+
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ }
+ HStack(spacing: self.groupSpacing) {
+ ForEach(dataSets.dataSets) { dataSet in
+ HStack(spacing: 0) {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: dataSet.setTitle, rotationAngle: .degrees(0))
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(dataSet.setTitle)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+ }
+ }
+
+ // MARK: Touch
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView()
+ }
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+
+ var points : [MultiBarChartDataPoint] = []
+
+ // Divide the chart into equal sections.
+ let superXSection : CGFloat = (chartSize.width / CGFloat(dataSets.dataSets.count))
+ let superIndex : Int = Int((touchLocation.x) / superXSection)
+
+ // Work out how much to remove from xSection due to groupSpacing.
+ let compensation : CGFloat = ((groupSpacing * CGFloat(dataSets.dataSets.count - 1)) / CGFloat(dataSets.dataSets.count))
+
+ // Make those sections take account of spacing between groups.
+ let xSection : CGFloat = (chartSize.width / CGFloat(dataSets.dataSets.count)) - compensation
+ let index : Int = Int((touchLocation.x - CGFloat((groupSpacing * CGFloat(superIndex)))) / xSection)
+
+ if index >= 0 && index < dataSets.dataSets.count && superIndex == index {
+ let dataSet = dataSets.dataSets[index]
+ let xSubSection : CGFloat = (xSection / CGFloat(dataSet.dataPoints.count))
+ let subIndex : Int = Int((touchLocation.x - CGFloat((groupSpacing * CGFloat(superIndex)))) / xSubSection) - (dataSet.dataPoints.count * index)
+ if subIndex >= 0 && subIndex < dataSet.dataPoints.count {
+ var dataPoint = dataSet.dataPoints[subIndex]
+ dataPoint.legendTag = dataSet.setTitle
+ points.append(dataPoint)
+ }
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+ public final func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+
+ // Divide the chart into equal sections.
+ let superXSection : CGFloat = (chartSize.width / CGFloat(dataSet.dataSets.count))
+ let superIndex : Int = Int((touchLocation.x) / superXSection)
+
+ // Work out how much to remove from xSection due to groupSpacing.
+ let compensation : CGFloat = ((groupSpacing * CGFloat(dataSet.dataSets.count - 1)) / CGFloat(dataSet.dataSets.count))
+
+ // Make those sections take account of spacing between groups.
+ let xSection : CGFloat = (chartSize.width / CGFloat(dataSet.dataSets.count)) - compensation
+ let ySection : CGFloat = chartSize.height / CGFloat(self.maxValue)
+
+ let index : Int = Int((touchLocation.x - CGFloat(groupSpacing * CGFloat(superIndex))) / xSection)
+
+ if index >= 0 && index < dataSet.dataSets.count && superIndex == index {
+
+ let subDataSet = dataSet.dataSets[index]
+ let xSubSection : CGFloat = (xSection / CGFloat(subDataSet.dataPoints.count))
+ let subIndex : Int = Int((touchLocation.x - CGFloat(groupSpacing * CGFloat(index))) / xSubSection) - (subDataSet.dataPoints.count * index)
+
+ if subIndex >= 0 && subIndex < subDataSet.dataPoints.count {
+ let element : CGFloat = (CGFloat(subIndex) * xSubSection) + (xSubSection / 2)
+ let section : CGFloat = (superXSection * CGFloat(superIndex))
+ let spacing : CGFloat = ((groupSpacing / CGFloat(dataSets.dataSets.count)) * CGFloat(superIndex))
+ return CGPoint(x: element + section + spacing,
+ y: (chartSize.height - CGFloat(subDataSet.dataPoints[subIndex].value) * ySection))
+ }
+ }
+ return nil
+ }
+
+ public typealias Set = MultiBarDataSets
+ public typealias DataPoint = MultiBarChartDataPoint
+ public typealias CTStyle = BarChartStyle
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift
new file mode 100644
index 00000000..200ff348
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift
@@ -0,0 +1,149 @@
+//
+// RangedBarChartData.swift
+//
+//
+// Created by Will Dale on 03/03/2021.
+//
+
+import SwiftUI
+
+public final class RangedBarChartData: CTRangedBarChartDataProtocol {
+
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public final var dataSets : RangedBarDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var barStyle : BarStyle
+ @Published public final var chartStyle : BarChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+
+ public final var noDataText : Text
+ public final let chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises a standard Bar Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the bars.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - barStyle: Control for the aesthetic of the bar chart.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : RangedBarDataSet,
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ barStyle : BarStyle = BarStyle(),
+ chartStyle : BarChartStyle = BarChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.barStyle = barStyle
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (.bar, .single)
+ self.setupLegends()
+ }
+
+ public final var average : Double {
+ let upperSum = dataSets.dataPoints.reduce(0) { $0 + $1.upperValue }
+ let lowerSum = dataSets.dataPoints.reduce(0) { $0 + $1.lowerValue }
+
+ let upperAverage = upperSum / Double(dataSets.dataPoints.count)
+ let lowerAverage = lowerSum / Double(dataSets.dataPoints.count)
+
+ return (upperAverage + lowerAverage) / 2
+ }
+
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+
+ HStack(spacing: 0) {
+ ForEach(dataSets.dataPoints) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.wrappedXAxisLabel)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+
+ case .chartData:
+
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ if data != labelArray[0] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ if data != labelArray[labelArray.count-1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Touch
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView()
+ }
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [RangedBarDataPoint] = []
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count)
+ let index : Int = Int((touchLocation.x) / xSection)
+ if index >= 0 && index < dataSets.dataPoints.count {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+ public final func getPointLocation(dataSet: RangedBarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count)
+ let index : Int = Int((touchLocation.x) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+
+ let value = CGFloat((dataSet.dataPoints[index].upperValue + dataSet.dataPoints[index].lowerValue) / 2) - CGFloat(self.minValue)
+
+ return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2),
+ y: (chartSize.size.height - (value / CGFloat(self.range)) * chartSize.size.height))
+ }
+ return nil
+ }
+
+ public typealias Set = RangedBarDataSet
+ public typealias DataPoint = RangedBarDataPoint
+ public typealias CTStyle = BarChartStyle
+}
+
+
+extension RangedBarChartData {
+ final func getBarPositionX(dataPoint: RangedBarDataPoint, height: CGFloat) -> CGFloat {
+ let value = CGFloat((dataPoint.upperValue + dataPoint.lowerValue) / 2) - CGFloat(self.minValue)
+ return (height - (value / CGFloat(self.range)) * height)
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift
new file mode 100644
index 00000000..0513c12f
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift
@@ -0,0 +1,279 @@
+//
+// StackedBarChartData.swift
+//
+//
+// Created by Will Dale on 12/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data model for drawing and styling a Stacked Bar Chart.
+
+ The grouping data informs the model as to how the datapoints are linked.
+
+ # Example
+ ```
+ static func makeData() -> StackedBarChartData {
+
+ enum Group {
+ case one
+ case two
+ case three
+ case four
+
+ var data : GroupingData {
+ switch self {
+ case .one:
+ return GroupingData(title: "One" , colour: .blue)
+ case .two:
+ return GroupingData(title: "Two" , colour: .red)
+ case .three:
+ return GroupingData(title: "Three", colour: .yellow)
+ case .four:
+ return GroupingData(title: "Four" , colour: .green)
+ }
+ }
+ }
+
+ let groups : [GroupingData] = [Group.one.data, Group.two.data, Group.three.data, Group.four.data]
+
+ let data = MultiBarDataSets(dataSets: [
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 10, xAxisLabel: "1.2", pointLabel: "One Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , group: Group.four.data)
+ ]),
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 10, xAxisLabel: "2.2", pointLabel: "Two Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Four" , group: Group.four.data)
+ ]),
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 50, xAxisLabel: "3.2", pointLabel: "Three Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", group: Group.three.data),
+ MultiBarChartDataPoint(value: 100, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data)
+ ]),
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 10, xAxisLabel: "4.2", pointLabel: "Four Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Four" , group: Group.four.data)
+ ])
+ ])
+
+
+ return StackedBarChartData(dataSets: data,
+ groups: groups,
+ metadata: ChartMetadata(title: "Hello", subtitle: "World"),
+ chartStyle: BarChartStyle(xAxisLabelsFrom: .dataPoint))
+ ```
+ */
+public final class StackedBarChartData: CTMultiBarChartDataProtocol {
+
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public final var dataSets : MultiBarDataSets
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var barStyle : BarStyle
+ @Published public final var chartStyle : BarChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+ @Published public final var groups : [GroupingData]
+
+ public final var noDataText : Text
+ public final var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises a Grouped Bar Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the bars.
+ /// - groups: Information for how to group the data points.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - barStyle: Control for the aesthetic of the bar chart.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : MultiBarDataSets,
+ groups : [GroupingData],
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ barStyle : BarStyle = BarStyle(),
+ chartStyle : BarChartStyle = BarChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.groups = groups
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.barStyle = barStyle
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (chartType: .bar, dataSetType: .multi)
+ self.setupLegends()
+ }
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+ HStack(spacing: 0) {
+ ForEach(groups) { group in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: group.title, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .lineLimit(1)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(group.title)"))
+
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ case .chartData:
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ }
+ HStack {
+ ForEach(dataSets.dataSets) { dataSet in
+ HStack(spacing: 0) {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ YAxisDataPointCell(chartData: self, label: dataSet.setTitle, rotationAngle: .degrees(0))
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(dataSet.setTitle)"))
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+ }
+ }
+ // MARK: Touch
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView()
+ }
+
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+
+ var points : [MultiBarChartDataPoint] = []
+
+ // Filter to get the right dataset based on the x axis.
+ let superXSection : CGFloat = chartSize.width / CGFloat(dataSets.dataSets.count)
+ let superIndex : Int = Int((touchLocation.x) / superXSection)
+
+ if superIndex >= 0 && superIndex < dataSets.dataSets.count {
+
+ let dataSet = dataSets.dataSets[superIndex]
+
+ // Get the max value of the dataset relative to max value of all datasets.
+ // This is used to set the height of the y axis filtering.
+ let setMaxValue = dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0
+ let allMaxValue = self.maxValue
+ let fraction : CGFloat = CGFloat(setMaxValue / allMaxValue)
+
+ // Gets the height of each datapoint
+ var heightOfElements : [CGFloat] = []
+ let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value }
+ dataSet.dataPoints.forEach { datapoint in
+ heightOfElements.append((chartSize.height * fraction) * CGFloat(datapoint.value / sum))
+ }
+
+ // Gets the highest point of each element.
+ var endPointOfElements : [CGFloat] = []
+ heightOfElements.enumerated().forEach { element in
+ var returnValue : CGFloat = 0
+ for index in 0...element.offset {
+ returnValue += heightOfElements[index]
+ }
+ endPointOfElements.append(returnValue)
+ }
+
+ let yIndex = endPointOfElements.enumerated().first(where: { $0.element > abs(touchLocation.y - chartSize.height) })
+
+ if let index = yIndex?.offset {
+ if index >= 0 && index < dataSet.dataPoints.count {
+ var dataPoint = dataSet.dataPoints[index]
+ dataPoint.legendTag = dataSet.setTitle
+ points.append(dataPoint)
+ }
+ }
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+
+ public final func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+ // Filter to get the right dataset based on the x axis.
+ let superXSection : CGFloat = chartSize.width / CGFloat(dataSet.dataSets.count)
+ let superIndex : Int = Int((touchLocation.x) / superXSection)
+
+ if superIndex >= 0 && superIndex < dataSet.dataSets.count {
+
+ let subDataSet = dataSet.dataSets[superIndex]
+
+ // Get the max value of the dataset relative to max value of all datasets.
+ // This is used to set the height of the y axis filtering.
+ let setMaxValue = subDataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0
+ let allMaxValue = self.maxValue
+ let fraction : CGFloat = CGFloat(setMaxValue / allMaxValue)
+
+ // Gets the height of each datapoint
+ var heightOfElements : [CGFloat] = []
+ let sum = subDataSet.dataPoints.reduce(0) { $0 + $1.value }
+ subDataSet.dataPoints.forEach { datapoint in
+ heightOfElements.append((chartSize.height * fraction) * CGFloat(datapoint.value / sum))
+ }
+
+ // Gets the highest point of each element.
+ var endPointOfElements : [CGFloat] = []
+ heightOfElements.enumerated().forEach { element in
+ var returnValue : CGFloat = 0
+ for index in 0...element.offset {
+ returnValue += heightOfElements[index]
+ }
+ endPointOfElements.append(returnValue)
+ }
+
+ let yIndex = endPointOfElements.enumerated().first(where: {
+ $0.element > abs(touchLocation.y - chartSize.height)
+ })
+
+ if let index = yIndex?.offset {
+ if index >= 0 && index < subDataSet.dataPoints.count {
+
+ return CGPoint(x: (CGFloat(superIndex) * superXSection) + (superXSection / 2),
+ y: (chartSize.height - endPointOfElements[index]))
+ }
+ }
+ }
+ return nil
+ }
+
+ public typealias Set = MultiBarDataSets
+ public typealias DataPoint = MultiBarChartDataPoint
+ public typealias CTStyle = BarChartStyle
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift
new file mode 100644
index 00000000..ffb080dd
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift
@@ -0,0 +1,23 @@
+//
+// CornerRadius.swift
+//
+//
+// Created by Will Dale on 04/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Corner radius of the bar shape.
+ */
+public struct CornerRadius: Hashable {
+
+ public let top : CGFloat
+ public let bottom : CGFloat
+
+ /// Set the coner radius for the bar shapes
+ public init(top: CGFloat = 15.0, bottom: CGFloat = 0.0) {
+ self.top = top
+ self.bottom = bottom
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift
new file mode 100644
index 00000000..6191316c
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift
@@ -0,0 +1,46 @@
+//
+// File.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set for a bar chart.
+
+ # Example
+ ```
+ let data = BarDataSet(dataPoints: [
+ BarChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"),
+ BarChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"),
+ BarChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"),
+ BarChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"),
+ BarChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"),
+ BarChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"),
+ BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday")
+ ],
+ legendTitle: "Data")
+ ```
+ */
+public struct BarDataSet: CTStandardBarChartDataSet {
+
+ public let id : UUID = UUID()
+ public var dataPoints : [BarChartDataPoint]
+ public var legendTitle : String
+
+ /// Initialises a new data set for standard Bar Charts.
+ /// - Parameters:
+ /// - dataPoints: Array of elements.
+ /// - legendTitle: label for the data in legend.
+ public init(dataPoints : [BarChartDataPoint],
+ legendTitle : String = ""
+ ) {
+ self.dataPoints = dataPoints
+ self.legendTitle = legendTitle
+ }
+
+ public typealias ID = UUID
+ public typealias DataPoint = BarChartDataPoint
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift
new file mode 100644
index 00000000..b776fff6
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift
@@ -0,0 +1,52 @@
+//
+// MultiBarDataSet.swift
+//
+//
+// Created by Will Dale on 04/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Main data set for a multi part bar charts.
+ */
+public struct MultiBarDataSets: CTMultiDataSetProtocol {
+
+ public let id : UUID = UUID()
+ public var dataSets : [MultiBarDataSet]
+
+ /// Initialises a new data set for Multiline Line Chart.
+ public init(dataSets: [MultiBarDataSet]) {
+ self.dataSets = dataSets
+ }
+}
+
+/**
+ Individual data sets for multi part bars charts.
+
+ # Example
+ ```
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, group: GroupingData(title: "One", colour: .blue)),
+ MultiBarChartDataPoint(value: 50, group: GroupingData(title: "Two", colour: .red))
+ ])
+ ```
+ */
+public struct MultiBarDataSet: CTMultiBarChartDataSet {
+
+ public let id : UUID = UUID()
+ public var dataPoints : [MultiBarChartDataPoint]
+ public var setTitle : String
+
+ /// Initialises a new data set for a Bar Chart.
+ public init(dataPoints: [MultiBarChartDataPoint],
+ setTitle : String = ""
+ ) {
+ self.dataPoints = dataPoints
+ self.setTitle = setTitle
+ }
+
+ public typealias ID = UUID
+ public typealias DataPoint = MultiBarChartDataPoint
+ public typealias Styling = BarStyle
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift
new file mode 100644
index 00000000..725c7c94
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift
@@ -0,0 +1,33 @@
+//
+// RangedBarDataSet.swift
+//
+//
+// Created by Will Dale on 05/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set for ranged bar charts.
+ */
+public struct RangedBarDataSet : CTRangedBarChartDataSet {
+
+ public var id: UUID = UUID()
+ public var dataPoints : [RangedBarDataPoint]
+ public var legendTitle : String
+
+ /// Initialises a new data set for ranged bar chart.
+ /// - Parameters:
+ /// - dataPoints: Array of elements.
+ /// - legendTitle: Label for the data in legend.
+ public init(dataPoints : [RangedBarDataPoint],
+ legendTitle : String = ""
+ ) {
+ self.dataPoints = dataPoints
+ self.legendTitle = legendTitle
+ }
+
+
+ public typealias ID = UUID
+ public typealias DataPoint = RangedBarDataPoint
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift
new file mode 100644
index 00000000..16a3e1f0
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift
@@ -0,0 +1,55 @@
+//
+// BarChartDataPoint.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single bar chart data point.
+
+ Colour can be solid or gradient.
+
+ # Example
+ ```
+ BarChartDataPoint(value: 90,
+ xAxisLabel: "T",
+ description: "Tuesday",
+ colour: ColourStyle(colour: .blue))
+ ```
+ */
+public struct BarChartDataPoint: CTStandardBarDataPoint {
+
+ public let id = UUID()
+
+ public var value : Double
+ public var xAxisLabel : String?
+ public var description: String?
+ public var date : Date?
+ public var colour : ColourStyle
+
+ public var legendTag : String = ""
+
+ // MARK: - Single colour
+ /// Data model for a single data point with colour for use with a bar chart.
+ /// - Parameters:
+ /// - value: Value of the data point.
+ /// - xAxisLabel: Label that can be shown on the X axis.
+ /// - description: A longer label that can be shown on touch input.
+ /// - date: Date of the data point if any data based calculations are required.
+ /// - colour: Colour styling for the fill.
+ public init(value : Double,
+ xAxisLabel : String? = nil,
+ description : String? = nil,
+ date : Date? = nil,
+ colour : ColourStyle = ColourStyle(colour: .red)
+ ) {
+ self.value = value
+ self.xAxisLabel = xAxisLabel
+ self.description = description
+ self.date = date
+ self.colour = colour
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift
new file mode 100644
index 00000000..aee09207
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift
@@ -0,0 +1,45 @@
+//
+// MultiBarChartDataPoint.swift
+//
+//
+// Created by Will Dale on 19/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single bar chart data point.
+
+ # Example
+ ```
+ MultiBarChartDataPoint(value: 10,
+ xAxisLabel: "1.1",
+ description: "One One",
+ group: GroupingData(title: "One", colour: .blue))
+ ```
+ */
+public struct MultiBarChartDataPoint: CTMultiBarDataPoint {
+
+ public let id : UUID = UUID()
+ public var value : Double
+ public var xAxisLabel : String? = nil
+ public var description : String?
+ public var date : Date?
+ public var group : GroupingData
+
+ public var legendTag : String = ""
+
+ public init(value : Double,
+ description : String? = nil,
+ date : Date? = nil,
+ group : GroupingData
+ ) {
+ self.value = value
+ self.description = description
+ self.date = date
+ self.group = group
+ }
+
+ public typealias ID = UUID
+}
+
diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift
new file mode 100644
index 00000000..b23d843a
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift
@@ -0,0 +1,49 @@
+//
+// RangedBarDataPoint.swift
+//
+//
+// Created by Will Dale on 05/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single ranged bar chart data point.
+ */
+public struct RangedBarDataPoint: CTRangedBarDataPoint {
+
+ public let id : UUID = UUID()
+ public var upperValue : Double
+ public var lowerValue : Double
+ public var xAxisLabel : String?
+ public var description : String?
+ public var date : Date?
+ public var colour : ColourStyle
+
+ public var legendTag : String = ""
+
+ /// Data model for a single data point with colour for use with a ranged bar chart.
+ /// - Parameters:
+ /// - lowerValue: Value of the lower range of the data point.
+ /// - upperValue: Value of the upper range of the data point.
+ /// - xAxisLabel: Label that can be shown on the X axis.
+ /// - description: A longer label that can be shown on touch input.
+ /// - date: Date of the data point if any data based calculations are required.
+ /// - colour: Colour styling for the fill.
+ public init(lowerValue : Double,
+ upperValue : Double,
+ xAxisLabel : String? = nil,
+ description : String? = nil,
+ date : Date? = nil,
+ colour : ColourStyle = ColourStyle(colour: .red)
+ ) {
+ self.upperValue = upperValue
+ self.lowerValue = lowerValue
+ self.xAxisLabel = xAxisLabel
+ self.description = description
+ self.date = date
+ self.colour = colour
+ }
+
+ public typealias ID = UUID
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift
new file mode 100644
index 00000000..a457c8b1
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift
@@ -0,0 +1,34 @@
+//
+// GroupingData.swift
+//
+//
+// Created by Will Dale on 23/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Model for grouping data points together so they can be drawn in the correct groupings.
+
+ # Example
+ ```
+ GroupingData(title: "One", colour: ColourStyle(colour: .blue))
+ ```
+ */
+public struct GroupingData: CTBarColourProtocol, Hashable, Identifiable {
+
+ public let id : UUID = UUID()
+ public var title : String
+ public var colour : ColourStyle
+
+ /// Group with single colour
+ /// - Parameters:
+ /// - title: Title for legends
+ /// - colour: Colour styling for the bars.
+ public init(title : String,
+ colour : ColourStyle
+ ) {
+ self.title = title
+ self.colour = colour
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift
new file mode 100644
index 00000000..b0f798eb
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift
@@ -0,0 +1,129 @@
+//
+// BarChartProtocols.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Chart Data
+/**
+ A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Bar Charts.
+ */
+public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol {
+
+ associatedtype BarStyle : CTBarStyle
+ /**
+ Overall styling for the bars
+ */
+ var barStyle : BarStyle { get set }
+}
+
+
+
+/**
+ A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts.
+ */
+public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol {
+
+ /**
+ Grouping data to inform the chart about the relationship between the datapoints.
+ */
+ var groups : [GroupingData] { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts.
+ */
+public protocol CTRangedBarChartDataProtocol: CTBarChartDataProtocol {}
+
+
+
+
+// MARK: - Style
+/**
+ A protocol to extend functionality of `CTLineBarChartStyle` specifically for Bar Charts.
+ */
+public protocol CTBarChartStyle: CTLineBarChartStyle {}
+
+public protocol CTBarStyle: CTBarColourProtocol, Hashable {
+ /// How much of the available width to use. 0...1
+ var barWidth : CGFloat { get set }
+ /// Corner radius of the bar shape.
+ var cornerRadius: CornerRadius { get set }
+ /// Where to get the colour data from.
+ var colourFrom : ColourFrom { get set }
+ /// Drawing style of the fill.
+ var colour : ColourStyle { get set }
+}
+
+
+
+
+
+
+// MARK: - DataSet
+/**
+ A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Standard Bar Charts.
+ */
+public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol {
+ /**
+ Label to display in the legend.
+ */
+ var legendTitle : String { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Multi Part Bar Charts.
+ */
+public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {}
+
+/**
+ A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Ranged Bar Charts.
+ */
+public protocol CTRangedBarChartDataSet: CTStandardBarChartDataSet {}
+
+
+
+
+
+// MARK: - DataPoints
+/**
+ A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts.
+
+ This is base to specify conformance for generics.
+ */
+public protocol CTBarDataPointBaseProtocol: CTLineBarDataPointProtocol {}
+
+/**
+ A protocol to a standard colour scheme for bar charts.
+ */
+public protocol CTBarColourProtocol {
+ /// Drawing style of the range fill.
+ var colour : ColourStyle { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for standard Bar Charts.
+ */
+public protocol CTStandardBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataPointProtocol, CTBarColourProtocol, CTnotRanged {}
+
+/**
+ A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for standard Bar Charts.
+ */
+public protocol CTRangedBarDataPoint: CTBarDataPointBaseProtocol, CTRangeDataPointProtocol, CTBarColourProtocol, CTisRanged {}
+
+/**
+ A protocol to extend functionality of `CTBarDataPointBaseProtocol` specifically for multi part Bar Charts.
+ i.e: Grouped or Stacked
+ */
+public protocol CTMultiBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataPointProtocol, CTnotRanged {
+
+ /**
+ For grouping data points together so they can be drawn in the correct groupings.
+ */
+ var group : GroupingData { get set }
+}
+
+
diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift
new file mode 100644
index 00000000..775de8a3
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift
@@ -0,0 +1,178 @@
+//
+// BarChartProtocolsExtensions.swift
+//
+//
+// Created by Will Dale on 03/03/2021.
+//
+
+import SwiftUI
+
+// MARK: - Markers
+extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType {
+ internal func markerSubView() -> some View {
+ Group {
+ if let position = self.getPointLocation(dataSet: dataSets as! Self.SetPoint,
+ touchLocation: self.infoView.touchLocation,
+ chartSize: self.infoView.chartSize) {
+ switch self.chartStyle.markerType {
+ case .none:
+ EmptyView()
+ case .vertical:
+
+ MarkerFull(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ case .full:
+
+ MarkerFull(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ case .bottomLeading:
+
+ MarkerBottomLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ case .bottomTrailing:
+
+ MarkerBottomTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ case .topLeading:
+
+ MarkerTopLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ case .topTrailing:
+
+ MarkerTopTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Legends
+// MARK: Standard / Ranged
+extension CTBarChartDataProtocol where Self.Set.ID == UUID,
+ Self.Set.DataPoint.ID == UUID,
+ Self.Set: CTStandardBarChartDataSet,
+ Self.Set.DataPoint: CTBarColourProtocol {
+ internal func setupLegends() {
+ switch self.barStyle.colourFrom {
+ case .barStyle:
+ if self.barStyle.colour.colourType == .colour,
+ let colour = self.barStyle.colour.colour
+ {
+ self.legends.append(LegendData(id : dataSets.id,
+ legend : dataSets.legendTitle,
+ colour : ColourStyle(colour: colour),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if self.barStyle.colour.colourType == .gradientColour,
+ let colours = self.barStyle.colour.colours
+ {
+ self.legends.append(LegendData(id : dataSets.id,
+ legend : dataSets.legendTitle,
+ colour : ColourStyle(colours: colours,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if self.barStyle.colour.colourType == .gradientStops,
+ let stops = self.barStyle.colour.stops
+ {
+ self.legends.append(LegendData(id : dataSets.id,
+ legend : dataSets.legendTitle,
+ colour : ColourStyle(stops: stops,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ }
+ case .dataPoints:
+
+ for data in dataSets.dataPoints {
+
+ if data.colour.colourType == .colour,
+ let colour = data.colour.colour,
+ let legend = data.description
+ {
+ self.legends.append(LegendData(id : data.id,
+ legend : legend,
+ colour : ColourStyle(colour: colour),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if data.colour.colourType == .gradientColour,
+ let colours = data.colour.colours,
+ let legend = data.description
+ {
+ self.legends.append(LegendData(id : data.id,
+ legend : legend,
+ colour : ColourStyle(colours: colours,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if data.colour.colourType == .gradientStops,
+ let stops = data.colour.stops,
+ let legend = data.description
+ {
+ self.legends.append(LegendData(id : data.id,
+ legend : legend,
+ colour : ColourStyle(stops: stops,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ }
+ }
+ }
+ }
+}
+
+// MARK: Multi Bar
+extension CTMultiBarChartDataProtocol {
+ internal func setupLegends() {
+
+ for group in self.groups {
+
+ if group.colour.colourType == .colour,
+ let colour = group.colour.colour
+ {
+ self.legends.append(LegendData(id : group.id,
+ legend : group.title,
+ colour : ColourStyle(colour: colour),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if group.colour.colourType == .gradientColour,
+ let colours = group.colour.colours
+ {
+ self.legends.append(LegendData(id : group.id,
+ legend : group.title,
+ colour : ColourStyle(colours: colours,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ } else if group.colour.colourType == .gradientStops,
+ let stops = group.colour.stops
+ {
+ self.legends.append(LegendData(id : group.id,
+ legend : group.title,
+ colour : ColourStyle(stops: stops,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .bar))
+ }
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift
new file mode 100644
index 00000000..d2a7348a
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift
@@ -0,0 +1,125 @@
+//
+// BarChartStyle.swift
+//
+//
+// Created by Will Dale on 25/01/2021.
+//
+
+import SwiftUI
+
+
+/**
+ Control of the overall aesthetic of the bar chart.
+
+ Controls the look of the chart as a whole, not including any styling
+ specific to the data set(s),
+ */
+public struct BarChartStyle: CTBarChartStyle {
+
+ public var infoBoxPlacement : InfoBoxPlacement
+ public var infoBoxValueColour : Color
+ public var infoBoxDescriptionColour: Color
+ public var infoBoxBackgroundColour : Color
+ public var infoBoxBorderColour : Color
+ public var infoBoxBorderStyle : StrokeStyle
+
+ public var markerType : BarMarkerType
+
+ public var xAxisGridStyle : GridStyle
+ public var xAxisLabelPosition : XAxisLabelPosistion
+ public var xAxisLabelColour : Color
+ public var xAxisLabelsFrom : LabelsFrom
+ public var xAxisTitle : String?
+
+ public var yAxisGridStyle : GridStyle
+ public var yAxisLabelPosition : YAxisLabelPosistion
+ public var yAxisLabelColour : Color
+ public var yAxisNumberOfLabels : Int
+ public var yAxisTitle : String?
+
+ public var baseline : Baseline
+ public var topLine : Topline
+
+ public var globalAnimation : Animation
+
+ /// Model for controlling the overall aesthetic of the Bar Chart.
+ ///
+ /// - Parameters:
+ /// - infoBoxPlacement: Placement of the information box that appears on touch input.
+ /// - infoBoxValueColour: Colour of the value part of the touch info.
+ /// - infoBoxDescriptionColour: Colour of the description part of the touch info.
+ /// - infoBoxBackgroundColour: Background colour of touch info.
+ /// - infoBoxBorderColour: Border colour of the touch info.
+ /// - infoBoxBorderStyle: Border style of the touch info.
+ ///
+ /// - markerType: Where the marker lines come from to meet at a specified point.
+ ///
+ /// - xAxisGridStyle: Style of the vertical lines breaking up the chart.
+ /// - xAxisLabelPosition: Location of the X axis labels - Top or Bottom.
+ /// - xAxisLabelsFrom: Where the label data come from. DataPoint or xAxisLabels.
+ /// - xAxisLabelColour: Text Colour for the labels on the X axis.
+ /// - xAxisTitle: Label to display next to the chart giving info about the axis.
+ ///
+ /// - yAxisGridStyle: Style of the horizontal lines breaking up the chart.
+ /// - yAxisLabelPosition: Location of the X axis labels - Leading or Trailing.
+ /// - yAxisNumberOfLabel: Number Of Labels on Y Axis.
+ /// - yAxisLabelColour: Text Colour for the labels on the Y axis.
+ /// - yAxisTitle: Label to display next to the chart giving info about the axis.
+ ///
+ /// - baseline: Whether the chart is drawn from baseline of zero or the minimum datapoint value.
+ /// - topLine: Where to finish drawing the chart from. Data set maximum or custom.
+ ///
+ /// - globalAnimation: Global control of animations.
+ public init(infoBoxPlacement : InfoBoxPlacement = .floating,
+ infoBoxValueColour : Color = Color.primary,
+ infoBoxDescriptionColour: Color = Color.primary,
+ infoBoxBackgroundColour : Color = Color.systemsBackground,
+ infoBoxBorderColour : Color = Color.clear,
+ infoBoxBorderStyle : StrokeStyle = StrokeStyle(lineWidth: 0),
+
+ markerType : BarMarkerType = .full,
+
+ xAxisGridStyle : GridStyle = GridStyle(),
+ xAxisLabelPosition : XAxisLabelPosistion = .bottom,
+ xAxisLabelColour : Color = Color.primary,
+ xAxisLabelsFrom : LabelsFrom = .dataPoint(rotation: .degrees(0)),
+ xAxisTitle : String? = nil,
+
+ yAxisGridStyle : GridStyle = GridStyle(),
+ yAxisLabelPosition : YAxisLabelPosistion = .leading,
+ yAxisLabelColour : Color = Color.primary,
+ yAxisNumberOfLabels : Int = 10,
+ yAxisTitle : String? = nil,
+
+ baseline : Baseline = .minimumValue,
+ topLine : Topline = .maximumValue,
+
+ globalAnimation : Animation = Animation.linear(duration: 1)
+ ) {
+ self.infoBoxPlacement = infoBoxPlacement
+ self.infoBoxValueColour = infoBoxValueColour
+ self.infoBoxDescriptionColour = infoBoxDescriptionColour
+ self.infoBoxBackgroundColour = infoBoxBackgroundColour
+ self.infoBoxBorderColour = infoBoxBorderColour
+ self.infoBoxBorderStyle = infoBoxBorderStyle
+
+ self.markerType = markerType
+
+ self.xAxisGridStyle = xAxisGridStyle
+ self.xAxisLabelPosition = xAxisLabelPosition
+ self.xAxisLabelColour = xAxisLabelColour
+ self.xAxisLabelsFrom = xAxisLabelsFrom
+ self.xAxisTitle = xAxisTitle
+
+ self.yAxisGridStyle = yAxisGridStyle
+ self.yAxisLabelPosition = yAxisLabelPosition
+ self.yAxisNumberOfLabels = yAxisNumberOfLabels
+ self.yAxisLabelColour = yAxisLabelColour
+ self.yAxisTitle = yAxisTitle
+
+ self.baseline = baseline
+ self.topLine = topLine
+
+ self.globalAnimation = globalAnimation
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift
new file mode 100644
index 00000000..a0657d11
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift
@@ -0,0 +1,45 @@
+//
+// BarStyle.swift
+//
+//
+// Created by Will Dale on 12/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Model for controlling the aesthetic of the bars.
+
+ # Example
+ ```
+ BarStyle(barWidth : 0.5,
+ cornerRadius: CornerRadius(top: 15),
+ colourFrom : .barStyle,
+ colour : ColourStyle(colour: .blue))
+ ```
+ */
+public struct BarStyle: CTBarStyle {
+
+ public var barWidth : CGFloat
+ public var cornerRadius: CornerRadius
+ public var colourFrom : ColourFrom
+ public var colour : ColourStyle
+
+ // MARK: - Single colour
+ /// Bar Chart with single colour
+ /// - Parameters:
+ /// - barWidth: How much of the available width to use. 0...1
+ /// - cornerRadius: Corner radius of the bar shape.
+ /// - colourFrom: Where to get the colour data from.
+ /// - colour: Single Colour
+ public init(barWidth : CGFloat = 1,
+ cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0),
+ colourFrom : ColourFrom = .barStyle,
+ colour : ColourStyle = ColourStyle(colour: .red)
+ ) {
+ self.barWidth = barWidth
+ self.cornerRadius = cornerRadius
+ self.colourFrom = colourFrom
+ self.colour = colour
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/RoundedRectangleBarShape.swift b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift
similarity index 72%
rename from Sources/SwiftUICharts/BarChart/RoundedRectangleBarShape.swift
rename to Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift
index e1195998..d1d5db95 100644
--- a/Sources/SwiftUICharts/BarChart/RoundedRectangleBarShape.swift
+++ b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift
@@ -7,14 +7,30 @@
import SwiftUI
-// https://stackoverflow.com/a/56763282
-struct RoundedRectangleBarShape: Shape {
- var tl: CGFloat = 0.0
- var tr: CGFloat = 0.0
- var bl: CGFloat = 0.0
- var br: CGFloat = 0.0
-
- func path(in rect: CGRect) -> Path {
+/**
+ Round rectange used for the bar shapes
+
+ [SO](https://stackoverflow.com/a/56763282)
+ */
+internal struct RoundedRectangleBarShape: Shape {
+
+ private let tl: CGFloat
+ private let tr: CGFloat
+ private let bl: CGFloat
+ private let br: CGFloat
+
+ internal init(tl: CGFloat,
+ tr: CGFloat,
+ bl: CGFloat,
+ br: CGFloat
+ ) {
+ self.tl = tl
+ self.tr = tr
+ self.bl = bl
+ self.br = br
+ }
+
+ internal func path(in rect: CGRect) -> Path {
var path = Path()
let w = rect.size.width
diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift
new file mode 100644
index 00000000..98a47aa5
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift
@@ -0,0 +1,71 @@
+//
+// BarChart.swift
+//
+//
+// Created by Will Dale on 11/01/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a bar chart.
+
+ Uses `BarChartData` data model.
+
+ # Declaration
+ ```
+ BarChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct BarChart: View where ChartData: BarChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a bar chart view.
+ /// - Parameter chartData: Must be BarChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ }
+
+ public var body: some View {
+ if chartData.isGreaterThanTwo() {
+ HStack(spacing: 0) {
+
+ switch chartData.barStyle.colourFrom {
+ case .barStyle:
+
+ BarChartBarStyleSubView(chartData: chartData)
+ .accessibilityLabel(Text("\(chartData.metadata.title)"))
+
+ case .dataPoints:
+
+ BarChartDataPointSubView(chartData: chartData)
+ .accessibilityLabel(Text("\(chartData.metadata.title)"))
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift
deleted file mode 100644
index 7fabb15f..00000000
--- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift
+++ /dev/null
@@ -1,124 +0,0 @@
-//
-// BarChartView.swift
-//
-//
-// Created by Will Dale on 11/01/2021.
-//
-
-import SwiftUI
-
-public struct BarChart: View {
- public init() {}
- public var body: some View {
- BarChartView()
- }
-}
-
-internal struct BarChartView: View {
-
- @EnvironmentObject var chartData: ChartData
-
- internal var body: some View {
-
- let maxValue: Double = chartData.maxValue()
- let style : BarStyle = chartData.barStyle
-
- return HStack(spacing: 0) {
- ForEach(chartData.dataPoints) { data in
-
- switch style.colourFrom {
- case .barStyle:
-
- if style.colourType == .colour,
- let colour = style.colour
- {
-
- ColourBar(colour, data, maxValue, chartData.chartStyle, style)
-
- } else if style.colourType == .gradientColour,
- let colours = style.colours,
- let startPoint = style.startPoint,
- let endPoint = style.endPoint
- {
-
- GradientColoursBar(colours, startPoint, endPoint, data, maxValue, chartData.chartStyle, style)
-
- } else if style.colourType == .gradientStops,
- let stops = style.stops,
- let startPoint = style.startPoint,
- let endPoint = style.endPoint
- {
-
- let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
- GradientStopsBar(safeStops, startPoint, endPoint, data, maxValue, chartData.chartStyle, style)
-
- }
-
-
- case .dataPoints:
- if data.colourType == .colour,
- let colour = data.colour
- {
- ColourBar(colour, data, maxValue, chartData.chartStyle, style)
- } else if data.colourType == .gradientColour,
- let colours = data.colours,
- let startPoint = data.startPoint,
- let endPoint = data.endPoint
- {
-
- GradientColoursBar(colours, startPoint, endPoint, data, maxValue, chartData.chartStyle, style)
-
- } else if data.colourType == .gradientStops,
- let stops = data.stops,
- let startPoint = data.startPoint,
- let endPoint = data.endPoint
- {
-
- let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
-
- GradientStopsBar(safeStops, startPoint, endPoint, data, maxValue, chartData.chartStyle, style)
- }
- }
- }
- }
- .onAppear {
- chartData.viewData.chartType = .bar
-
- guard let lineLegend = chartData.metadata?.lineLegend else { return }
- let style : BarStyle = chartData.barStyle
-
- if !chartData.legends.contains(where: { $0.legend == lineLegend }) { // init twice
- if style.colourType == .colour,
- let colour = style.colour
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- colour : colour,
- strokeStyle: nil,
- prioity : 1,
- chartType : .bar))
- } else if style.colourType == .gradientColour,
- let colours = style.colours
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- colours : colours,
- startPoint : .leading,
- endPoint : .trailing,
- strokeStyle: nil,
- prioity : 1,
- chartType : .bar))
- } else if style.colourType == .gradientStops,
- let stops = style.stops
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- stops : stops,
- startPoint : .leading,
- endPoint : .trailing,
- strokeStyle: nil,
- prioity : 1,
- chartType : .bar))
- }
- }
- }
- }
-
-}
diff --git a/Sources/SwiftUICharts/BarChart/Views/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/Bars.swift
deleted file mode 100644
index 42177f3c..00000000
--- a/Sources/SwiftUICharts/BarChart/Views/Bars.swift
+++ /dev/null
@@ -1,137 +0,0 @@
-//
-// Bars.swift
-//
-//
-// Created by Will Dale on 12/01/2021.
-//
-
-import SwiftUI
-
-struct ColourBar: View {
-
- let colour : Color
- let data : ChartDataPoint
- let maxValue : Double
- let chartStyle : ChartStyle
- let style : BarStyle
-
- init(_ colour : Color,
- _ data : ChartDataPoint,
- _ maxValue : Double,
- _ chartStyle : ChartStyle,
- _ style : BarStyle
- ) {
- self.colour = colour
- self.data = data
- self.maxValue = maxValue
- self.chartStyle = chartStyle
- self.style = style
- }
-
- @State var startAnimation : Bool = false
-
- var body: some View {
- RoundedRectangleBarShape(tl: style.cornerRadius.top, tr: style.cornerRadius.top, bl: style.cornerRadius.bottom, br: style.cornerRadius.bottom)
- .fill(colour)
- .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom)
- .scaleEffect(x: style.barWidth, anchor: .center)
- .animateOnAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = true
- }
- .animateOnDisAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = false
- }
- }
-}
-
-struct GradientColoursBar: View {
-
- let colours : [Color]
- let startPoint : UnitPoint
- let endPoint : UnitPoint
- let data : ChartDataPoint
- let maxValue : Double
- let chartStyle : ChartStyle
- let style : BarStyle
-
- init(_ colours : [Color],
- _ startPoint : UnitPoint,
- _ endPoint : UnitPoint,
- _ data : ChartDataPoint,
- _ maxValue : Double,
- _ chartStyle : ChartStyle,
- _ style : BarStyle
- ) {
- self.colours = colours
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.data = data
- self.maxValue = maxValue
- self.chartStyle = chartStyle
- self.style = style
- }
-
- @State var startAnimation : Bool = false
-
- var body: some View {
- RoundedRectangleBarShape(tl: style.cornerRadius.top, tr: style.cornerRadius.top, bl: style.cornerRadius.bottom, br: style.cornerRadius.bottom)
- .fill(LinearGradient(gradient: Gradient(colors: colours),
- startPoint: startPoint,
- endPoint: endPoint))
- .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom)
- .scaleEffect(x: style.barWidth, anchor: .center)
- .animateOnAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = true
- }
- .animateOnDisAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = false
- }
- }
-}
-
-struct GradientStopsBar: View {
-
- let stops : [Gradient.Stop]
- let startPoint : UnitPoint
- let endPoint : UnitPoint
- let data : ChartDataPoint
- let maxValue : Double
- let chartStyle : ChartStyle
- let style : BarStyle
-
- init(_ stops : [Gradient.Stop],
- _ startPoint : UnitPoint,
- _ endPoint : UnitPoint,
- _ data : ChartDataPoint,
- _ maxValue : Double,
- _ chartStyle : ChartStyle,
- _ style : BarStyle
- ) {
- self.stops = stops
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.data = data
- self.maxValue = maxValue
- self.chartStyle = chartStyle
- self.style = style
- }
-
- @State var startAnimation : Bool = false
-
- var body: some View {
- RoundedRectangleBarShape(tl: style.cornerRadius.top, tr: style.cornerRadius.top, bl: style.cornerRadius.bottom, br: style.cornerRadius.bottom)
- .fill(LinearGradient(gradient: Gradient(stops: stops),
- startPoint: startPoint,
- endPoint: endPoint))
- .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom)
- .scaleEffect(x: style.barWidth, anchor: .center)
- .animateOnAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = true
- }
- .animateOnDisAppear(using: chartStyle.globalAnimation) {
- self.startAnimation = false
- }
- }
-}
-
-
diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift
new file mode 100644
index 00000000..79ef419d
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift
@@ -0,0 +1,113 @@
+//
+// GroupedBarChart.swift
+//
+//
+// Created by Will Dale on 25/01/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a grouped bar chart.
+
+ Uses `GroupedBarChartData` data model.
+
+ # Declaration
+ ```
+ GroupedBarChart(chartData: data, groupSpacing: 25)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct GroupedBarChart: View where ChartData: GroupedBarChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ private let groupSpacing : CGFloat
+
+ /// Initialises a grouped bar chart view.
+ /// - Parameters:
+ /// - chartData: Must be GroupedBarChartData model.
+ /// - groupSpacing: Spacing between groups of bars.
+ public init(chartData: ChartData, groupSpacing: CGFloat) {
+ self.chartData = chartData
+ self.groupSpacing = groupSpacing
+ self.chartData.groupSpacing = groupSpacing
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+ if chartData.isGreaterThanTwo() {
+ HStack(spacing: groupSpacing) {
+ ForEach(chartData.dataSets.dataSets) { dataSet in
+ HStack(spacing: 0) {
+ ForEach(dataSet.dataPoints) { dataPoint in
+
+ if dataPoint.group.colour.colourType == .colour,
+ let colour = dataPoint.group.colour.colour
+ {
+
+ ColourBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : colour)
+ .accessibilityLabel(Text("\(chartData.metadata.title)"))
+
+ } else if dataPoint.group.colour.colourType == .gradientColour,
+ let colours = dataPoint.group.colour.colours,
+ let startPoint = dataPoint.group.colour.startPoint,
+ let endPoint = dataPoint.group.colour.endPoint
+ {
+
+ GradientColoursBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colours : colours,
+ startPoint : startPoint,
+ endPoint : endPoint)
+ .accessibilityLabel( Text("\(chartData.metadata.title)"))
+
+ } else if dataPoint.group.colour.colourType == .gradientStops,
+ let stops = dataPoint.group.colour.stops,
+ let startPoint = dataPoint.group.colour.startPoint,
+ let endPoint = dataPoint.group.colour.endPoint
+ {
+
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ GradientStopsBar(chartData : chartData,
+ dataPoint : dataPoint,
+ stops : safeStops,
+ startPoint : startPoint,
+ endPoint : endPoint)
+
+ .accessibilityLabel( Text("\(chartData.metadata.title)"))
+
+ }
+ }
+ }
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift
new file mode 100644
index 00000000..7ebd4a09
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift
@@ -0,0 +1,70 @@
+//
+// RangedBarChart.swift
+//
+//
+// Created by Will Dale on 05/03/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a grouped bar chart.
+
+ Uses `RangedBarChartData` data model.
+
+ # Declaration
+ ```
+ RangedBarChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct RangedBarChart: View where ChartData: RangedBarChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a bar chart view.
+ /// - Parameter chartData: Must be RangedBarChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ }
+
+ public var body: some View {
+ if chartData.isGreaterThanTwo() {
+ HStack(spacing: 0) {
+
+ switch chartData.barStyle.colourFrom {
+ case .barStyle:
+
+ RangedBarChartBarStyleSubView(chartData: chartData)
+ .accessibilityLabel( Text("\(chartData.metadata.title)"))
+ case .dataPoints:
+
+ RangedBarChartDataPointSubView(chartData: chartData)
+ .accessibilityLabel( Text("\(chartData.metadata.title)"))
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift
new file mode 100644
index 00000000..b192948b
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift
@@ -0,0 +1,78 @@
+//
+// StackedBarChart.swift
+//
+//
+// Created by Will Dale on 12/02/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a stacked bar chart.
+
+ Uses `StackedBarChartData` data model.
+
+ # Declaration
+ ```
+ StackedBarChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct StackedBarChart: View where ChartData: StackedBarChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a stacked bar chart view.
+ /// - Parameters:
+ /// - chartData: Must be StackedBarChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+
+ if chartData.isGreaterThanTwo() {
+
+ HStack(alignment: .bottom, spacing: 0) {
+ ForEach(chartData.dataSets.dataSets) { dataSet in
+
+ StackElementSubView(dataSet: dataSet, specifier: chartData.infoView.touchSpecifier)
+ .scaleEffect(y: startAnimation ? CGFloat(dataSet.maxValue() / chartData.maxValue) : 0, anchor: .bottom)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityLabel( Text("\(chartData.metadata.title)"))
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift
new file mode 100644
index 00000000..808c03d4
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift
@@ -0,0 +1,233 @@
+//
+// BarChartSubViews.swift
+//
+//
+// Created by Will Dale on 26/01/2021.
+//
+
+import SwiftUI
+
+// MARK: - Standard
+// MARK: Bar Style
+/**
+ Bar segment where the colour information comes from chart style.
+ */
+internal struct BarChartBarStyleSubView: View {
+
+ private let chartData: CD
+
+ internal init(chartData: CD) {
+ self.chartData = chartData
+ }
+
+ internal var body: some View {
+ if chartData.barStyle.colour.colourType == .colour,
+ let colour = chartData.barStyle.colour.colour
+ {
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ ColourBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : colour)
+ }
+
+ } else if chartData.barStyle.colour.colourType == .gradientColour,
+ let colours = chartData.barStyle.colour.colours,
+ let startPoint = chartData.barStyle.colour.startPoint,
+ let endPoint = chartData.barStyle.colour.endPoint
+ {
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GradientColoursBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colours : colours,
+ startPoint : startPoint,
+ endPoint : endPoint)
+ }
+
+ } else if chartData.barStyle.colour.colourType == .gradientStops,
+ let stops = chartData.barStyle.colour.stops,
+ let startPoint = chartData.barStyle.colour.startPoint,
+ let endPoint = chartData.barStyle.colour.endPoint
+ {
+
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GradientStopsBar(chartData : chartData,
+ dataPoint : dataPoint,
+ stops : safeStops,
+ startPoint : startPoint,
+ endPoint : endPoint)
+ }
+
+ }
+ }
+}
+
+// MARK: DataPoints
+/**
+ Bar segment where the colour information comes from datapoints.
+ */
+internal struct BarChartDataPointSubView: View {
+
+ private let chartData: CD
+
+ internal init(chartData: CD) {
+ self.chartData = chartData
+ }
+
+ internal var body: some View {
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+
+ if dataPoint.colour.colourType == .colour,
+ let colour = dataPoint.colour.colour
+ {
+
+ ColourBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : colour)
+
+ } else if dataPoint.colour.colourType == .gradientColour,
+ let colours = dataPoint.colour.colours,
+ let startPoint = dataPoint.colour.startPoint,
+ let endPoint = dataPoint.colour.endPoint
+ {
+
+ GradientColoursBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colours : colours,
+ startPoint : startPoint,
+ endPoint : endPoint)
+
+ } else if dataPoint.colour.colourType == .gradientStops,
+ let stops = dataPoint.colour.stops,
+ let startPoint = dataPoint.colour.startPoint,
+ let endPoint = dataPoint.colour.endPoint
+ {
+
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ GradientStopsBar(chartData : chartData,
+ dataPoint : dataPoint,
+ stops : safeStops,
+ startPoint : startPoint,
+ endPoint : endPoint)
+
+ } else {
+ ColourBar(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : .blue)
+ }
+ }
+ }
+}
+
+// MARK: - Ranged
+// MARK: BarStyle
+
+internal struct RangedBarChartBarStyleSubView: View {
+
+ private let chartData : CD
+
+ internal init(chartData: CD) {
+ self.chartData = chartData
+ }
+
+ var body: some View {
+
+ if chartData.barStyle.colour.colourType == .colour,
+ let colour = chartData.barStyle.colour.colour {
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GeometryReader { geo in
+ RangedBarChartColourCell(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : colour,
+ barSize : geo.frame(in: .local))
+ }
+ }
+ } else if chartData.barStyle.colour.colourType == .gradientColour,
+ let colours = chartData.barStyle.colour.colours,
+ let startPoint = chartData.barStyle.colour.startPoint,
+ let endPoint = chartData.barStyle.colour.endPoint {
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GeometryReader { geo in
+ RangedBarChartColoursCell(chartData : chartData,
+ dataPoint : dataPoint,
+ colours : colours,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ barSize : geo.frame(in: .local))
+ }
+ }
+ } else if chartData.barStyle.colour.colourType == .gradientStops,
+ let stops = chartData.barStyle.colour.stops,
+ let startPoint = chartData.barStyle.colour.startPoint,
+ let endPoint = chartData.barStyle.colour.endPoint {
+
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GeometryReader { geo in
+ RangedBarChartStopsCell(chartData : chartData,
+ dataPoint : dataPoint,
+ stops : safeStops,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ barSize : geo.frame(in: .local))
+ }
+ }
+ }
+ }
+}
+
+// MARK: DataPoints
+internal struct RangedBarChartDataPointSubView: View {
+
+ private let chartData : CD
+
+ internal init(chartData: CD) {
+ self.chartData = chartData
+ }
+
+ internal var body: some View {
+
+ ForEach(chartData.dataSets.dataPoints) { dataPoint in
+ GeometryReader { geo in
+ if dataPoint.colour.colourType == .colour,
+ let colour = dataPoint.colour.colour {
+
+ RangedBarChartColourCell(chartData : chartData,
+ dataPoint : dataPoint,
+ colour : colour,
+ barSize : geo.frame(in: .local))
+
+ } else if dataPoint.colour.colourType == .gradientColour,
+ let colours = dataPoint.colour.colours,
+ let startPoint = dataPoint.colour.startPoint,
+ let endPoint = dataPoint.colour.endPoint {
+
+ RangedBarChartColoursCell(chartData : chartData,
+ dataPoint : dataPoint,
+ colours : colours,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ barSize : geo.frame(in: .local))
+ } else if dataPoint.colour.colourType == .gradientStops,
+ let stops = dataPoint.colour.stops,
+ let startPoint = dataPoint.colour.startPoint,
+ let endPoint = dataPoint.colour.endPoint {
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ RangedBarChartStopsCell(chartData : chartData,
+ dataPoint : dataPoint,
+ stops : safeStops,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ barSize : geo.frame(in: .local))
+ }
+ }
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift
new file mode 100644
index 00000000..e074c160
--- /dev/null
+++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift
@@ -0,0 +1,464 @@
+//
+// Bars.swift
+//
+//
+// Created by Will Dale on 12/01/2021.
+//
+
+import SwiftUI
+
+// MARK: Standard
+/**
+ Sub view of a single bar using a single colour.
+
+ For Standard and Grouped Bar Charts.
+ */
+internal struct ColourBar: View {
+
+ private let chartData : CD
+ private let colour : Color
+ private let dataPoint : DP
+
+ internal init(chartData : CD,
+ dataPoint : DP,
+ colour : Color
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.colour = colour
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(colour)
+ .scaleEffect(y: startAnimation ? CGFloat(dataPoint.value / chartData.maxValue) : 0, anchor: .bottom)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
+
+
+
+/**
+ Sub view of a single bar using colour gradient.
+
+ For Standard and Grouped Bar Charts.
+ */
+internal struct GradientColoursBar: View {
+
+ private let chartData : CD
+ private let dataPoint : DP
+ private let colours : [Color]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+
+ internal init(chartData : CD,
+ dataPoint : DP,
+ colours : [Color],
+ startPoint : UnitPoint,
+ endPoint : UnitPoint
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.colours = colours
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .scaleEffect(y: startAnimation ? CGFloat(dataPoint.value / chartData.maxValue) : 0, anchor: .bottom)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
+
+/**
+ Sub view of a single bar using colour gradient with stop control.
+
+ For Standard and Grouped Bar Charts.
+ */
+internal struct GradientStopsBar: View {
+
+ private let chartData : CD
+ private let dataPoint : DP
+ private let stops : [Gradient.Stop]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+
+ internal init(chartData : CD,
+ dataPoint : DP,
+ stops : [Gradient.Stop],
+ startPoint: UnitPoint,
+ endPoint : UnitPoint
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.stops = stops
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .scaleEffect(y: startAnimation ? CGFloat(dataPoint.value / chartData.maxValue) : 0, anchor: .bottom)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
+
+// MARK: - Stacked
+/**
+ Individual elements that make up a single bar.
+ */
+internal struct StackElementSubView: View {
+
+ private let dataSet : MultiBarDataSet
+ private let specifier : String
+
+ internal init(dataSet: MultiBarDataSet, specifier: String) {
+ self.dataSet = dataSet
+ self.specifier = specifier
+ }
+
+ internal var body: some View {
+ GeometryReader { geo in
+
+ VStack(spacing: 0) {
+ ForEach(dataSet.dataPoints.reversed()) { dataPoint in
+
+ if dataPoint.group.colour.colourType == .colour,
+ let colour = dataPoint.group.colour.colour
+ {
+
+ ColourPartBar(colour, getHeight(height : geo.size.height,
+ dataSet : dataSet,
+ dataPoint : dataPoint))
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: specifier))
+
+ } else if dataPoint.group.colour.colourType == .gradientColour,
+ let colours = dataPoint.group.colour.colours,
+ let startPoint = dataPoint.group.colour.startPoint,
+ let endPoint = dataPoint.group.colour.endPoint
+ {
+
+ GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height,
+ dataSet : dataSet,
+ dataPoint : dataPoint))
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: specifier))
+
+ } else if dataPoint.group.colour.colourType == .gradientStops,
+ let stops = dataPoint.group.colour.stops,
+ let startPoint = dataPoint.group.colour.startPoint,
+ let endPoint = dataPoint.group.colour.endPoint
+ {
+
+ let safeStops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height,
+ dataSet : dataSet,
+ dataPoint : dataPoint))
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: specifier))
+ }
+
+ }
+ }
+ }
+ }
+
+ /// Sets the height of each element.
+ /// - Parameters:
+ /// - height: Hiehgt of the whole bar.
+ /// - dataSet: Which data set the bar comes from.
+ /// - dataPoint: Data point to draw.
+ /// - Returns: Height of the element.
+ private func getHeight(height: CGFloat,
+ dataSet: MultiBarDataSet,
+ dataPoint: MultiBarChartDataPoint
+ ) -> CGFloat {
+ let value = dataPoint.value
+ let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value }
+ return height * CGFloat(value / sum)
+ }
+}
+
+
+/**
+ Sub view of an element of a bar using a single colour.
+
+ For Stacked Bar Charts.
+ */
+internal struct ColourPartBar: View {
+
+ private let colour : Color
+ private let height : CGFloat
+
+ internal init(_ colour : Color,
+ _ height : CGFloat
+ ) {
+ self.colour = colour
+ self.height = height
+ }
+
+ internal var body: some View {
+ Rectangle()
+ .fill(colour)
+ .frame(height: height)
+ }
+}
+
+/**
+ Sub view of an element of a bar using colour gradient.
+
+ For Standard and Grouped Bar Charts.
+ */
+internal struct GradientColoursPartBar: View {
+
+ private let colours : [Color]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+ private let height : CGFloat
+
+ internal init(_ colours : [Color],
+ _ startPoint : UnitPoint,
+ _ endPoint : UnitPoint,
+ _ height : CGFloat
+ ) {
+ self.colours = colours
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.height = height
+ }
+
+ internal var body: some View {
+ Rectangle()
+ .fill(LinearGradient(gradient : Gradient(colors: colours),
+ startPoint : startPoint,
+ endPoint : endPoint))
+ .frame(height: height)
+ }
+}
+
+/**
+ Sub view of an element of a bar using colour gradient with stop control.
+
+ For Standard and Grouped Bar Charts.
+ */
+internal struct GradientStopsPartBar: View {
+
+ private let stops : [Gradient.Stop]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+ private let height : CGFloat
+
+ internal init(_ stops : [Gradient.Stop],
+ _ startPoint : UnitPoint,
+ _ endPoint : UnitPoint,
+ _ height : CGFloat
+ ) {
+ self.stops = stops
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.height = height
+ }
+
+ internal var body: some View {
+ Rectangle()
+ .fill(LinearGradient(gradient : Gradient(stops: stops),
+ startPoint : startPoint,
+ endPoint : endPoint))
+ .frame(height: height)
+ }
+}
+
+// MARK: - Ranged
+internal struct RangedBarChartColourCell: View {
+
+ private let chartData: CD
+ private let dataPoint: CD.Set.DataPoint
+ private let colour : Color
+ private let barSize : CGRect
+
+ internal init(chartData : CD,
+ dataPoint : CD.Set.DataPoint,
+ colour : Color,
+ barSize : CGRect
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.colour = colour
+ self.barSize = barSize
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(colour)
+
+ .scaleEffect(y: startAnimation ? CGFloat((dataPoint.upperValue - dataPoint.lowerValue) / chartData.range) : 0, anchor: .center)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .position(x: barSize.midX,
+ y: chartData.getBarPositionX(dataPoint: dataPoint, height: barSize.height))
+
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
+
+
+internal struct RangedBarChartColoursCell: View {
+
+ private let chartData : CD
+ private let dataPoint : CD.Set.DataPoint
+ private let colours : [Color]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+ private let barSize : CGRect
+
+ internal init(chartData : CD,
+ dataPoint : CD.Set.DataPoint,
+ colours : [Color],
+ startPoint: UnitPoint,
+ endPoint : UnitPoint,
+ barSize : CGRect
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.colours = colours
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.barSize = barSize
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(LinearGradient(gradient : Gradient(colors: colours),
+ startPoint : startPoint,
+ endPoint : endPoint))
+
+ .scaleEffect(y: startAnimation ? CGFloat((dataPoint.upperValue - dataPoint.lowerValue) / chartData.range) : 0, anchor: .center)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .position(x: barSize.midX,
+ y: chartData.getBarPositionX(dataPoint: dataPoint, height: barSize.height))
+
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
+
+internal struct RangedBarChartStopsCell: View {
+
+ private let chartData : CD
+ private let dataPoint : CD.Set.DataPoint
+ private let stops : [Gradient.Stop]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+ private let barSize : CGRect
+
+ internal init(chartData : CD,
+ dataPoint : CD.Set.DataPoint,
+ stops : [Gradient.Stop],
+ startPoint: UnitPoint,
+ endPoint : UnitPoint,
+ barSize : CGRect
+ ) {
+ self.chartData = chartData
+ self.dataPoint = dataPoint
+ self.stops = stops
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.barSize = barSize
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ RoundedRectangleBarShape(tl: chartData.barStyle.cornerRadius.top,
+ tr: chartData.barStyle.cornerRadius.top,
+ bl: chartData.barStyle.cornerRadius.bottom,
+ br: chartData.barStyle.cornerRadius.bottom)
+ .fill(LinearGradient(gradient : Gradient(stops: stops),
+ startPoint : startPoint,
+ endPoint : endPoint))
+
+ .scaleEffect(y: startAnimation ? CGFloat((dataPoint.upperValue - dataPoint.lowerValue) / chartData.range) : 0, anchor: .center)
+ .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center)
+ .position(x: barSize.midX,
+ y: chartData.getBarPositionX(dataPoint: dataPoint, height: barSize.height))
+
+ .background(Color(.gray).opacity(0.000000001))
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift
new file mode 100644
index 00000000..24cff73d
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift
@@ -0,0 +1,117 @@
+//
+// LineChartEnums.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import Foundation
+
+/**
+ Drawing style of the line
+ ```
+ case line // Straight line from point to point
+ case curvedLine // Dual control point curved line
+ ```
+ */
+public enum LineType {
+ /// Straight line from point to point
+ case line
+ /// Dual control point curved line
+ case curvedLine
+}
+
+/**
+ Style of the point marks
+ ```
+ case filled // Just fill
+ case outline // Just stroke
+ case filledOutLine // Both fill and stroke
+ ```
+ */
+public enum PointType {
+ /// Just fill
+ case filled
+ /// Just stroke
+ case outline
+ /// Both fill and stroke
+ case filledOutLine
+}
+
+/**
+ Shape of the points
+ ```
+ case circle
+ case square
+ case roundSquare
+ ```
+ */
+public enum PointShape {
+ /// Circle Shape
+ case circle
+ /// Square Shape
+ case square
+ /// Rounded Square Shape
+ case roundSquare
+}
+
+/**
+ Where the Y and X touch markers should attach themselves to.
+ ```
+ case line(dot: Dot) // Attached to the line.
+ case point // Attached to the data points.
+ ```
+ */
+public enum MarkerAttachemnt {
+ /// Attached to the line.
+ case line(dot: Dot)
+ /// Attached to the data points.
+ case point
+}
+
+/**
+ Where the marker lines come from to meet at a specified point.
+ ```
+ case none // No overlay markers.
+ case indicator(style: DotStyle) // Dot that follows the path.
+ case vertical(attachment: MarkerAttachemnt) // Vertical line from top to bottom.
+ case full(attachment: MarkerAttachemnt) // Full width and height of view intersecting at a specified point.
+ case bottomLeading(attachment: MarkerAttachemnt) // From bottom and leading edges meeting at a specified point.
+ case bottomTrailing(attachment: MarkerAttachemnt) // From bottom and trailing edges meeting at a specified point.
+ case topLeading(attachment: MarkerAttachemnt) // From top and leading edges meeting at a specified point.
+ case topTrailing(attachment: MarkerAttachemnt) // From top and trailing edges meeting at a specified point.
+ ```
+ */
+public enum LineMarkerType: MarkerType {
+ /// No overlay markers.
+ case none
+ /// Dot that follows the path.
+ case indicator(style: DotStyle)
+ /// Vertical line from top to bottom.
+ case vertical(attachment: MarkerAttachemnt)
+ /// Full width and height of view intersecting at a specified point.
+ case full(attachment: MarkerAttachemnt)
+ /// From bottom and leading edges meeting at a specified point.
+ case bottomLeading(attachment: MarkerAttachemnt)
+ /// From bottom and trailing edges meeting at a specified point.
+ case bottomTrailing(attachment: MarkerAttachemnt)
+ /// From top and leading edges meeting at a specified point.
+ case topLeading(attachment: MarkerAttachemnt)
+ /// From top and trailing edges meeting at a specified point.
+ case topTrailing(attachment: MarkerAttachemnt)
+}
+
+/**
+ Whether or not to show a dot on the line
+
+ ```
+ case none // No Dot
+ case style(_ style: DotStyle) // Adds a dot the line at point of touch.
+ ```
+ */
+public enum Dot {
+ /// No Dot
+ case none
+ /// Adds a dot the line at point of touch.
+ case style(_ style: DotStyle)
+}
diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift
new file mode 100644
index 00000000..54eb721d
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift
@@ -0,0 +1,226 @@
+//
+// PathExtensions.swift
+//
+//
+// Created by Will Dale on 10/02/2021.
+//
+
+import SwiftUI
+
+extension Path {
+ /// Draws straight lines between data points.
+ static func straightLine(
+ rect: CGRect,
+ dataPoints: [DP],
+ minValue: Double,
+ range: Double,
+ isFilled: Bool,
+ ignoreZero: Bool
+ ) -> Path {
+ let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1)
+ let y : CGFloat = rect.height / CGFloat(range)
+ var path = Path()
+ let firstPoint = CGPoint(x: 0,
+ y: (CGFloat(dataPoints[0].value - minValue) * -y) + rect.height)
+
+ path.move(to: firstPoint)
+
+ for index in 1 ..< dataPoints.count {
+ let nextPoint = CGPoint(x: CGFloat(index) * x,
+ y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addLine(to: nextPoint)
+ } else {
+ if dataPoints[index].value != 0 {
+ path.addLine(to: nextPoint)
+ }
+ }
+
+ }
+ if isFilled {
+ path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * x, y: rect.height))
+ path.addLine(to: CGPoint(x: 0, y: rect.height))
+ path.closeSubpath()
+ }
+ return path
+ }
+
+ /// Draws cubic BĂ©zier curved lines between data points.
+ static func curvedLine(
+ rect: CGRect,
+ dataPoints: [DP],
+ minValue: Double,
+ range: Double,
+ isFilled: Bool,
+ ignoreZero: Bool
+ ) -> Path {
+ let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1)
+ let y : CGFloat = rect.height / CGFloat(range)
+ var path = Path()
+ let firstPoint = CGPoint(x: 0,
+ y: (CGFloat(dataPoints[0].value - minValue) * -y) + rect.height)
+ path.move(to: firstPoint)
+ var previousPoint = firstPoint
+ var lastIndex : Int = 0
+ for index in 1 ..< dataPoints.count {
+ let nextPoint = CGPoint(x: CGFloat(index) * x,
+ y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ lastIndex = index
+ } else {
+ if dataPoints[index].value != 0 {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ lastIndex = index
+ }
+ }
+
+ previousPoint = nextPoint
+ }
+ if isFilled {
+ // Draw line straight down from last value
+ path.addLine(to: CGPoint(x: CGFloat(lastIndex) * x,
+ y: rect.height))
+
+ // Draw line back to start along x axis
+ path.addLine(to: CGPoint(x: 0,
+ y: rect.height))
+ // close back to first data point
+ path.closeSubpath()
+ }
+ return path
+ }
+
+
+ /// Draws straight lines between data points.
+ static func straightLineBox(
+ rect: CGRect,
+ dataPoints: [DP],
+ minValue: Double,
+ range: Double,
+ ignoreZero: Bool
+ ) -> Path {
+ let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1)
+ let y : CGFloat = rect.height / CGFloat(range)
+
+ var path = Path()
+
+ // Upper Path
+ let firstPointUpper = CGPoint(x: 0,
+ y: (CGFloat(dataPoints[0].upperValue - minValue) * -y) + rect.height)
+ path.move(to: firstPointUpper)
+ for indexUpper in 1 ..< dataPoints.count {
+ let nextPointUpper = CGPoint(x: CGFloat(indexUpper) * x,
+ y: (CGFloat(dataPoints[indexUpper].upperValue - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addLine(to: nextPointUpper)
+ } else {
+ if dataPoints[indexUpper].value != 0 {
+ path.addLine(to: nextPointUpper)
+
+ }
+ }
+ }
+
+ // Lower Path
+ for indexLower in (0 ..< dataPoints.count).reversed() {
+ let nextPointLower = CGPoint(x: CGFloat(indexLower) * x,
+ y: (CGFloat(dataPoints[indexLower].lowerValue - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addLine(to: nextPointLower)
+ } else {
+ if dataPoints[indexLower].value != 0 {
+ path.addLine(to: nextPointLower)
+ }
+ }
+ }
+
+ path.addLine(to: firstPointUpper)
+
+ return path
+ }
+
+ /// Draws straight lines between data points.
+ static func curvedLineBox(
+ rect: CGRect,
+ dataPoints: [DP],
+ minValue: Double,
+ range: Double,
+ ignoreZero: Bool
+ ) -> Path {
+ let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1)
+ let y : CGFloat = rect.height / CGFloat(range)
+
+ var path = Path()
+
+ // Upper Path
+ let firstPointUpper = CGPoint(x: 0,
+ y: (CGFloat(dataPoints[0].upperValue - minValue) * -y) + rect.height)
+ path.move(to: firstPointUpper)
+
+ var previousPoint = firstPointUpper
+
+ for indexUpper in 1 ..< dataPoints.count {
+
+ let nextPoint = CGPoint(x: CGFloat(indexUpper) * x,
+ y: (CGFloat(dataPoints[indexUpper].upperValue - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ } else {
+ if dataPoints[indexUpper].value != 0 {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ }
+ }
+ previousPoint = nextPoint
+ }
+
+ // Lower Path
+ for indexLower in (0 ..< dataPoints.count).reversed() {
+ let nextPoint = CGPoint(x: CGFloat(indexLower) * x,
+ y: (CGFloat(dataPoints[indexLower].lowerValue - minValue) * -y) + rect.height)
+
+ if !ignoreZero {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ } else {
+ if dataPoints[indexLower].value != 0 {
+ path.addCurve(to: nextPoint,
+ control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
+ y: previousPoint.y),
+ control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
+ y: nextPoint.y))
+ }
+ }
+ previousPoint = nextPoint
+ }
+
+ path.addLine(to: firstPointUpper)
+
+ return path
+
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift
new file mode 100644
index 00000000..2d10fd74
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift
@@ -0,0 +1,168 @@
+//
+// LineChartData.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling a single line, line chart.
+
+ This model contains the data and styling information for a single line, line chart.
+ */
+public final class LineChartData: CTLineChartDataProtocol {
+
+ // MARK: Properties
+ public final let id : UUID = UUID()
+
+ @Published public final var dataSets : LineDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var chartStyle : LineChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+
+ public final var noDataText : Text
+ public final var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ internal final var isFilled : Bool = false
+
+ // MARK: Initializer
+ /// Initialises a Single Line Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style a line.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : LineDataSet,
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ chartStyle : LineChartStyle = LineChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (chartType: .line, dataSetType: .single)
+ self.setupLegends()
+ }
+ // , calc : @escaping (LineDataSet) -> LineDataSet
+
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+
+ HStack(spacing: 0) {
+ ForEach(dataSets.dataPoints) { data in
+ YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.wrappedXAxisLabel)"))
+ if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+
+ case .chartData:
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ if data != labelArray[labelArray.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+ }
+ }
+ }
+ }
+
+ // MARK: Points
+ public final func getPointMarker() -> some View {
+ PointsSubView(dataSets : dataSets,
+ minValue : self.minValue,
+ range : self.range,
+ animation : self.chartStyle.globalAnimation,
+ isFilled : self.isFilled)
+ }
+
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView(dataSet: dataSets,
+ dataPoints: dataSets.dataPoints,
+ lineType: dataSets.style.lineType,
+ touchLocation: touchLocation,
+ chartSize: chartSize)
+ }
+
+ public typealias Set = LineDataSet
+ public typealias DataPoint = LineChartDataPoint
+}
+
+// MARK: - Touch
+extension LineChartData {
+
+ public final func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+
+ let minValue : Double = self.minValue
+ let range : Double = self.range
+
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count - 1)
+ let ySection : CGFloat = chartSize.height / CGFloat(range)
+
+ let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+
+ if !dataSet.style.ignoreZero {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ } else {
+ if dataSet.dataPoints[index].value != 0 {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ }
+ }
+ }
+ return nil
+ }
+
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+
+ var points : [LineChartDataPoint] = []
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count - 1)
+ let index = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSets.dataPoints.count {
+ if !dataSets.style.ignoreZero {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ } else {
+ if dataSets.dataPoints[index].value != 0 {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ }
+ }
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift
new file mode 100644
index 00000000..911337ea
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift
@@ -0,0 +1,187 @@
+//
+// MultiLineChartData.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling a multi line, line chart.
+
+ This model contains all the data and styling information for a single line, line chart.
+ */
+public final class MultiLineChartData: CTLineChartDataProtocol {
+
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public final var dataSets : MultiLineDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var xAxisLabels : [String]?
+ @Published public final var chartStyle : LineChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var viewData : ChartViewData
+ @Published public final var infoView : InfoViewData = InfoViewData()
+
+ public final var noDataText : Text
+ public final var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializers
+ /// Initialises a Multi Line Chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the lines.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : MultiLineDataSet,
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ chartStyle : LineChartStyle = LineChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (.line, .multi)
+ self.setupLegends()
+ }
+
+ // MARK: Labels
+ public final func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+
+ HStack(spacing: 0) {
+ ForEach(dataSets.dataSets[0].dataPoints) { data in
+ YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.wrappedXAxisLabel)"))
+ if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+
+ case .chartData:
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ if data != labelArray[labelArray.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+ }
+ }
+ }
+ }
+
+ // MARK: Points
+ public final func getPointMarker() -> some View {
+ ForEach(self.dataSets.dataSets, id: \.id) { dataSet in
+ PointsSubView(dataSets : dataSet,
+ minValue : self.minValue,
+ range : self.range,
+ animation : self.chartStyle.globalAnimation,
+ isFilled : false)
+ }
+ }
+
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ ZStack {
+ ForEach(self.dataSets.dataSets, id: \.id) { dataSet in
+ self.markerSubView(dataSet: dataSet,
+ dataPoints: dataSet.dataPoints,
+ lineType: dataSet.style.lineType,
+ touchLocation: touchLocation,
+ chartSize: chartSize)
+ }
+ }
+ }
+
+ // MARK: Accessibility
+ public func getAccessibility() -> some View {
+ ForEach(self.dataSets.dataSets, id: \.self) { dataSet in
+ ForEach(dataSet.dataPoints.indices, id: \.self) { point in
+ AccessibilityRectangle(dataPointCount : dataSet.dataPoints.count,
+ dataPointNo : point)
+ .foregroundColor(Color(.gray).opacity(0.000000001))
+ .accessibilityLabel(Text("\(self.metadata.title)"))
+ .accessibilityValue(dataSet.dataPoints[point].getCellAccessibilityValue(specifier: self.infoView.touchSpecifier))
+ }
+ }
+ }
+
+ public typealias Set = MultiLineDataSet
+ public typealias DataPoint = LineChartDataPoint
+ public typealias CTStyle = LineChartStyle
+}
+
+
+// MARK: - Touch
+extension MultiLineChartData {
+ public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+
+ let minValue : Double = self.minValue
+ let range : Double = self.range
+
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count - 1)
+ let ySection : CGFloat = chartSize.height / CGFloat(range)
+
+ let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+
+ if !dataSet.style.ignoreZero {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ } else {
+ if dataSet.dataPoints[index].value != 0 {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ }
+ }
+ }
+ return nil
+ }
+
+ public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [LineChartDataPoint] = []
+ for dataSet in dataSets.dataSets {
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count - 1)
+ let index = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+ if !dataSet.style.ignoreZero {
+ var dataPoint = dataSet.dataPoints[index]
+ dataPoint.legendTag = dataSet.legendTitle
+ points.append(dataPoint)
+ } else {
+
+ if dataSet.dataPoints[index].value != 0 {
+ var dataPoint = dataSet.dataPoints[index]
+ dataPoint.legendTag = dataSet.legendTitle
+ points.append(dataPoint)
+ }
+ }
+ }
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift
new file mode 100644
index 00000000..160dc1bf
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift
@@ -0,0 +1,167 @@
+//
+// RangedLineChartData.swift
+//
+//
+// Created by Will Dale on 01/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling ranged line chart.
+
+ This model contains the data and styling information for a ranged line chart.
+ */
+public final class RangedLineChartData: CTLineChartDataProtocol {
+
+ // MARK: Properties
+ public let id : UUID = UUID()
+
+ @Published public var dataSets : RangedLineDataSet
+ @Published public var metadata : ChartMetadata
+ @Published public var xAxisLabels : [String]?
+ @Published public var chartStyle : LineChartStyle
+ @Published public var legends : [LegendData]
+ @Published public var viewData : ChartViewData
+ @Published public var infoView : InfoViewData = InfoViewData()
+
+ public var noDataText : Text
+ public var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises a ranged line chart.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style a line.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - xAxisLabels: Labels for the X axis instead of the labels in the data points.
+ /// - chartStyle: The style data for the aesthetic of the chart.
+ /// - noDataText: Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : RangedLineDataSet,
+ metadata : ChartMetadata = ChartMetadata(),
+ xAxisLabels : [String]? = nil,
+ chartStyle : LineChartStyle = LineChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.xAxisLabels = xAxisLabels
+ self.chartStyle = chartStyle
+ self.noDataText = noDataText
+ self.legends = [LegendData]()
+ self.viewData = ChartViewData()
+ self.chartType = (chartType: .line, dataSetType: .single)
+
+ self.setupLegends()
+ self.setupRangeLegends()
+ }
+
+ public var average : Double {
+ let sum = dataSets.dataPoints.reduce(0) { $0 + $1.value }
+ return sum / Double(dataSets.dataPoints.count)
+ }
+
+ // MARK: Labels
+ public func getXAxisLabels() -> some View {
+ Group {
+ switch self.chartStyle.xAxisLabelsFrom {
+ case .dataPoint(let angle):
+
+ HStack(spacing: 0) {
+ ForEach(dataSets.dataPoints) { data in
+ YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data.wrappedXAxisLabel)"))
+ if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+
+ case .chartData:
+ if let labelArray = self.xAxisLabels {
+ HStack(spacing: 0) {
+ ForEach(labelArray, id: \.self) { data in
+ YAxisChartDataCell(chartData: self, label: data)
+ .foregroundColor(self.chartStyle.xAxisLabelColour)
+ .accessibilityLabel(Text("X Axis Label"))
+ .accessibilityValue(Text("\(data)"))
+ if data != labelArray[labelArray.count - 1] {
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ }
+ .padding(.horizontal, -4)
+ }
+ }
+ }
+ }
+
+ // MARK: Points
+ public func getPointMarker() -> some View {
+ PointsSubView(dataSets : dataSets,
+ minValue : self.minValue,
+ range : self.range,
+ animation : self.chartStyle.globalAnimation,
+ isFilled : false)
+ }
+
+ public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View {
+ self.markerSubView(dataSet: dataSets,
+ dataPoints: dataSets.dataPoints,
+ lineType: dataSets.style.lineType,
+ touchLocation: touchLocation,
+ chartSize: chartSize)
+ }
+
+ public func getPointLocation(dataSet: RangedLineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+
+ let minValue : Double = self.minValue
+ let range : Double = self.range
+
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count - 1)
+ let ySection : CGFloat = chartSize.height / CGFloat(range)
+
+ let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSet.dataPoints.count {
+ if !dataSet.style.ignoreZero {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ } else {
+ if dataSet.dataPoints[index].value != 0 {
+ return CGPoint(x: CGFloat(index) * xSection,
+ y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height)
+ }
+ }
+ }
+ return nil
+ }
+
+ public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [RangedLineChartDataPoint] = []
+ let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count - 1)
+ let index = Int((touchLocation.x + (xSection / 2)) / xSection)
+ if index >= 0 && index < dataSets.dataPoints.count {
+
+ if !dataSets.style.ignoreZero {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ } else {
+ if dataSets.dataPoints[index].value != 0 {
+ var dataPoint = dataSets.dataPoints[index]
+ dataPoint.legendTag = dataSets.legendTitle
+ points.append(dataPoint)
+ }
+ }
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+
+ public typealias Set = RangedLineDataSet
+ public typealias DataPoint = RangedLineChartDataPoint
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift
new file mode 100644
index 00000000..95e615d5
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift
@@ -0,0 +1,47 @@
+//
+// LineChartDataPoint.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single data point.
+
+ # Example
+ ```
+ LineChartDataPoint(value : 20,
+ xAxisLabel : "M",
+ description: "Monday",
+ date : Date())
+ ```
+ */
+public struct LineChartDataPoint: CTStandardLineDataPoint {
+
+ public let id : UUID = UUID()
+ public var value : Double
+ public var xAxisLabel : String?
+ public var description : String?
+ public var date : Date?
+
+ public var legendTag : String = ""
+
+ /// Data model for a single data point with colour for use with a line chart.
+ /// - Parameters:
+ /// - value: Value of the data point
+ /// - xAxisLabel: Label that can be shown on the X axis.
+ /// - description: A longer label that can be shown on touch input.
+ /// - date: Date of the data point if any data based calculations are required.
+ public init(value : Double,
+ xAxisLabel : String? = nil,
+ description : String? = nil,
+ date : Date? = nil
+ ) {
+ self.value = value
+ self.xAxisLabel = xAxisLabel
+ self.description = description
+ self.date = date
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift
new file mode 100644
index 00000000..10578e18
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift
@@ -0,0 +1,56 @@
+//
+// RangedLineChartDataPoint.swift
+//
+//
+// Created by Will Dale on 02/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single ranged data point.
+
+ # Example
+ ```
+ RangedLineChartDataPoint(value: 10,
+ upperValue: 20,
+ lowerValue: 0,
+ xAxisLabel: "M",
+ description: "Monday")
+ ```
+ */
+public struct RangedLineChartDataPoint: CTRangedLineDataPoint {
+
+ public let id : UUID = UUID()
+ public var value : Double
+ public var upperValue : Double
+ public var lowerValue : Double
+ public var xAxisLabel : String?
+ public var description : String?
+ public var date : Date?
+
+ public var legendTag : String = ""
+
+ /// Data model for a single data point with colour for use with a ranged line chart.
+ /// - Parameters:
+ /// - value: Value of the data point.
+ /// - upperValue: Value of the upper range of the data point.
+ /// - lowerValue: Value of the lower range of the data point.
+ /// - xAxisLabel: Label that can be shown on the X axis.
+ /// - description: A longer label that can be shown on touch input.
+ /// - date: Date of the data point if any data based calculations are required.
+ public init(value : Double,
+ upperValue : Double,
+ lowerValue : Double,
+ xAxisLabel : String? = nil,
+ description : String? = nil,
+ date : Date? = nil
+ ) {
+ self.value = value
+ self.upperValue = upperValue
+ self.lowerValue = lowerValue
+ self.xAxisLabel = xAxisLabel
+ self.description = description
+ self.date = date
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift
new file mode 100644
index 00000000..d9049dd5
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift
@@ -0,0 +1,43 @@
+//
+// LineDataSet.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set for a single line
+
+ Contains information specific to each line within the chart .
+ */
+public struct LineDataSet: CTLineChartDataSet {
+
+ public let id : UUID = UUID()
+ public var dataPoints : [LineChartDataPoint]
+ public var legendTitle : String
+ public var pointStyle : PointStyle
+ public var style : LineStyle
+
+
+ /// Initialises a data set for a line in a Line Chart.
+ /// - Parameters:
+ /// - dataPoints: Array of elements.
+ /// - legendTitle: Label for the data in legend.
+ /// - pointStyle: Styling information for the data point markers.
+ /// - style: Styling for how the line will be draw in.
+ public init(dataPoints : [LineChartDataPoint],
+ legendTitle : String = "",
+ pointStyle : PointStyle = PointStyle(),
+ style : LineStyle = LineStyle()
+ ) {
+ self.dataPoints = dataPoints
+ self.legendTitle = legendTitle
+ self.pointStyle = pointStyle
+ self.style = style
+ }
+
+ public typealias ID = UUID
+ public typealias Styling = LineStyle
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift
new file mode 100644
index 00000000..0a5c67e8
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift
@@ -0,0 +1,25 @@
+//
+// MultiLineDataSet.swift
+//
+//
+// Created by Will Dale on 04/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set containing multiple data sets for multiple lines
+
+ Contains information about each of lines within the chart.
+ */
+public struct MultiLineDataSet: CTMultiLineChartDataSet {
+
+ public let id : UUID = UUID()
+ public var dataSets : [LineDataSet]
+
+ /// Initialises a new data set for multi-line Line Charts.
+ public init(dataSets: [LineDataSet]) {
+ self.dataSets = dataSets
+ }
+}
+
diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift
new file mode 100644
index 00000000..5fe00e8d
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift
@@ -0,0 +1,47 @@
+//
+// RangedLineDataSet.swift
+//
+//
+// Created by Will Dale on 02/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set for a ranged line.
+
+ Contains information specific to the line and range fill.
+ */
+public struct RangedLineDataSet: CTRangedLineChartDataSet {
+
+ public let id : UUID = UUID()
+ public var dataPoints : [RangedLineChartDataPoint]
+ public var legendTitle : String
+ public var legendFillTitle: String
+ public var pointStyle : PointStyle
+ public var style : RangedLineStyle
+
+ /// Initialises a data set for a line in a ranged line chart.
+ /// - Parameters:
+ /// - dataPoints: Array of elements.
+ /// - legendTitle: Label for the data in legend.
+ /// - legendFillTitle: Label for the range data in legend.
+ /// - pointStyle: Styling information for the data point markers.
+ /// - style: Styling for how the line will be draw in.
+ public init(dataPoints : [RangedLineChartDataPoint],
+ legendTitle : String = "",
+ legendFillTitle : String = "",
+ pointStyle : PointStyle = PointStyle(),
+ style : RangedLineStyle = RangedLineStyle()
+ ) {
+ self.dataPoints = dataPoints
+ self.legendTitle = legendTitle
+ self.legendFillTitle = legendFillTitle
+ self.pointStyle = pointStyle
+ self.style = style
+ }
+
+ public typealias ID = UUID
+ public typealias Styling = RangedLineStyle
+
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift
deleted file mode 100644
index 8c86913c..00000000
--- a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift
+++ /dev/null
@@ -1,151 +0,0 @@
-//
-// LineStyle.swift
-// LineChart
-//
-// Created by Will Dale on 31/12/2020.
-//
-
-import SwiftUI
-
-/// Model for controlling the aesthetic of the line chart.
-public struct LineStyle {
-
- /// Type of colour styling for the chart.
- public var colourType : ColourType
- /// Drawing style of the line
- public var lineType : LineType
-
- public var baseline : Baseline
-
- public var strokeStyle : StrokeStyle
-
- /// Single Colour
- public var colour : Color?
- /// Colours for Gradient
- public var colours : [Color]?
- /// Colours and Stops for Gradient with stop control
- public var stops : [GradientStop]?
-
- /// Start point for Gradient
- public var startPoint : UnitPoint?
- /// End point for Gradient
- public var endPoint : UnitPoint?
-
- /**
- Whether the chart should skip data points who's value is 0.
-
- This might be useful when showing trends over time but each day does not necessarily have data.
-
- The default is false.
- */
- public var ignoreZero : Bool
-
- /// Single Colour
- /// - Parameters:
- /// - colour: Single Colour
- /// - lineType: Drawing style of the line
- /// - strokeStyle: Stroke Style
- /// - ignoreZero: Whether the chart should skip data points who's value is 0.
- public init(colour : Color = Color(.red),
- lineType : LineType = .curvedLine,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 3,
- lineCap: .round,
- lineJoin: .round,
- miterLimit: 10,
- dash: [CGFloat](),
- dashPhase: 0),
- baseline : Baseline = .minimumValue,
- ignoreZero : Bool = false
- ) {
- self.colourType = .colour
- self.lineType = lineType
- self.strokeStyle = strokeStyle
-
- self.colour = colour
- self.colours = nil
- self.stops = nil
- self.startPoint = nil
- self.endPoint = nil
-
- self.baseline = baseline
- self.ignoreZero = ignoreZero
- }
-
- /// Gradient Colour Line
- /// - Parameters:
- /// - colours: Colours for Gradient.
- /// - startPoint: Start point for Gradient.
- /// - endPoint: End point for Gradient.
- /// - lineType: Drawing style of the line.
- /// - strokeStyle: Stroke Style.
- /// - ignoreZero: Whether the chart should skip data points who's value is 0.
- public init(colours : [Color] = [Color(.red), Color(.blue)],
- startPoint : UnitPoint = .leading,
- endPoint : UnitPoint = .trailing,
- lineType : LineType = .curvedLine,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 3,
- lineCap: .round,
- lineJoin: .round,
- miterLimit: 10,
- dash: [CGFloat](),
- dashPhase: 0),
- baseline : Baseline = .minimumValue,
- ignoreZero : Bool = false
- ) {
- self.colourType = .gradientColour
- self.lineType = lineType
- self.strokeStyle = strokeStyle
-
- self.colour = nil
- self.stops = nil
- self.colours = colours
- self.startPoint = startPoint
- self.endPoint = endPoint
-
- self.baseline = baseline
- self.ignoreZero = ignoreZero
- }
-
- /// Gradient with Stops Line
- /// - Parameters:
- /// - stops: Colours and Stops for Gradient with stop control.
- /// - startPoint: Start point for Gradient.
- /// - endPoint: End point for Gradient.
- /// - lineType: Drawing style of the line.
- /// - strokeStyle: Stroke Style.
- /// - ignoreZero: Whether the chart should skip data points who's value is 0.
- public init(stops : [GradientStop] = [GradientStop(color: Color(.red), location: 0.0)],
- startPoint : UnitPoint = .leading,
- endPoint : UnitPoint = .trailing,
- lineType : LineType = .curvedLine,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 3,
- lineCap: .round,
- lineJoin: .round,
- miterLimit: 10,
- dash: [CGFloat](),
- dashPhase: 0),
- baseline : Baseline = .minimumValue,
- ignoreZero : Bool = false
- ) {
- self.colourType = .gradientStops
- self.lineType = lineType
-
- self.strokeStyle = strokeStyle
- self.colour = nil
- self.colours = nil
- self.stops = stops
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.baseline = baseline
-
- self.ignoreZero = ignoreZero
- }
-
- public enum Baseline {
- case minimumValue
- case minimumWithMaximum(of: Double)
- case zero
- }
-}
-
-
diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift
new file mode 100644
index 00000000..6b8283b2
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift
@@ -0,0 +1,129 @@
+//
+// LineChartProtocols.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Chart Data
+/**
+ A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Line Charts.
+ */
+public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol {
+
+ /// A type representing opaque View
+ associatedtype Points : View
+ /// A type representing opaque View
+ associatedtype Access : View
+
+ /**
+ Displays Shapes over the data points.
+
+ - Returns: Relevent view containing point markers based the chosen parameters.
+ */
+ func getPointMarker() -> Points
+
+ /**
+ Ensures that line charts have an accessibility layer.
+
+ - Returns: A view with invisible rectangles over the data point.
+ */
+ func getAccessibility() -> Access
+}
+
+// MARK: - Style
+/**
+ A protocol to extend functionality of `CTLineBarChartStyle` specifically for Line Charts.
+ */
+public protocol CTLineChartStyle : CTLineBarChartStyle {}
+
+/**
+ Protocol to set up the styling for individual lines.
+ */
+public protocol CTLineStyle {
+ /// Drawing style of the line.
+ var lineType : LineType { get set }
+
+ /// Colour styling of the line.
+ var lineColour : ColourStyle { get set }
+
+ /**
+ Styling for stroke
+
+ Replica of Apple’s StrokeStyle that conforms to Hashable
+ */
+ var strokeStyle : Stroke { get set }
+
+ var ignoreZero : Bool { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTLineStyle` specifically for Ranged Line Charts.
+ */
+public protocol CTRangedLineStyle: CTLineStyle {
+ /// Drawing style of the range fill.
+ var fillColour : ColourStyle { get set }
+}
+
+
+
+// MARK: - DataSet
+/**
+ A protocol to extend functionality of `SingleDataSet` specifically for Line Charts.
+ */
+public protocol CTLineChartDataSet: CTSingleDataSetProtocol {
+
+ /// A type representing colour styling
+ associatedtype Styling : CTLineStyle
+
+ /**
+ Label to display in the legend.
+ */
+ var legendTitle : String { get set }
+
+ /**
+ Sets the style for the Data Set (as opposed to Chart Data Style).
+ */
+ var style : Styling { get set }
+
+ /**
+ Sets the look of the markers over the data points.
+
+ The markers are layed out when the ViewModifier `PointMarkers`
+ is applied.
+ */
+ var pointStyle : PointStyle { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTLineChartDataSet` specifically for Ranged Line Charts.
+ */
+public protocol CTRangedLineChartDataSet: CTLineChartDataSet {
+ var legendFillTitle : String { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTMultiDataSetProtocol` specifically for Multi Line Charts.
+ */
+public protocol CTMultiLineChartDataSet: CTMultiDataSetProtocol {}
+
+
+
+// MARK: - Data Point
+/**
+ A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for Line and Bar Charts.
+ */
+public protocol CTLineDataPointProtocol: CTLineBarDataPointProtocol {}
+
+/**
+ A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Ranged Line Charts.
+ */
+public protocol CTStandardLineDataPoint: CTLineDataPointProtocol, CTStandardDataPointProtocol, CTnotRanged {}
+
+/**
+ A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Ranged Line Charts.
+ */
+public protocol CTRangedLineDataPoint: CTLineDataPointProtocol, CTStandardDataPointProtocol, CTRangeDataPointProtocol, CTisRanged {}
+
diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift
new file mode 100644
index 00000000..08a86905
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift
@@ -0,0 +1,596 @@
+//
+// LineChartProtocolsExtensions.swift
+//
+//
+// Created by Will Dale on 13/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Position Indicator
+extension CTLineChartDataProtocol {
+ /**
+ Gets the position on a line relative to where the location of the touch or pointer interaction.
+ */
+ public static func getIndicatorLocation(
+ rect: CGRect,
+ dataPoints: [DP],
+ touchLocation: CGPoint,
+ lineType: LineType,
+ minValue: Double,
+ range: Double,
+ ignoreZero: Bool
+ ) -> CGPoint {
+
+ let path = Self.getPath(lineType : lineType,
+ rect : rect,
+ dataPoints : dataPoints,
+ minValue : minValue,
+ range : range,
+ isFilled : false,
+ ignoreZero : ignoreZero)
+ return Self.locationOnPath(Self.getPercentageOfPath(path: path, touchLocation: touchLocation), path)
+ }
+}
+extension CTLineChartDataProtocol {
+ /**
+ Returns the relevent path based on the line type.
+
+ - Parameters:
+ - lineType: Drawing style of the line.
+ - rect: Frame the line will be in.
+ - dataPoints: Data points to draw the line.
+ - minValue: Lowest value in the dataset.
+ - range: Difference between the highest and lowest numbers in the dataset.
+ - touchLocation: Location of the touch or pointer input.
+ - isFilled: Whether it is a normal or filled line.
+ - Returns: The relevent path based on the line type
+ */
+ static func getPath(
+ lineType: LineType,
+ rect: CGRect,
+ dataPoints: [DP],
+ minValue: Double,
+ range: Double,
+ isFilled: Bool,
+ ignoreZero : Bool
+ ) -> Path {
+ switch lineType {
+ case .line:
+ return Path.straightLine(rect : rect,
+ dataPoints : dataPoints,
+ minValue : minValue,
+ range : range,
+ isFilled : isFilled,
+ ignoreZero : ignoreZero)
+ case .curvedLine:
+ return Path.curvedLine(rect : rect,
+ dataPoints : dataPoints,
+ minValue : minValue,
+ range : range,
+ isFilled : isFilled,
+ ignoreZero : ignoreZero)
+ }
+ }
+
+ /**
+ How far along the path the touch or pointer is as a percent of the total.
+ .
+ - Parameters:
+ - path: Path being acted on.
+ - touchLocation: Location of the touch or pointer input.
+ - Returns: How far along the path the touch is.
+ */
+ static func getPercentageOfPath(path: Path, touchLocation: CGPoint) -> CGFloat {
+ let totalLength = self.getTotalLength(of: path)
+ let lengthToTouch = self.getLength(to: touchLocation, on: path)
+ let pointLocation = lengthToTouch / totalLength
+ return pointLocation
+ }
+
+ /**
+ The total length of the path.
+
+ # Reference
+ [Apple](https://developer.apple.com/documentation/swiftui/path/element)
+
+ - Parameter path: Path to measure.
+ - Returns: Total length of the path.
+ */
+ static func getTotalLength(of path: Path) -> CGFloat {
+ var total : CGFloat = 0
+ var currentPoint: CGPoint = .zero
+ path.forEach { (element) in
+ switch element {
+ case .move(to: let first):
+ currentPoint = first
+ case .line(to: let next):
+ total += distance(from: currentPoint, to: next)
+ currentPoint = next
+ case .curve(to: let next, control1: _, control2: _):
+ total += distance(from: currentPoint, to: next)
+ currentPoint = next
+ case .quadCurve(to: let next, control: _):
+ total += distance(from: currentPoint, to: next)
+ currentPoint = next
+ case .closeSubpath:
+ // No reason for this to fire
+ total += 0
+ }
+ }
+ return total
+ }
+
+ /**
+ The length from the start of the path to touch location.
+
+ - Parameters:
+ - touchLocation: Location of the touch or pointer input.
+ - path: Path to take measurement from.
+ - Returns: Length of path to touch point.
+ */
+ static func getLength(to touchLocation: CGPoint, on path: Path) -> CGFloat {
+ var total : CGFloat = 0
+ var currentPoint: CGPoint = .zero
+ var isComplete : Bool = false
+ path.forEach { (element) in
+ if isComplete { return }
+ switch element {
+ case .move(to: let point):
+ if touchLocation.x < point.x {
+ isComplete = true
+ return
+ } else {
+ currentPoint = point
+ }
+ case .line(to: let nextPoint):
+ if touchLocation.x < nextPoint.x {
+ total += distanceToTouch(from : currentPoint,
+ to : nextPoint,
+ touchX: touchLocation.x)
+ isComplete = true
+ return
+ } else {
+ total += distance(from: currentPoint, to: nextPoint)
+ currentPoint = nextPoint
+ }
+ case .curve(to: let nextPoint, control1: _, control2: _ ):
+ if touchLocation.x < nextPoint.x {
+ total += distanceToTouch(from : currentPoint,
+ to : nextPoint,
+ touchX: touchLocation.x)
+ isComplete = true
+ return
+ } else {
+ total += distance(from: currentPoint, to: nextPoint)
+ currentPoint = nextPoint
+ }
+ case .quadCurve(to: let nextPoint, control: _):
+ if touchLocation.x < nextPoint.x {
+ total += distanceToTouch(from : currentPoint,
+ to : nextPoint,
+ touchX: touchLocation.x)
+ isComplete = true
+ return
+ } else {
+ total += distance(from: currentPoint, to: nextPoint)
+ currentPoint = nextPoint
+ }
+ case .closeSubpath:
+ // No reason for this to fire
+ total += 0
+
+ }
+ }
+ return total
+ }
+
+ /**
+ Returns a point on the path based on the location of the touch
+ or pointer input on the X axis.
+
+ - Parameters:
+ - from: First point
+ - to: Second point
+ - touchX: Location on the X axis of the touch or pointer input.
+ - Returns: A point on the path
+ */
+ static func relativePoint(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGPoint {
+ CGPoint(x: touchX,
+ y: from.y + (touchX - from.x) * ((to.y - from.y) / (to.x - from.x)))
+ }
+
+ /**
+ Returns the length along the path from a point to the touch locatiions X axis.
+
+ - Parameters:
+ - from: First point
+ - to: Second point
+ - touchX: Location on the X axis of the touch or pointer input.
+ - Returns: Length from of a path element to touch location
+ */
+ static func distanceToTouch(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGFloat {
+ distance(from: from, to: relativePoint(from: from, to: to, touchX: touchX))
+ }
+
+ /**
+ Returns the distance between two points.
+
+ - Parameters:
+ - from: First point
+ - to: Second point
+ - Returns: Distance between two points.
+ */
+ static func distance(from: CGPoint, to: CGPoint) -> CGFloat {
+ sqrt((from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y))
+ }
+
+
+
+ /**
+ Returns a point on the path based on the X axis of the users touch input.
+
+ # Reference
+ [SwiftUI Lab](https://swiftui-lab.com/swiftui-animations-part2/)
+
+ - Parameters:
+ - percent: The distance along the path as a percentage.
+ - path: Path to find location on.
+ - Returns: Point on path.
+ */
+ static func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint {
+ // percent difference between points
+ let diff: CGFloat = 0.001
+ let comp: CGFloat = 1 - diff
+
+ // handle limits
+ let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
+
+ let from = pct > comp ? comp : pct
+ let to = pct > comp ? 1 : pct + diff
+ let trimmedPoint = path.trimmedPath(from: from, to: to)
+
+ return CGPoint(x: trimmedPoint.boundingRect.midX,
+ y: trimmedPoint.boundingRect.midY)
+ }
+}
+
+// MARK: - Markers
+extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType {
+
+ internal func markerSubView
+ (dataSet : DS,
+ dataPoints : [DP],
+ lineType : LineType,
+ touchLocation : CGPoint,
+ chartSize : CGRect) -> some View {
+ Group {
+ switch self.chartStyle.markerType {
+ case .none:
+ EmptyView()
+ case .indicator(let style):
+
+ PosistionIndicator(fillColour: style.fillColour,
+ lineColour: style.lineColour,
+ lineWidth: style.lineWidth)
+ .frame(width: style.size, height: style.size)
+ .position(Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero))
+
+ case .vertical(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ Vertical(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+ case .point:
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+ Vertical(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+
+ case .full(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ MarkerFull(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+
+ case .point:
+
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+
+ MarkerFull(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+
+ case .bottomLeading(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ MarkerBottomLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+ case .point:
+
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+
+ MarkerBottomLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+
+ case .bottomTrailing(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ MarkerBottomTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+ case .point:
+
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+
+ MarkerBottomTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+
+ case .topLeading(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ MarkerTopLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+ case .point:
+
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+
+ MarkerTopLeading(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+
+ case .topTrailing(attachment: let attach):
+
+ switch attach {
+ case .line(dot: let indicator):
+
+ let position = Self.getIndicatorLocation(rect: chartSize,
+ dataPoints: dataPoints,
+ touchLocation: touchLocation,
+ lineType: lineType,
+ minValue: self.minValue,
+ range: self.range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ MarkerTopTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+
+ IndicatorSwitch(indicator: indicator, location: position)
+
+ case .point:
+
+ if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint,
+ touchLocation: touchLocation,
+ chartSize: chartSize) {
+
+ MarkerTopTrailing(position: position)
+ .stroke(Color.primary, lineWidth: 2)
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ Sub view for laying out and styling the indicator dot.
+ */
+internal struct IndicatorSwitch: View {
+
+ private let indicator: Dot
+ private let location : CGPoint
+
+ internal init(indicator: Dot, location: CGPoint) {
+ self.indicator = indicator
+ self.location = location
+ }
+
+ internal var body: some View {
+ switch indicator {
+ case .none: EmptyView()
+ case .style(let style):
+ PosistionIndicator(fillColour: style.fillColour, lineColour: style.lineColour, lineWidth: style.lineWidth)
+ .frame(width: style.size, height: style.size)
+ .position(location)
+
+ }
+ }
+
+}
+
+// MARK: - Legends
+extension CTLineChartDataProtocol where Self.Set.ID == UUID,
+ Self.Set : CTLineChartDataSet {
+ internal func setupLegends() {
+ lineLegendSetup(dataSet: dataSets)
+ }
+}
+
+extension CTLineChartDataProtocol where Self.Set == MultiLineDataSet {
+ internal func setupLegends() {
+ for dataSet in dataSets.dataSets {
+ lineLegendSetup(dataSet: dataSet)
+ }
+ }
+}
+extension CTLineChartDataProtocol {
+ internal func lineLegendSetup(dataSet: DS) where DS.ID == UUID {
+ if dataSet.style.lineColour.colourType == .colour,
+ let colour = dataSet.style.lineColour.colour
+ {
+ self.legends.append(LegendData(id : dataSet.id,
+ legend : dataSet.legendTitle,
+ colour : ColourStyle(colour: colour),
+ strokeStyle: dataSet.style.strokeStyle,
+ prioity : 1,
+ chartType : .line))
+
+ } else if dataSet.style.lineColour.colourType == .gradientColour,
+ let colours = dataSet.style.lineColour.colours
+ {
+ self.legends.append(LegendData(id : dataSet.id,
+ legend : dataSet.legendTitle,
+ colour : ColourStyle(colours: colours,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: dataSet.style.strokeStyle,
+ prioity : 1,
+ chartType : .line))
+
+ } else if dataSet.style.lineColour.colourType == .gradientStops,
+ let stops = dataSet.style.lineColour.stops
+ {
+ self.legends.append(LegendData(id : dataSet.id,
+ legend : dataSet.legendTitle,
+ colour : ColourStyle(stops: stops,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: dataSet.style.strokeStyle,
+ prioity : 1,
+ chartType : .line))
+ }
+ }
+}
+extension CTLineChartDataProtocol where Self.Set.ID == UUID,
+ Self.Set: CTRangedLineChartDataSet,
+ Self.Set.Styling: CTRangedLineStyle {
+ internal func setupRangeLegends() {
+ if dataSets.style.fillColour.colourType == .colour,
+ let colour = dataSets.style.fillColour.colour
+ {
+ self.legends.append(LegendData(id : UUID(),
+ legend : dataSets.legendFillTitle,
+ colour : ColourStyle(colour: colour),
+ strokeStyle: dataSets.style.strokeStyle,
+ prioity : 1,
+ chartType : .bar))
+
+ } else if dataSets.style.fillColour.colourType == .gradientColour,
+ let colours = dataSets.style.fillColour.colours
+ {
+ self.legends.append(LegendData(id : UUID(),
+ legend : dataSets.legendFillTitle,
+ colour : ColourStyle(colours: colours,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: dataSets.style.strokeStyle,
+ prioity : 1,
+ chartType : .bar))
+
+ } else if dataSets.style.fillColour.colourType == .gradientStops,
+ let stops = dataSets.style.fillColour.stops
+ {
+ self.legends.append(LegendData(id : UUID(),
+ legend : dataSets.legendFillTitle,
+ colour : ColourStyle(stops: stops,
+ startPoint: .leading,
+ endPoint: .trailing),
+ strokeStyle: dataSets.style.strokeStyle,
+ prioity : 1,
+ chartType : .bar))
+ }
+ }
+}
+
+// MARK: - Accessibility
+extension CTLineChartDataProtocol where Set: CTLineChartDataSet {
+ public func getAccessibility() -> some View {
+ ForEach(dataSets.dataPoints.indices, id: \.self) { point in
+
+ AccessibilityRectangle(dataPointCount : self.dataSets.dataPoints.count,
+ dataPointNo : point)
+
+ .foregroundColor(Color(.gray).opacity(0.000000001))
+ .accessibilityLabel(Text("\(self.metadata.title)"))
+ .accessibilityValue(self.dataSets.dataPoints[point].getCellAccessibilityValue(specifier: self.infoView.touchSpecifier))
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift
new file mode 100644
index 00000000..bbdd637f
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift
@@ -0,0 +1,123 @@
+//
+// LineChartStyle.swift
+//
+//
+// Created by Will Dale on 25/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Control of the overall aesthetic of the line chart.
+
+ Controls the look of the chart as a whole, not including any styling
+ specific to the data set(s),
+ */
+public struct LineChartStyle: CTLineChartStyle {
+
+ public var infoBoxPlacement : InfoBoxPlacement
+ public var infoBoxValueColour : Color
+ public var infoBoxDescriptionColour : Color
+ public var infoBoxBackgroundColour : Color
+ public var infoBoxBorderColour : Color
+ public var infoBoxBorderStyle : StrokeStyle
+
+ public var markerType : LineMarkerType
+
+ public var xAxisGridStyle : GridStyle
+ public var xAxisLabelPosition : XAxisLabelPosistion
+ public var xAxisLabelColour : Color
+ public var xAxisLabelsFrom : LabelsFrom
+ public var xAxisTitle : String?
+
+ public var yAxisGridStyle : GridStyle
+ public var yAxisLabelPosition : YAxisLabelPosistion
+ public var yAxisLabelColour : Color
+ public var yAxisNumberOfLabels : Int
+ public var yAxisTitle : String?
+
+ public var baseline : Baseline
+ public var topLine : Topline
+
+ public var globalAnimation : Animation
+
+ /// Model for controlling the overall aesthetic of the chart.
+ /// - Parameters:
+ /// - infoBoxPlacement: Placement of the information box that appears on touch input.
+ /// - infoBoxValueColour: Colour of the value part of the touch info.
+ /// - infoBoxDescriptionColour: Colour of the description part of the touch info.
+ /// - infoBoxBackgroundColour: Background colour of touch info.
+ /// - infoBoxBorderColour: Border colour of the touch info.
+ /// - infoBoxBorderStyle: Border style of the touch info.
+ ///
+ /// - markerType: Where the marker lines come from to meet at a specified point.
+ ///
+ /// - xAxisGridStyle: Style of the vertical lines breaking up the chart.
+ /// - xAxisLabelPosition: Location of the X axis labels - Top or Bottom.
+ /// - xAxisLabelColour: Text Colour for the labels on the X axis.
+ /// - xAxisLabelsFrom: Where the label data come from. DataPoint or xAxisLabels.
+ /// - xAxisTitle: Label to display next to the chart giving info about the axis.
+ ///
+ /// - yAxisGridStyle: Style of the horizontal lines breaking up the chart.
+ /// - yAxisLabelPosition: Location of the X axis labels - Leading or Trailing.
+ /// - yAxisLabelColour: Text Colour for the labels on the Y axis.
+ /// - yAxisNumberOfLabel: Number Of Labels on Y Axis.
+ /// - yAxisTitle: Label to display next to the chart giving info about the axis.
+ ///
+ /// - baseline: Whether the chart is drawn from baseline of zero or the minimum datapoint value.
+ /// - topLine: Where to finish drawing the chart from. Data set maximum or custom.
+ ///
+ /// - globalAnimation: Global control of animations.
+ public init(infoBoxPlacement : InfoBoxPlacement = .floating,
+ infoBoxValueColour : Color = Color.primary,
+ infoBoxDescriptionColour: Color = Color.primary,
+ infoBoxBackgroundColour : Color = Color.systemsBackground,
+ infoBoxBorderColour : Color = Color.clear,
+ infoBoxBorderStyle : StrokeStyle = StrokeStyle(lineWidth: 0),
+
+ markerType : LineMarkerType = .indicator(style: DotStyle()),
+
+ xAxisGridStyle : GridStyle = GridStyle(),
+ xAxisLabelPosition : XAxisLabelPosistion = .bottom,
+ xAxisLabelColour : Color = Color.primary,
+ xAxisLabelsFrom : LabelsFrom = .dataPoint(rotation: .degrees(0)),
+ xAxisTitle : String? = nil,
+
+ yAxisGridStyle : GridStyle = GridStyle(),
+ yAxisLabelPosition : YAxisLabelPosistion = .leading,
+ yAxisLabelColour : Color = Color.primary,
+ yAxisNumberOfLabels : Int = 10,
+ yAxisTitle : String? = nil,
+
+ baseline : Baseline = .minimumValue,
+ topLine : Topline = .maximumValue,
+
+ globalAnimation : Animation = Animation.linear(duration: 1)
+ ) {
+ self.infoBoxPlacement = infoBoxPlacement
+ self.infoBoxValueColour = infoBoxValueColour
+ self.infoBoxDescriptionColour = infoBoxDescriptionColour
+ self.infoBoxBackgroundColour = infoBoxBackgroundColour
+ self.infoBoxBorderColour = infoBoxBorderColour
+ self.infoBoxBorderStyle = infoBoxBorderStyle
+
+ self.markerType = markerType
+
+ self.xAxisGridStyle = xAxisGridStyle
+ self.xAxisLabelPosition = xAxisLabelPosition
+ self.xAxisLabelsFrom = xAxisLabelsFrom
+ self.xAxisLabelColour = xAxisLabelColour
+ self.xAxisTitle = xAxisTitle
+
+ self.yAxisGridStyle = yAxisGridStyle
+ self.yAxisLabelPosition = yAxisLabelPosition
+ self.yAxisNumberOfLabels = yAxisNumberOfLabels
+ self.yAxisLabelColour = yAxisLabelColour
+ self.yAxisTitle = yAxisTitle
+
+ self.baseline = baseline
+ self.topLine = topLine
+
+ self.globalAnimation = globalAnimation
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift
new file mode 100644
index 00000000..de9096c4
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift
@@ -0,0 +1,49 @@
+//
+// LineStyle.swift
+// LineChart
+//
+// Created by Will Dale on 31/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Model for controlling the styling for individual lines.
+ */
+public struct LineStyle: CTLineStyle, Hashable {
+
+ public var lineColour : ColourStyle
+ public var lineType : LineType
+ public var strokeStyle : Stroke
+
+ /**
+ Whether the chart should skip data points who's value is 0.
+
+ This might be useful when showing trends over time but each day does not necessarily have data.
+
+ The default is false.
+ */
+ public var ignoreZero : Bool
+
+ /// Style of the line.
+ /// - Parameters:
+ /// - lineColour: Colour styling of the line.
+ /// - lineType: Drawing style of the line
+ /// - strokeStyle: Stroke Style
+ /// - ignoreZero: Whether the chart should skip data points who's value is 0.
+ public init(lineColour : ColourStyle = ColourStyle(colour: .red),
+ lineType : LineType = .curvedLine,
+ strokeStyle : Stroke = Stroke(lineWidth : 3,
+ lineCap : .round,
+ lineJoin : .round,
+ miterLimit: 10,
+ dash : [CGFloat](),
+ dashPhase : 0),
+ ignoreZero : Bool = false
+ ) {
+ self.lineColour = lineColour
+ self.lineType = lineType
+ self.strokeStyle = strokeStyle
+ self.ignoreZero = ignoreZero
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift
similarity index 74%
rename from Sources/SwiftUICharts/Shared/Models/PointStyle.swift
rename to Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift
index 34da8d29..41671e74 100644
--- a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift
+++ b/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift
@@ -7,23 +7,42 @@
import SwiftUI
-/// Model for controlling the aesthetic of the point markers.
-public struct PointStyle {
+/**
+ Model for controlling the aesthetic of the point markers.
+
+ Point markers are placed on top of the line, marking where the data points are.
+
+ # Example
+ ```
+ PointStyle(pointSize: 9,
+ borderColour: .primary,
+ fillColour: .red,
+ lineWidth: 2,
+ pointType: .filledOutLine,
+ pointShape: .circle)
+ ```
+ */
+public struct PointStyle: Hashable {
/// Overall size of the mark
public var pointSize : CGFloat
+
/// Outter ring colour
public var borderColour: Color
+
/// Center fill colour
public var fillColour : Color
+
/// Outter ring line width
public var lineWidth : CGFloat
+
/// Style of the point marks
public var pointType : PointType
+
/// Shape of the points
public var pointShape : PointShape
- /// Style of the point markers.
+ /// Styling for the point markers.
/// - Parameters:
/// - pointSize: Overall size of the mark
/// - borderColour: Outter ring colour
diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift
new file mode 100644
index 00000000..8b24a97d
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift
@@ -0,0 +1,54 @@
+//
+// RangedLineStyle.swift
+//
+//
+// Created by Will Dale on 02/03/2021.
+//
+
+import SwiftUI
+/**
+ Model for controlling the aesthetic of the ranged line chart.
+ */
+public struct RangedLineStyle: CTRangedLineStyle, Hashable {
+
+ public var lineColour : ColourStyle
+ public var fillColour : ColourStyle
+
+ public var lineType : LineType
+ public var strokeStyle : Stroke
+
+ /**
+ Whether the chart should skip data points who's value is 0.
+
+ This might be useful when showing trends over time but each day does not necessarily have data.
+
+ The default is false.
+ */
+ public var ignoreZero : Bool
+
+ // MARK: Initializer
+ /// Initialize the styling for ranged line chart.
+ ///
+ /// - Parameters:
+ /// - colour: Single Colour
+ /// - lineType: Drawing style of the line
+ /// - strokeStyle: Stroke Style
+ /// - ignoreZero: Whether the chart should skip data points who's value is 0.
+ public init(lineColour : ColourStyle = ColourStyle(),
+ fillColour : ColourStyle = ColourStyle(),
+ lineType : LineType = .curvedLine,
+ strokeStyle : Stroke = Stroke(lineWidth : 3,
+ lineCap : .round,
+ lineJoin : .round,
+ miterLimit: 10,
+ dash : [CGFloat](),
+ dashPhase : 0),
+ ignoreZero : Bool = false
+ ) {
+ self.lineColour = lineColour
+ self.fillColour = fillColour
+ self.lineType = lineType
+ self.strokeStyle = strokeStyle
+ self.ignoreZero = ignoreZero
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Shapes/LegendLine.swift b/Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift
similarity index 93%
rename from Sources/SwiftUICharts/Shared/Shapes/LegendLine.swift
rename to Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift
index 1f6a0d6d..07e80dc1 100644
--- a/Sources/SwiftUICharts/Shared/Shapes/LegendLine.swift
+++ b/Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift
@@ -10,7 +10,7 @@ import SwiftUI
/// Draw line in legend view
internal struct LegendLine : Shape {
- let width : CGFloat
+ private let width : CGFloat
internal init(width : CGFloat) {
self.width = width
diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift
index d012b870..b9ee6aea 100644
--- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift
+++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift
@@ -7,25 +7,35 @@
import SwiftUI
-internal struct LineShape: Shape {
+/**
+ Main line shape
+ */
+internal struct LineShape: Shape where DP: CTStandardDataPointProtocol {
- private let chartData : ChartData
-
- /// Drawing style of the line
+ private let dataPoints : [DP]
private let lineType : LineType
- /// If it's to be filled some extra lines need to be drawn
private let isFilled : Bool
private let minValue : Double
private let range : Double
+<<<<<<< HEAD
internal init(chartData : ChartData,
+=======
+ private let ignoreZero: Bool
+
+ internal init(dataPoints: [DP],
+>>>>>>> version-2
lineType : LineType,
- isFilled : Bool
+ isFilled : Bool,
+ minValue : Double,
+ range : Double,
+ ignoreZero: Bool
) {
- self.chartData = chartData
+ self.dataPoints = dataPoints
self.lineType = lineType
self.isFilled = isFilled
+<<<<<<< HEAD
switch chartData.lineStyle.baseline {
case .minimumValue:
@@ -86,18 +96,59 @@ internal struct LineShape: Shape {
path.closeSubpath()
}
return path
+=======
+ self.minValue = minValue
+ self.range = range
+ self.ignoreZero = ignoreZero
}
-
- internal func lineSwitch(_ path: inout Path, _ nextPoint: CGPoint, _ previousPoint: CGPoint) {
+
+ internal func path(in rect: CGRect) -> Path {
switch lineType {
+ case .curvedLine:
+ return Path.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled, ignoreZero: ignoreZero)
case .line:
- path.addLine(to: nextPoint)
+ return Path.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled, ignoreZero: ignoreZero)
+ }
+>>>>>>> version-2
+ }
+}
+
+/**
+ Background fill based on the upper and lower values
+ for a Ranged Line Chart.
+ */
+internal struct RangedLineFillShape: Shape where DP: CTRangedLineDataPoint {
+
+ private let dataPoints : [DP]
+ private let lineType : LineType
+
+ private var minValue : Double
+ private let range : Double
+
+ private let ignoreZero: Bool
+
+ internal init(dataPoints: [DP],
+ lineType : LineType,
+ minValue : Double,
+ range : Double,
+ ignoreZero: Bool
+ ) {
+ self.dataPoints = dataPoints
+ self.lineType = lineType
+ self.minValue = minValue
+ self.range = range
+ self.ignoreZero = ignoreZero
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+
+ switch lineType {
case .curvedLine:
- path.addCurve(to: nextPoint,
- control1: CGPoint(x: previousPoint.x + (nextPoint.x - previousPoint.x) / 2,
- y: previousPoint.y),
- control2: CGPoint(x: nextPoint.x - (nextPoint.x - previousPoint.x) / 2,
- y: nextPoint.y))
+ return Path.curvedLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, ignoreZero: ignoreZero)
+ case .line:
+ return Path.straightLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, ignoreZero: ignoreZero)
}
+
}
}
+
diff --git a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift
new file mode 100644
index 00000000..bfde15e2
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift
@@ -0,0 +1,98 @@
+//
+// PointShape.swift
+// LineChart
+//
+// Created by Will Dale on 24/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Draws point markers over the data point locations.
+ */
+internal struct Point: Shape where T: CTLineChartDataSet,
+ T.DataPoint: CTStandardDataPointProtocol {
+
+ private let dataSet : T
+
+ private let minValue : Double
+ private let range : Double
+
+
+ internal init(dataSet : T,
+ minValue : Double,
+ range : Double
+ ) {
+ self.dataSet = dataSet
+ self.minValue = minValue
+ self.range = range
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let x = rect.width / CGFloat(dataSet.dataPoints.count-1)
+ let y = rect.height / CGFloat(range)
+
+ let firstPointX : CGFloat = (CGFloat(0) * x) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let firstPointY : CGFloat = ((CGFloat(dataSet.dataPoints[0].value - minValue) * -y) + rect.height) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let firstPoint : CGRect = CGRect(x : firstPointX,
+ y : firstPointY,
+ width : dataSet.pointStyle.pointSize,
+ height: dataSet.pointStyle.pointSize)
+ if !dataSet.style.ignoreZero {
+ pointSwitch(&path, firstPoint)
+ } else {
+ if dataSet.dataPoints[0].value != 0 {
+ pointSwitch(&path, firstPoint)
+ }
+ }
+
+ for index in 1 ..< dataSet.dataPoints.count - 1 {
+ let pointX : CGFloat = (CGFloat(index) * x) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let pointY : CGFloat = ((CGFloat(dataSet.dataPoints[index].value - minValue) * -y) + rect.height) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let point : CGRect = CGRect(x : pointX,
+ y : pointY,
+ width : dataSet.pointStyle.pointSize,
+ height: dataSet.pointStyle.pointSize)
+ if !dataSet.style.ignoreZero {
+ pointSwitch(&path, point)
+ } else {
+ if dataSet.dataPoints[index].value != 0 {
+ pointSwitch(&path, point)
+ }
+ }
+ }
+
+
+ let lastPointX : CGFloat = (CGFloat(dataSet.dataPoints.count-1) * x) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let lastPointY : CGFloat = ((CGFloat(dataSet.dataPoints[dataSet.dataPoints.count-1].value - minValue) * -y) + rect.height) - dataSet.pointStyle.pointSize / CGFloat(2)
+ let lastPoint : CGRect = CGRect(x : lastPointX,
+ y : lastPointY,
+ width : dataSet.pointStyle.pointSize,
+ height: dataSet.pointStyle.pointSize)
+ if !dataSet.style.ignoreZero {
+ pointSwitch(&path, lastPoint)
+ } else {
+ if dataSet.dataPoints[dataSet.dataPoints.count-1].value != 0 {
+ pointSwitch(&path, lastPoint)
+ }
+ }
+
+ return path
+ }
+
+ /// Draws the points based on chosen parameters.
+ /// - Parameters:
+ /// - path: Path to draw on.
+ /// - point: Position to draw the point.
+ internal func pointSwitch(_ path: inout Path, _ point: CGRect) {
+ switch dataSet.pointStyle.pointShape {
+ case .circle:
+ path.addEllipse(in: point)
+ case .square:
+ path.addRect(point)
+ case .roundSquare:
+ path.addRoundedRect(in: point, cornerSize: CGSize(width: 3, height: 3))
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift
new file mode 100644
index 00000000..82d719f7
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift
@@ -0,0 +1,67 @@
+//
+// LineChartPoints.swift
+// LineChart
+//
+// Created by Will Dale on 24/12/2020.
+//
+
+import SwiftUI
+
+/**
+ ViewModifier for for laying out point markers.
+ */
+internal struct PointMarkers: ViewModifier where T: CTLineChartDataProtocol {
+
+ @ObservedObject var chartData: T
+
+ private let minValue : Double
+ private let range : Double
+
+ internal init(chartData : T) {
+ self.chartData = chartData
+ self.minValue = chartData.minValue
+ self.range = chartData.range
+ }
+ internal func body(content: Content) -> some View {
+ ZStack {
+ if chartData.isGreaterThanTwo() {
+ content
+ chartData.getPointMarker()
+ } else { content }
+ }
+ }
+}
+
+extension View {
+ /**
+ Lays out markers over each of the data point.
+
+ The style of the markers is set in the PointStyle data model as parameter in the Chart Data.
+
+ - Requires:
+ Chart Data to conform to CTLineChartDataProtocol.
+ - LineChartData
+ - MultiLineChartData
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+
+ # Unavailable for:
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with point markers.
+
+ */
+ public func pointMarkers(chartData: T) -> some View {
+ self.modifier(PointMarkers(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift
new file mode 100644
index 00000000..32675140
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift
@@ -0,0 +1,117 @@
+//
+// FilledLineChart.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a filled line chart.
+
+ Uses `LineChartData` data model.
+
+ # Declaration
+ ```
+ FilledLineChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .pointMarkers(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3,dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+
+public struct FilledLineChart: View where ChartData: LineChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ private let minValue : Double
+ private let range : Double
+
+ /// Initialises a filled line chart
+ /// - Parameter chartData: Must be LineChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ self.minValue = chartData.minValue
+ self.range = chartData.range
+ self.chartData.isFilled = true
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+
+ if chartData.isGreaterThanTwo() {
+
+ ZStack {
+
+ chartData.getAccessibility()
+
+ if chartData.dataSets.style.lineColour.colourType == .colour,
+ let colour = chartData.dataSets.style.lineColour.colour
+ {
+
+ LineChartColourSubView(chartData: chartData,
+ dataSet : chartData.dataSets,
+ minValue : minValue,
+ range : range,
+ colour : colour,
+ isFilled : true)
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientColour,
+ let colours = chartData.dataSets.style.lineColour.colours,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+
+ LineChartColoursSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : minValue,
+ range : range,
+ colours : colours,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ isFilled : true)
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientStops,
+ let stops = chartData.dataSets.style.lineColour.stops,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ LineChartStopsSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : minValue,
+ range : range,
+ stops : stops,
+ startPoint : startPoint,
+ endPoint : endPoint,
+ isFilled : true)
+
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChart.swift b/Sources/SwiftUICharts/LineChart/Views/LineChart.swift
deleted file mode 100644
index c3784c35..00000000
--- a/Sources/SwiftUICharts/LineChart/Views/LineChart.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// LineChart.swift
-//
-//
-// Created by Will Dale on 30/12/2020.
-//
-
-import SwiftUI
-
-public struct LineChart: View {
- public init() {}
- public var body: some View {
- LineChartView(isFilled: false)
- }
-}
-
-public struct FilledLineChart: View {
- public init() {}
- public var body: some View {
- LineChartView(isFilled: true)
- }
-}
diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift
index d16da0e1..08c81959 100644
--- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift
+++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift
@@ -7,25 +7,58 @@
import SwiftUI
-internal struct LineChartView: View {
+/**
+ View for drawing a line chart.
+
+ Uses `LineChartData` data model.
+
+ # Declaration
+ ```
+ LineChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .pointMarkers(chartData: data)
+ .touchOverlay(chartData: data, specifier: "%.0f")
+ .yAxisPOI(chartData: data,
+ markerName: "Something",
+ markerValue: 110,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.white,
+ labelBackground: Color.blue,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data, columns: [GridItem(.flexible()), GridItem(.flexible())])
+ ```
+ */
+public struct LineChart: View where ChartData: LineChartData {
- @EnvironmentObject var chartData: ChartData
+ @ObservedObject var chartData: ChartData
- @State var startAnimation : Bool = false
-
- let isFilled : Bool
-
- internal init(isFilled : Bool) {
- self.isFilled = isFilled
+ /// Initialises a line chart view.
+ /// - Parameter chartData: Must be LineChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
}
-
- internal var body: some View {
-
- let style : LineStyle = chartData.lineStyle
- let strokeStyle = style.strokeStyle
+
+ public var body: some View {
- if chartData.dataPoints.count > 2 {
+ if chartData.isGreaterThanTwo() {
+<<<<<<< HEAD
if style.colourType == .colour,
let colour = style.colour
{
@@ -117,66 +150,55 @@ internal struct LineChartView: View {
// .animateOnDisAppear(using: chartData.chartStyle.globalAnimation) {
// self.startAnimation = false
// }
+=======
+ ZStack {
+
+ chartData.getAccessibility()
+
+ if chartData.dataSets.style.lineColour.colourType == .colour,
+ let colour = chartData.dataSets.style.lineColour.colour
+ {
+ LineChartColourSubView(chartData: chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ colour : colour,
+ isFilled : false)
+
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientColour,
+ let colours = chartData.dataSets.style.lineColour.colours,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+
+ LineChartColoursSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ colours : colours,
+ startPoint : startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientStops,
+ let stops = chartData.dataSets.style.lineColour.stops,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ LineChartStopsSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ stops : stops,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+>>>>>>> version-2
}
}
} else { CustomNoDataView(chartData: chartData) }
}
}
-
-internal struct LineShapeModifiers: ViewModifier {
-
- private let chartData : ChartData
-
-
- internal init(_ chartData : ChartData) {
- self.chartData = chartData
- }
-
- func body(content: Content) -> some View {
- content
- .background(Color(.gray).opacity(0.01))
- .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder() }
- .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder() }
- .onAppear(perform: setupLegends)
- }
-
-
- internal func setupLegends() {
-
- guard let lineLegend = chartData.metadata?.lineLegend else { return }
- let style : LineStyle = chartData.lineStyle
-
- if !chartData.legends.contains(where: { $0.legend == lineLegend }) { // init twice
- if style.colourType == .colour,
- let colour = style.colour
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- colour : colour,
- strokeStyle: Stroke.strokeStyleToStroke(strokeStyle: style.strokeStyle),
- prioity : 1,
- chartType : .line))
- } else if style.colourType == .gradientColour,
- let colours = style.colours
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- colours : colours,
- startPoint : .leading,
- endPoint : .trailing,
- strokeStyle: Stroke.strokeStyleToStroke(strokeStyle: style.strokeStyle),
- prioity : 1,
- chartType : .line))
- } else if style.colourType == .gradientStops,
- let stops = style.stops
- {
- self.chartData.legends.append(LegendData(legend : lineLegend,
- stops : stops,
- startPoint : .leading,
- endPoint : .trailing,
- strokeStyle: Stroke.strokeStyleToStroke(strokeStyle: style.strokeStyle),
- prioity : 1,
- chartType : .line))
- }
- }
- chartData.viewData.chartType = .line
- }
-}
diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift
new file mode 100644
index 00000000..a75bcfaf
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift
@@ -0,0 +1,118 @@
+//
+// MultiLineChart.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+/**
+ View for drawing a multi-line, line chart.
+
+ Uses `MultiLineChartData` data model.
+
+ # Declaration
+ ```
+ MultiLineChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .pointMarkers(chartData: data)
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .yAxisPOI(chartData: data,
+ markerName: "50",
+ markerValue: 50,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct MultiLineChart: View where ChartData: MultiLineChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ private let minValue : Double
+ private let range : Double
+
+ /// Initialises a multi-line, line chart.
+ /// - Parameter chartData: Must be MultiLineChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ self.minValue = chartData.minValue
+ self.range = chartData.range
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+
+ if chartData.isGreaterThanTwo() {
+
+ ZStack {
+
+ chartData.getAccessibility()
+
+ ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in
+
+ if dataSet.style.lineColour.colourType == .colour,
+ let colour = dataSet.style.lineColour.colour
+ {
+
+ LineChartColourSubView(chartData: chartData,
+ dataSet : dataSet,
+ minValue : minValue,
+ range : range,
+ colour : colour,
+ isFilled : false)
+
+ } else if dataSet.style.lineColour.colourType == .gradientColour,
+ let colours = dataSet.style.lineColour.colours,
+ let startPoint = dataSet.style.lineColour.startPoint,
+ let endPoint = dataSet.style.lineColour.endPoint
+ {
+
+ LineChartColoursSubView(chartData : chartData,
+ dataSet : dataSet,
+ minValue : minValue,
+ range : range,
+ colours : colours,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+
+ } else if dataSet.style.lineColour.colourType == .gradientStops,
+ let stops = dataSet.style.lineColour.stops,
+ let startPoint = dataSet.style.lineColour.startPoint,
+ let endPoint = dataSet.style.lineColour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ LineChartStopsSubView(chartData : chartData,
+ dataSet : dataSet,
+ minValue : minValue,
+ range : range,
+ stops : stops,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+
+ }
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift
new file mode 100644
index 00000000..fd295e8e
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift
@@ -0,0 +1,158 @@
+//
+// RangedLineChart.swift
+//
+//
+// Created by Will Dale on 01/03/2021.
+//
+
+import SwiftUI
+
+/**
+ View for drawing a line chart with upper and lower range values .
+
+ Uses `RangedLineChartData` data model.
+
+ # Declaration
+ ```
+ RangedLineChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .pointMarkers(chartData: data)
+ .touchOverlay(chartData: data, specifier: "%.0f")
+ .yAxisPOI(chartData: data,
+ markerName: "Something",
+ markerValue: 110,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.white,
+ labelBackground: Color.blue,
+ lineColour: Color.blue,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .averageLine(chartData: data,
+ strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10]))
+ .xAxisGrid(chartData: data)
+ .yAxisGrid(chartData: data)
+ .xAxisLabels(chartData: data)
+ .yAxisLabels(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data, columns: [GridItem(.flexible()), GridItem(.flexible())])
+ ```
+ */
+public struct RangedLineChart: View where ChartData: RangedLineChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a line chart view.
+ /// - Parameter chartData: Must be RangedLineChartData model.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ }
+
+ public var body: some View {
+
+ if chartData.isGreaterThanTwo() {
+
+ ZStack {
+
+ chartData.getAccessibility()
+
+ // MARK: Ranged Box
+ if chartData.dataSets.style.fillColour.colourType == .colour,
+ let colour = chartData.dataSets.style.fillColour.colour
+ {
+
+ RangedLineFillShape(dataPoints: chartData.dataSets.dataPoints,
+ lineType: chartData.dataSets.style.lineType,
+ minValue: chartData.minValue,
+ range: chartData.range,
+ ignoreZero: chartData.dataSets.style.ignoreZero)
+ .fill(colour)
+
+
+ } else if chartData.dataSets.style.fillColour.colourType == .gradientColour,
+ let colours = chartData.dataSets.style.fillColour.colours,
+ let startPoint = chartData.dataSets.style.fillColour.startPoint,
+ let endPoint = chartData.dataSets.style.fillColour.endPoint
+ {
+
+ RangedLineFillShape(dataPoints: chartData.dataSets.dataPoints,
+ lineType: chartData.dataSets.style.lineType,
+ minValue: chartData.minValue,
+ range: chartData.range,
+ ignoreZero: chartData.dataSets.style.ignoreZero)
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint))
+
+ } else if chartData.dataSets.style.fillColour.colourType == .gradientStops,
+ let stops = chartData.dataSets.style.fillColour.stops,
+ let startPoint = chartData.dataSets.style.fillColour.startPoint,
+ let endPoint = chartData.dataSets.style.fillColour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ RangedLineFillShape(dataPoints: chartData.dataSets.dataPoints,
+ lineType: chartData.dataSets.style.lineType,
+ minValue: chartData.minValue,
+ range: chartData.range,
+ ignoreZero: chartData.dataSets.style.ignoreZero)
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint))
+
+ }
+
+ // MARK: Main Line
+ if chartData.dataSets.style.lineColour.colourType == .colour,
+ let colour = chartData.dataSets.style.lineColour.colour
+ {
+
+ LineChartColourSubView(chartData: chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ colour : colour,
+ isFilled : false)
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientColour,
+ let colours = chartData.dataSets.style.lineColour.colours,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+
+ LineChartColoursSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ colours : colours,
+ startPoint : startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+
+ } else if chartData.dataSets.style.lineColour.colourType == .gradientStops,
+ let stops = chartData.dataSets.style.lineColour.stops,
+ let startPoint = chartData.dataSets.style.lineColour.startPoint,
+ let endPoint = chartData.dataSets.style.lineColour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+
+ LineChartStopsSubView(chartData : chartData,
+ dataSet : chartData.dataSets,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ stops : stops,
+ startPoint: startPoint,
+ endPoint : endPoint,
+ isFilled : false)
+
+ }
+ }
+ } else { CustomNoDataView(chartData: chartData) }
+ }
+}
diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift
new file mode 100644
index 00000000..01c4bb9e
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift
@@ -0,0 +1,259 @@
+//
+// LineChartSubViews.swift
+//
+//
+// Created by Will Dale on 26/01/2021.
+//
+
+import SwiftUI
+
+struct AccessibilityRectangle: Shape {
+
+ let dataPointCount : Int
+ let dataPointNo : Int
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+
+ let x = rect.width / CGFloat(dataPointCount-1)
+ let pointX : CGFloat = (CGFloat(dataPointNo) * x) - x / CGFloat(2)
+
+ let point : CGRect = CGRect(x : pointX,
+ y : 0,
+ width : x,
+ height: rect.height)
+
+ path.addRoundedRect(in: point, cornerSize: CGSize(width: 10, height: 10))
+
+ return path
+ }
+}
+
+// MARK: - Single colour
+/**
+ Sub view gets the line drawn, sets the colour and sets up the animations.
+
+ Single colour
+ */
+internal struct LineChartColourSubView: View where CD: CTLineChartDataProtocol,
+ DS: CTLineChartDataSet,
+ DS.DataPoint: CTStandardDataPointProtocol {
+
+ private let chartData : CD
+ private let dataSet : DS
+ private let minValue : Double
+ private let range : Double
+ private let colour : Color
+ private let isFilled : Bool
+
+ internal init(chartData : CD,
+ dataSet : DS,
+ minValue : Double,
+ range : Double,
+ colour : Color,
+ isFilled : Bool
+ ) {
+ self.chartData = chartData
+ self.dataSet = dataSet
+ self.minValue = minValue
+ self.range = range
+ self.colour = colour
+ self.isFilled = isFilled
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+
+ LineShape(dataPoints: dataSet.dataPoints,
+ lineType : dataSet.style.lineType,
+ isFilled : isFilled,
+ minValue : minValue,
+ range : range,
+ ignoreZero: dataSet.style.ignoreZero)
+ .ifElse(isFilled, if: {
+ $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .fill(colour)
+ }, else: {
+ $0.trim(to: startAnimation ? 1 : 0)
+ .stroke(colour, style: dataSet.style.strokeStyle.strokeToStrokeStyle())
+ })
+ .background(Color(.gray).opacity(0.000000001))
+ .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) }
+ .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) }
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+}
+
+
+// MARK: - Gradient colour
+/**
+ Sub view gets the line drawn, sets the colour and sets up the animations.
+
+ Gradient colour
+ */
+internal struct LineChartColoursSubView: View where CD: CTLineChartDataProtocol,
+ DS: CTLineChartDataSet,
+ DS.DataPoint: CTStandardDataPointProtocol {
+
+ private let chartData : CD
+ private let dataSet : DS
+
+ private let minValue : Double
+ private let range : Double
+ private let colours : [Color]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+
+ private let isFilled : Bool
+
+ internal init(chartData : CD,
+ dataSet : DS,
+ minValue : Double,
+ range : Double,
+ colours : [Color],
+ startPoint: UnitPoint,
+ endPoint : UnitPoint,
+ isFilled : Bool
+ ) {
+ self.chartData = chartData
+ self.dataSet = dataSet
+ self.minValue = minValue
+ self.range = range
+ self.colours = colours
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.isFilled = isFilled
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+
+ ZStack {
+
+ chartData.getAccessibility()
+
+ LineShape(dataPoints: dataSet.dataPoints,
+ lineType : dataSet.style.lineType,
+ isFilled : isFilled,
+ minValue : minValue,
+ range : range,
+ ignoreZero: dataSet.style.ignoreZero)
+ .ifElse(isFilled, if: {
+ $0
+ .scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ }, else: {
+ $0
+ .trim(to: startAnimation ? 1 : 0)
+ .stroke(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint),
+ style: dataSet.style.strokeStyle.strokeToStrokeStyle())
+ })
+
+
+ .background(Color(.gray).opacity(0.000000001))
+ .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) }
+ .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) }
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+ }
+}
+
+// MARK: - Gradient with stops
+/**
+ Sub view gets the line drawn, sets the colour and sets up the animations.
+
+ Gradient with stops
+ */
+internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtocol,
+ DS: CTLineChartDataSet,
+ DS.DataPoint: CTStandardDataPointProtocol {
+
+ private let chartData : CD
+ private let dataSet : DS
+
+ private let minValue : Double
+ private let range : Double
+ private let stops : [Gradient.Stop]
+ private let startPoint : UnitPoint
+ private let endPoint : UnitPoint
+
+ private let isFilled : Bool
+
+ internal init(chartData : CD,
+ dataSet : DS,
+ minValue : Double,
+ range : Double,
+ stops : [Gradient.Stop],
+ startPoint: UnitPoint,
+ endPoint : UnitPoint,
+ isFilled : Bool
+ ) {
+ self.chartData = chartData
+ self.dataSet = dataSet
+ self.minValue = minValue
+ self.range = range
+ self.stops = stops
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ self.isFilled = isFilled
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+
+ ZStack {
+
+ chartData.getAccessibility()
+
+ LineShape(dataPoints: dataSet.dataPoints,
+ lineType : dataSet.style.lineType,
+ isFilled : isFilled,
+ minValue : minValue,
+ range : range,
+ ignoreZero: dataSet.style.ignoreZero)
+
+ .ifElse(isFilled, if: {
+ $0
+ .scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ }, else: {
+ $0
+ .trim(to: startAnimation ? 1 : 0)
+ .stroke(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint),
+ style: dataSet.style.strokeStyle.strokeToStrokeStyle())
+ })
+
+ .background(Color(.gray).opacity(0.000000001))
+ .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) }
+ .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) }
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+ }
+}
+
diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift
new file mode 100644
index 00000000..af4738b2
--- /dev/null
+++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift
@@ -0,0 +1,104 @@
+//
+// PointsSubView.swift
+//
+//
+// Created by Will Dale on 04/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Sub view gets the point markers drawn, sets the styling and sets up the animations.
+ */
+internal struct PointsSubView: View where DS: CTLineChartDataSet,
+ DS.DataPoint: CTStandardDataPointProtocol {
+
+ private let dataSets : DS
+ private let minValue : Double
+ private let range : Double
+ private let animation: Animation
+ private let isFilled : Bool
+
+ internal init(dataSets : DS,
+ minValue : Double,
+ range : Double,
+ animation : Animation,
+ isFilled : Bool
+ ) {
+ self.dataSets = dataSets
+ self.minValue = minValue
+ self.range = range
+ self.animation = animation
+ self.isFilled = isFilled
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal var body: some View {
+ switch dataSets.pointStyle.pointType {
+ case .filled:
+
+ Point(dataSet : dataSets,
+ minValue : minValue,
+ range : range)
+ .ifElse(!isFilled, if: {
+ $0.trim(to: startAnimation ? 1 : 0)
+ .fill(dataSets.pointStyle.fillColour)
+ }, else: {
+ $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .fill(dataSets.pointStyle.fillColour)
+ })
+ .animateOnAppear(using: animation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: animation) {
+ self.startAnimation = false
+ }
+
+ case .outline:
+
+ Point(dataSet : dataSets,
+ minValue : minValue,
+ range : range)
+ .ifElse(!isFilled, if: {
+ $0.trim(to: startAnimation ? 1 : 0)
+ .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth)
+ }, else: {
+ $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth)
+ })
+ .animateOnAppear(using: animation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: animation) {
+ self.startAnimation = false
+ }
+
+ case .filledOutLine:
+
+ Point(dataSet : dataSets,
+ minValue : minValue,
+ range : range)
+ .ifElse(!isFilled, if: {
+ $0.trim(to: startAnimation ? 1 : 0)
+ .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth)
+ }, else: {
+ $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom)
+ .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth)
+ })
+
+ .background(Point(dataSet : dataSets,
+ minValue : minValue,
+ range : range)
+ .foregroundColor(dataSets.pointStyle.fillColour)
+ )
+ .animateOnAppear(using: animation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: animation) {
+ self.startAnimation = false
+ }
+ }
+ }
+}
+
diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift
new file mode 100644
index 00000000..4124cec2
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift
@@ -0,0 +1,77 @@
+//
+// DoughnutChartData.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling a doughnut chart.
+
+ This model contains the data and styling information for a doughnut chart.
+ */
+public final class DoughnutChartData: CTDoughnutChartDataProtocol {
+
+ // MARK: Properties
+ public var id : UUID = UUID()
+ @Published public final var dataSets : PieDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var chartStyle : DoughnutChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var infoView : InfoViewData
+
+ public final var noDataText: Text
+ public final var chartType : (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises Doughnut Chart data.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the chart.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - chartStyle : The style data for the aesthetic of the chart.
+ /// - noDataText : Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : PieDataSet,
+ metadata : ChartMetadata,
+ chartStyle : DoughnutChartStyle = DoughnutChartStyle(),
+ noDataText : Text
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.chartStyle = chartStyle
+ self.legends = [LegendData]()
+ self.infoView = InfoViewData()
+ self.noDataText = noDataText
+ self.chartType = (chartType: .pie, dataSetType: .single)
+
+ self.setupLegends()
+ self.makeDataPoints()
+ }
+
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() }
+
+ public typealias Set = PieDataSet
+ public typealias DataPoint = PieChartDataPoint
+ public typealias CTStyle = DoughnutChartStyle
+}
+
+// MARK: - Touch
+extension DoughnutChartData {
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [PieChartDataPoint] = []
+ let touchDegree = degree(from: touchLocation, in: chartSize)
+
+ let dataPoint = self.dataSets.dataPoints.first(where: { $0.startAngle * Double(180 / Double.pi) <= Double(touchDegree) && ($0.startAngle * Double(180 / Double.pi)) + ($0.amount * Double(180 / Double.pi)) >= Double(touchDegree) } )
+ if let data = dataPoint {
+ var finalDataPoint = data
+ finalDataPoint.legendTag = dataSets.legendTitle
+ points.append(finalDataPoint)
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+ public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+ return nil
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift
new file mode 100644
index 00000000..c6546364
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift
@@ -0,0 +1,78 @@
+//
+// PieChartData.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for drawing and styling a pie chart.
+
+ This model contains the data and styling information for a pie chart.
+ */
+public final class PieChartData: CTPieChartDataProtocol {
+
+ // MARK: Properties
+ public var id : UUID = UUID()
+ @Published public final var dataSets : PieDataSet
+ @Published public final var metadata : ChartMetadata
+ @Published public final var chartStyle : PieChartStyle
+ @Published public final var legends : [LegendData]
+ @Published public final var infoView : InfoViewData
+
+ public final var noDataText: Text
+ public final var chartType: (chartType: ChartType, dataSetType: DataSetType)
+
+ // MARK: Initializer
+ /// Initialises Pie Chart data.
+ ///
+ /// - Parameters:
+ /// - dataSets: Data to draw and style the chart.
+ /// - metadata: Data model containing the charts Title, Subtitle and the Title for Legend.
+ /// - chartStyle : The style data for the aesthetic of the chart.
+ /// - noDataText : Customisable Text to display when where is not enough data to draw the chart.
+ public init(dataSets : PieDataSet,
+ metadata : ChartMetadata,
+ chartStyle : PieChartStyle = PieChartStyle(),
+ noDataText : Text = Text("No Data")
+ ) {
+ self.dataSets = dataSets
+ self.metadata = metadata
+ self.chartStyle = chartStyle
+ self.legends = [LegendData]()
+ self.infoView = InfoViewData()
+ self.noDataText = noDataText
+ self.chartType = (chartType: .pie, dataSetType: .single)
+
+ self.setupLegends()
+
+ self.makeDataPoints()
+ }
+
+ public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() }
+
+ public typealias Set = PieDataSet
+ public typealias DataPoint = PieChartDataPoint
+ public typealias CTStyle = PieChartStyle
+}
+
+// MARK: - Touch
+extension PieChartData {
+ public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) {
+ var points : [PieChartDataPoint] = []
+ let touchDegree = degree(from: touchLocation, in: chartSize)
+
+ let dataPoint = self.dataSets.dataPoints.first(where: { $0.startAngle * Double(180 / Double.pi) <= Double(touchDegree) && ($0.startAngle * Double(180 / Double.pi)) + ($0.amount * Double(180 / Double.pi)) >= Double(touchDegree) } )
+ if let data = dataPoint {
+ var finalDataPoint = data
+ finalDataPoint.legendTag = dataSets.legendTitle
+ points.append(finalDataPoint)
+ }
+ self.infoView.touchOverlayInfo = points
+ }
+ public final func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? {
+ return nil
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift
new file mode 100644
index 00000000..11d867e2
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift
@@ -0,0 +1,54 @@
+//
+// PieChartDataPoint.swift
+//
+//
+// Created by Will Dale on 01/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data for a single segement of a pie chart.
+ */
+public struct PieChartDataPoint: CTPieDataPoint {
+
+ public var id : UUID = UUID()
+ public var value : Double
+ public var description : String?
+ public var date : Date?
+ public var colour : Color
+ public var startAngle : Double = 0
+ public var amount : Double = 0
+
+ public var legendTag : String = ""
+
+ /// Data model for a single data point for a pie chart.
+ /// - Parameters:
+ /// - value: Value of the data point
+ /// - description: A longer label that can be shown on touch input.
+ /// - date: Date of the data point if any data based calculations are required.
+ /// - colour: Colour of the segment.
+ public init(value : Double,
+ description : String? = nil,
+ date : Date? = nil,
+ colour : Color = Color.red
+ ) {
+ self.value = value
+ self.description = description
+ self.date = date
+ self.colour = colour
+ }
+}
+
+
+extension PieChartDataPoint {
+ // Remove legend tag from compare
+ public static func == (left: PieChartDataPoint, right: PieChartDataPoint) -> Bool {
+ return (left.id == right.id) &&
+ (left.amount == right.amount) &&
+ (left.startAngle == right.startAngle) &&
+ (left.value == right.value) &&
+ (left.date == right.date) &&
+ (left.description == right.description)
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift
new file mode 100644
index 00000000..2f19f8c3
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift
@@ -0,0 +1,34 @@
+//
+// PieDataSet.swift
+//
+//
+// Created by Will Dale on 01/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data set for a pie chart.
+ */
+public struct PieDataSet: CTSingleDataSetProtocol {
+
+ public var id : UUID = UUID()
+ public var dataPoints : [PieChartDataPoint]
+ public var legendTitle : String
+
+ /// Initialises a new data set for a standard pie chart.
+ /// - Parameters:
+ /// - dataPoints: Array of elements.
+ /// - legendTitle: Label for the data in legend.
+ public init(dataPoints : [PieChartDataPoint],
+ legendTitle : String//,
+ ) {
+ self.dataPoints = dataPoints
+ self.legendTitle = legendTitle
+ }
+
+ public typealias ID = UUID
+ public typealias DataPoint = PieChartDataPoint
+}
+
+
diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift
new file mode 100644
index 00000000..8059b456
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift
@@ -0,0 +1,76 @@
+//
+// PieChartProtocols.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Chart Data
+/**
+ A protocol to extend functionality of `CTChartData` specifically for Pie and Doughnut Charts.
+ */
+public protocol CTPieDoughnutChartDataProtocol: CTChartData {}
+
+/**
+ A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Pie Charts.
+ */
+public protocol CTPieChartDataProtocol : CTPieDoughnutChartDataProtocol {}
+
+/**
+ A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Doughnut Charts.
+ */
+public protocol CTDoughnutChartDataProtocol : CTPieDoughnutChartDataProtocol {}
+
+
+// MARK: - DataPoints
+/**
+ A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Pie and Doughnut Charts.
+ */
+public protocol CTPieDataPoint: CTStandardDataPointProtocol, CTnotRanged {
+
+ /**
+ Where the data point should start drawing from
+ based on where the prvious one finished.
+
+ In radians.
+ */
+ var startAngle : Double { get set }
+
+ /**
+ The data points value in radians.
+ */
+ var amount : Double { get set }
+
+ var colour : Color { get set }
+}
+
+
+
+
+
+
+// MARK: - Style
+/**
+ A protocol to extend functionality of `CTChartStyle` specifically for Pie and Doughnut Charts.
+ */
+public protocol CTPieAndDoughnutChartStyle: CTChartStyle {}
+
+
+/**
+ A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Pie Charts.
+ */
+public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {}
+
+
+/**
+ A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Doughnut Charts.
+ */
+public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle {
+
+ /**
+ Width / Delta of the Doughnut Chart
+ */
+ var strokeWidth: CGFloat { get set }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift
new file mode 100644
index 00000000..6e952577
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift
@@ -0,0 +1,72 @@
+//
+// PieChartProtocolExtentions.swift
+//
+//
+// Created by Will Dale on 23/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Extentions
+
+extension CTPieDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint {
+
+ /**
+ Sets up the data points in a way that can be sent to renderer for drawing.
+
+ It configures each data point with startAngle and amount variables in radians.
+ */
+ internal func makeDataPoints() {
+ let total = self.dataSets.dataPoints.reduce(0) { $0 + $1.value }
+ var startAngle = -Double.pi / 2
+ self.dataSets.dataPoints.indices.forEach { (point) in
+ let amount = .pi * 2 * (self.dataSets.dataPoints[point].value / total)
+ self.dataSets.dataPoints[point].amount = amount
+ self.dataSets.dataPoints[point].startAngle = startAngle
+ startAngle += amount
+ }
+ }
+
+ /**
+ Gets the number of degrees around the chart from 'north'.
+
+ # Reference
+ [Atan2](http://www.cplusplus.com/reference/cmath/atan2/)
+
+ [Rotate to north](https://stackoverflow.com/a/25398191)
+
+ - Parameters:
+ - touchLocation: Current location of the touch.
+ - rect: The size of the chart view as the parent view.
+ - Returns: Degrees around the chart.
+ */
+ func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat {
+ let center = CGPoint(x: rect.midX, y: rect.midY)
+ let coordinates = CGPoint(x: touchLocation.x - center.x,
+ y: touchLocation.y - center.y)
+ // -90 is north
+ let degrees = atan2(-coordinates.x, -coordinates.y) * CGFloat(180 / Double.pi)
+ if (degrees > 0) {
+ return 270 - degrees
+ } else {
+ return -90 - degrees
+ }
+ }
+}
+
+extension CTPieDoughnutChartDataProtocol where Self.Set.DataPoint.ID == UUID,
+ Self.Set: CTSingleDataSetProtocol,
+ Self.Set.DataPoint: CTPieDataPoint {
+ internal func setupLegends() {
+ for data in dataSets.dataPoints {
+ if let legend = data.description {
+ self.legends.append(LegendData(id : data.id,
+ legend : legend,
+ colour : ColourStyle(colour: data.colour),
+ strokeStyle: nil,
+ prioity : 1,
+ chartType : .pie))
+ }
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift
new file mode 100644
index 00000000..082f6487
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift
@@ -0,0 +1,56 @@
+//
+// DoughnutChartStyle.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Model for controlling the overall aesthetic of the chart.
+ */
+public struct DoughnutChartStyle: CTDoughnutChartStyle {
+
+ public var infoBoxPlacement : InfoBoxPlacement
+ public var infoBoxValueColour : Color
+ public var infoBoxDescriptionColour : Color
+ public var infoBoxBackgroundColour : Color
+ public var infoBoxBorderColour : Color
+ public var infoBoxBorderStyle : StrokeStyle
+
+ public var globalAnimation : Animation
+
+ public var strokeWidth : CGFloat
+
+ /// Model for controlling the overall aesthetic of the chart.
+ /// - Parameters:
+ /// - infoBoxPlacement: Placement of the information box that appears on touch input.
+ /// - infoBoxValueColour: Colour of the value part of the touch info.
+ /// - infoBoxDescriptionColour: Colour of the description part of the touch info.
+ /// - infoBoxBackgroundColour: Background colour of touch info.
+ /// - infoBoxBorderColour: Border colour of the touch info.
+ /// - infoBoxBorderStyle: Border style of the touch info.
+ /// - globalAnimation: Global control of animations.
+ /// - strokeWidth: Width / Delta of the Doughnut Chart
+ public init(infoBoxPlacement : InfoBoxPlacement = .floating,
+ infoBoxValueColour : Color = Color.primary,
+ infoBoxDescriptionColour: Color = Color.primary,
+ infoBoxBackgroundColour : Color = Color.systemsBackground,
+ infoBoxBorderColour : Color = Color.clear,
+ infoBoxBorderStyle : StrokeStyle = StrokeStyle(lineWidth: 0),
+
+ globalAnimation : Animation = Animation.linear(duration: 1),
+ strokeWidth : CGFloat = 30
+ ) {
+ self.infoBoxPlacement = infoBoxPlacement
+ self.infoBoxValueColour = infoBoxValueColour
+ self.infoBoxDescriptionColour = infoBoxDescriptionColour
+ self.infoBoxBackgroundColour = infoBoxBackgroundColour
+ self.infoBoxBorderColour = infoBoxBorderColour
+ self.infoBoxBorderStyle = infoBoxBorderStyle
+
+ self.globalAnimation = globalAnimation
+ self.strokeWidth = strokeWidth
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift
new file mode 100644
index 00000000..a22f11b5
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift
@@ -0,0 +1,51 @@
+//
+// PieChartStyle.swift
+//
+//
+// Created by Will Dale on 25/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Model for controlling the overall aesthetic of the chart.
+ */
+public struct PieChartStyle: CTPieChartStyle {
+
+ public var infoBoxPlacement : InfoBoxPlacement
+ public var infoBoxValueColour : Color
+ public var infoBoxDescriptionColour : Color
+ public var infoBoxBackgroundColour : Color
+ public var infoBoxBorderColour : Color
+ public var infoBoxBorderStyle : StrokeStyle
+
+ public var globalAnimation : Animation
+
+ /// Model for controlling the overall aesthetic of the chart.
+ /// - Parameters:
+ /// - infoBoxPlacement: Placement of the information box that appears on touch input.
+ /// - infoBoxValueColour: Colour of the value part of the touch info.
+ /// - infoBoxDescriptionColour: Colour of the description part of the touch info.
+ /// - infoBoxBackgroundColour: Background colour of touch info.
+ /// - infoBoxBorderColour: Border colour of the touch info.
+ /// - infoBoxBorderStyle: Border style of the touch info.
+ /// - globalAnimation: Global control of animations.
+ public init(infoBoxPlacement : InfoBoxPlacement = .floating,
+ infoBoxValueColour : Color = Color.primary,
+ infoBoxDescriptionColour: Color = Color.primary,
+ infoBoxBackgroundColour : Color = Color.systemsBackground,
+ infoBoxBorderColour : Color = Color.clear,
+ infoBoxBorderStyle : StrokeStyle = StrokeStyle(lineWidth: 0),
+ globalAnimation : Animation = Animation.linear(duration: 1)
+ ) {
+ self.infoBoxPlacement = infoBoxPlacement
+ self.infoBoxValueColour = infoBoxValueColour
+ self.infoBoxDescriptionColour = infoBoxDescriptionColour
+ self.infoBoxBackgroundColour = infoBoxBackgroundColour
+ self.infoBoxBorderColour = infoBoxBorderColour
+ self.infoBoxBorderStyle = infoBoxBorderStyle
+ self.globalAnimation = globalAnimation
+ }
+}
+
+
diff --git a/Sources/SwiftUICharts/PieChart/Shapes/DoughnutSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/DoughnutSegmentShape.swift
new file mode 100644
index 00000000..3d3256f1
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Shapes/DoughnutSegmentShape.swift
@@ -0,0 +1,40 @@
+//
+// DoughnutSegmentShape.swift
+//
+//
+// Created by Will Dale on 23/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Draws a doughnut segment.
+ */
+internal struct DoughnutSegmentShape: InsettableShape, Identifiable {
+
+ var id : UUID
+ var startAngle : Double
+ var amount : Double
+ var insetAmount : CGFloat = 0
+
+ func inset(by amount: CGFloat) -> some InsettableShape {
+ var arc = self
+ arc.insetAmount += amount
+ return arc
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+
+ let radius = min(rect.width, rect.height) / 2
+ let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
+
+ var path = Path()
+
+ path.addRelativeArc(center : center,
+ radius : radius - insetAmount,
+ startAngle : Angle(radians: startAngle),
+ delta : Angle(radians: amount))
+
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift
new file mode 100644
index 00000000..eb4b7f79
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift
@@ -0,0 +1,33 @@
+//
+// PieSegmentShape.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Draws a pie segment.
+ */
+internal struct PieSegmentShape: Shape, Identifiable {
+
+ var id : UUID
+ var startAngle : Double
+ var amount : Double
+
+ internal func path(in rect: CGRect) -> Path {
+
+ let radius = min(rect.width, rect.height) / 2
+ let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
+
+ var path = Path()
+ path.move(to: center)
+ path.addRelativeArc(center : center,
+ radius : radius,
+ startAngle : Angle(radians: startAngle),
+ delta : Angle(radians: amount))
+
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift
new file mode 100644
index 00000000..2658876c
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift
@@ -0,0 +1,72 @@
+//
+// DoughnutChart.swift
+//
+//
+// Created by Will Dale on 01/02/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a doughnut chart.
+
+ Uses `DoughnutChartData` data model.
+
+ # Declaration
+ ```
+ DoughnutChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct DoughnutChart: View where ChartData: DoughnutChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a bar chart view.
+ /// - Parameter chartData: Must be DoughnutChartData.
+ public init(chartData : ChartData) {
+ self.chartData = chartData
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+ ZStack {
+ ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in
+ DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id,
+ startAngle: chartData.dataSets.dataPoints[data].startAngle,
+ amount: chartData.dataSets.dataPoints[data].amount)
+ .stroke(chartData.dataSets.dataPoints[data].colour, lineWidth: chartData.chartStyle.strokeWidth)
+ .scaleEffect(startAnimation ? 1 : 0)
+ .opacity(startAnimation ? 1 : 0)
+ .animation(Animation.spring().delay(Double(data) * 0.06))
+ .if(chartData.infoView.touchOverlayInfo == [chartData.dataSets.dataPoints[data]]) {
+ $0
+ .scaleEffect(1.1)
+ .zIndex(1)
+ .shadow(color: Color.primary, radius: 10)
+ }
+ .accessibilityLabel(Text("\(chartData.metadata.title)"))
+ .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+ }
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+
+}
diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift
new file mode 100644
index 00000000..90bd25b3
--- /dev/null
+++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift
@@ -0,0 +1,73 @@
+//
+// PieChart.swift
+//
+//
+// Created by Will Dale on 24/01/2021.
+//
+
+import SwiftUI
+
+/**
+ View for creating a pie chart.
+
+ Uses `PieChartData` data model.
+
+ # Declaration
+ ```
+ PieChart(chartData: data)
+ ```
+
+ # View Modifiers
+ The order of the view modifiers is some what important
+ as the modifiers are various types for stacks that wrap
+ around the previous views.
+ ```
+ .touchOverlay(chartData: data)
+ .infoBox(chartData: data)
+ .floatingInfoBox(chartData: data)
+ .headerBox(chartData: data)
+ .legends(chartData: data)
+ ```
+ */
+public struct PieChart: View where ChartData: PieChartData {
+
+ @ObservedObject var chartData: ChartData
+
+ /// Initialises a bar chart view.
+ /// - Parameter chartData: Must be PieChartData.
+ public init(chartData: ChartData) {
+ self.chartData = chartData
+ }
+
+ @State private var startAnimation : Bool = false
+
+ public var body: some View {
+
+ ZStack {
+ ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in
+ PieSegmentShape(id: chartData.dataSets.dataPoints[data].id,
+ startAngle: chartData.dataSets.dataPoints[data].startAngle,
+ amount: chartData.dataSets.dataPoints[data].amount)
+ .fill(chartData.dataSets.dataPoints[data].colour)
+ .scaleEffect(startAnimation ? 1 : 0)
+ .opacity(startAnimation ? 1 : 0)
+ .animation(Animation.spring().delay(Double(data) * 0.06))
+ .if(chartData.infoView.touchOverlayInfo == [chartData.dataSets.dataPoints[data]]) {
+ $0
+ .scaleEffect(1.1)
+ .zIndex(1)
+ .shadow(color: Color.primary, radius: 10)
+ }
+ .accessibilityLabel(Text("\(chartData.metadata.title)"))
+ .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier))
+ }
+ }
+
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/API.swift b/Sources/SwiftUICharts/Shared/API.swift
new file mode 100644
index 00000000..d00fa25f
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/API.swift
@@ -0,0 +1,269 @@
+//
+// API.swift
+//
+//
+// Created by Will Dale on 07/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Displays the data points value with the unit.
+ */
+public struct InfoValue : View where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ public init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ public var body: some View {
+ ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in
+ chartData.infoValueUnit(info: point)
+ }
+ }
+}
+
+/**
+ Displays the data points description.
+ */
+public struct InfoDescription : View where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ public init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ public var body: some View {
+ ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in
+ chartData.infoDescription(info: point)
+ }
+ }
+}
+
+/**
+ Option the as a String between the Value and the Description.
+ */
+public struct InfoExtra: View where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ private let text: String
+
+ public init(chartData: T, text: String) {
+ self.chartData = chartData
+ self.text = text
+ }
+
+ public var body: some View {
+ if chartData.infoView.isTouchCurrent {
+ Text(text)
+ } else {
+ EmptyView()
+ }
+ }
+}
+
+extension LegendData {
+ /**
+ Get the legend as a view.
+
+ - Parameter textColor: Colour of the text
+ - Returns: The relevent legend as a view.
+ */
+ public func getLegend(textColor: Color) -> some View {
+ Group {
+ switch self.chartType {
+ case .line:
+ if let stroke = self.strokeStyle {
+ let strokeStyle = stroke.strokeToStrokeStyle()
+ if let colour = self.colour.colour {
+ HStack {
+ LegendLine(width: 40)
+ .stroke(colour, style: strokeStyle)
+ .frame(width: 40, height: 3)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+
+ } else if let colours = self.colour.colours {
+ HStack {
+ LegendLine(width: 40)
+ .stroke(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: .leading,
+ endPoint: .trailing),
+ style: strokeStyle)
+ .frame(width: 40, height: 3)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+ } else if let stops = self.colour.stops {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+ HStack {
+ LegendLine(width: 40)
+ .stroke(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: .leading,
+ endPoint: .trailing),
+ style: strokeStyle)
+ .frame(width: 40, height: 3)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+ }
+ }
+
+ case.bar:
+ Group {
+ if let colour = self.colour.colour
+ {
+ HStack {
+ Rectangle()
+ .fill(colour)
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+ } else if let colours = self.colour.colours,
+ let startPoint = self.colour.startPoint,
+ let endPoint = self.colour.endPoint
+ {
+ HStack {
+ Rectangle()
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+ } else if let stops = self.colour.stops,
+ let startPoint = self.colour.startPoint,
+ let endPoint = self.colour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+ HStack {
+ Rectangle()
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+ }
+ }
+ case .pie:
+ if let colour = self.colour.colour {
+ HStack {
+ Circle()
+ .fill(colour)
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+
+ } else if let colours = self.colour.colours,
+ let startPoint = self.colour.startPoint,
+ let endPoint = self.colour.endPoint
+ {
+ HStack {
+ Circle()
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+
+ } else if let stops = self.colour.stops,
+ let startPoint = self.colour.startPoint,
+ let endPoint = self.colour.endPoint
+ {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+ HStack {
+ Circle()
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: startPoint,
+ endPoint: endPoint))
+ .frame(width: 20, height: 20)
+ Text(self.legend)
+ .font(.caption)
+ }
+ }
+ }
+ }
+ }
+ /**
+ Get the legend as a view where the colour is indicated by a Circle.
+
+ - Parameter textColor: Colour of the text
+ - Returns: The relevent legend as a view.
+ */
+ public func getLegendAsCircle(textColor: Color) -> some View {
+ Group {
+ if let colour = self.colour.colour {
+ HStack {
+ Circle()
+ .fill(colour)
+ .frame(width: 12, height: 12)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+
+ } else if let colours = self.colour.colours {
+ HStack {
+ Circle()
+ .fill(LinearGradient(gradient: Gradient(colors: colours),
+ startPoint: .leading,
+ endPoint: .trailing))
+ .frame(width: 12, height: 12)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+ } else if let stops = self.colour.stops {
+ let stops = GradientStop.convertToGradientStopsArray(stops: stops)
+ HStack {
+ Circle()
+ .fill(LinearGradient(gradient: Gradient(stops: stops),
+ startPoint: .leading,
+ endPoint: .trailing))
+ .frame(width: 12, height: 12)
+ Text(self.legend)
+ .font(.caption)
+ .foregroundColor(textColor)
+ }
+ } else { EmptyView() }
+ }
+ }
+
+ internal func accessibilityLegendLabel() -> String {
+ switch self.chartType {
+ case .line:
+ if self.prioity == 1 {
+ return "Line Chart Legend"
+ } else {
+ return "P O I Marker Legend"
+ }
+ case .bar:
+ if self.prioity == 1 {
+ return "Bar Chart Legend"
+ } else {
+ return "P O I Marker Legend"
+ }
+ case .pie:
+ if self.prioity == 1 {
+ return "Pie Chart Legend"
+ } else {
+ return "P O I Marker Legend"
+ }
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift
deleted file mode 100644
index 363e33e1..00000000
--- a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift
+++ /dev/null
@@ -1,133 +0,0 @@
-//
-// Calculations.swift
-//
-//
-// Created by Will Dale on 14/01/2021.
-//
-
-import SwiftUI
-
-internal struct Calculations {
- /// Get an array of data points converted into and array of data points averaged by their calendar month.
- /// - Parameter dataPoints: Array of ChartDataPoint.
- /// - Returns: Array of ChartDataPoint averaged by their calendar month.
- static internal func monthlyAverage(dataPoints: [ChartDataPoint]) -> [ChartDataPoint]? {
- let calendar = Calendar.current
-
- let formatterForXAxisLabel = DateFormatter()
- formatterForXAxisLabel.locale = .current
- formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("MMM")
- let formatterForPointLabel = DateFormatter()
- formatterForPointLabel.locale = .current
- formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY")
-
- guard let firstDataPoint = dataPoints.first?.date else { return nil }
- guard let lastDataPoint = dataPoints.last?.date else { return nil }
-
- guard let numberOfMonths = calendar.dateComponents([.month],
- from: firstDataPoint,
- to: lastDataPoint).month else { return nil }
- var outputData : [ChartDataPoint] = []
- for index in 0...numberOfMonths {
- if let date = calendar.date(byAdding: .month, value: index, to: firstDataPoint) {
-
- let requestedMonth = calendar.dateComponents([.year, .month], from: date)
-
- let monthOfData = dataPoints.filter { (dataPoint) -> Bool in
- let month = calendar.dateComponents([.year, .month], from: dataPoint.date ?? Date())
- return month == requestedMonth
- }
- let sum = monthOfData.reduce(0) { $0 + $1.value }
- let average = sum / Double(monthOfData.count)
-
- outputData.append(ChartDataPoint(value: average,
- xAxisLabel: formatterForXAxisLabel.string(from: date),
- pointLabel: formatterForPointLabel.string(from: date)))
- }
- }
- return outputData
- }
-
-
- /// Get an array of data points converted into and array of data points averaged by their week.
- /// - Parameter dataPoints: Array of ChartDataPoint.
- /// - Returns: Array of ChartDataPoint averaged by their week.
- static internal func weeklyAverage(dataPoints: [ChartDataPoint]) -> [ChartDataPoint]? {
- let calendar = Calendar.current
-
- let formatterForXAxisLabel = DateFormatter()
- formatterForXAxisLabel.locale = .current
- formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d")
- let formatterForPointLabel = DateFormatter()
- formatterForPointLabel.locale = .current
- formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY")
-
- guard let firstDataPoint = dataPoints.first?.date else { return nil }
- guard let lastDataPoint = dataPoints.last?.date else { return nil }
-
- guard let numberOfWeeks = calendar.dateComponents([.weekOfYear],
- from: firstDataPoint,
- to: lastDataPoint).weekOfYear else { return nil }
-
- var outputData : [ChartDataPoint] = []
- for index in 0...numberOfWeeks {
- if let date = calendar.date(byAdding: .weekOfYear, value: index, to: firstDataPoint) {
-
- let requestedWeek = calendar.dateComponents([.year, .weekOfYear], from: date)
-
- let weekOfData = dataPoints.filter { (dataPoint) -> Bool in
- let week = calendar.dateComponents([.year, .weekOfYear], from: dataPoint.date ?? Date())
- return week == requestedWeek
- }
- let sum = weekOfData.reduce(0) { $0 + $1.value }
- let average = sum / Double(weekOfData.count)
-
- outputData.append(ChartDataPoint(value: average,
- xAxisLabel: formatterForXAxisLabel.string(from: date),
- pointLabel: formatterForPointLabel.string(from: date)))
- }
- }
- return outputData
- }
-
- /// Get an array of data points converted into and array of data points averaged by their day.
- /// - Parameter dataPoints: Array of ChartDataPoint.
- /// - Returns: Array of ChartDataPoint averaged by their day.
- static internal func dailyAverage(dataPoints: [ChartDataPoint]) -> [ChartDataPoint]? {
- let calendar = Calendar.current
-
- let formatterForXAxisLabel = DateFormatter()
- formatterForXAxisLabel.locale = .current
- formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d")
- let formatterForPointLabel = DateFormatter()
- formatterForPointLabel.locale = .current
- formatterForPointLabel.setLocalizedDateFormatFromTemplate("dd MMMM YYYY")
-
- guard let firstDataPoint = dataPoints.first?.date else { return nil }
- guard let lastDataPoint = dataPoints.last?.date else { return nil }
-
- guard let numberOfWeeks = calendar.dateComponents([.day],
- from: firstDataPoint,
- to: lastDataPoint).day else { return nil }
-
- var outputData : [ChartDataPoint] = []
- for index in 0...numberOfWeeks {
- if let date = calendar.date(byAdding: .day, value: index, to: firstDataPoint) {
-
- let requestedDay = calendar.dateComponents([.year, .day], from: date)
-
- let dayOfData = dataPoints.filter { (dataPoint) -> Bool in
- let day = calendar.dateComponents([.year, .day], from: dataPoint.date ?? Date())
- return day == requestedDay
- }
- let sum = dayOfData.reduce(0) { $0 + $1.value }
- let average = sum / Double(dayOfData.count)
-
- outputData.append(ChartDataPoint(value: average,
- xAxisLabel: formatterForXAxisLabel.string(from: date),
- pointLabel: formatterForPointLabel.string(from: date)))
- }
- }
- return outputData
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift
deleted file mode 100644
index c58695fe..00000000
--- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift
+++ /dev/null
@@ -1,244 +0,0 @@
-//
-// Enums.swift
-//
-//
-// Created by Will Dale on 10/01/2021.
-//
-
-import Foundation
-
-
-// MARK: - DataPoints
-/**
- Inbuild functions for manipulating the datapoints before drawing the chart.
- ```
- case none // No function
- case averageMonth // Monthly Average
- case averageWeek // Weekly Average
- case averageDay // Daily Average
- ```
- */
-public enum CalculationType {
- /// No function
- case none
- /// Monthly Average
- case averageMonth
- /// Weekly Average
- case averageWeek
- /// Daily Average
- case averageDay
-}
-
-// MARK: - ChartViewData
-/**
- Pass the type of chart being used to view modifiers.
- ```
- case line // Line Chart Type
- case bar // Bar Chart Type
- ```
- */
-public enum ChartType {
- /// Line Chart Type
- case line
- /// Bar Chart Type
- case bar
-}
-
-// MARK: - Style
-/**
- Type of colour styling for the chart.
- ```
- case colour // Single Colour
- case gradientColour // Colour Gradient
- case gradientStops // Colour Gradient with stop control
- ```
- */
-public enum ColourType {
- /// Single Colour
- case colour
- /// Colour Gradient
- case gradientColour
- /// Colour Gradient with stop control
- case gradientStops
-}
-
-// MARK: - LineShape
-/**
- Drawing style of the line
- ```
- case line // Straight line from point to point
- case curvedLine // Dual control point curved line
- ```
- */
-public enum LineType {
- /// Straight line from point to point
- case line
- /// Dual control point curved line
- case curvedLine
-}
-
-// MARK: - BarStyle
-/**
- Where to get the colour data from.
- ```
- case barStyle // From BarStyle data model
- case dataPoints // From each data point
- ```
- */
-public enum ColourFrom {
- case barStyle
- case dataPoints
-}
-
-// MARK: - TouchOverlayMarker
-/**
- Where the marker lines come from to meet at a specified point.
- ```
- case fullWidth // Full width and height of view intersecting at touch location
- case bottomLeading // From bottom and leading edges meeting at touch location
- case bottomTrailing // From bottom and trailing edges meeting at touch location
- case topLeading // From top and leading edges meeting at touch location
- case topTrailing // From top and trailing edges meeting at touch location
- ```
- */
-public enum MarkerLineType {
- /// Full width and height of view intersecting at a specified point
- case fullWidth
- /// From bottom and leading edges meeting at a specified point
- case bottomLeading
- /// From bottom and trailing edges meeting at a specified point
- case bottomTrailing
- /// From top and leading edges meeting at a specified point
- case topLeading
- /// From top and trailing edges meeting at a specified point
- case topTrailing
-}
-
-// MARK: - PointMarkers
-/**
- Style of the point marks
- ```
- case filled // Just fill
- case outline // Just stroke
- case filledOutLine // Both fill and stroke
- ```
- */
-public enum PointType {
- /// Just fill
- case filled
- /// Just stroke
- case outline
- /// Both fill and stroke
- case filledOutLine
-}
-/**
- Shape of the points
- ```
- case circle
- case square
- case roundSquare
- ```
- */
-public enum PointShape {
- /// Circle Shape
- case circle
- /// Square Shape
- case square
- /// Rounded Square Shape
- case roundSquare
-}
-
-// MARK: - TouchOverlay
-/**
- Placement of the data point information panel when touch overlay modifier is applied.
- ```
- case floating // Follows input across the chart
- case header // Fix in the Header box. Must have .headerBox()
-
- ```
- */
-public enum InfoBoxPlacement {
- /// Follows input across the chart
- case floating
- /// Fix in the Header box. Must have .headerBox()
- case header
-}
-
-// MARK: - XAxisLabels
-/**
-Location of the X axis labels
- ```
- case top
- case bottom
- ```
- */
-public enum XAxisLabelPosistion {
- case top
- case bottom
-}
-/**
- Where the label data come from.
-
- xAxisLabel comes from ChartData --> DataPoint model.
-
- xAxisLabels comes from ChartData --> xAxisLabels
- ```
- case dataPoint // ChartData --> DataPoint --> xAxisLabel
- case chartData // ChartData --> xAxisLabels
- ```
- */
-public enum LabelsFrom {
- /// ChartData --> DataPoint --> xAxisLabel
- case dataPoint
- /// ChartData --> xAxisLabels
- case chartData
-}
-
-// MARK: - YAxisLabels
-/**
-Location of the Y axis labels
- ```
- case leading
- case trailing
- ```
- */
-public enum YAxisLabelPosistion {
- case leading
- case trailing
-}
-
-/**
- Option to display the markers' value inline with the marker..
-
- ```
- case none // No label.
- case yAxis(specifier: String) // Places the label in the yAxis labels.
- case center(specifier: String) // Places the label in the center of chart.
- ```
- */
-public enum DisplayValue {
- /// No label.
- case none
- /// Places the label in the yAxis labels.
- case yAxis(specifier: String)
- /// Places the label in the center of chart.
- case center(specifier: String)
-}
-
-/**
- Option to display units before or after values.
-
- ```
- case none // No units
- case prefix(of: String) // Before value
- case suffix(of: String) // After value
- ```
- */
-public enum Units {
- /// No units
- case none
- /// Before value
- case prefix(of: String)
- /// After value
- case suffix(of: String)
-}
diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift
index d7e39122..eca00d1a 100644
--- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift
+++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift
@@ -7,10 +7,13 @@
import SwiftUI
-// https://stackoverflow.com/a/62962375
extension View {
- @ViewBuilder
- func `if`(_ condition: Bool, transform: (Self) -> Transform) -> some View {
+ /**
+ View modifier to conditionally add a view modifier.
+
+ [SO](https://stackoverflow.com/a/62962375)
+ */
+ @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
@@ -32,6 +35,7 @@ extension View {
}
extension View {
+<<<<<<< HEAD
@ViewBuilder
func `ifElseElseIf`(_ condition: Bool,
_ secondCondition: Bool,
@@ -44,14 +48,33 @@ extension View {
ifTransform(self)
} else if secondCondition {
elseIfTransform(self)
+=======
+ /**
+ View modifier to conditionally add a view modifier else add a different one.
+
+ [Five Stars](https://fivestars.blog/swiftui/conditional-modifiers.html)
+ */
+ @ViewBuilder func `ifElse`(_ condition: Bool,
+ if ifTransform: (Self) -> TrueContent,
+ else elseTransform: (Self) -> FalseContent
+ ) -> some View {
+ if condition {
+ ifTransform(self)
+>>>>>>> version-2
} else {
elseTransform(self)
}
}
}
-// https://www.hackingwithswift.com/quick-start/swiftui/how-to-start-an-animation-immediately-after-a-view-appears
+
+//
extension View {
+ /**
+ Start animation when the view appears.
+
+ [HWS](https://www.hackingwithswift.com/quick-start/swiftui/how-to-start-an-animation-immediately-after-a-view-appears)
+ */
func animateOnAppear(using animation: Animation = Animation.easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View {
return onAppear {
withAnimation(animation) {
@@ -60,11 +83,38 @@ extension View {
}
}
+<<<<<<< HEAD
func animateOnDisAppear(using animation: Animation = Animation.easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View {
+=======
+ /**
+ Reverse animation when the view disappears.
+
+ [HWS](https://www.hackingwithswift.com/quick-start/swiftui/how-to-start-an-animation-immediately-after-a-view-appears)
+ */
+ func animateOnDisappear(using animation: Animation = Animation.easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View {
+>>>>>>> version-2
return onDisappear {
withAnimation(animation) {
action()
}
}
}
+<<<<<<< HEAD
+=======
+}
+
+extension Color {
+ /// Returns the relevant system background colour for the device.
+ public static var systemsBackground: Color {
+ #if os(iOS)
+ return Color(.systemBackground)
+ #elseif os(watchOS)
+ return Color(.black)
+ #elseif os(tvOS)
+ return Color(.darkGray)
+ #elseif os(macOS)
+ return Color(.windowBackgroundColor)
+ #endif
+ }
+>>>>>>> version-2
}
diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift
new file mode 100644
index 00000000..25b1b44b
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift
@@ -0,0 +1,92 @@
+//
+// Enums.swift
+//
+//
+// Created by Will Dale on 10/01/2021.
+//
+
+import Foundation
+
+// MARK: - ChartViewData
+/**
+ The type of `DataSet` being used
+ ```
+ case single // Single data set - i.e LineDataSet
+ case multi // Multi data set - i.e MultiLineDataSet
+ ```
+ */
+public enum DataSetType {
+ case single
+ case multi
+}
+
+/**
+ The type of chart being used.
+ ```
+ case line // Line Chart Type
+ case bar // Bar Chart Type
+ case pie // Pie Chart Type
+ ```
+ */
+public enum ChartType {
+ /// Line Chart Type
+ case line
+ /// Bar Chart Type
+ case bar
+ /// Pie Chart Type
+ case pie
+}
+
+// MARK: - Style
+/**
+ Type of colour styling.
+ ```
+ case colour // Single Colour
+ case gradientColour // Colour Gradient
+ case gradientStops // Colour Gradient with stop control
+ ```
+ */
+public enum ColourType {
+ /// Single Colour
+ case colour
+ /// Colour Gradient
+ case gradientColour
+ /// Colour Gradient with stop control
+ case gradientStops
+}
+
+// MARK: - TouchOverlay
+/**
+ Placement of the data point information panel when touch overlay modifier is applied.
+ ```
+ case floating // Follows input across the chart.
+ case infoBox(isStatic: Bool) // Display in the InfoBox. Must have .infoBox()
+ case header // Fix in the Header box. Must have .headerBox().
+ ```
+ */
+public enum InfoBoxPlacement {
+ /// Follows input across the chart.
+ case floating
+ /// Display in the InfoBox. Must have .infoBox()
+ case infoBox(isStatic: Bool = false)
+ /// Display in the Header box. Must have .headerBox().
+ case header
+}
+
+/**
+ Option to display units before or after values.
+
+ ```
+ case none // No unit
+ case prefix(of: String) // Before value
+ case suffix(of: String) // After value
+ ```
+ */
+public enum TouchUnit {
+ /// No units
+ case none
+ /// Before value
+ case prefix(of: String)
+ /// After value
+ case suffix(of: String)
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/ChartData.swift b/Sources/SwiftUICharts/Shared/Models/ChartData.swift
deleted file mode 100644
index f6ee4807..00000000
--- a/Sources/SwiftUICharts/Shared/Models/ChartData.swift
+++ /dev/null
@@ -1,150 +0,0 @@
-//
-// ChartData.swift
-// LineChart
-//
-// Created by Will Dale on 24/12/2020.
-//
-
-import SwiftUI
-
-/// The central model from which the chart is drawn.
-public class ChartData: ObservableObject, Identifiable {
-
- public let id = UUID()
-
- /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart.
- @Published public var dataPoints : [ChartDataPoint]
- /// Data model containing: the charts Title, the charts Subtitle and the Line Legend.
- @Published public var metadata : ChartMetadata?
-
- /// Array of strings for the labels on the X Axis instead of the the dataPoints labels.
- @Published public var xAxisLabels : [String]?
-
- /// Data model conatining the style data for the chart.
- @Published public var chartStyle : ChartStyle
- /// Data model conatining the style data for the line chart.
- @Published public var lineStyle : LineStyle
- /// Data model conatining the style data for the line chart.
- @Published public var barStyle : BarStyle
- /// Data model containing the style data for the data point markers.
- @Published public var pointStyle : PointStyle
-
- /// Array of data to populate the chart legend.
- @Published var legends : [LegendData]
- /// Data model to hold data about the Views layout.
- @Published var viewData : ChartViewData
-
- public var noDataText : Text = Text("No Data")
-
- // MARK: - init: Calculations
- /// ChartData is the central model from which the chart is drawn.
- /// - Parameters:
- /// - dataPoints: Array of ChartDataPoints.
- /// - metadata: Data to fill in the metadata box above the chart.
- /// - xAxisLabels: Array of Strings for when there are too many data points to show all xAxisLabels.
- /// - chartStyle : The parameters for the aesthetic of the chart.
- /// - lineStyle: The parameters for the aesthetic of the line chart.
- /// - barStyle: The parameters for the aesthetic of the bar chart.
- /// - pointStyle: The parameters for the aesthetic of the data point markers.
- /// - calculations: Choose whether to perform calculations on the data points. If so then by what means.
- public init(dataPoints : [ChartDataPoint],
- metadata : ChartMetadata? = nil,
- xAxisLabels : [String]? = nil,
- chartStyle : ChartStyle = ChartStyle(),
- lineStyle : LineStyle = LineStyle(),
- barStyle : BarStyle = BarStyle(),
- pointStyle : PointStyle = PointStyle(),
- calculations: CalculationType = .none
- ) {
- switch calculations {
- case .none:
- self.dataPoints = dataPoints
- case .averageMonth:
- self.dataPoints = Calculations.monthlyAverage(dataPoints: dataPoints) ?? [ChartDataPoint(value: 0)]
- case .averageWeek:
- self.dataPoints = Calculations.weeklyAverage(dataPoints: dataPoints) ?? [ChartDataPoint(value: 0)]
- case .averageDay:
- self.dataPoints = Calculations.dailyAverage(dataPoints: dataPoints) ?? [ChartDataPoint(value: 0)]
- }
-
- self.metadata = metadata
- self.xAxisLabels = xAxisLabels
- self.chartStyle = chartStyle
- self.lineStyle = lineStyle
- self.barStyle = barStyle
- self.pointStyle = pointStyle
- self.legends = [LegendData]()
- self.viewData = ChartViewData()
- }
-
- // MARK: - init: Custom Calculations
- /// ChartData is the central model from which the chart is drawn. This init has the option to add
- /// - Parameters:
- /// - dataPoints: Array of ChartDataPoints.
- /// - metadata: Data to fill in the metadata box above the chart.
- /// - xAxisLabels: Array of Strings for when there are too many data points to show all xAxisLabels.
- /// - chartStyle : The parameters for the aesthetic of the chart.
- /// - lineStyle: The parameters for the aesthetic of the line chart.
- /// - barStyle: The parameters for the aesthetic of the bar chart.
- /// - pointStyle: The parameters for the aesthetic of the data point markers.
- /// - customCalc: Allows for custom calculations to be performed on the input data points before the chart is drawn custom calculations.
- public init(dataPoints : [ChartDataPoint],
- metadata : ChartMetadata? = nil,
- xAxisLabels : [String]? = nil,
- chartStyle : ChartStyle = ChartStyle(),
- lineStyle : LineStyle = LineStyle(),
- barStyle : BarStyle = BarStyle(),
- pointStyle : PointStyle = PointStyle(),
- customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]?
- ) {
- self.dataPoints = customCalc(dataPoints) ?? [ChartDataPoint(value: 0)]
- self.metadata = metadata
- self.xAxisLabels = xAxisLabels
- self.chartStyle = chartStyle
- self.lineStyle = lineStyle
- self.barStyle = barStyle
- self.pointStyle = pointStyle
- self.legends = [LegendData]()
- self.viewData = ChartViewData()
-
- }
-
- // MARK: - Functions
- /// Get the highest value from dataPoints array.
- /// - Returns: Highest value.
- func maxValue() -> Double {
- return dataPoints.max { $0.value < $1.value }?.value ?? 0
- }
- /// Get the Lowest value from dataPoints array.
- /// - Returns: Lowest value.
- func minValue() -> Double {
- return dataPoints.min { $0.value < $1.value }?.value ?? 0
- }
- /// Get the average of all the dataPoints.
- /// - Returns: Average.
- func average() -> Double {
- let sum = dataPoints.reduce(0) { $0 + $1.value }
- return sum / Double(dataPoints.count)
- }
- /// Get the difference between the hightest and lowest value in the dataPoints array.
- /// - Returns: Difference.
- func range() -> Double {
- let maxValue = dataPoints.max { $0.value < $1.value }?.value ?? 0
- let minValue = dataPoints.min { $0.value < $1.value }?.value ?? 0
-
- /*
- Adding 0.001 stops the following error if there is no variation in value of the dataPoints
- 2021-01-07 13:59:50.490962+0000 LineChart[4519:237208] [Unknown process name] Error: this application, or a library it uses, has passed an invalid numeric value (NaN, or not-a-number) to CoreGraphics API and this value is being ignored. Please fix this problem.
- */
- return (maxValue - minValue) + 0.001
- }
-
- /// Sets the order the Legends are layed out in.
- /// - Returns: Ordered array of Legends.
- func legendOrder() -> [LegendData] {
- return legends.sorted { $0.prioity < $1.prioity}
- }
-}
-
-
-
diff --git a/Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift b/Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift
deleted file mode 100644
index f70df638..00000000
--- a/Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift
+++ /dev/null
@@ -1,126 +0,0 @@
-//
-// ChartDataPoints.swift
-// LineChart
-//
-// Created by Will Dale on 02/01/2021.
-//
-
-import SwiftUI
-
-/// Data model for a data point.
-public struct ChartDataPoint: Hashable, Identifiable {
-
- public let id = UUID()
-
- /// Value of the data point
- public var value : Double
- /// Label that can be shown on the X axis.
- public var xAxisLabel : String?
- /// A longer label that can be shown on touch input.
- public var pointDescription : String?
- /// Date of the data point if any data based calculations are asked for.
- public var date : Date?
-
- /// Type of colour styling for the chart.
- public var colourType : ColourType
- /// Single Colour
- public var colour : Color?
- /// Colours for Gradient
- public var colours : [Color]?
- /// Colours and Stops for Gradient with stop control
- public var stops : [GradientStop]?
-
- /// Start point for Gradient
- public var startPoint : UnitPoint?
- /// End point for Gradient
- public var endPoint : UnitPoint?
-
- // MARK: - init: single colour
- /// Data model for a single data point with colour for use with a bar chart.
- /// - Parameters:
- /// - value: Value of the data point
- /// - xAxisLabel: Label that can be shown on the X axis.
- /// - pointLabel: A longer label that can be shown on touch input.
- /// - date: Date of the data point if any data based calculations are required.
- /// - colour: Colour for use with a bar chart.
- public init(value : Double,
- xAxisLabel : String? = nil,
- pointLabel : String? = nil,
- date : Date? = nil,
- colour : Color? = nil
- ) {
- self.value = value
- self.xAxisLabel = xAxisLabel
- self.pointDescription = pointLabel
- self.date = date
- self.colour = colour
- self.colours = nil
- self.stops = nil
- self.startPoint = nil
- self.endPoint = nil
- self.colourType = .colour
- }
-
- // MARK: - init: gradient colour
- /// Data model for a single data point with colour for use with a bar chart.
- /// - Parameters:
- /// - value: Value of the data point
- /// - xAxisLabel: Label that can be shown on the X axis.
- /// - pointLabel: A longer label that can be shown on touch input.
- /// - date: Date of the data point if any data based calculations are required.
- /// - colours: Array of colours for use with a bar chart.
- /// - startPoint: Start point for Gradient.
- /// - endPoint : End point for Gradient.
- public init(value : Double,
- xAxisLabel : String? = nil,
- pointLabel : String? = nil,
- date : Date? = nil,
-
- colours : [Color]? = nil,
- startPoint : UnitPoint? = nil,
- endPoint : UnitPoint? = nil
- ) {
- self.value = value
- self.xAxisLabel = xAxisLabel
- self.pointDescription = pointLabel
- self.date = date
-
- self.colour = nil
- self.stops = nil
- self.colours = colours
- self.startPoint = startPoint
- self.endPoint = endPoint
-
- self.colourType = .gradientColour
- }
-
- // MARK: - init: gradient with stops
- /// Data model for a single data point with colour for use with a bar chart.
- /// - Parameters:
- /// - value: Value of the data point
- /// - xAxisLabel: Label that can be shown on the X axis.
- /// - pointLabel: A longer label that can be shown on touch input.
- /// - date: Date of the data point if any data based calculations are required.
- /// - stops: Array of GradientStop for use with a bar chart.
- /// - startPoint: Start point for Gradient.
- /// - endPoint : End point for Gradient.
- public init(value : Double,
- xAxisLabel : String? = nil,
- pointLabel : String? = nil,
- date : Date? = nil,
- stops : [GradientStop]? = nil,
- startPoint : UnitPoint? = nil,
- endPoint : UnitPoint? = nil
- ) {
- self.value = value
- self.xAxisLabel = xAxisLabel
- self.pointDescription = pointLabel
- self.date = date
- self.colour = nil
- self.colours = nil
- self.stops = stops
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.colourType = .gradientStops
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift
index 57e4eeaa..ee671b3f 100644
--- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift
+++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift
@@ -5,29 +5,46 @@
// Created by Will Dale on 03/01/2021.
//
-import Foundation
+import SwiftUI
-/// Data model for the chart's metadata
+/**
+ Data model for the chart's metadata
+
+ Contains the Title, Subtitle and colour information for them.
+ */
public struct ChartMetadata {
+<<<<<<< HEAD
/// The charts Title
public var title : String?
/// The charts subtitle
public var subtitle : String?
/// The title for the legend
public var lineLegend : String?
+=======
+ /// The charts title
+ public var title : String
+ /// The charts subtitle
+ public var subtitle : String
+ /// Color of the title
+ public var titleColour : Color
+ /// Color of the subtitle
+ public var subtitleColour: Color
+>>>>>>> version-2
/// Model to hold the metadata for the chart.
/// - Parameters:
- /// - title: The charts Title
+ /// - title: The charts title
/// - subtitle: The charts subtitle
- /// - lineLegend: The title for the legend
- public init(title : String? = nil,
- subtitle : String? = nil,
- lineLegend : String? = nil
+ /// - titleColour: Color of the title
+ /// - subtitleColour: Color of the subtitle
+ public init(title : String = "",
+ subtitle : String = "",
+ titleColour : Color = Color.primary,
+ subtitleColour: Color = Color.primary
) {
- self.title = title
- self.subtitle = subtitle
- self.lineLegend = lineLegend
-
+ self.title = title
+ self.subtitle = subtitle
+ self.titleColour = titleColour
+ self.subtitleColour = subtitleColour
}
}
diff --git a/Sources/SwiftUICharts/Shared/Models/ChartStyle.swift b/Sources/SwiftUICharts/Shared/Models/ChartStyle.swift
deleted file mode 100644
index e5823441..00000000
--- a/Sources/SwiftUICharts/Shared/Models/ChartStyle.swift
+++ /dev/null
@@ -1,66 +0,0 @@
-//
-// ChartStyle.swift
-//
-//
-// Created by Will Dale on 12/01/2021.
-//
-
-import SwiftUI
-
-/// Model for controlling the overall aesthetic of the chart.
-public struct ChartStyle {
-
- /// Placement of the information box that appears on touch input.
- public var infoBoxPlacement : InfoBoxPlacement
-
- /// Style of the vertical lines breaking up the chart.
- public var xAxisGridStyle : GridStyle
- /// Style of the horizontal lines breaking up the chart.
- public var yAxisGridStyle : GridStyle
-
- /// Location of the X axis labels - Top or Bottom
- public var xAxisLabelPosition: XAxisLabelPosistion
- /// Where the label data come from. DataPoint or xAxisLabels
- public var xAxisLabelsFrom : LabelsFrom
-
- /// Location of the X axis labels - Leading or Trailing
- public var yAxisLabelPosition : YAxisLabelPosistion
- /// Number Of Labels on Y Axis
- public var yAxisNumberOfLabels : Int
-
- /// Gobal control of animations.
- public var globalAnimation : Animation
-
- /// Model for controlling the overall aesthetic of the chart.
- /// - Parameters:
- /// - infoBoxPlacement: Placement of the information box that appears on touch input.
- /// - xAxisGridStyle: Style of the vertical lines breaking up the chart.
- /// - yAxisGridStyle: Style of the horizontal lines breaking up the chart.
- /// - xAxisLabelPosition: Location of the X axis labels - Top or Bottom
- /// - xAxisLabelsFrom: Where the label data come from. DataPoint or xAxisLabels
- /// - yAxisLabelPosition: Location of the X axis labels - Leading or Trailing
- /// - yAxisNumberOfLabel: Number Of Labels on Y Axis
- /// - globalAnimation: Gobal control of animations.
- public init(infoBoxPlacement : InfoBoxPlacement = .floating,
- xAxisGridStyle : GridStyle = GridStyle(),
- yAxisGridStyle : GridStyle = GridStyle(),
- xAxisLabelPosition : XAxisLabelPosistion = .bottom,
- xAxisLabelsFrom : LabelsFrom = .dataPoint,
- yAxisLabelPosition : YAxisLabelPosistion = .leading,
- yAxisNumberOfLabels : Int = 10,
- globalAnimation : Animation = Animation.linear(duration: 1)
- ) {
- self.infoBoxPlacement = infoBoxPlacement
- self.xAxisGridStyle = xAxisGridStyle
- self.yAxisGridStyle = yAxisGridStyle
-
- self.xAxisLabelPosition = xAxisLabelPosition
- self.xAxisLabelsFrom = xAxisLabelsFrom
- self.yAxisLabelPosition = yAxisLabelPosition
- self.yAxisNumberOfLabels = yAxisNumberOfLabels
-
- self.globalAnimation = globalAnimation
- }
-}
-
-
diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift
deleted file mode 100644
index 5867602c..00000000
--- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// ChartViewData.swift
-// LineChart
-//
-// Created by Will Dale on 03/01/2021.
-//
-
-import Foundation
-
-/// Data model to pass view information internally so the layout can configure its self.
-internal struct ChartViewData {
-
- /// Pass the type of chart being used to view modifiers.
- var chartType : ChartType = .line
-
- /// If the chart has labels on the X axis, the Y axis needs a different layout
- var hasXAxisLabels : Bool = false
-
- /// If the chart has labels on the Y axis, the X axis needs a different layout
- var hasYAxisLabels : Bool = false
-
- /**
- Is there currently input (touch or click) on the chart
-
- Set from TouchOverlay
-
- Used by TitleBox
- */
- var isTouchCurrent : Bool = false
- /**
- Closest data point to input
-
- Set from TouchOverlay
-
- Used by TitleBox
- */
- var touchOverlayInfo : ChartDataPoint?
- /**
- Set specifier of data point readout
-
- Set from TouchOverlay
-
- Used by TitleBox
- */
- var touchSpecifier : String = "%.0f"
-
- var units : Units = .none
-}
diff --git a/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift
new file mode 100644
index 00000000..97100366
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift
@@ -0,0 +1,72 @@
+//
+// ColourStyle.swift
+//
+//
+// Created by Will Dale on 02/03/2021.
+//
+
+import SwiftUI
+
+/**
+ Model for setting up colour styling.
+ */
+public struct ColourStyle: CTColourStyle, Hashable {
+
+ public var colourType : ColourType
+ public var colour : Color?
+ public var colours : [Color]?
+ public var stops : [GradientStop]?
+ public var startPoint : UnitPoint?
+ public var endPoint : UnitPoint?
+
+ // MARK: Single colour
+ /// Single Colour
+ /// - Parameters:
+ /// - colour: Single Colour
+ public init(colour: Color = Color(.red)
+ ) {
+ self.colourType = .colour
+ self.colour = colour
+ self.colours = nil
+ self.stops = nil
+ self.startPoint = nil
+ self.endPoint = nil
+ }
+
+ // MARK: Gradient colour
+ /// Gradient Colour Line
+ /// - Parameters:
+ /// - colours: Colours for Gradient.
+ /// - startPoint: Start point for Gradient.
+ /// - endPoint: End point for Gradient.
+ public init(colours : [Color] = [Color(.red), Color(.blue)],
+ startPoint : UnitPoint = .leading,
+ endPoint : UnitPoint = .trailing
+
+ ) {
+ self.colourType = .gradientColour
+ self.colour = nil
+ self.colours = colours
+ self.stops = nil
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ }
+
+ // MARK: Gradient with stops
+ /// Gradient with Stops Line
+ /// - Parameters:
+ /// - stops: Colours and Stops for Gradient with stop control.
+ /// - startPoint: Start point for Gradient.
+ /// - endPoint: End point for Gradient.
+ public init(stops : [GradientStop] = [GradientStop(color: Color(.red), location: 0.0)],
+ startPoint : UnitPoint = .leading,
+ endPoint : UnitPoint = .trailing
+ ) {
+ self.colourType = .gradientStops
+ self.colour = nil
+ self.colours = nil
+ self.stops = stops
+ self.startPoint = startPoint
+ self.endPoint = endPoint
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift
new file mode 100644
index 00000000..f9942375
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift
@@ -0,0 +1,76 @@
+//
+// InfoViewData.swift
+//
+//
+// Created by Will Dale on 04/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Data model to pass view information internally for the `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+public struct InfoViewData {
+
+ /**
+ Is there currently input (touch or click) on the chart.
+
+ Set from TouchOverlay via the relevant protocol.
+
+ Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+ var isTouchCurrent: Bool = false
+
+ /**
+ Closest data points to input.
+
+ Set from TouchOverlay via the relevant protocol.
+
+ Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+ var touchOverlayInfo: [DP] = []
+
+ /**
+ Set specifier of data point readout.
+
+ Set from TouchOverlay via the relevant protocol.
+
+ Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+ var touchSpecifier: String = "%.0f"
+
+ /**
+ X axis posistion of the overlay box.
+
+ Used to set the location of the data point readout View.
+
+ Set from TouchOverlay via the relevant protocol.
+
+ Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+ var touchLocation: CGPoint = .zero
+
+ /**
+ Size of the chart.
+
+ Used to set the location of the data point readout View.
+
+ Set from TouchOverlay via the relevant protocol.
+
+ Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`.
+ */
+ var chartSize: CGRect = .zero
+
+ /**
+ Current width of the `YAxisLabels`
+
+ Needed line up the touch overlay to compensate for
+ the loss of width.
+ */
+ var yAxisLabelWidth: CGFloat = 0
+
+ /**
+ Option to display units before or after values.
+ */
+ var touchUnit: TouchUnit = .none
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift
index a661c619..e41a5b97 100644
--- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift
+++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift
@@ -7,106 +7,44 @@
import SwiftUI
-/// Data model for Legends
-internal struct LegendData: Hashable {
-
- var chartType : ChartType
-
+/**
+ Data model to hold data for Legends
+ */
+ public struct LegendData: Hashable, Identifiable {
+
+ public var id : UUID
+ /// The type of chart being used.
+ public var chartType : ChartType
/// Text to be displayed
- var legend : String
-
+ public var legend : String
/// Style of the stroke
- var strokeStyle : Stroke?
-
- /// Single Colour
- var colour : Color?
- /// Colours for Gradient
- var colours : [Color]?
- /// Colours and Stops for Gradient with stop control
- var stops : [GradientStop]?
-
- /// Start point for Gradient
- var startPoint : UnitPoint?
- /// End point for Gradient
- var endPoint : UnitPoint?
+ public var strokeStyle : Stroke?
/// Used to make sure the charts data legend is first
- let prioity : Int
+ public let prioity : Int
- /// Legend with single colour
- /// - Parameters:
- /// - legend: Text to be displayed
- /// - colour: Single Colour
- /// - strokeStyle: Stroke Style
- /// - prioity: Used to make sure the charts data legend is first
- internal init(legend : String,
- colour : Color,
- strokeStyle: Stroke?,
- prioity : Int,
- chartType : ChartType
- ) {
- self.legend = legend
- self.colour = colour
- self.colours = nil
- self.stops = nil
- self.startPoint = nil
- self.endPoint = nil
- self.strokeStyle = strokeStyle
- self.prioity = prioity
- self.chartType = chartType
- }
+ public var colour : ColourStyle
- /// Legend with a gradient colour
+ /// Legend.
/// - Parameters:
- /// - legend: Text to be displayed
- /// - colours: Colours for Gradient
- /// - startPoint: Start point for Gradient
- /// - endPoint: End point for Gradient
- /// - strokeStyle: Stroke Style
- /// - prioity: Used to make sure the charts data legend is first
- internal init(legend : String,
- colours : [Color],
- startPoint : UnitPoint,
- endPoint : UnitPoint,
- strokeStyle: Stroke?,
- prioity : Int,
- chartType : ChartType
+ /// - legend: Text to be displayed.
+ /// - colour: Colour styling.
+ /// - strokeStyle: Stroke Style.
+ /// - prioity: Used to make sure the charts data legend is first.
+ /// - chartType: Type of chart being used.
+ public init(id : UUID,
+ legend : String,
+ colour : ColourStyle,
+ strokeStyle: Stroke?,
+ prioity : Int,
+ chartType : ChartType
) {
+ self.id = id
self.legend = legend
- self.colour = nil
- self.colours = colours
- self.stops = nil
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.strokeStyle = strokeStyle
- self.prioity = prioity
- self.chartType = chartType
- }
-
- /// Legend with a gradient with stop control
- /// - Parameters:
- /// - legend: Text to be displayed
- /// - stops: Colours and Stops for Gradient with stop control
- /// - startPoint: Start point for Gradient
- /// - endPoint: End point for Gradient
- /// - strokeStyle: Stroke Style
- /// - prioity: Used to make sure the charts data legend is first
- internal init(legend : String,
- stops : [GradientStop],
- startPoint : UnitPoint,
- endPoint : UnitPoint,
- strokeStyle: Stroke?,
- prioity : Int,
- chartType : ChartType
- ) {
- self.legend = legend
- self.colour = nil
- self.colours = nil
- self.stops = stops
- self.startPoint = startPoint
- self.endPoint = endPoint
+ self.colour = colour
self.strokeStyle = strokeStyle
self.prioity = prioity
self.chartType = chartType
+
}
}
diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift
new file mode 100644
index 00000000..fb647f56
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift
@@ -0,0 +1,326 @@
+//
+// SharedProtocols.swift
+//
+//
+// Created by Will Dale on 23/01/2021.
+//
+
+import SwiftUI
+
+
+// MARK: Chart Data
+/**
+ Main protocol for passing data around library.
+
+ All Chart Data models ultimately conform to this.
+ */
+public protocol CTChartData: ObservableObject, Identifiable {
+
+ /// A type representing a data set. -- `CTDataSetProtocol`
+ associatedtype Set: CTDataSetProtocol
+
+ /// A type representing a data set. -- `CTDataSetProtocol`
+ associatedtype SetPoint: CTDataSetProtocol
+
+ /// A type representing a data point. -- `CTChartDataPoint`
+ associatedtype DataPoint: CTDataPointBaseProtocol
+
+ /// A type representing the chart style. -- `CTChartStyle`
+ associatedtype CTStyle: CTChartStyle
+
+ /// A type representing a view for the results of the touch interaction.
+ associatedtype Touch: View
+
+ var id: ID { get }
+
+ /**
+ Data model containing datapoints and styling information.
+ */
+ var dataSets: Set { get set }
+
+ /**
+ Data model containing the charts Title, Subtitle and the Title for Legend.
+ */
+ var metadata: ChartMetadata { get set }
+
+ /**
+ Array of `LegendData` to populate the charts legend.
+
+ This is populated automatically from within each view.
+ */
+ var legends: [LegendData] { get set }
+
+ /**
+ Data model pass data from `TouchOverlay` ViewModifier to
+ `HeaderBox` or `InfoBox` for display.
+ */
+ var infoView: InfoViewData { get set }
+
+ /**
+ Data model conatining the style data for the chart.
+ */
+ var chartStyle: CTStyle { get set }
+
+ /**
+ Customisable `Text` to display when where is not enough data to draw the chart.
+ */
+ var noDataText: Text { get set }
+
+ /**
+ Holds data about the charts type.
+
+ Allows for internal logic based on the type of chart.
+ */
+ var chartType: (chartType: ChartType, dataSetType: DataSetType) { get }
+
+
+ /**
+ Returns whether there are two or more data points.
+ */
+ func isGreaterThanTwo() -> Bool
+
+ // MARK: Touch
+ /**
+ Takes in the required data to set up all the touch interactions.
+
+ Output via `getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> Touch`
+
+ - Parameters:
+ - touchLocation: Current location of the touch
+ - chartSize: The size of the chart view as the parent view.
+ */
+ func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect)
+
+ /**
+ Takes touch location and return a view based on the chart type and configuration.
+
+ Inputs from `setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect)`
+
+ - Parameters:
+ - touchLocation: Current location of the touch
+ - chartSize: The size of the chart view as the parent view.
+ - Returns: The relevent view for the chart type and options.
+ */
+ func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> Touch
+
+ /**
+ Gets the nearest data points to the touch location.
+ - Parameters:
+ - touchLocation: Current location of the touch.
+ - chartSize: The size of the chart view as the parent view.
+ - Returns: Array of data points.
+ */
+ func getDataPoint(touchLocation: CGPoint, chartSize: CGRect)
+
+ /**
+ Gets the location of the data point in the view.
+ - Parameters:
+ - dataSet: Data set to work with.
+ - touchLocation: Current location of the touch.
+ - chartSize: The size of the chart view as the parent view.
+ - Returns: Array of points with the location on screen of data points.
+ */
+ func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint?
+
+}
+
+// MARK: - Data Sets
+/**
+ Main protocol to set conformace for types of Data Sets.
+ */
+public protocol CTDataSetProtocol: Hashable, Identifiable {
+ var id: ID { get }
+
+ /**
+ Returns the highest value in the data set.
+ - Parameter dataSet: Target data set.
+ - Returns: Highest value in data set.
+ */
+ func maxValue() -> Double
+
+ /**
+ Returns the lowest value in the data set.
+ - Parameter dataSet: Target data set.
+ - Returns: Lowest value in data set.
+ */
+ func minValue() -> Double
+
+ /**
+ Returns the average value from the data set.
+ - Parameter dataSet: Target data set.
+ - Returns: Average of values in data set.
+ */
+ func average() -> Double
+
+}
+
+/**
+ Protocol for data sets that only require a single set of data .
+ */
+public protocol CTSingleDataSetProtocol: CTDataSetProtocol {
+ /// A type representing a data point. -- `CTChartDataPoint`
+ associatedtype DataPoint: CTDataPointBaseProtocol
+
+ /**
+ Array of data points.
+ */
+ var dataPoints: [DataPoint] { get set }
+
+}
+
+/**
+ Protocol for data sets that require a multiple sets of data .
+ */
+public protocol CTMultiDataSetProtocol: CTDataSetProtocol {
+
+ /// A type representing a single data set -- `SingleDataSet`
+ associatedtype DataSet: CTSingleDataSetProtocol
+
+ /**
+ Array of single data sets.
+ */
+ var dataSets: [DataSet] { get set }
+}
+
+
+
+
+
+// MARK: - Data Points
+/**
+ Protocol to set base configuration for data points.
+ */
+public protocol CTDataPointBaseProtocol: Hashable, Identifiable {
+ var id: ID { get }
+
+ /**
+ A label that can be displayed on touch input
+
+ It can be displayed in a floating box that tracks the users input location
+ or placed in the header.
+ */
+ var description: String? { get set }
+
+ /**
+ Date can be used for optionally performing additional calculations.
+ */
+ var date: Date? { get set }
+
+ var legendTag : String { get set }
+
+ /**
+ Gets the relevant value(s) from the data point.
+
+ - Parameter specifier: Specifier
+ - Returns: Value as a string.
+ */
+ func valueAsString(specifier: String) -> String
+}
+
+/**
+ A protocol to extend functionality of `CTDataPointBaseProtocol` for any chart
+ type that needs a value.
+ */
+public protocol CTStandardDataPointProtocol: CTDataPointBaseProtocol {
+ /**
+ Value of the data point
+ */
+ var value: Double { get set }
+}
+
+/**
+ A protocol to extend functionality of `CTDataPointBaseProtocol` for any chart
+ type that needs a upper and lower values.
+ */
+public protocol CTRangeDataPointProtocol: CTDataPointBaseProtocol {
+ /// Value of the upper range of the data point.
+ var upperValue: Double { get set }
+
+ /// Value of the lower range of the data point.
+ var lowerValue: Double { get set }
+}
+
+
+
+
+
+// MARK: - Styles
+/**
+ Protocol to set the styling data for the chart.
+ */
+public protocol CTChartStyle {
+
+ /**
+ Placement of the information box that appears on touch input.
+ */
+ var infoBoxPlacement: InfoBoxPlacement { get set }
+
+ /**
+ Colour of the value part of the touch info.
+ */
+ var infoBoxValueColour: Color { get set }
+
+ /**
+ Colour of the description part of the touch info.
+ */
+ var infoBoxDescriptionColour: Color { get set }
+
+ /**
+ Colour of the background of the touch info.
+ */
+ var infoBoxBackgroundColour: Color { get set }
+
+ /**
+ Border colour of the touch info.
+ */
+ var infoBoxBorderColour: Color { get set }
+ /**
+ Border style of the touch info.
+ */
+ var infoBoxBorderStyle: StrokeStyle { get set }
+
+ /**
+ Global control of animations.
+
+ ```
+ Animation.linear(duration: 1)
+ ```
+ */
+ var globalAnimation : Animation { get set }
+}
+
+
+/**
+ A protocol to set colour styling.
+
+ Allows for single colour, gradient or gradient with stops control.
+ */
+public protocol CTColourStyle {
+
+ /**
+ Selection for the style of colour.
+ */
+ var colourType: ColourType { get set }
+
+ /// Single Colour
+ var colour: Color? { get set }
+
+ /// Array of colours for gradient
+ var colours: [Color]? { get set }
+
+ /**
+ Array of Gradient Stops.
+
+ `GradientStop` is a Hashable version of Gradient.Stop
+ */
+ var stops: [GradientStop]? { get set }
+
+ /// Start point for the gradient
+ var startPoint: UnitPoint? { get set }
+
+ /// End point for the gradient
+ var endPoint: UnitPoint? { get set }
+}
+
+public protocol CTisRanged {}
+public protocol CTnotRanged {}
diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift
new file mode 100644
index 00000000..da097e80
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift
@@ -0,0 +1,275 @@
+//
+// SharedProtocolsExtensions.swift
+//
+//
+// Created by Will Dale on 13/02/2021.
+//
+
+import SwiftUI
+
+extension CTChartData where Set: CTSingleDataSetProtocol {
+ public func isGreaterThanTwo() -> Bool {
+ return dataSets.dataPoints.count > 2
+ }
+}
+
+extension CTChartData where Set: CTMultiDataSetProtocol {
+ public func isGreaterThanTwo() -> Bool {
+ var returnValue: Bool = true
+ dataSets.dataSets.forEach { dataSet in
+ returnValue = dataSet.dataPoints.count > 2
+ }
+ return returnValue
+ }
+}
+// MARK: Touch
+extension CTChartData {
+ public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) {
+ self.infoView.isTouchCurrent = true
+ self.infoView.touchLocation = touchLocation
+ self.infoView.chartSize = chartSize
+ self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize)
+ }
+}
+
+extension CTChartData {
+
+ /**
+ Displays the data points value with the unit.
+
+ - Parameter info: A data point
+ - Returns: Text View with the value with relevent info.
+ */
+ public func infoValueUnit(info: DataPoint) -> some View {
+ switch self.infoView.touchUnit {
+ case .none:
+ return Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier))")
+ case .prefix(of: let unit):
+ return Text("\(unit) \(info.valueAsString(specifier: self.infoView.touchSpecifier))")
+ case .suffix(of: let unit):
+ return Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier)) \(unit)")
+ }
+ }
+
+ /**
+ Displays the data points value without the unit.
+
+ - Parameter info: A data point
+ - Returns: Text View with the value with relevent info.
+ */
+ public func infoValue(info: DataPoint) -> some View {
+ Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier))")
+ }
+
+ /**
+ Displays the unit.
+
+ - Parameter info: A data point
+ - Returns: Text View of the unit.
+ */
+ public func infoUnit() -> some View {
+ switch self.infoView.touchUnit {
+ case .none:
+ return Text("")
+ case .prefix(of: let unit):
+ return Text("\(unit)")
+ case .suffix(of: let unit):
+ return Text("\(unit)")
+ }
+ }
+
+ /**
+ Displays the data points description.
+
+ - Parameter info: A data point
+ - Returns: Text View with the points description.
+ */
+ public func infoDescription(info: DataPoint) -> some View {
+ Text("\(info.wrappedDescription)")
+ }
+
+ /**
+ Displays the relevent Legend for the data point.
+
+ - Parameter info: A data point
+ - Returns: A View of a Legend.
+ */
+ @ViewBuilder public func infoLegend(info: DataPoint) -> some View {
+ if let legend = self.legends.first(where: {
+ $0.prioity == 1 &&
+ $0.legend == info.legendTag
+ }) {
+ legend.getLegendAsCircle(textColor: .primary)
+ } else {
+ EmptyView()
+ }
+ }
+}
+
+extension CTChartData {
+
+ /// Sets the data point info box location while keeping it within the parent view.
+ ///
+ /// - Parameters:
+ /// - boxFrame: The size of the point info box.
+ /// - chartSize: The size of the chart view as the parent view.
+ internal func setBoxLocationation(touchLocation: CGFloat, boxFrame: CGRect, chartSize: CGRect) -> CGFloat {
+ var returnPoint : CGFloat = .zero
+ if touchLocation < chartSize.minX + (boxFrame.width / 2) {
+ returnPoint = chartSize.minX + (boxFrame.width / 2)
+ } else if touchLocation > chartSize.maxX - (boxFrame.width / 2) {
+ returnPoint = chartSize.maxX - (boxFrame.width / 2)
+ } else {
+ returnPoint = touchLocation
+ }
+ return returnPoint + self.infoView.yAxisLabelWidth
+ }
+}
+
+// MARK: - Data Set
+extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProtocol & CTnotRanged {
+ /**
+ Returns the highest value in the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Highest value in data set.
+ */
+ public func maxValue() -> Double {
+ return self.dataPoints.max { $0.value < $1.value }?.value ?? 0
+ }
+
+ /**
+ Returns the lowest value in the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Lowest value in data set.
+ */
+ public func minValue() -> Double {
+ return self.dataPoints.min { $0.value < $1.value }?.value ?? 0
+ }
+
+ /**
+ Returns the average value from the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Average of values in data set.
+ */
+ public func average() -> Double {
+ let sum = self.dataPoints.reduce(0) { $0 + $1.value }
+ return sum / Double(self.dataPoints.count)
+ }
+
+}
+extension CTSingleDataSetProtocol where Self.DataPoint: CTRangeDataPointProtocol & CTisRanged {
+ /**
+ Returns the highest value in the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Highest value in data set.
+ */
+ public func maxValue() -> Double {
+ return self.dataPoints.max { $0.upperValue < $1.upperValue }?.upperValue ?? 0
+ }
+
+ /**
+ Returns the lowest value in the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Lowest value in data set.
+ */
+ public func minValue() -> Double {
+ return self.dataPoints.min { $0.lowerValue < $1.lowerValue }?.lowerValue ?? 0
+ }
+
+ /**
+ Returns the average value from the data set.
+
+ - Parameter dataSet: Target data set.
+ - Returns: Average of values in data set.
+ */
+ public func average() -> Double {
+ let sum = self.dataPoints.reduce(0) { $0 + ($1.upperValue - $1.lowerValue) }
+ return sum / Double(self.dataPoints.count)
+ }
+
+}
+
+extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPointProtocol {
+ /**
+ Returns the highest value in the data sets
+
+ - Parameter dataSet: Target data sets.
+ - Returns: Highest value in data sets.
+ */
+ public func maxValue() -> Double {
+ var setHolder : [Double] = []
+ for set in self.dataSets {
+ setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0)
+ }
+ return setHolder.max { $0 < $1 } ?? 0
+ }
+
+ /**
+ Returns the lowest value in the data sets.
+
+ - Parameter dataSet: Target data sets.
+ - Returns: Lowest value in data sets.
+ */
+ public func minValue() -> Double {
+ var setHolder : [Double] = []
+ for set in dataSets {
+ setHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0)
+ }
+ return setHolder.min { $0 < $1 } ?? 0
+ }
+
+ /**
+ Returns the average value from the data sets.
+
+ - Parameter dataSet: Target data sets.
+ - Returns: Average of values in data sets.
+ */
+ public func average() -> Double {
+ var setHolder : [Double] = []
+ for set in dataSets {
+ let sum = set.dataPoints.reduce(0) { $0 + $1.value }
+ setHolder.append(sum / Double(set.dataPoints.count))
+ }
+ let sum = setHolder.reduce(0) { $0 + $1 }
+ return sum / Double(setHolder.count)
+ }
+}
+
+// MARK: - Data Point
+extension CTDataPointBaseProtocol {
+
+ /// Returns information about the data point for use in accessibility tags.
+ func getCellAccessibilityValue(specifier: String) -> Text {
+ Text(self.valueAsString(specifier: specifier) + ", " + self.wrappedDescription)
+ }
+}
+
+extension CTDataPointBaseProtocol {
+ /// Unwraps description
+ public var wrappedDescription : String {
+ self.description ?? ""
+ }
+}
+extension CTStandardDataPointProtocol {
+ /// Data point's value as a string
+ public func valueAsString(specifier: String) -> String {
+ String(format: specifier, self.value)
+ }
+}
+extension CTRangeDataPointProtocol {
+ /// Data point's value as a string
+ public func valueAsString(specifier: String) -> String {
+ String(format: specifier, self.lowerValue) + "-" + String(format: specifier, self.upperValue)
+ }
+}
+extension CTRangedLineDataPoint {
+ /// Data point's value as a string
+ public func valueAsString(specifier: String) -> String {
+ String(format: specifier, self.lowerValue) + "-" + String(format: specifier, self.upperValue)
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/Stroke.swift b/Sources/SwiftUICharts/Shared/Models/Stroke.swift
deleted file mode 100644
index d864e20e..00000000
--- a/Sources/SwiftUICharts/Shared/Models/Stroke.swift
+++ /dev/null
@@ -1,58 +0,0 @@
-//
-// Stroke.swift
-//
-//
-// Created by Will Dale on 14/01/2021.
-//
-
-import SwiftUI
-
-/// Replica of Apple's `StrokeStyle` that conforms to `Hashable`
-public struct Stroke: Hashable {
-
- var lineWidth : CGFloat
- var lineCap : CGLineCap
- var lineJoin : CGLineJoin
- var miterLimit : CGFloat
- var dash : [CGFloat]
- var dashPhase : CGFloat
-
- public init(lineWidth : CGFloat = 3,
- lineCap : CGLineCap = .round,
- lineJoin : CGLineJoin = .round,
- miterLimit: CGFloat = 10,
- dash : [CGFloat] = [CGFloat](),
- dashPhase : CGFloat = 0
- ) {
- self.lineWidth = lineWidth
- self.lineCap = lineCap
- self.lineJoin = lineJoin
- self.miterLimit = miterLimit
- self.dash = dash
- self.dashPhase = dashPhase
- }
-
- /// Convert `StrokeStyle` to `Stroke`
- /// - Parameter strokeStyle: StrokeStyle
- /// - Returns: Stroke
- static internal func strokeStyleToStroke(strokeStyle: StrokeStyle) -> Stroke {
- return Stroke(lineWidth : strokeStyle.lineWidth,
- lineCap : strokeStyle.lineCap,
- lineJoin : strokeStyle.lineJoin,
- miterLimit: strokeStyle.miterLimit,
- dash : strokeStyle.dash,
- dashPhase : strokeStyle.dashPhase)
- }
- /// Convert `Stroke` to `StrokeStyle`
- /// - Parameter strokeStyle: StrokeStyle
- /// - Returns: Stroke
- static internal func strokeToStrokeStyle(stroke: Stroke) -> StrokeStyle {
- return StrokeStyle(lineWidth : stroke.lineWidth,
- lineCap : stroke.lineCap,
- lineJoin : stroke.lineJoin,
- miterLimit: stroke.miterLimit,
- dash : stroke.dash,
- dashPhase : stroke.dashPhase)
- }
-
-}
diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift
deleted file mode 100644
index 494cbad3..00000000
--- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift
+++ /dev/null
@@ -1,133 +0,0 @@
-//
-// PointShape.swift
-// LineChart
-//
-// Created by Will Dale on 24/12/2020.
-//
-
-import SwiftUI
-
-internal struct Point: Shape {
-
- private let chartData : ChartData
- private let pointSize : CGFloat
- private let pointType : PointShape
- private let cornerSize : Int
-
- private let chartType : ChartType
-
- internal init(chartData : ChartData,
- pointSize : CGFloat = 2,
- pointType : PointShape,
- cornerSize: Int = 3,
- chartType : ChartType
- ) {
- self.chartData = chartData
- self.pointSize = pointSize
- self.pointType = pointType
- self.cornerSize = cornerSize
- self.chartType = chartType
- }
-
- internal func path(in rect: CGRect) -> Path {
- var path = Path()
-
- switch chartType {
- case .line:
-
- let minValue : Double
- let range : Double
-
- switch chartData.lineStyle.baseline {
- case .minimumValue:
- minValue = chartData.minValue()
- range = chartData.range()
- case .minimumWithMaximum(of: let value):
- minValue = min(chartData.minValue(), value)
- range = chartData.maxValue() - min(chartData.minValue(), value)
- case .zero:
- minValue = 0
- range = chartData.maxValue()
- }
- lineChartDrawPoints(&path, rect, minValue, range)
- case .bar:
- barChartDrawPoints(&path, rect, chartData.minValue(), chartData.maxValue())
- }
- return path
- }
-
- internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ maxValue: Double) {
-
- let x = rect.width / CGFloat(chartData.dataPoints.count)
- let y = rect.height / CGFloat(maxValue)
-
- for index in 0 ..< chartData.dataPoints.count {
-
- let pointX : CGFloat = (CGFloat(index) * x) - (pointSize / CGFloat(2)) + (x / 2)
- let pointY : CGFloat = (rect.height - (pointSize / CGFloat(2)) - CGFloat(chartData.dataPoints[index].value) * y)
-
- let point : CGRect = CGRect(x : pointX,
- y : pointY,
- width : pointSize,
- height: pointSize)
- pointSwitch(&path, point)
- }
- }
-
- internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ range: Double) {
-
- let x = rect.width / CGFloat(chartData.dataPoints.count-1)
- let y = rect.height / CGFloat(range)
-
- let firstPointX : CGFloat = (CGFloat(0) * x) - pointSize / CGFloat(2)
- let firstPointY : CGFloat = ((CGFloat(chartData.dataPoints[0].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2)
- let firstPoint : CGRect = CGRect(x : firstPointX,
- y : firstPointY,
- width : pointSize,
- height : pointSize)
- pointSwitch(&path, firstPoint)
-
- if !chartData.lineStyle.ignoreZero {
- for index in 1 ..< chartData.dataPoints.count - 1 {
- let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2)
- let pointY : CGFloat = ((CGFloat(chartData.dataPoints[index].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2)
- let point : CGRect = CGRect(x : pointX,
- y : pointY,
- width : pointSize,
- height: pointSize)
- pointSwitch(&path, point)
- }
- } else {
- for index in 1 ..< chartData.dataPoints.count - 1 {
- if chartData.dataPoints[index].value != 0 {
- let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2)
- let pointY : CGFloat = ((CGFloat(chartData.dataPoints[index].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2)
- let point : CGRect = CGRect(x : pointX,
- y : pointY,
- width : pointSize,
- height: pointSize)
- pointSwitch(&path, point)
- }
- }
- }
-
- let lastPointX : CGFloat = (CGFloat(chartData.dataPoints.count-1) * x) - pointSize / CGFloat(2)
- let lastPointY : CGFloat = ((CGFloat(chartData.dataPoints[chartData.dataPoints.count-1].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2)
- let lastPoint : CGRect = CGRect(x : lastPointX,
- y : lastPointY,
- width : pointSize,
- height : pointSize)
- pointSwitch(&path, lastPoint)
- }
-
- internal func pointSwitch(_ path: inout Path, _ point: CGRect) {
- switch pointType {
- case .circle:
- path.addEllipse(in: point)
- case .square:
- path.addRect(point)
- case .roundSquare:
- path.addRoundedRect(in: point, cornerSize: CGSize(width: cornerSize, height: cornerSize))
- }
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift
index b74f5336..67a7c0b6 100644
--- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift
+++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift
@@ -7,57 +7,147 @@
import SwiftUI
-/// Lines on the both axes (yes, apprently that is the plural of axis) meeting at a specified point.
-internal struct TouchOverlayMarker: Shape {
+/// Vertical line from top to bottom.
+internal struct Vertical: Shape {
- /// Where the marker lines come from to meet at a specified point
- private var type : MarkerLineType = .fullWidth
- /// Point that the marker lines should intersect
private var position : CGPoint
- internal init(type : MarkerLineType = .fullWidth,
- position : CGPoint
- ) {
- self.type = type
+ internal init(position : CGPoint) {
self.position = position
}
internal func path(in rect: CGRect) -> Path {
+ var verticalPath = Path()
+
+ verticalPath.move(to: CGPoint(x: position.x, y: 0))
+ verticalPath.addLine(to: CGPoint(x: position.x,
+ y: rect.height))
+ return verticalPath
+ }
+}
+
+/// Full width and height of view intersecting at a specified point.
+internal struct MarkerFull: Shape {
+
+ private var position : CGPoint
+
+ internal init(position : CGPoint) {
+ self.position = position
+ }
+
+ internal func path(in rect: CGRect) -> Path {
var combinedPaths = Path()
var horizontalPath = Path()
var verticalPath = Path()
- switch type {
- case .fullWidth:
- horizontalPath.move(to: CGPoint(x: 0, y: position.y))
- horizontalPath.addLine(to: CGPoint(x: rect.width, y: position.y))
- verticalPath.move(to: CGPoint(x: position.x, y: 0))
- verticalPath.addLine(to: CGPoint(x: position.x, y: rect.height))
- case .bottomLeading:
- horizontalPath.move(to: CGPoint(x: 0, y: position.y))
- horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- verticalPath.move(to: CGPoint(x: position.x, y: rect.height))
- verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- case .bottomTrailing:
- horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
- horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- verticalPath.move(to: CGPoint(x: position.x, y: rect.height))
- verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- case .topLeading:
- horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
- horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- verticalPath.move(to: CGPoint(x: position.x, y: 0))
- verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- case .topTrailing:
- horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
- horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- verticalPath.move(to: CGPoint(x: position.x, y: 0))
- verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
- }
+ horizontalPath.move(to: CGPoint(x: 0, y: position.y))
+ horizontalPath.addLine(to: CGPoint(x: rect.width, y: position.y))
+ verticalPath.move(to: CGPoint(x: position.x, y: 0))
+ verticalPath.addLine(to: CGPoint(x: position.x, y: rect.height))
+
combinedPaths.addPath(horizontalPath)
combinedPaths.addPath(verticalPath)
+ return combinedPaths
+ }
+}
+
+/// From bottom and leading edges meeting at a specified point.
+internal struct MarkerBottomLeading: Shape {
+
+ private var position : CGPoint
+
+ internal init(position : CGPoint) {
+ self.position = position
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+ var combinedPaths = Path()
+ var horizontalPath = Path()
+ var verticalPath = Path()
+ horizontalPath.move(to: CGPoint(x: 0, y: position.y))
+ horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+ verticalPath.move(to: CGPoint(x: position.x, y: rect.height))
+ verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+
+ combinedPaths.addPath(horizontalPath)
+ combinedPaths.addPath(verticalPath)
+ return combinedPaths
+ }
+}
+
+/// From bottom and trailing edges meeting at a specified point.
+internal struct MarkerBottomTrailing: Shape {
+
+ private var position : CGPoint
+
+ internal init(position : CGPoint) {
+ self.position = position
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+ var combinedPaths = Path()
+ var horizontalPath = Path()
+ var verticalPath = Path()
+
+ horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
+ horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+ verticalPath.move(to: CGPoint(x: position.x, y: rect.height))
+ verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+
+ combinedPaths.addPath(horizontalPath)
+ combinedPaths.addPath(verticalPath)
+ return combinedPaths
+ }
+}
+
+// From top and leading edges meeting at a specified point.
+internal struct MarkerTopLeading: Shape {
+
+ private var position : CGPoint
+
+ internal init(position : CGPoint) {
+ self.position = position
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+ var combinedPaths = Path()
+ var horizontalPath = Path()
+ var verticalPath = Path()
+
+ horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
+ horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+ verticalPath.move(to: CGPoint(x: position.x, y: 0))
+ verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+
+ combinedPaths.addPath(horizontalPath)
+ combinedPaths.addPath(verticalPath)
+ return combinedPaths
+ }
+}
+
+// From top and trailing edges meeting at a specified point.
+internal struct MarkerTopTrailing: Shape {
+
+ private var position : CGPoint
+
+ internal init(position : CGPoint) {
+ self.position = position
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+ var combinedPaths = Path()
+ var horizontalPath = Path()
+ var verticalPath = Path()
+
+ horizontalPath.move(to: CGPoint(x: rect.width, y: position.y))
+ horizontalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+ verticalPath.move(to: CGPoint(x: position.x, y: 0))
+ verticalPath.addLine(to: CGPoint(x: position.x, y: position.y))
+
+ combinedPaths.addPath(horizontalPath)
+ combinedPaths.addPath(verticalPath)
return combinedPaths
}
}
diff --git a/Sources/SwiftUICharts/Shared/Models/GradientStop.swift b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift
similarity index 84%
rename from Sources/SwiftUICharts/Shared/Models/GradientStop.swift
rename to Sources/SwiftUICharts/Shared/Types/GradientStop.swift
index c7e8e1f7..e265179a 100644
--- a/Sources/SwiftUICharts/Shared/Models/GradientStop.swift
+++ b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift
@@ -7,9 +7,11 @@
import SwiftUI
-/// A mediator for `Gradient.Stop`. to allow it to be stored in LegendData
-///
-/// Gradient.Stop doesn't conform to Hashable
+/**
+ A mediator for `Gradient.Stop` to allow it to be stored in `LegendData`.
+
+ Gradient.Stop doesn't conform to Hashable.
+ */
public struct GradientStop: Hashable {
public var color : Color
public var location: CGFloat
@@ -20,7 +22,9 @@ public struct GradientStop: Hashable {
self.color = color
self.location = location
}
-
+}
+
+extension GradientStop {
/// Convert an array of GradientStop into and array of Gradient.Stop
/// - Parameter stops: Array of GradientStop
/// - Returns: Array of Gradient.Stop
@@ -32,4 +36,3 @@ public struct GradientStop: Hashable {
return stopsArray
}
}
-
diff --git a/Sources/SwiftUICharts/Shared/Types/Stroke.swift b/Sources/SwiftUICharts/Shared/Types/Stroke.swift
new file mode 100644
index 00000000..eb562003
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Types/Stroke.swift
@@ -0,0 +1,64 @@
+//
+// Stroke.swift
+//
+//
+// Created by Will Dale on 14/01/2021.
+//
+
+import SwiftUI
+
+/**
+ A hashable version of StrokeStyle
+
+ StrokeStyle doesn't conform to Hashable.
+ */
+public struct Stroke: Hashable, Identifiable {
+
+ public let id : UUID = UUID()
+
+ private let lineWidth : CGFloat
+ private let lineCap : CGLineCap
+ private let lineJoin : CGLineJoin
+ private let miterLimit : CGFloat
+ private let dash : [CGFloat]
+ private let dashPhase : CGFloat
+
+ public init(lineWidth : CGFloat = 3,
+ lineCap : CGLineCap = .round,
+ lineJoin : CGLineJoin = .round,
+ miterLimit: CGFloat = 10,
+ dash : [CGFloat] = [CGFloat](),
+ dashPhase : CGFloat = 0
+ ) {
+ self.lineWidth = lineWidth
+ self.lineCap = lineCap
+ self.lineJoin = lineJoin
+ self.miterLimit = miterLimit
+ self.dash = dash
+ self.dashPhase = dashPhase
+ }
+}
+
+extension Stroke {
+ /// Convert `Stroke` to `StrokeStyle`
+ internal func strokeToStrokeStyle() -> StrokeStyle {
+ StrokeStyle(lineWidth : self.lineWidth,
+ lineCap : self.lineCap,
+ lineJoin : self.lineJoin,
+ miterLimit: self.miterLimit,
+ dash : self.dash,
+ dashPhase : self.dashPhase)
+ }
+}
+
+extension StrokeStyle {
+ /// Convert `StrokeStyle` to `Stroke`
+ internal func toStroke() -> Stroke {
+ Stroke(lineWidth : self.lineWidth,
+ lineCap : self.lineCap,
+ lineJoin : self.lineJoin,
+ miterLimit: self.miterLimit,
+ dash : self.dash,
+ dashPhase : self.dashPhase)
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift
deleted file mode 100644
index 6abe3ab7..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift
+++ /dev/null
@@ -1,78 +0,0 @@
-//
-// AxisDividers.swift
-// LineChart
-//
-// Created by Will Dale on 02/01/2021.
-//
-
-import SwiftUI
-
-internal struct XAxisBorder: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- @ViewBuilder
- internal func body(content: Content) -> some View {
-
- let labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top
- let labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom
-
- if labelsAndBottom {
- VStack {
- ZStack(alignment: .bottom) {
- content
- Divider()
- }
- }
- } else if labelsAndTop {
- VStack {
- ZStack(alignment: .top) {
- content
- Divider()
- }
- }
- } else {
- content
- }
- }
-}
-
-internal struct YAxisBorder: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- @ViewBuilder
- internal func body(content: Content) -> some View {
-
- let labelsAndLeading = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .leading
- let labelsAndTrailing = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .trailing
-
- if labelsAndLeading {
- HStack {
- ZStack(alignment: .leading) {
- content
- Divider()
- }
- }
- } else if labelsAndTrailing {
- HStack {
- ZStack(alignment: .trailing) {
- content
- Divider()
- }
- }
- } else {
- content
- }
- }
-}
-
-extension View {
- internal func xAxisBorder() -> some View {
- self.modifier(XAxisBorder())
- }
-
- internal func yAxisBorder() -> some View {
- self.modifier(YAxisBorder())
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift
new file mode 100644
index 00000000..a6af4aff
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift
@@ -0,0 +1,65 @@
+//
+// FloatingInfoBox.swift
+//
+//
+// Created by Will Dale on 12/03/2021.
+//
+
+import SwiftUI
+
+/**
+ A view that displays information from `TouchOverlay`.
+ */
+internal struct FloatingInfoBox: ViewModifier where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 0, height: 70)
+
+ internal func body(content: Content) -> some View {
+ Group {
+ switch chartData.chartStyle.infoBoxPlacement {
+ case .floating:
+ ZStack {
+ floating
+ content
+ }
+ case .infoBox:
+ content
+ case .header:
+ content
+ }
+ }
+ }
+
+ private var floating: some View {
+ TouchOverlayBox(chartData: chartData,
+ boxFrame : $boxFrame)
+ .position(x: chartData.setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x,
+ boxFrame : boxFrame,
+ chartSize : chartData.infoView.chartSize),
+ y: boxFrame.midY - 10)
+ .padding(.horizontal, 6)
+ .zIndex(1)
+ }
+}
+
+extension View {
+ /**
+ A view that displays information from `TouchOverlay`.
+
+ Places the info box on top of the chart.
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with a view to
+ display touch overlay information.
+ */
+ public func floatingInfoBox(chartData: T) -> some View {
+ self.modifier(FloatingInfoBox(chartData: chartData))
+ }
+}
+
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift
index d5308657..27ab78c6 100644
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift
+++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift
@@ -6,42 +6,33 @@
//
import SwiftUI
-
-internal struct HeaderBox: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
+
+/**
+ Displays the metadata about the chart as well as optionally touch overlay information.
+ */
+internal struct HeaderBox: ViewModifier where T: CTChartData {
- let showTitle : Bool
- let showSubtitle: Bool
+ @ObservedObject var chartData: T
- init(showTitle : Bool = true,
- showSubtitle : Bool = true
- ) {
- self.showTitle = showTitle
- self.showSubtitle = showSubtitle
+ init(chartData: T) {
+ self.chartData = chartData
}
var titleBox: some View {
VStack(alignment: .leading) {
- if showTitle, let title = chartData.metadata?.title {
- Text(title)
- .font(.title3)
- } else {
- Text("")
- .font(.title3)
- }
- if showSubtitle, let subtitle = chartData.metadata?.subtitle {
- Text(subtitle)
- .font(.subheadline)
- } else {
- Text("")
- .font(.subheadline)
- }
+ Text(chartData.metadata.title)
+ .font(.title3)
+ .foregroundColor(chartData.metadata.titleColour)
+
+ Text(chartData.metadata.subtitle)
+ .font(.subheadline)
+ .foregroundColor(chartData.metadata.subtitleColour)
}
}
-
var touchOverlay: some View {
+
VStack(alignment: .trailing) {
+<<<<<<< HEAD
if chartData.viewData.isTouchCurrent,
let value = chartData.viewData.touchOverlayInfo?.value {
@@ -69,53 +60,89 @@ internal struct HeaderBox: ViewModifier {
Text("\(label)")
.font(.subheadline)
} else {
+=======
+ if chartData.infoView.isTouchCurrent {
+ ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in
+
+ chartData.infoValueUnit(info: point)
+ .font(.title3)
+ .foregroundColor(chartData.chartStyle.infoBoxValueColour)
+
+ chartData.infoDescription(info: point)
+ .font(.subheadline)
+ .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour)
+
+ }
+ } else {
+ Text("")
+ .font(.title3)
+>>>>>>> version-2
Text("")
.font(.subheadline)
}
}
}
- @ViewBuilder
+
internal func body(content: Content) -> some View {
- if chartData.dataPoints.count > 2 {
+ Group {
#if !os(tvOS)
- if chartData.chartStyle.infoBoxPlacement == .floating {
- VStack(alignment: .leading) {
- titleBox
- content
- }
- } else if chartData.chartStyle.infoBoxPlacement == .header {
- VStack(alignment: .leading) {
- HStack(spacing: 0) {
- HStack(spacing: 0) {
- titleBox
- Spacer()
- }
- .frame(minWidth: 0, maxWidth: .infinity)
- Spacer()
+ if chartData.isGreaterThanTwo() {
+ switch chartData.chartStyle.infoBoxPlacement {
+ case .floating:
+ VStack(alignment: .leading) {
+ titleBox
+ content
+ }
+ case .infoBox:
+ VStack(alignment: .leading) {
+ titleBox
+ content
+ }
+ case .header:
+ VStack(alignment: .leading) {
HStack(spacing: 0) {
+ HStack(spacing: 0) {
+ titleBox
+ Spacer()
+ }
+ .frame(minWidth: 0, maxWidth: .infinity)
Spacer()
- touchOverlay
+ HStack(spacing: 0) {
+ Spacer()
+ touchOverlay
+ }
+ .frame(minWidth: 0, maxWidth: .infinity)
}
- .frame(minWidth: 0, maxWidth: .infinity)
+ content
}
- content
}
- }
+ } else { content }
#elseif os(tvOS)
- VStack(alignment: .leading) {
- titleBox
- content
- }
+ if chartData.isGreaterThanTwo() {
+ VStack(alignment: .leading) {
+ titleBox
+ content
+ }
+ } else { content }
#endif
- } else { content }
+ }
}
}
extension View {
- /// Displays the metadata about the chart
- /// - Returns: Chart title and subtitle.
- public func headerBox() -> some View {
- self.modifier(HeaderBox())
+ /**
+ Displays the metadata about the chart.
+
+ Adds a view above the chart that displays the title and subtitle.
+ If infoBoxPlacement is set to .header then the datapoint info will
+ be displayed here as well.
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with a view above
+ to display metadata.
+ */
+ public func headerBox(chartData: T) -> some View {
+ self.modifier(HeaderBox(chartData: chartData))
}
}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift
new file mode 100644
index 00000000..8ee1e6a4
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift
@@ -0,0 +1,83 @@
+//
+// InfoBox.swift
+//
+//
+// Created by Will Dale on 15/02/2021.
+//
+
+import SwiftUI
+
+/**
+ A view that displays information from `TouchOverlay`.
+ */
+internal struct InfoBox: ViewModifier where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 0, height: 70)
+
+ internal func body(content: Content) -> some View {
+ Group {
+ switch chartData.chartStyle.infoBoxPlacement {
+ case .floating:
+ content
+ case .infoBox(let isStatic):
+ switch isStatic {
+ case true:
+ VStack {
+ fixed
+ content
+ }
+ case false:
+ VStack {
+ floating
+ content
+ }
+ }
+ case .header:
+ content
+ }
+ }
+ }
+
+ private var floating: some View {
+ TouchOverlayBox(chartData: chartData,
+ boxFrame : $boxFrame)
+ .position(x: chartData.setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x,
+ boxFrame : boxFrame,
+ chartSize : chartData.infoView.chartSize),
+ y: 35)
+ .frame(height: 70)
+ .padding(.horizontal, 6)
+ .zIndex(1)
+ }
+
+
+ private var fixed: some View {
+ TouchOverlayBox(chartData: chartData,
+ boxFrame : $boxFrame)
+ .frame(height: 70)
+ .padding(.horizontal, 6)
+ .zIndex(1)
+ }
+
+
+
+}
+
+extension View {
+ /**
+ A view that displays information from `TouchOverlay`.
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with a view to
+ display touch overlay information.
+ */
+ public func infoBox(chartData: T) -> some View {
+ self.modifier(InfoBox(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift
index 5df61044..026195f7 100644
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift
+++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift
@@ -7,21 +7,49 @@
import SwiftUI
-internal struct Legends: ViewModifier {
+/**
+ Displays legends under the chart.
+ */
+internal struct Legends: ViewModifier where T: CTChartData {
- @EnvironmentObject var chartData: ChartData
+ @ObservedObject var chartData: T
+ private let columns : [GridItem]
+ private let textColor : Color
+
+ init(chartData: T,
+ columns : [GridItem],
+ textColor: Color
+ ) {
+ self.chartData = chartData
+ self.columns = columns
+ self.textColor = textColor
+ }
internal func body(content: Content) -> some View {
- VStack {
- content
- LegendView(chartData: chartData)
+ Group {
+ if chartData.isGreaterThanTwo() {
+ VStack {
+ content
+ LegendView(chartData: chartData, columns: columns, textColor: textColor)
+
+ }
+ } else { content }
}
}
}
+
extension View {
- /// Displays legends under the chart.
- /// - Returns: Legends from the charts data and any markers.
- public func legends() -> some View {
- self.modifier(Legends())
+ /**
+ Displays legends under the chart.
+
+ - Parameters:
+ - chartData: Chart data model.
+ - columns: How to layout the legends.
+ - textColor: Colour of the text.
+ - Returns: A new view containing the chart with chart legends under.
+ */
+ public func legends(chartData: T, columns: [GridItem] = [GridItem(.flexible())], textColor: Color = Color.primary) -> some View {
+ self.modifier(Legends(chartData: chartData, columns: columns, textColor: textColor))
}
}
+
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift
deleted file mode 100644
index 70dd6bed..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-//
-// LineChartPoints.swift
-// LineChart
-//
-// Created by Will Dale on 24/12/2020.
-//
-
-import SwiftUI
-
-internal struct PointMarkers: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- internal func body(content: Content) -> some View {
-
- let pointStyle = chartData.pointStyle
- return ZStack {
- content
- if chartData.dataPoints.count > 2 {
- switch pointStyle.pointType {
- case .filled:
- Point(chartData: chartData, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType)
- .fill(pointStyle.fillColour)
- case .outline:
- Point(chartData: chartData, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType)
- .stroke(pointStyle.borderColour, lineWidth: pointStyle.lineWidth)
- case .filledOutLine:
- Point(chartData: chartData, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType)
- .stroke(pointStyle.borderColour, lineWidth: pointStyle.lineWidth)
- .background(Point(chartData: chartData,
- pointSize: pointStyle.pointSize,
- pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType)
- .foregroundColor(pointStyle.fillColour)
- )
- }
- }
- }
- }
-}
-extension View {
- /// Lays out markers over each of the data point.
- ///
- /// The style of the markers is set in the PointStyle data model as parameter in ChartData
- public func pointMarkers() -> some View {
- self.modifier(PointMarkers())
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift
index 043e45d8..7250fc7a 100644
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift
+++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift
@@ -8,6 +8,7 @@
import SwiftUI
#if !os(tvOS)
+<<<<<<< HEAD
/// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information.
internal struct TouchOverlay: ViewModifier {
@@ -40,27 +41,37 @@ internal struct TouchOverlay: ViewModifier {
internal init(specifier: String, units: Units) {
self.specifier = specifier
self.units = units
+=======
+/**
+ Finds the nearest data point and displays the relevent information.
+ */
+internal struct TouchOverlay: ViewModifier where T: CTChartData {
+
+ @ObservedObject var chartData: T
+
+ internal init(chartData : T,
+ specifier : String,
+ unit : TouchUnit
+ ) {
+ self.chartData = chartData
+ self.chartData.infoView.touchSpecifier = specifier
+ self.chartData.infoView.touchUnit = unit
+>>>>>>> version-2
}
-
- @ViewBuilder internal func body(content: Content) -> some View {
- if chartData.dataPoints.count > 2 {
- GeometryReader { geo in
- ZStack {
- content
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { (value) in
- touchLocation = value.location
- isTouchCurrent = true
-
- switch chartData.viewData.chartType {
- case .line:
- getPointLocationLineChart(touchLocation: touchLocation, chartSize: geo)
- getDataPointLineChart(touchLocation: touchLocation, chartSize: geo)
- case .bar:
- getPointLocationBarChart(touchLocation: touchLocation, chartSize: geo)
- getDataPointBarChart(touchLocation: touchLocation, chartSize: geo)
+
+ internal func body(content: Content) -> some View {
+ Group {
+ if chartData.isGreaterThanTwo() {
+ GeometryReader { geo in
+ ZStack {
+ content
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { (value) in
+ chartData.setTouchInteraction(touchLocation: value.location,
+ chartSize: geo.frame(in: .local))
}
+<<<<<<< HEAD
if chartData.chartStyle.infoBoxPlacement == .floating {
setBoxLocationation(boxFrame: boxFrame, chartSize: geo)
@@ -220,6 +231,20 @@ internal struct TouchOverlay: ViewModifier {
return chartSize.frame(in: .local).maxY
} else {
return touchLocation.y
+=======
+ .onEnded { _ in
+ chartData.infoView.isTouchCurrent = false
+ chartData.infoView.touchOverlayInfo = []
+ }
+ )
+ if chartData.infoView.isTouchCurrent {
+ chartData.getTouchInteraction(touchLocation: chartData.infoView.touchLocation,
+ chartSize: geo.frame(in: .local))
+ }
+ }
+ }
+ } else { content }
+>>>>>>> version-2
}
}
}
@@ -227,6 +252,7 @@ internal struct TouchOverlay: ViewModifier {
extension View {
#if !os(tvOS)
+<<<<<<< HEAD
/// Adds an overlay to detect touch and display the relivent information from the nearest data point.
/// - Parameter specifier: Decimal precision for labels
public func touchOverlay(specifier: String = "%.0f", units: Units = .none) -> some View {
@@ -234,8 +260,52 @@ extension View {
}
#elseif os(tvOS)
public func touchOverlay(specifier: String = "%.0f", units: Units = .none) -> some View {
+=======
+ /**
+ Adds touch interaction with the chart.
+
+ Adds an overlay to detect touch and display the relivent information from the nearest data point.
+
+ - Requires:
+ If ChartStyle --> infoBoxPlacement is set to .header
+ then `.headerBox` is required.
+
+ If ChartStyle --> infoBoxPlacement is set to .infoBox
+ then `.infoBox` is required.
+
+ If ChartStyle --> infoBoxPlacement is set to .floating
+ then `.floatingInfoBox` is required.
+
+ - Attention:
+ Unavailable in tvOS
+
+ - Parameters:
+ - chartData: Chart data model.
+ - specifier: Decimal precision for labels.
+ - unit: Unit to put before or after the value.
+ - Returns: A new view containing the chart with a touch overlay.
+ */
+ public func touchOverlay(chartData: T,
+ specifier: String = "%.0f",
+ unit : TouchUnit = .none
+ ) -> some View {
+ self.modifier(TouchOverlay(chartData: chartData,
+ specifier: specifier,
+ unit : unit))
+ }
+ #elseif os(tvOS)
+ /**
+ Adds touch interaction with the chart.
+
+ - Attention:
+ Unavailable in tvOS
+ */
+ public func touchOverlay(chartData: T,
+ specifier: String = "%.0f",
+ unit : TouchUnit = .none
+ ) -> some View {
+>>>>>>> version-2
self.modifier(EmptyModifier())
}
#endif
-
}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift
deleted file mode 100644
index aca63648..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift
+++ /dev/null
@@ -1,77 +0,0 @@
-//
-// XAxisGrid.swift
-// LineChart
-//
-// Created by Will Dale on 26/12/2020.
-//
-
-import SwiftUI
-
-internal struct XAxisGrid: ViewModifier {
-
- @EnvironmentObject var chartData : ChartData
-
- internal func body(content: Content) -> some View {
- ZStack {
- if chartData.dataPoints.count > 2 {
- HStack {
- ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines), id: \.self) { index in
- if index != 0 {
- VerticalGridView(chartData: chartData)
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- }
- }
- VerticalGridView(chartData: chartData)
- }
- }
- content
- }
- }
-}
-
-extension View {
- /**
- Adds vertical lines along the X axis.
- */
- public func xAxisGrid() -> some View {
- self.modifier(XAxisGrid())
- }
-}
-
-
-internal struct VerticalGridView: View {
-
- var chartData : ChartData
-
- @State var startAnimation : Bool = false
-
- var body: some View {
- VerticalGridShape()
- .trim(to: startAnimation ? 1 : 0)
- .stroke(chartData.chartStyle.xAxisGridStyle.lineColour,
- style: StrokeStyle(lineWidth: chartData.chartStyle.xAxisGridStyle.lineWidth,
- dash : chartData.chartStyle.xAxisGridStyle.dash,
- dashPhase: chartData.chartStyle.xAxisGridStyle.dashPhase))
- .frame(width: chartData.chartStyle.xAxisGridStyle.lineWidth)
- .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
- self.startAnimation = true
- }
- .animateOnDisAppear(using: chartData.chartStyle.globalAnimation) {
- self.startAnimation = false
- }
- }
-}
-internal struct VerticalGridShape: Shape {
-
- internal func path(in rect: CGRect) -> Path {
-
- var path = Path()
-
- path.move(to: CGPoint(x: 0, y: rect.height))
- path.addLine(to: CGPoint(x: 0, y: 0))
-
- return path
- }
-
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift
deleted file mode 100644
index 74af66ce..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift
+++ /dev/null
@@ -1,133 +0,0 @@
-//
-// XAxisLabels.swift
-// LineChart
-//
-// Created by Will Dale on 26/12/2020.
-//
-
-import SwiftUI
-
-internal struct XAxisLabels: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- @ViewBuilder
- internal var labels: some View {
-
- switch chartData.chartStyle.xAxisLabelsFrom {
- case .dataPoint:
- // ChartData -> DataPoints -> xAxisLabel
- switch chartData.viewData.chartType {
- case .line:
- HStack(spacing: 0) {
- ForEach(chartData.dataPoints, id: \.self) { data in
- if let label = data.xAxisLabel {
- Text(label)
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.75)
- }
- if data != chartData.dataPoints[chartData.dataPoints.count - 1] {
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- }
- }
- }
- .padding(.horizontal, -4)
- .onAppear {
- chartData.viewData.hasXAxisLabels = true
- }
-
- case .bar:
- HStack(spacing: 0) {
- ForEach(chartData.dataPoints, id: \.self) { data in
- if let label = data.xAxisLabel {
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
-
- Text(label)
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.75)
-
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- }
- }
- }
- .onAppear {
- chartData.viewData.hasXAxisLabels = true
- }
- }
-
-
-
- case .chartData:
- switch chartData.viewData.chartType {
- case .line:
- // ChartData -> xAxisLabels
- if let labelArray = chartData.xAxisLabels {
- HStack(spacing: 0) {
- ForEach(labelArray, id: \.self) { data in
- Text(data)
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.5)
- if data != labelArray[labelArray.count - 1] {
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- }
- }
- }
- .padding(.horizontal, -4)
- .onAppear {
- chartData.viewData.hasXAxisLabels = true
- }
- }
- case .bar:
- if let labelArray = chartData.xAxisLabels {
- HStack(spacing: 0) {
- ForEach(labelArray, id: \.self) { data in
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- Text(data)
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.5)
- Spacer()
- .frame(minWidth: 0, maxWidth: 500)
- }
- }
- .onAppear {
- chartData.viewData.hasXAxisLabels = true
- }
- }
- }
- }
- }
-
- @ViewBuilder
- internal func body(content: Content) -> some View {
- switch chartData.chartStyle.xAxisLabelPosition {
- case .top:
- VStack {
- labels
- content
- }
-
- case .bottom:
- VStack {
- content
- labels
- }
-
- }
- }
-}
-
-extension View {
- /// Labels for the X axis.
- public func xAxisLabels() -> some View {
- self.modifier(XAxisLabels())
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift
deleted file mode 100644
index 832ed0cb..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-//
-// YAxisGrid.swift
-// LineChart
-//
-// Created by Will Dale on 24/12/2020.
-//
-
-import SwiftUI
-
-internal struct YAxisGrid: ViewModifier {
-
- @EnvironmentObject var chartData : ChartData
-
- internal func body(content: Content) -> some View {
- ZStack {
- if chartData.dataPoints.count > 2 {
- VStack {
- ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in
- if index != 0 {
-
- HorizontalGridView(chartData: chartData)
-
- Spacer()
- .frame(minHeight: 0, maxHeight: 500)
- }
- }
- HorizontalGridView(chartData: chartData)
- }
- }
- content
- }
- }
-}
-
-extension View {
- /**
- Adds horizontal lines along the Y axis.
- - Parameter numberOfLines: Number of lines subdividing the chart
- - Returns: View of evenly spaced horizontal lines
- */
- public func yAxisGrid() -> some View {
- self.modifier(YAxisGrid())
- }
-}
-
-
-internal struct HorizontalGridView: View {
-
- var chartData : ChartData
-
- @State var startAnimation : Bool = false
-
- var body: some View {
- HorizontalGridShape()
- .trim(to: startAnimation ? 1 : 0)
- .stroke(chartData.chartStyle.yAxisGridStyle.lineColour,
- style: StrokeStyle(lineWidth: chartData.chartStyle.yAxisGridStyle.lineWidth,
- dash : chartData.chartStyle.yAxisGridStyle.dash,
- dashPhase: chartData.chartStyle.yAxisGridStyle.dashPhase))
- .frame(height: chartData.chartStyle.yAxisGridStyle.lineWidth)
- .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
- self.startAnimation = true
- }
- .animateOnDisAppear(using: chartData.chartStyle.globalAnimation) {
- self.startAnimation = false
- }
-
- }
-}
-internal struct HorizontalGridShape: Shape {
-
- internal func path(in rect: CGRect) -> Path {
-
- var path = Path()
-
- path.move(to: CGPoint(x: 0, y: 0))
- path.addLine(to: CGPoint(x: rect.width, y: 0))
-
- return path
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift
deleted file mode 100644
index a03e4d35..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift
+++ /dev/null
@@ -1,133 +0,0 @@
-//
-// YAxisLabels.swift
-// LineChart
-//
-// Created by Will Dale on 24/12/2020.
-//
-
-import SwiftUI
-
-internal struct YAxisLabels: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- let specifier : String
- var labelsArray : [Double] { getLabels() }
-
- internal init(specifier: String) {
- self.specifier = specifier
- }
-
- internal var labels: some View {
- let labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top
- let labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom
- let numberOfLabels = chartData.chartStyle.yAxisNumberOfLabels
-
- return VStack {
- if labelsAndTop {
- Text("")
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.5)
- Spacer()
- .frame(minHeight: 0, maxHeight: 500)
- }
- ForEach((0...numberOfLabels).reversed(), id: \.self) { i in
- Text("\(labelsArray[i], specifier: specifier)")
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.5)
- if i != 0 {
- Spacer()
- .frame(minHeight: 0, maxHeight: 500)
- }
- }
- if labelsAndBottom {
- Text("")
- .font(.caption)
- .lineLimit(1)
- .minimumScaleFactor(0.5)
- }
- }
- .if(labelsAndBottom) { $0.padding(.top, -8) }
- .if(labelsAndTop) { $0.padding(.bottom, -8) }
- .onAppear {
- chartData.viewData.hasYAxisLabels = true
- }
- }
-
- @ViewBuilder
- internal func body(content: Content) -> some View {
- switch chartData.chartStyle.yAxisLabelPosition {
- case .leading:
- HStack {
- if chartData.dataPoints.count > 2 {
- labels
- }
- content
- }
- case .trailing:
- HStack {
- content
- if chartData.dataPoints.count > 2 {
- labels
- }
- }
- }
- }
-
- internal func getLabels() -> [Double] {
- let numberOfLabels = chartData.chartStyle.yAxisNumberOfLabels
- switch chartData.viewData.chartType {
- case .line:
- return self.getYLabelsLineChart(numberOfLabels)
- case .bar:
- return self.getYLabelsBarChart(numberOfLabels)
- }
- }
-
- internal func getYLabelsLineChart(_ numberOfLabels: Int) -> [Double] {
-
- let minValue : Double
- let dataRange: Double
-
- switch chartData.lineStyle.baseline {
- case .minimumValue:
- minValue = chartData.minValue()
- dataRange = chartData.range()
- case .minimumWithMaximum(of: let value):
- minValue = min(chartData.minValue(), value)
- dataRange = chartData.maxValue() - min(chartData.minValue(), value)
- case .zero:
- minValue = 0
- dataRange = chartData.maxValue()
- }
-
- var labels : [Double] = [Double]()
- let labelRange : Double = dataRange / Double(numberOfLabels)
- labels.append(minValue)
- for index in 1...numberOfLabels {
- labels.append(minValue + labelRange * Double(index))
- }
- return labels
- }
- internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] {
- var labels : [Double] = [Double]()
- for index in 0...numberOfLabels {
- labels.append(chartData.maxValue() / Double(numberOfLabels) * Double(index))
- }
- return labels
- }
-}
-
-extension View {
- /**
- Automatically generated labels for the Y axis
- - Parameters:
- - specifier: Decimal precision specifier
- - Returns: HStack of labels
- */
- public func yAxisLabels(specifier: String = "%.0f") -> some View {
- self.modifier(YAxisLabels(specifier: specifier))
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift
deleted file mode 100644
index be443d73..00000000
--- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift
+++ /dev/null
@@ -1,214 +0,0 @@
-//
-// YAxisPOI.swift
-// LineChart
-//
-// Created by Will Dale on 31/12/2020.
-//
-
-import SwiftUI
-
-/// Configurable Point of interest
-///
-/// This is a mess - tidied up in V2
-internal struct YAxisPOI: ViewModifier {
-
- @EnvironmentObject var chartData: ChartData
-
- private let markerName : String
- private let markerValue : Double
- private let lineColour : Color
- private let strokeStyle : StrokeStyle
- private let labelPosition : DisplayValue
- private let labelBackground: Color
- private let isAverage : Bool
-
- internal init(markerName : String,
- markerValue : Double,
- labelPosition : DisplayValue,
- labelBackground: Color,
- lineColour : Color,
- strokeStyle : StrokeStyle,
- isAverage : Bool
- ) {
- self.markerName = markerName
- self.markerValue = markerValue
- self.labelPosition = labelPosition
- self.labelBackground = labelBackground
- self.lineColour = lineColour
- self.strokeStyle = strokeStyle
- self.isAverage = isAverage
- }
-
- internal func body(content: Content) -> some View {
- ZStack {
- content
- if chartData.dataPoints.count > 2 {
-
- ZStack {
-
- Marker(chartData : chartData,
- markerValue : markerValue,
- isAverage : isAverage,
- chartType : chartData.viewData.chartType)
-
- .stroke(lineColour, style: strokeStyle)
- .onAppear {
- if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice
- chartData.legends.append(LegendData(legend : markerName,
- colour : lineColour,
- strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle),
- prioity : 2,
- chartType : .line))
- }
- }
- ValuePositionView(chartData : chartData,
- markerValue : markerValue,
- lineColour : lineColour,
- strokeStyle : strokeStyle,
- labelPosition : labelPosition,
- labelBackground: labelBackground,
- isAverage : isAverage)
- }
- }
- }
- }
-}
-
-internal struct ValuePositionView: View {
-
- private let chartData : ChartData
- private let minValue : Double
- private let range : Double
-
- private let markerValue : Double
- private let lineColour : Color
- private let strokeStyle : StrokeStyle
-
- private let labelPosition : DisplayValue
- private let labelBackground : Color
-
- private let isAverage : Bool
-
- internal init(chartData : ChartData,
- markerValue : Double,
- lineColour : Color,
- strokeStyle : StrokeStyle,
- labelPosition : DisplayValue,
- labelBackground : Color,
- isAverage : Bool
- ) {
- self.chartData = chartData
- self.markerValue = markerValue
- self.lineColour = lineColour
- self.strokeStyle = strokeStyle
- self.labelPosition = labelPosition
- self.labelBackground = labelBackground
- self.isAverage = isAverage
-
- switch chartData.lineStyle.baseline {
- case .minimumValue:
- self.minValue = chartData.minValue()
- self.range = chartData.range()
- case .minimumWithMaximum(of: let value):
- self.minValue = min(chartData.minValue(), value)
- self.range = chartData.maxValue() - min(chartData.minValue(), value)
- case .zero:
- self.minValue = 0
- self.range = chartData.maxValue()
- }
- }
-
- var body: some View {
-
- GeometryReader { geo in
-
- let value : Double = isAverage ? chartData.average() : markerValue
-
- let y = geo.size.height / CGFloat(range)
- let pointY = (CGFloat(value - minValue) * -y) + geo.size.height
-
- switch labelPosition {
- case .none:
-
- EmptyView()
-
- case .yAxis(specifier: let specifier):
-
- Text("\(value, specifier: specifier)")
- .padding(4)
- .background(labelBackground)
- .font(.caption)
- .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: {
- $0.position(x: -18,
- y: pointY)
- }, else: {
- $0.position(x: geo.size.width + 18,
- y: pointY)
- })
-
- case .center(specifier: let specifier):
-
- Text("\(value, specifier: specifier)")
- .font(.caption)
- .padding()
- .background(labelBackground)
- .clipShape(DiamondShape())
- .overlay(DiamondShape()
- .stroke(lineColour, style: strokeStyle)
- )
- .position(x: geo.size.width / 2, y: pointY)
- }
- }
- }
-}
-
-extension View {
- /// Shows a marker line at chosen point.
- /// - Parameters:
- /// - markerName: Title of marker, for the legend
- /// - markerValue : Chosen point.
- /// - labelPosition: Option to add a label inline with the marker.
- /// - labelBackground: Background colour for optional label.
- /// - lineColour: Line Colour
- /// - strokeStyle: Style of Stroke
- /// - Returns: A marker line at the average of all the data points.
- public func yAxisPOI(markerName : String,
- markerValue : Double,
- labelPosition : DisplayValue = .none,
- labelBackground: Color = Color.clear,
- lineColour : Color = Color(.blue),
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0)
- ) -> some View {
- self.modifier(YAxisPOI(markerName : markerName,
- markerValue : markerValue,
- labelPosition: labelPosition,
- labelBackground: labelBackground,
- lineColour : lineColour,
- strokeStyle : strokeStyle,
- isAverage : false))
- }
-
-
- /// Shows a marker line at the average of all the data points.
- /// - Parameters:
- /// - markerName: Title of marker, for the legend.
- /// - labelPosition: Option to add a label inline with the marker.
- /// - labelBackground: Background colour for optional label.
- /// - lineColour: Line Colour
- /// - strokeStyle: Style of Stroke
- /// - Returns: A marker line at the average of all the data points.
- public func averageLine(markerName : String = "Average",
- labelPosition : DisplayValue = .none,
- labelBackground : Color = Color.clear,
- lineColour : Color = Color.primary,
- strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0)
- ) -> some View {
- self.modifier(YAxisPOI(markerName : markerName,
- markerValue : 0,
- labelPosition: labelPosition,
- labelBackground: labelBackground,
- lineColour : lineColour,
- strokeStyle : strokeStyle,
- isAverage : true))
- }
-}
diff --git a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift
index 4cf8331d..bfae4c4f 100644
--- a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift
+++ b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift
@@ -7,11 +7,14 @@
import SwiftUI
-public struct CustomNoDataView: View {
+/**
+ View to display text if there is not enough data to draw the chart.
+ */
+public struct CustomNoDataView: View where T: CTChartData {
- let chartData : ChartData
+ let chartData : T
- init(chartData: ChartData) {
+ init(chartData: T) {
self.chartData = chartData
}
diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift
index 235780c0..434172fc 100644
--- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift
+++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift
@@ -7,104 +7,69 @@
import SwiftUI
-internal struct LegendView: View {
+/**
+ Sub view to setup and display the legends.
+ */
+internal struct LegendView: View where T: CTChartData {
- @ObservedObject var chartData : ChartData
-
- internal init(chartData: ChartData) {
+ @ObservedObject var chartData : T
+ private let columns : [GridItem]
+ private let textColor : Color
+
+ internal init(chartData: T,
+ columns : [GridItem],
+ textColor: Color
+ ) {
self.chartData = chartData
+ self.columns = columns
+ self.textColor = textColor
}
- // Expose to API ??
- let columns = [
-// GridItem(.flexible()),
- GridItem(.flexible())
- //geo.size.width / CGFloat(columns.count) / 2
- ]
-
internal var body: some View {
LazyVGrid(columns: columns, alignment: .leading) {
- ForEach(chartData.legendOrder(), id: \.self) { legend in
-
- switch legend.chartType {
+ ForEach(chartData.legends, id: \.id) { legend in
- case .line:
- if let stroke = legend.strokeStyle {
- let strokeStyle = Stroke.strokeToStrokeStyle(stroke: stroke)
- if let colour = legend.colour {
- HStack {
- LegendLine(width: 40)
- .stroke(colour, style: strokeStyle)
- .frame(width: 40, height: 3)
- Text(legend.legend)
- .font(.caption)
- }
- } else if let colours = legend.colours {
- HStack {
- LegendLine(width: 40)
- .stroke(LinearGradient(gradient: Gradient(colors: colours),
- startPoint: .leading,
- endPoint: .trailing),
- style: strokeStyle)
- .frame(width: 40, height: 3)
- Text(legend.legend)
- .font(.caption)
- }
- } else if let stops = legend.stops {
- let stops = GradientStop.convertToGradientStopsArray(stops: stops)
- HStack {
- LegendLine(width: 40)
- .stroke(LinearGradient(gradient: Gradient(stops: stops),
- startPoint: .leading,
- endPoint: .trailing),
- style: strokeStyle)
- .frame(width: 40, height: 3)
- Text(legend.legend)
- .font(.caption)
- }
- }
- }
- case .bar:
- if let colour = legend.colour
- {
- HStack {
- Rectangle()
- .fill(colour)
- .frame(width: 20, height: 20)
- Text(legend.legend)
- .font(.caption)
- }
- } else if let colours = legend.colours,
- let startPoint = legend.startPoint,
- let endPoint = legend.endPoint
- {
- HStack {
- Rectangle()
- .fill(LinearGradient(gradient: Gradient(colors: colours),
- startPoint: startPoint,
- endPoint: endPoint))
- .frame(width: 20, height: 20)
- Text(legend.legend)
- .font(.caption)
- }
- } else if let stops = legend.stops,
- let startPoint = legend.startPoint,
- let endPoint = legend.endPoint
- {
- let stops = GradientStop.convertToGradientStopsArray(stops: stops)
- HStack {
- Rectangle()
- .fill(LinearGradient(gradient: Gradient(stops: stops),
- startPoint: startPoint,
- endPoint: endPoint))
- .frame(width: 20, height: 20)
- Text(legend.legend)
- .font(.caption)
- }
- }
- }
+ legend.getLegend(textColor: textColor)
+ .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) }
+ .if(scaleLegendPie(legend: legend)) {$0.scaleEffect(1.2, anchor: .leading) }
+
+ .accessibilityLabel(Text(legend.accessibilityLegendLabel()))
+ .accessibilityValue(Text("\(legend.legend)"))
+ }
+ }
+ }
+
+ /// Detects whether to run the scale effect on the legend.
+ private func scaleLegendBar(legend: LegendData) -> Bool {
+
+ if chartData is BarChartData {
+ if let datapointID = chartData.infoView.touchOverlayInfo.first?.id as? UUID {
+ return chartData.infoView.isTouchCurrent && legend.id == datapointID
+ } else {
+ return false
+ }
+ } else if chartData is GroupedBarChartData || chartData is StackedBarChartData {
+ if let datapoint = chartData.infoView.touchOverlayInfo.first as? MultiBarChartDataPoint {
+ return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour
+ } else {
+ return false
}
+ } else {
+ return false
}
}
+ /// Detects whether to run the scale effect on the legend.
+ private func scaleLegendPie(legend: LegendData) -> Bool {
+
+ if chartData is PieChartData || chartData is DoughnutChartData {
+ if let datapointID = chartData.infoView.touchOverlayInfo.first?.id as? UUID {
+ return chartData.infoView.isTouchCurrent && legend.id == datapointID
+ } else {
+ return false
+ }
+ } else {
+ return false
+ }
+ }
}
diff --git a/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift
new file mode 100644
index 00000000..23bb8e7c
--- /dev/null
+++ b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift
@@ -0,0 +1,63 @@
+//
+// PosistionIndicator.swift
+//
+//
+// Created by Will Dale on 19/02/2021.
+//
+
+import SwiftUI
+
+/**
+ A dot that follows the line on touch events.
+ */
+internal struct PosistionIndicator: View {
+
+ private let fillColour : Color
+ private let lineColour : Color
+ private let lineWidth : CGFloat
+
+ internal init(fillColour : Color = Color.primary,
+ lineColour : Color = Color.blue,
+ lineWidth : CGFloat = 3
+ ) {
+ self.fillColour = fillColour
+ self.lineColour = lineColour
+ self.lineWidth = lineWidth
+ }
+
+ internal var body: some View {
+ Circle()
+ .fill(fillColour)
+ .overlay(Circle()
+ .strokeBorder(lineColour, lineWidth: lineWidth)
+ )
+ }
+}
+
+/**
+ Styling of the dot that follows the line on touch events.
+ */
+public struct DotStyle {
+
+ let size : CGFloat
+ let fillColour : Color
+ let lineColour : Color
+ let lineWidth : CGFloat
+
+ /// Sets the style of the Posistion Indicator
+ /// - Parameters:
+ /// - size: Size of the Indicator.
+ /// - fillColour: Fill colour.
+ /// - lineColour: Border colour.
+ /// - lineWidth: Border width.
+ public init(size : CGFloat = 15,
+ fillColour : Color = Color.primary,
+ lineColour : Color = Color.blue,
+ lineWidth : CGFloat = 3
+ ) {
+ self.size = size
+ self.fillColour = fillColour
+ self.lineColour = lineColour
+ self.lineWidth = lineWidth
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift
index 76170f08..35441a9a 100644
--- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift
+++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift
@@ -7,15 +7,23 @@
import SwiftUI
-internal struct TouchOverlayBox: View {
+/**
+View that displays information from the touch events.
+ */
+internal struct TouchOverlayBox: View {
+<<<<<<< HEAD
private var selectedPoint : ChartDataPoint?
private var specifier : String
private var units : Units
private var ignoreZero : Bool
+=======
+ @ObservedObject var chartData: T
+>>>>>>> version-2
- @Binding private var boxFrame : CGRect
+ @Binding private var boxFrame: CGRect
+<<<<<<< HEAD
internal init(selectedPoint : ChartDataPoint?,
specifier : String = "%.0f",
units : Units,
@@ -62,38 +70,51 @@ internal struct TouchOverlayBox: View {
} else if let label = selectedPoint?.xAxisLabel {
Text(label)
.font(.subheadline)
+=======
+ internal init(chartData: T,
+ boxFrame : Binding
+ ) {
+ self.chartData = chartData
+ self._boxFrame = boxFrame
+ }
+
+ internal var body: some View {
+
+ VStack(alignment: .leading, spacing: 0) {
+ ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in
+
+ chartData.infoDescription(info: point)
+ .font(.subheadline)
+ .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour)
+
+ chartData.infoValueUnit(info: point)
+ .font(.title3)
+ .foregroundColor(chartData.chartStyle.infoBoxValueColour)
+
+ chartData.infoLegend(info: point)
+ .font(.subheadline)
+ .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour)
+ Spacer()
+>>>>>>> version-2
}
}
+
.padding(.all, 8)
.background(
GeometryReader { geo in
- ZStack {
- #if os(iOS)
- RoundedRectangle(cornerRadius: 15.0, style: .continuous)
- .shadow(color: Color(.systemGray), radius: 6, x: 0, y: 0)
- RoundedRectangle(cornerRadius: 15.0, style: .continuous)
- .fill(Color(.systemBackground))
- #elseif os(macOS)
- RoundedRectangle(cornerRadius: 15.0, style: .continuous)
- .shadow(color: Color(.highlightColor), radius: 6, x: 0, y: 0)
- RoundedRectangle(cornerRadius: 15.0, style: .continuous)
- .fill(Color(.windowBackgroundColor))
- #endif
-
- }
- .overlay(
- Group {
- #if os(iOS)
- RoundedRectangle(cornerRadius: 15.0)
- .stroke(Color.primary, lineWidth: 1)
- #elseif os(macOS)
- RoundedRectangle(cornerRadius: 15.0)
- .stroke(Color.primary, lineWidth: 2)
- #endif
- }
- )
- .onChange(of: geo.frame(in: .local)) { frame in
- self.boxFrame = frame
+ if chartData.infoView.isTouchCurrent {
+ RoundedRectangle(cornerRadius: 5.0, style: .continuous)
+ .fill(chartData.chartStyle.infoBoxBackgroundColour)
+ .overlay(
+ RoundedRectangle(cornerRadius: 5.0, style: .continuous)
+ .stroke(chartData.chartStyle.infoBoxBorderColour, style: chartData.chartStyle.infoBoxBorderStyle)
+ )
+ .onAppear {
+ self.boxFrame = geo.frame(in: .local)
+ }
+ .onChange(of: geo.frame(in: .local)) { frame in
+ self.boxFrame = frame
+ }
}
}
)
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift
new file mode 100644
index 00000000..e2f1f6b4
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift
@@ -0,0 +1,101 @@
+//
+// LineAndBarEnums.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - XAxisLabels
+/**
+Location of the X axis labels
+ ```
+ case top
+ case bottom
+ ```
+ */
+public enum XAxisLabelPosistion {
+ case top
+ case bottom
+}
+
+/**
+ Where the label data come from.
+
+ xAxisLabel comes from ChartData --> DataPoint model.
+
+ xAxisLabels comes from ChartData --> xAxisLabels
+ ```
+ case dataPoint // ChartData --> DataPoint --> xAxisLabel
+ case chartData // ChartData --> xAxisLabels
+ ```
+ */
+public enum LabelsFrom {
+ /// ChartData --> DataPoint --> xAxisLabel
+ case dataPoint(rotation: Angle)
+ /// ChartData --> xAxisLabels
+ case chartData
+}
+
+// MARK: - YAxisLabels
+/**
+Location of the Y axis labels
+ ```
+ case leading
+ case trailing
+ ```
+ */
+public enum YAxisLabelPosistion {
+ case leading
+ case trailing
+}
+
+/**
+ Option to display the markers' value inline with the marker..
+
+ ```
+ case none // No label.
+ case yAxis(specifier: String) // Places the label in the yAxis labels.
+ case center(specifier: String) // Places the label in the center of chart.
+ ```
+ */
+public enum DisplayValue {
+ /// No label.
+ case none
+ /// Places the label in the yAxis labels.
+ case yAxis(specifier: String)
+ /// Places the label in the center of chart.
+ case center(specifier: String)
+}
+
+/**
+ Where to start drawing the line chart from.
+ ```
+ case minimumValue // Lowest value in the data set(s)
+ case minimumWithMaximum(of: Double) // Set a custom baseline
+ case zero // Set 0 as the lowest value
+ ```
+ */
+public enum Baseline {
+ /// Lowest value in the data set(s)
+ case minimumValue
+ /// Set a custom baseline
+ case minimumWithMaximum(of: Double)
+ /// Set 0 as the lowest value
+ case zero
+}
+
+/**
+ Where to end drawing the chart.
+ ```
+ case maximumValue // Highest value in the data set(s)
+ case maximum(of: Double) // Set a custom topline
+ ```
+ */
+public enum Topline {
+ /// Highest value in the data set(s)
+ case maximumValue
+ /// Set a custom topline
+ case maximum(of: Double)
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift
new file mode 100644
index 00000000..c35bf6f2
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift
@@ -0,0 +1,32 @@
+//
+// ChartViewData.swift
+//
+// Created by Will Dale on 03/01/2021.
+//
+
+import SwiftUI
+
+/// Data model to pass view information internally so the layout can configure its self.
+public struct ChartViewData {
+
+ /// If the chart has labels on the X axis, the Y axis needs a different layout
+ var hasXAxisLabels : Bool = false
+
+ /**
+ The hieght of X Axis Title if it is there.
+
+ Needed to set the position of the Y Axis labels.
+ */
+ var xAxisTitleHeight : CGFloat = 0
+
+ /**
+ The hieght of X Axis labels if they are there.
+
+ Needed to set the position of the Y Axis labels.
+ */
+ var xAxisLabelHeights : [CGFloat] = []
+
+ /// If the chart has labels on the Y axis, the X axis needs a different layout
+ var hasYAxisLabels : Bool = false
+
+}
diff --git a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift
similarity index 95%
rename from Sources/SwiftUICharts/Shared/Models/GridStyle.swift
rename to Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift
index 32be3bd7..49cab48f 100644
--- a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift
@@ -7,7 +7,9 @@
import SwiftUI
-/// Model for controlling the look of the Grid
+/**
+ Controlling for the look of the Grid
+ */
public struct GridStyle {
/// Number of lines to break up the axis
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift
new file mode 100644
index 00000000..b073057f
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift
@@ -0,0 +1,170 @@
+//
+// LineAndBarProtocols.swift
+//
+//
+// Created by Will Dale on 02/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Chart Data
+/**
+ A protocol to extend functionality of `CTChartData` specifically for Line and Bar Charts.
+ */
+public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBarChartStyle {
+
+ /// A type representing a View for displaying labels on the X axis.
+ associatedtype XLabels : View
+
+ /**
+ Returns the difference between the highest and lowest numbers in the data set or data sets.
+ */
+ var range: Double { get }
+
+ /**
+ Returns the lowest value in the data set or data sets.
+ */
+ var minValue: Double { get }
+
+ /**
+ Returns the highest value in the data set or data sets
+ */
+ var maxValue: Double { get }
+
+ /**
+ Returns the average value from the data set or data sets.
+ */
+ var average : Double { get }
+
+ /**
+ Array of strings for the labels on the X Axis instead of the labels in the data points.
+ */
+ var xAxisLabels: [String]? { get set }
+
+ /**
+ Data model to hold data about the Views layout.
+
+ This informs some `ViewModifiers` whether the chart has X and/or Y
+ axis labels so they can configure thier layouts appropriately.
+ */
+ var viewData: ChartViewData { get set }
+
+ /**
+ Labels to display on the Y axis
+
+ The labels are generated based on the range between the lowest number in the
+ data set (or 0) and highest number in the data set.
+
+ - Returns: Array of evenly spaced numbers.
+ */
+ func getYLabels() -> [Double]
+
+ /**
+ Displays a view for the labels on the X Axis.
+ */
+ func getXAxisLabels() -> XLabels
+
+}
+
+
+// MARK: - Style
+/**
+ A protocol to get the correct touch overlay marker.
+ */
+public protocol MarkerType {}
+
+/**
+ A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts.
+ */
+public protocol CTLineBarChartStyle: CTChartStyle {
+
+ /// A type representing touch overlay marker type. -- `MarkerType`
+ associatedtype Mark: MarkerType
+
+ /**
+ Where the marker lines come from to meet at a specified point.
+ */
+ var markerType : Mark { get set }
+
+ /**
+ Style of the vertical lines breaking up the chart.
+ */
+ var xAxisGridStyle: GridStyle { get set }
+
+ /**
+ Location of the X axis labels - Top or Bottom.
+ */
+ var xAxisLabelPosition: XAxisLabelPosistion { get set }
+
+ /**
+ Text Colour for the labels on the X axis.
+ */
+ var xAxisLabelColour: Color { get set }
+
+ /**
+ Where the label data come from. DataPoint or ChartData.
+ */
+ var xAxisLabelsFrom: LabelsFrom { get set }
+
+ /**
+ Label to display next to the chart giving info about the axis.
+ */
+ var xAxisTitle: String? { get set }
+
+ /**
+ Style of the horizontal lines breaking up the chart.
+ */
+ var yAxisGridStyle: GridStyle { get set }
+
+ /**
+ Location of the X axis labels - Leading or Trailing.
+ */
+ var yAxisLabelPosition: YAxisLabelPosistion { get set }
+
+ /**
+ Text Colour for the labels on the Y axis.
+ */
+ var yAxisLabelColour: Color { get set }
+
+ /**
+ Number Of Labels on Y Axis
+ */
+ var yAxisNumberOfLabels: Int { get set }
+
+ /**
+ Label to display next to the chart giving info about the axis.
+ */
+ var yAxisTitle: String? { get set }
+
+ /**
+ Where to start drawing the line chart from. Zero, data set minium or custom.
+ */
+ var baseline: Baseline { get set }
+
+ /**
+ Where to finish drawing the chart from. Data set maximum or custom.
+ */
+ var topLine: Topline { get set }
+
+}
+
+// MARK: - DataPoints
+/**
+ A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Line and Bar Charts.
+ */
+public protocol CTLineBarDataPointProtocol: CTDataPointBaseProtocol {
+
+ /**
+ Data points label for the X axis.
+ */
+ var xAxisLabel: String? { get set }
+}
+
+extension CTLineBarDataPointProtocol {
+ /**
+ Unwarpped xAxisLabel
+ */
+ var wrappedXAxisLabel : String {
+ self.xAxisLabel ?? ""
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift
new file mode 100644
index 00000000..f71e8ad7
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift
@@ -0,0 +1,80 @@
+//
+// LineAndBarProtocolsExtentions.swift
+//
+//
+// Created by Will Dale on 13/02/2021.
+//
+
+import SwiftUI
+
+// MARK: - Data Set
+extension CTLineBarChartDataProtocol {
+ public var range : Double {
+ get {
+ var _lowestValue : Double
+ var _highestValue : Double
+
+ switch self.chartStyle.baseline {
+ case .minimumValue:
+ _lowestValue = self.dataSets.minValue()
+ case .minimumWithMaximum(of: let value):
+ _lowestValue = min(self.dataSets.minValue(), value)
+ case .zero:
+ _lowestValue = 0
+ }
+
+ switch self.chartStyle.topLine {
+ case .maximumValue:
+ _highestValue = self.dataSets.maxValue()
+ case .maximum(of: let value):
+ _highestValue = max(self.dataSets.maxValue(), value)
+ }
+
+ return (_highestValue - _lowestValue) + 0.001
+ }
+ }
+
+ public var minValue : Double {
+ get {
+ switch self.chartStyle.baseline {
+ case .minimumValue:
+ return self.dataSets.minValue()
+ case .minimumWithMaximum(of: let value):
+ return min(self.dataSets.minValue(), value)
+ case .zero:
+ return 0
+ }
+ }
+ }
+
+ public var maxValue : Double {
+ get {
+ switch self.chartStyle.topLine {
+ case .maximumValue:
+ return self.dataSets.maxValue()
+ case .maximum(of: let value):
+ return max(self.dataSets.maxValue(), value)
+ }
+ }
+ }
+
+ public var average : Double {
+ return self.dataSets.average()
+ }
+}
+
+
+// MARK: - Y Labels
+extension CTLineBarChartDataProtocol {
+ public func getYLabels() -> [Double] {
+ var labels : [Double] = [Double]()
+ let dataRange : Double = self.range
+ let minValue : Double = self.minValue
+ let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels-1)
+ labels.append(minValue)
+ for index in 1...self.chartStyle.yAxisNumberOfLabels-1 {
+ labels.append(minValue + range * Double(index))
+ }
+ return labels
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift
new file mode 100644
index 00000000..baa8f4e8
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift
@@ -0,0 +1,23 @@
+//
+// DiamondShape.swift
+//
+//
+// Created by Will Dale on 07/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Shape used in POI Markers when displaying value in the center.
+ */
+public struct DiamondShape: Shape {
+ public func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
+ path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
+ path.closeSubpath()
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift
new file mode 100644
index 00000000..f88233f2
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift
@@ -0,0 +1,20 @@
+//
+// HorizontalGridShape.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Horizontal line.
+ */
+internal struct HorizontalGridShape: Shape {
+ internal func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: 0, y: 0))
+ path.addLine(to: CGPoint(x: rect.width, y: 0))
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift
new file mode 100644
index 00000000..3addfdb2
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift
@@ -0,0 +1,40 @@
+//
+// LabelShape.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Shape used in POI Markers when displaying value in the Y axis labels on the leading edge.
+ */
+public struct LeadingLabelShape: Shape {
+ public func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
+ path.addLine(to: CGPoint(x: rect.maxX - (rect.width / 5), y: rect.maxY))
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
+ path.addLine(to: CGPoint(x: rect.maxX - (rect.width / 5), y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
+ path.closeSubpath()
+ return path
+ }
+}
+
+/**
+ Shape used in POI Markers when displaying value in the Y axis labels on the trailing edge.
+ */
+public struct TrailingLabelShape: Shape {
+ public func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: rect.maxX, y: rect.maxY))
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.minX + (rect.width / 5), y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
+ path.addLine(to: CGPoint(x: rect.minX + (rect.width / 5), y: rect.maxY))
+ path.closeSubpath()
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift
similarity index 66%
rename from Sources/SwiftUICharts/Shared/Shapes/Marker.swift
rename to Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift
index 74e4afa7..2ad55fee 100644
--- a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift
@@ -8,14 +8,12 @@
import SwiftUI
/// Generic line drawn horrizontally across the chart
-internal struct Marker: Shape {
-
- private let chartData : ChartData
- private let markerValue : Double
- private let isAverage : Bool
+internal struct Marker: Shape {
+ private let value : Double
private let chartType : ChartType
+<<<<<<< HEAD:Sources/SwiftUICharts/Shared/Shapes/Marker.swift
private let minValue : Double
private let range : Double
@@ -46,6 +44,27 @@ internal struct Marker: Shape {
let value : Double = isAverage ? chartData.average() : markerValue
+=======
+ let range : Double
+ let minValue: Double
+ let maxValue: Double
+
+ internal init(value : Double,
+ range : Double,
+ minValue : Double,
+ maxValue : Double,
+ chartType : ChartType
+ ) {
+ self.value = value
+ self.range = range
+ self.minValue = minValue
+ self.maxValue = maxValue
+ self.chartType = chartType
+ }
+
+ internal func path(in rect: CGRect) -> Path {
+
+>>>>>>> version-2:Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift
var path = Path()
let pointY : CGFloat
@@ -54,8 +73,10 @@ internal struct Marker: Shape {
let y = rect.height / CGFloat(range)
pointY = (CGFloat(value - minValue) * -y) + rect.height
case .bar:
- let y = rect.height / CGFloat(chartData.maxValue())
- pointY = rect.height - CGFloat(value) * y
+ let y = CGFloat(value - minValue)
+ pointY = (rect.height - (y / CGFloat(range)) * rect.height)
+ case .pie:
+ pointY = 0
}
let firstPoint = CGPoint(x: 0,
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift
new file mode 100644
index 00000000..8a4be76b
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift
@@ -0,0 +1,20 @@
+//
+// VerticalGridShape.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Vertical line.
+ */
+internal struct VerticalGridShape: Shape {
+ internal func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: 0, y: rect.height))
+ path.addLine(to: CGPoint(x: 0, y: 0))
+ return path
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift
new file mode 100644
index 00000000..b385041e
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift
@@ -0,0 +1,97 @@
+//
+// AxisDividers.swift
+// LineChart
+//
+// Created by Will Dale on 02/01/2021.
+//
+
+import SwiftUI
+
+/**
+ Dividing line drawn between the X axis labels and the chart.
+ */
+internal struct XAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+ private let labelsAndTop : Bool
+ private let labelsAndBottom : Bool
+
+ init(chartData: T) {
+ self.chartData = chartData
+
+ self.labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top
+ self.labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom
+ }
+
+ internal func body(content: Content) -> some View {
+ Group {
+ if chartData.isGreaterThanTwo() {
+ if labelsAndBottom {
+ VStack {
+ ZStack(alignment: .bottom) {
+ content
+ Divider()
+ }
+ }
+ } else if labelsAndTop {
+ VStack {
+ ZStack(alignment: .top) {
+ content
+ Divider()
+ }
+ }
+ } else {
+ content
+ }
+ } else { content }
+ }
+ }
+}
+
+/**
+ Dividing line drawn between the Y axis labels and the chart.
+ */
+internal struct YAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+ private let labelsAndLeading : Bool
+ private let labelsAndTrailing: Bool
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ self.labelsAndLeading = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .leading
+ self.labelsAndTrailing = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .trailing
+ }
+
+ internal func body(content: Content) -> some View {
+ Group {
+ if labelsAndLeading {
+ HStack {
+ ZStack(alignment: .leading) {
+ content
+ Divider()
+ }
+ }
+ } else if labelsAndTrailing {
+ HStack {
+ ZStack(alignment: .trailing) {
+ content
+ Divider()
+ }
+ }
+ } else {
+ content
+ }
+ }
+ }
+}
+
+extension View {
+ internal func xAxisBorder(chartData: T) -> some View {
+ self.modifier(XAxisBorder(chartData: chartData))
+ }
+
+ internal func yAxisBorder(chartData: T) -> some View {
+ self.modifier(YAxisBorder(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift
new file mode 100644
index 00000000..cf3157f6
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift
@@ -0,0 +1,66 @@
+//
+// XAxisGrid.swift
+// LineChart
+//
+// Created by Will Dale on 26/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Adds vertical lines along the X axis.
+ */
+internal struct XAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData : T
+
+ internal func body(content: Content) -> some View {
+ ZStack {
+ if chartData.isGreaterThanTwo() {
+ HStack {
+ ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines-1), id: \.self) { index in
+ if index != 0 {
+ VerticalGridView(chartData: chartData)
+ Spacer()
+ .frame(minWidth: 0, maxWidth: 500)
+ }
+ }
+ VerticalGridView(chartData: chartData)
+ }
+ content
+ } else { content }
+
+ }
+ }
+}
+
+extension View {
+ /**
+ Adds vertical lines along the X axis.
+
+ The style is set in ChartData --> ChartStyle --> xAxisGridStyle
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with vertical lines under it.
+ */
+ public func xAxisGrid(chartData: T) -> some View {
+ self.modifier(XAxisGrid(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift
new file mode 100644
index 00000000..cbf55906
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift
@@ -0,0 +1,90 @@
+//
+// XAxisLabels.swift
+// LineChart
+//
+// Created by Will Dale on 26/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Labels for the X axis.
+ */
+internal struct XAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ self.chartData.viewData.hasXAxisLabels = true
+ }
+
+ internal func body(content: Content) -> some View {
+ Group {
+ switch chartData.chartStyle.xAxisLabelPosition {
+ case .bottom:
+ if chartData.isGreaterThanTwo() {
+ VStack {
+ content
+ chartData.getXAxisLabels()
+ axisTitle
+ }
+ } else { content }
+ case .top:
+ if chartData.isGreaterThanTwo() {
+ VStack {
+ axisTitle
+ chartData.getXAxisLabels()
+ content
+ }
+ } else { content }
+ }
+ }
+ }
+
+ @ViewBuilder private var axisTitle: some View {
+ if let title = chartData.chartStyle.xAxisTitle {
+ Text(title)
+ .font(.caption)
+ .frame(height: 20)
+ .onAppear {
+ chartData.viewData.xAxisTitleHeight = 20
+ }
+ }
+ }
+}
+
+extension View {
+ /**
+ Labels for the X axis.
+
+ The labels can either come from ChartData --> xAxisLabels
+ or ChartData --> DataSets --> DataPoints
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with labels marking the x axis.
+ */
+ public func xAxisLabels(chartData: T) -> some View {
+ self.modifier(XAxisLabels(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift
new file mode 100644
index 00000000..bb28dfa6
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift
@@ -0,0 +1,65 @@
+//
+// YAxisGrid.swift
+// LineChart
+//
+// Created by Will Dale on 24/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Adds horizontal lines along the X axis.
+ */
+internal struct YAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData : T
+
+ internal func body(content: Content) -> some View {
+ ZStack {
+ if chartData.isGreaterThanTwo() {
+ VStack {
+ ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines-1), id: \.self) { index in
+ if index != 0 {
+ HorizontalGridView(chartData: chartData)
+ Spacer()
+ .frame(minHeight: 0, maxHeight: 500)
+ }
+ }
+ HorizontalGridView(chartData: chartData)
+ }
+ content
+ } else { content }
+ }
+ }
+}
+
+extension View {
+ /**
+ Adds horizontal lines along the X axis.
+
+ The style is set in ChartData --> LineChartStyle --> yAxisGridStyle
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameter chartData: Chart data model.
+ - Returns: A new view containing the chart with horizontal lines under it.
+ */
+ public func yAxisGrid(chartData: T) -> some View {
+ self.modifier(YAxisGrid(chartData: chartData))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift
new file mode 100644
index 00000000..7d6149b8
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift
@@ -0,0 +1,141 @@
+//
+// YAxisLabels.swift
+// LineChart
+//
+// Created by Will Dale on 24/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Automatically generated labels for the Y axis.
+ */
+internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+
+ private let specifier : String
+ private var labelsArray : [Double] { chartData.getYLabels() }
+
+ private let labelsAndTop : Bool
+ private let labelsAndBottom : Bool
+
+ internal init(chartData: T,
+ specifier: String
+ ) {
+ self.chartData = chartData
+ self.specifier = specifier
+ chartData.viewData.hasYAxisLabels = true
+
+ labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top
+ labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom
+ }
+
+ @State private var height : CGFloat = 0
+ @State private var axisLabelWidth : CGFloat = 0
+
+ @ViewBuilder private var axisTitle: some View {
+ if let title = chartData.chartStyle.yAxisTitle {
+ VStack {
+ Text(title)
+ .font(.caption)
+ .rotationEffect(Angle.init(degrees: -90), anchor: .center)
+ .fixedSize()
+ .frame(width: axisLabelWidth)
+ Spacer()
+ .frame(height: (self.chartData.viewData.xAxisLabelHeights.max(by: { $0 < $1 }) ?? 0) + axisLabelWidth)
+ }
+ .onAppear {
+ axisLabelWidth = 20
+ }
+ }
+ }
+
+ private var labels: some View {
+ VStack {
+ ForEach((0...chartData.chartStyle.yAxisNumberOfLabels-1).reversed(), id: \.self) { i in
+ Text("\(labelsArray[i], specifier: specifier)")
+ .font(.caption)
+ .foregroundColor(chartData.chartStyle.yAxisLabelColour)
+ .lineLimit(1)
+ .accessibilityLabel(Text("Y Axis Label"))
+ .accessibilityValue(Text("\(labelsArray[i], specifier: specifier)"))
+ if i != 0 {
+ Spacer()
+ .frame(minHeight: 0, maxHeight: 500)
+ }
+ }
+ Spacer()
+ .frame(height: (chartData.viewData.xAxisLabelHeights.max(by: { $0 < $1 }) ?? 0) + chartData.viewData.xAxisTitleHeight)
+ }
+ .if(labelsAndBottom) { $0.padding(.top, -8) }
+ .if(labelsAndTop) { $0.padding(.bottom, -8) }
+ .padding(.trailing, 10)
+ .background(
+ GeometryReader { geo in
+ Rectangle()
+ .foregroundColor(Color.clear)
+ .onAppear {
+ chartData.infoView.yAxisLabelWidth = geo.frame(in: .local).size.width
+ self.height = geo.frame(in: .local).height
+ }
+ .onChange(of: axisLabelWidth) { width in
+ chartData.infoView.yAxisLabelWidth = geo.frame(in: .local).size.width + width
+ }
+ }
+ )
+ }
+
+ internal func body(content: Content) -> some View {
+ Group {
+ if chartData.isGreaterThanTwo() {
+ switch chartData.chartStyle.yAxisLabelPosition {
+ case .leading:
+ HStack(spacing: 0) {
+ axisTitle
+ labels
+ content
+ }
+ case .trailing:
+ HStack(spacing: 0) {
+ content
+ labels
+ axisTitle
+ }
+ }
+ } else { content }
+ }
+ }
+}
+
+extension View {
+ /**
+ Automatically generated labels for the Y axis.
+
+ Controls are in ChartData --> ChartStyle
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameters:
+ - specifier: Decimal precision specifier
+ - Returns: HStack of labels
+ */
+ public func yAxisLabels(chartData: T, specifier: String = "%.0f") -> some View {
+ self.modifier(YAxisLabels(chartData: chartData, specifier: specifier))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift
new file mode 100644
index 00000000..f795720f
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift
@@ -0,0 +1,293 @@
+//
+// YAxisPOI.swift
+// LineChart
+//
+// Created by Will Dale on 31/12/2020.
+//
+
+import SwiftUI
+
+/**
+ Configurable Point of interest
+ */
+internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+
+ private let uuid : UUID = UUID()
+
+ private let markerName : String
+ private var markerValue : Double
+ private let lineColour : Color
+ private let strokeStyle : StrokeStyle
+
+ private let labelPosition : DisplayValue
+ private let labelColour : Color
+ private let labelBackground : Color
+
+ private let range : Double
+ private let minValue : Double
+ private let maxValue : Double
+
+ internal init(chartData : T,
+ markerName : String,
+ markerValue : Double = 0,
+ labelPosition : DisplayValue,
+ labelColour : Color,
+ labelBackground: Color,
+ lineColour : Color,
+ strokeStyle : StrokeStyle,
+ isAverage : Bool
+ ) {
+ self.chartData = chartData
+ self.markerName = markerName
+ self.lineColour = lineColour
+ self.strokeStyle = strokeStyle
+
+ self.labelPosition = labelPosition
+ self.labelColour = labelColour
+ self.labelBackground = labelBackground
+
+ self.markerValue = isAverage ? chartData.average : markerValue
+ self.maxValue = chartData.maxValue
+ self.range = chartData.range
+ self.minValue = chartData.minValue
+
+ self.setupPOILegends()
+ }
+
+ @State private var startAnimation : Bool = false
+
+ internal func body(content: Content) -> some View {
+ ZStack {
+ if chartData.isGreaterThanTwo() {
+ content
+ marker
+ valueLabel
+ } else { content }
+ }
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+
+ var marker: some View {
+ Marker(value : markerValue,
+ range : range,
+ minValue : minValue,
+ maxValue : maxValue,
+ chartType : chartData.chartType.chartType)
+ .trim(to: startAnimation ? 1 : 0)
+ .stroke(lineColour, style: strokeStyle)
+ }
+
+ var valueLabel: some View {
+ GeometryReader { geo in
+
+ switch labelPosition {
+ case .none:
+
+ EmptyView()
+
+ case .yAxis(specifier: let specifier):
+
+ ValueLabelYAxisSubView(chartData : chartData,
+ markerValue : markerValue,
+ specifier : specifier,
+ labelColour : labelColour,
+ labelBackground: labelBackground,
+ lineColour : lineColour)
+ .position(x: -(chartData.infoView.yAxisLabelWidth / 2) - 6,
+ y: getYPoint(chartType: chartData.chartType.chartType,
+ height: geo.size.height))
+ .accessibilityLabel(Text("P O I Marker"))
+ .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)"))
+
+ case .center(specifier: let specifier):
+
+ ValueLabelCenterSubView(chartData : chartData,
+ markerValue : markerValue,
+ specifier : specifier,
+ labelColour : labelColour,
+ labelBackground : labelBackground,
+ lineColour : lineColour,
+ strokeStyle : strokeStyle)
+ .position(x: geo.frame(in: .local).width / 2,
+ y: getYPoint(chartType: chartData.chartType.chartType, height: geo.size.height))
+
+ .accessibilityLabel(Text("P O I Marker"))
+ .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)"))
+ }
+ }
+ }
+ private func getYPoint(chartType: ChartType, height: CGFloat) -> CGFloat {
+ switch chartData.chartType.chartType {
+ case .line:
+ let y = height / CGFloat(chartData.range)
+ return (CGFloat(markerValue - chartData.minValue) * -y) + height
+ case .bar:
+ let value = CGFloat(markerValue) - CGFloat(chartData.minValue)
+ return (height - (value / CGFloat(chartData.range)) * height)
+
+ case .pie:
+ return 0
+ }
+ }
+ private func setupPOILegends() {
+ if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice
+ chartData.legends.append(LegendData(id : uuid,
+ legend : markerName,
+ colour : ColourStyle(colour: lineColour),
+ strokeStyle : strokeStyle.toStroke(),
+ prioity : 2,
+ chartType : .line))
+ }
+ }
+}
+
+extension View {
+ /**
+ Horizontal line marking a custom value.
+
+ Shows a marker line at a specified value.
+
+ # Example
+ ```
+ .yAxisPOI(chartData: data,
+ markerName: "Marker",
+ markerValue: 110,
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.white,
+ labelBackground: Color.red,
+ lineColour: .blue,
+ strokeStyle: StrokeStyle(lineWidth: 2,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 10,
+ dash: [8],
+ dashPhase: 0))
+ ```
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameters:
+ - chartData: Chart data model.
+ - markerName: Title of marker, for the legend.
+ - markerValue: Value to mark
+ - labelPosition: Option to display the markers’ value inline with the marker.
+ - labelColour: Colour of the `Text`.
+ - labelBackground: Colour of the background.
+ - lineColour: Line Colour.
+ - strokeStyle: Style of Stroke.
+ - Returns: A new view containing the chart with a marker line at a specified value.
+ */
+ public func yAxisPOI(
+ chartData : T,
+ markerName : String,
+ markerValue : Double,
+ labelPosition : DisplayValue = .center(specifier: "%.0f"),
+ labelColour : Color = Color.primary,
+ labelBackground: Color = Color.systemsBackground,
+ lineColour : Color = Color(.blue),
+ strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0)
+ ) -> some View {
+ self.modifier(YAxisPOI(chartData : chartData,
+ markerName : markerName,
+ markerValue : markerValue,
+ labelPosition : labelPosition,
+ labelColour : labelColour,
+ labelBackground: labelBackground,
+ lineColour : lineColour,
+ strokeStyle : strokeStyle,
+ isAverage : false))
+ }
+
+
+ /**
+ Horizontal line marking the average.
+
+ Shows a marker line at the average of all the data points within
+ the relevant data set(s).
+
+ # Example
+ ```
+ .averageLine(chartData: data,
+ markerName: "Average",
+ labelPosition: .center(specifier: "%.0f"),
+ labelColour: Color.white,
+ labelBackground: Color.red,
+ lineColour: .primary,
+ strokeStyle: StrokeStyle(lineWidth: 2,
+ lineCap: .round,
+ lineJoin: .round,
+ miterLimit: 10,
+ dash: [8],
+ dashPhase: 0))
+ ```
+
+ - Requires:
+ Chart Data to conform to CTLineBarChartDataProtocol.
+
+ # Available for:
+ - Line Chart
+ - Multi Line Chart
+ - Filled Line Chart
+ - Ranged Line Chart
+ - Bar Chart
+ - Grouped Bar Chart
+ - Stacked Bar Chart
+ - Ranged Bar Chart
+
+ # Unavailable for:
+ - Pie Chart
+ - Doughnut Chart
+
+ - Parameters:
+ - chartData: Chart data model.
+ - markerName: Title of marker, for the legend.
+ - labelPosition: Option to display the markers’ value inline with the marker.
+ - labelColour: Colour of the `Text`.
+ - labelBackground: Colour of the background.
+ - lineColour: Line Colour.
+ - strokeStyle: Style of Stroke.
+ - Returns: A new view containing the chart with a marker line at the average.
+
+ - Tag: AverageLine
+ */
+ public func averageLine(
+ chartData : T,
+ markerName : String = "Average",
+ labelPosition : DisplayValue = .yAxis(specifier: "%.0f"),
+ labelColour : Color = Color.primary,
+ labelBackground: Color = Color.systemsBackground,
+ lineColour : Color = Color.primary,
+ strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0)
+ ) -> some View {
+ self.modifier(YAxisPOI(chartData : chartData,
+ markerName : markerName,
+ labelPosition : labelPosition,
+ labelColour : labelColour,
+ labelBackground: labelBackground,
+ lineColour : lineColour,
+ strokeStyle : strokeStyle,
+ isAverage : true))
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift
new file mode 100644
index 00000000..9cca9cf2
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift
@@ -0,0 +1,38 @@
+//
+// HorizontalGridView.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Sub view of the Y axis grid view modifier.
+ */
+internal struct HorizontalGridView: View where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject private var chartData : T
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ @State private var startAnimation : Bool = false
+
+ var body: some View {
+ HorizontalGridShape()
+ .trim(to: startAnimation ? 1 : 0)
+ .stroke(chartData.chartStyle.yAxisGridStyle.lineColour,
+ style: StrokeStyle(lineWidth: chartData.chartStyle.yAxisGridStyle.lineWidth,
+ dash : chartData.chartStyle.yAxisGridStyle.dash,
+ dashPhase: chartData.chartStyle.yAxisGridStyle.dashPhase))
+ .frame(height: chartData.chartStyle.yAxisGridStyle.lineWidth)
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift
new file mode 100644
index 00000000..c65cc48b
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift
@@ -0,0 +1,59 @@
+//
+// ValueLabelCenterSubView.swift
+//
+//
+// Created by Will Dale on 27/02/2021.
+//
+
+import SwiftUI
+
+internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProtocol {
+
+ private let chartData : T
+ private let markerValue : Double
+ private let specifier : String
+ private let labelColour : Color
+ private let labelBackground : Color
+ private let lineColour : Color
+ private let strokeStyle : StrokeStyle
+
+ internal init(chartData : T,
+ markerValue : Double,
+ specifier : String,
+ labelColour : Color,
+ labelBackground : Color,
+ lineColour : Color,
+ strokeStyle : StrokeStyle
+ ) {
+ self.chartData = chartData
+ self.markerValue = markerValue
+ self.specifier = specifier
+ self.labelColour = labelColour
+ self.labelBackground = labelBackground
+ self.lineColour = lineColour
+ self.strokeStyle = strokeStyle
+ }
+
+ @State private var startAnimation : Bool = false
+
+ var body: some View {
+ Text("\(markerValue, specifier: specifier)")
+ .font(.caption)
+ .foregroundColor(labelColour)
+ .padding()
+ .background(labelBackground)
+ .clipShape(DiamondShape())
+ .overlay(DiamondShape()
+ .stroke(lineColour, style: strokeStyle)
+ )
+ .opacity(startAnimation ? 1 : 0)
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+
+ }
+}
+
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift
new file mode 100644
index 00000000..f6b9e851
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift
@@ -0,0 +1,54 @@
+//
+// ValueLabelYAxisSubView.swift
+//
+//
+// Created by Will Dale on 27/02/2021.
+//
+
+import SwiftUI
+
+internal struct ValueLabelYAxisSubView: View where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject var chartData: T
+ private let markerValue : Double
+ private let specifier : String
+ private let labelColour : Color
+ private let labelBackground : Color
+ private let lineColour : Color
+
+ internal init(chartData : T,
+ markerValue : Double,
+ specifier : String,
+ labelColour : Color,
+ labelBackground : Color,
+ lineColour : Color
+ ) {
+ self.chartData = chartData
+ self.markerValue = markerValue
+ self.specifier = specifier
+ self.labelColour = labelColour
+ self.labelBackground = labelBackground
+ self.lineColour = lineColour
+ }
+
+ var body: some View {
+ Text("\(markerValue, specifier: specifier)")
+ .font(.caption)
+ .foregroundColor(labelColour)
+ .padding(4)
+ .background(labelBackground)
+ .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: {
+ $0
+ .clipShape(LeadingLabelShape())
+ .overlay(LeadingLabelShape()
+ .stroke(lineColour)
+ )
+ }, else: {
+ $0
+ .clipShape(TrailingLabelShape())
+ .overlay(TrailingLabelShape()
+ .stroke(lineColour)
+ )
+ })
+ }
+}
diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift
new file mode 100644
index 00000000..33b50037
--- /dev/null
+++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift
@@ -0,0 +1,38 @@
+//
+// VerticalGridView.swift
+//
+//
+// Created by Will Dale on 08/02/2021.
+//
+
+import SwiftUI
+
+/**
+ Sub view of the X axis grid view modifier.
+ */
+internal struct VerticalGridView: View where T: CTLineBarChartDataProtocol {
+
+ @ObservedObject private var chartData : T
+
+ internal init(chartData: T) {
+ self.chartData = chartData
+ }
+
+ @State private var startAnimation : Bool = false
+
+ var body: some View {
+ VerticalGridShape()
+ .trim(to: startAnimation ? 1 : 0)
+ .stroke(chartData.chartStyle.xAxisGridStyle.lineColour,
+ style: StrokeStyle(lineWidth: chartData.chartStyle.xAxisGridStyle.lineWidth,
+ dash : chartData.chartStyle.xAxisGridStyle.dash,
+ dashPhase: chartData.chartStyle.xAxisGridStyle.dashPhase))
+ .frame(width: chartData.chartStyle.xAxisGridStyle.lineWidth)
+ .animateOnAppear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = true
+ }
+ .animateOnDisappear(using: chartData.chartStyle.globalAnimation) {
+ self.startAnimation = false
+ }
+ }
+}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 8e7efabb..1521b8d5 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -1,7 +1,12 @@
import XCTest
-import SwiftUIChartsTests
+import LineChartPathTests
var tests = [XCTestCaseEntry]()
-tests += SwiftUIChartsTests.allTests()
+tests += BarChartTests.allTests()
+tests += GroupedBarChartTests.allTests()
+tests += StackedBarChartTests.allTests()
+tests += LineChartTests.allTests()
+tests += MultiLineChartTest.allTests()
+tests += LineChartPathTests.allTests()
XCTMain(tests)
diff --git a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift
new file mode 100644
index 00000000..86d77418
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift
@@ -0,0 +1,148 @@
+import XCTest
+@testable import SwiftUICharts
+
+final class BarChartTests: XCTestCase {
+ // MARK: - Set Up
+ let dataPoints = [
+ BarChartDataPoint(value: 10),
+ BarChartDataPoint(value: 40),
+ BarChartDataPoint(value: 30),
+ BarChartDataPoint(value: 60)
+ ]
+
+ // MARK: - Data
+ func testBarMaxValue() {
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.maxValue, 60)
+ }
+ func testBarMinValue() {
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.minValue, 10)
+ }
+ func testBarAverage() {
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.average, 35)
+ }
+ func testBarRange() {
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.range, 50.001)
+ }
+ func testBarIsGreaterThanTwoTrue() {
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertTrue(chartData.isGreaterThanTwo())
+ }
+
+ func testBarIsGreaterThanTwoFalse() {
+ let dataPoints = [
+ BarChartDataPoint(value: 10),
+ BarChartDataPoint(value: 60)
+ ]
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+ XCTAssertFalse(chartData.isGreaterThanTwo())
+ }
+
+ // MARK: - Labels
+ func testBarGetYLabels() {
+
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints),
+ chartStyle: BarChartStyle(yAxisNumberOfLabels: 3))
+
+ chartData.chartStyle.topLine = .maximumValue
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.000, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 30.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 60.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumValue
+ XCTAssertEqual(chartData.getYLabels()[0], 10.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 35.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 60.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumWithMaximum(of: 5)
+ XCTAssertEqual(chartData.getYLabels()[0], 5.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 32.50, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 60.00, accuracy: 0.01)
+
+ chartData.chartStyle.topLine = .maximum(of: 100)
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+ }
+
+ // MARK: - Touch
+ func testBarGetDataPoint() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+
+ let touchLocationOne: CGPoint = CGPoint(x: 5, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOne, chartSize: rect)
+ let testOutputOne = chartData.infoView.touchOverlayInfo
+ let testAgainstOne = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputOne[0], testAgainstOne[0])
+
+ let touchLocationTwo: CGPoint = CGPoint(x: 25, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwo, chartSize: rect)
+ let testOutputTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstTwo = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputTwo[0], testAgainstTwo[1])
+
+ let touchLocationThree: CGPoint = CGPoint(x: 50, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThree, chartSize: rect)
+ let testOutputThree = chartData.infoView.touchOverlayInfo
+ let testAgainstThree = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputThree[0], testAgainstThree[2])
+
+ let touchLocationFour: CGPoint = CGPoint(x: 85, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFour, chartSize: rect)
+ let testOutputFour = chartData.infoView.touchOverlayInfo
+ let testAgainstFour = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputFour[0], testAgainstFour[3])
+ }
+
+
+ func testBarGetPointLocation() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints))
+
+ // Data point 1
+ let touchLocationOne: CGPoint = CGPoint(x: 5, y: 25)
+ let testOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationOne,
+ chartSize: rect)!
+ let testAgainstOne = CGPoint(x: 12.5, y: 83.33)
+ XCTAssertEqual(testOne.x, testAgainstOne.x, accuracy: 0.01)
+ XCTAssertEqual(testOne.y, testAgainstOne.y, accuracy: 0.01)
+
+ // Data point 3
+ let touchLocationTwo: CGPoint = CGPoint(x: 62.5, y: 25)
+ let testTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationTwo,
+ chartSize: rect)!
+ let testAgainstTwo = CGPoint(x: 62.50, y: 50.00)
+ XCTAssertEqual(testTwo.x, testAgainstTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testTwo.y, testAgainstTwo.y, accuracy: 0.01)
+ }
+
+ // MARK: - All Tests
+ static var allTests = [
+ // Data
+ ("testBarMaxValue", testBarMaxValue),
+ ("testBarMinValue", testBarMinValue),
+ ("testBarAverage", testBarAverage),
+ ("testBarRange", testBarRange),
+ // Greater
+ ("testBarIsGreaterThanTwoTrue", testBarIsGreaterThanTwoTrue),
+ ("testBarIsGreaterThanTwoFalse", testBarIsGreaterThanTwoFalse),
+ // Labels
+ ("testBarGetYLabels", testBarGetYLabels),
+ // Touch
+ ("testBarGetDataPoint", testBarGetDataPoint),
+ ("testBarGetPointLocation", testBarGetPointLocation),
+
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift
new file mode 100644
index 00000000..3365fc00
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift
@@ -0,0 +1,243 @@
+import XCTest
+@testable import SwiftUICharts
+
+final class GroupedBarChartTests: XCTestCase {
+
+ // MARK: - Set Up
+ enum Group {
+ case one
+ case two
+ case three
+ case four
+
+ var data : GroupingData {
+ switch self {
+ case .one:
+ return GroupingData(title: "One" , colour: ColourStyle(colour: .blue))
+ case .two:
+ return GroupingData(title: "Two" , colour: ColourStyle(colour: .red))
+ case .three:
+ return GroupingData(title: "Three", colour: ColourStyle(colour: .yellow))
+ case .four:
+ return GroupingData(title: "Four" , colour: ColourStyle(colour: .green))
+ }
+ }
+ }
+
+ let groups : [GroupingData] = [Group.one.data, Group.two.data, Group.three.data, Group.four.data]
+
+ let data = MultiBarDataSets(dataSets: [
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 50, description: "One Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, description: "One Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 40, description: "One Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 60, description: "Two Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 40, description: "Two Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 60, description: "Two Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 30, description: "Three One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 70, description: "Three Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, description: "Three Three", group: Group.three.data),
+ MultiBarChartDataPoint(value: 90, description: "Three Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 80, description: "Four Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 20, description: "Four Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 50, description: "Four Four" , group: Group.four.data)
+ ])
+ ])
+
+ // MARK: - Data
+ func testGroupedBarMaxValue() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.maxValue, 90)
+ }
+ func testGroupedBarMinValue() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.minValue, 10)
+ }
+ func testGroupedBarAverage() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.average, 45)
+ }
+ func testGroupedBarRange() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.range, 80.001)
+ }
+
+ // MARK: Greater
+ func testGroupedBarIsGreaterThanTwoTrue() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+
+ XCTAssertTrue(chartData.isGreaterThanTwo())
+ }
+
+ func testGroupedBarIsGreaterThanTwoFalse() {
+ let data = MultiBarDataSets(dataSets: [
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 30, description: "Three One", group: Group.one.data)
+
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data)
+ ])
+ ])
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ XCTAssertFalse(chartData.isGreaterThanTwo())
+ }
+
+ // MARK: - Labels
+ func testGroupedBarGetYLabels() {
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups,
+ chartStyle: BarChartStyle(yAxisNumberOfLabels: 3))
+
+ chartData.chartStyle.topLine = .maximumValue
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.000, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 45.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumValue
+ XCTAssertEqual(chartData.getYLabels()[0], 10.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumWithMaximum(of: 5)
+ XCTAssertEqual(chartData.getYLabels()[0], 5.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 47.50, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.topLine = .maximum(of: 100)
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+ }
+ // MARK: - Touch
+ func testGroupedBarGetDataPoint() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ chartData.groupSpacing = 10
+
+ // Group 1
+ let touchLocationOne: CGPoint = CGPoint(x: 0, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOne, chartSize: rect)
+ let testOutputOne = chartData.infoView.touchOverlayInfo
+ let testAgainstOne = chartData.dataSets.dataSets[0].dataPoints
+ XCTAssertEqual(testOutputOne[0], testAgainstOne[0])
+
+ // Group 2
+ let touchLocationTwo: CGPoint = CGPoint(x: 30, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwo, chartSize: rect)
+ let testOutputTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstTwo = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputTwo[0], testAgainstTwo[0])
+
+ // None
+ let touchLocationThree: CGPoint = CGPoint(x: 50, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThree, chartSize: rect)
+ let testOutputThree = chartData.infoView.touchOverlayInfo
+ XCTAssertEqual(testOutputThree, [])
+
+ // Group 3
+ let touchLocationFour: CGPoint = CGPoint(x: 55, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFour, chartSize: rect)
+ let testOutputFour = chartData.infoView.touchOverlayInfo
+ let testAgainstFour = chartData.dataSets.dataSets[2].dataPoints
+ XCTAssertEqual(testOutputFour[0], testAgainstFour[0])
+
+ // Group 4
+ let touchLocationFive: CGPoint = CGPoint(x: 83, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFive, chartSize: rect)
+ let testOutputFive = chartData.infoView.touchOverlayInfo
+ let testAgainstFive = chartData.dataSets.dataSets[3].dataPoints
+ XCTAssertEqual(testOutputFive[0], testAgainstFive[0])
+ }
+
+ func testGroupedBarGetPointLocation() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = GroupedBarChartData(dataSets: data, groups: groups)
+ chartData.groupSpacing = 10
+
+ // Group 1
+ let touchLocationOne: CGPoint = CGPoint(x: 0, y: 25)
+
+ let testOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationOne,
+ chartSize: rect)!
+ let testAgainstOne = CGPoint(x: 2.18, y: 88.88)
+ XCTAssertEqual(testOne.x, testAgainstOne.x, accuracy: 0.01)
+ XCTAssertEqual(testOne.y, testAgainstOne.y, accuracy: 0.01)
+
+ // Group 2
+ let touchLocationTwo: CGPoint = CGPoint(x: 30, y: 25)
+
+ let testTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationTwo,
+ chartSize: rect)!
+ let testAgainstTwo = CGPoint(x: 29.68, y: 77.77)
+ XCTAssertEqual(testTwo.x, testAgainstTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testTwo.y, testAgainstTwo.y, accuracy: 0.01)
+
+ // Group 3
+ let touchLocationThree: CGPoint = CGPoint(x: 55, y: 25)
+
+ let testThree: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationThree,
+ chartSize: rect)!
+ let testAgainstThree = CGPoint(x: 57.18, y: 66.66)
+ XCTAssertEqual(testThree.x, testAgainstThree.x, accuracy: 0.01)
+ XCTAssertEqual(testThree.y, testAgainstThree.y, accuracy: 0.01)
+
+ // Group 4
+ let touchLocationFour: CGPoint = CGPoint(x: 83, y: 25)
+
+ let testFour: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationFour,
+ chartSize: rect)!
+ let testAgainstFour = CGPoint(x: 84.68, y: 55.55)
+ XCTAssertEqual(testFour.x, testAgainstFour.x, accuracy: 0.01)
+ XCTAssertEqual(testFour.y, testAgainstFour.y, accuracy: 0.01)
+ }
+
+ // MARK: - All Tests
+ static var allTests = [
+ // Data
+ ("testGroupedBarMaxValue", testGroupedBarMaxValue),
+ ("testGroupedBarMinValue", testGroupedBarMinValue),
+ ("testGroupedBarAverage", testGroupedBarAverage),
+ ("testGroupedBarRange", testGroupedBarRange),
+ // Greater
+ ("testGroupedBarIsGreaterThanTwoTrue", testGroupedBarIsGreaterThanTwoTrue),
+ ("testGroupedBarIsGreaterThanTwoFalse", testGroupedBarIsGreaterThanTwoFalse),
+ // Labels
+ ("testGroupedBarGetYLabels", testGroupedBarGetYLabels),
+ // Touch
+ ("testMultiLineGetDataPoint", testGroupedBarGetDataPoint),
+ ("testGroupedBarGetPointLocation", testGroupedBarGetPointLocation),
+
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift
new file mode 100644
index 00000000..4f9ac532
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift
@@ -0,0 +1,297 @@
+import XCTest
+@testable import SwiftUICharts
+
+final class StackedBarChartTests: XCTestCase {
+
+ // MARK: - Set Up
+ enum Group {
+ case one
+ case two
+ case three
+ case four
+
+ var data : GroupingData {
+ switch self {
+ case .one:
+ return GroupingData(title: "One" , colour: ColourStyle(colour: .blue))
+ case .two:
+ return GroupingData(title: "Two" , colour: ColourStyle(colour: .red))
+ case .three:
+ return GroupingData(title: "Three", colour: ColourStyle(colour: .yellow))
+ case .four:
+ return GroupingData(title: "Four" , colour: ColourStyle(colour: .green))
+ }
+ }
+ }
+
+ let groups : [GroupingData] = [Group.one.data, Group.two.data, Group.three.data, Group.four.data]
+
+ let data = MultiBarDataSets(dataSets: [
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 50, description: "One Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, description: "One Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 40, description: "One Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 60, description: "Two Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 40, description: "Two Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 60, description: "Two Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 30, description: "Three One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 70, description: "Three Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 30, description: "Three Three", group: Group.three.data),
+ MultiBarChartDataPoint(value: 90, description: "Three Four" , group: Group.four.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data),
+ MultiBarChartDataPoint(value: 80, description: "Four Two" , group: Group.two.data),
+ MultiBarChartDataPoint(value: 20, description: "Four Three" , group: Group.three.data),
+ MultiBarChartDataPoint(value: 50, description: "Four Four" , group: Group.four.data)
+ ])
+ ])
+
+ // MARK: - Data
+ func testStackedBarMaxValue() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.maxValue, 90)
+ }
+ func testStackedBarMinValue() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.minValue, 10)
+ }
+ func testStackedBarAverage() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.average, 45)
+ }
+ func testStackedBarRange() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+ XCTAssertEqual(chartData.range, 80.001)
+ }
+
+ // MARK: Greater
+ func testStackedBarIsGreaterThanTwoTrue() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+
+ XCTAssertTrue(chartData.isGreaterThanTwo())
+ }
+
+ func testStackedBarIsGreaterThanTwoFalse() {
+ let data = MultiBarDataSets(dataSets: [
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 30, description: "Three One", group: Group.one.data)
+ ]),
+
+ MultiBarDataSet(dataPoints: [
+ MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data)
+ ])
+ ])
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+ XCTAssertFalse(chartData.isGreaterThanTwo())
+ }
+
+ // MARK: Labels
+ func testStackedBarGetYLabels() {
+ let chartData = StackedBarChartData(dataSets: data, groups: groups,
+ chartStyle: BarChartStyle(yAxisNumberOfLabels: 3))
+
+ chartData.chartStyle.topLine = .maximumValue
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.000, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 45.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumValue
+ XCTAssertEqual(chartData.getYLabels()[0], 10.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumWithMaximum(of: 5)
+ XCTAssertEqual(chartData.getYLabels()[0], 5.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 47.50, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 90.00, accuracy: 0.01)
+
+ chartData.chartStyle.topLine = .maximum(of: 100)
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+
+ }
+ // MARK: - Touch
+ func testStackedBarGetDataPoint() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+
+ // Stack 1 - Point 2
+ let touchLocationOneTwo: CGPoint = CGPoint(x: 5, y: 95)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOneTwo, chartSize: rect)
+ let testOutputOneTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstOneTwo = chartData.dataSets.dataSets[0].dataPoints
+ XCTAssertEqual(testOutputOneTwo[0], testAgainstOneTwo[1])
+
+ // Stack 1 - Point 4
+ let touchLocationOneFour: CGPoint = CGPoint(x: 5, y: 60)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOneFour, chartSize: rect)
+ let testOutputOneFour = chartData.infoView.touchOverlayInfo
+ let testAgainstOneFour = chartData.dataSets.dataSets[0].dataPoints
+ XCTAssertEqual(testOutputOneFour[0], testAgainstOneFour[3])
+
+ // Stack 2 - Point 1
+ let touchLocationTwoOne: CGPoint = CGPoint(x: 30, y: 95)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwoOne, chartSize: rect)
+ let testOutputTwoOne = chartData.infoView.touchOverlayInfo
+ let testAgainstTwoOne = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputTwoOne[0], testAgainstTwoOne[0])
+
+ // Stack 2 - Point 3
+ let touchLocationTwoThree: CGPoint = CGPoint(x: 30, y: 66)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwoThree, chartSize: rect)
+ let testOutputTwoThree = chartData.infoView.touchOverlayInfo
+ let testAgainstTwoThree = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputTwoThree[0], testAgainstTwoThree[2])
+
+ // Stack 3 - Point 1
+ let touchLocationThreeOne: CGPoint = CGPoint(x: 55, y: 95)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThreeOne, chartSize: rect)
+ let testOutputThreeOne = chartData.infoView.touchOverlayInfo
+ let testAgainstThreeOne = chartData.dataSets.dataSets[2].dataPoints
+ XCTAssertEqual(testOutputThreeOne[0], testAgainstThreeOne[0])
+
+ // Stack 3 - Point 4
+ let touchLocationThreeFour: CGPoint = CGPoint(x: 55, y: 10)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThreeFour, chartSize: rect)
+ let testOutputThreeFour = chartData.infoView.touchOverlayInfo
+ let testAgainstThreeFour = chartData.dataSets.dataSets[2].dataPoints
+ XCTAssertEqual(testOutputThreeFour[0], testAgainstThreeFour[3])
+
+ // Stack 4 - Point 2
+ let touchLocationFourTwo: CGPoint = CGPoint(x: 83, y: 50)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFourTwo, chartSize: rect)
+ let testOutputFourTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstFourTwo = chartData.dataSets.dataSets[3].dataPoints
+ XCTAssertEqual(testOutputFourTwo[0], testAgainstFourTwo[1])
+
+ // Stack 4 - Point 3
+ let touchLocationFourThree: CGPoint = CGPoint(x: 83, y: 40)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFourThree, chartSize: rect)
+ let testOutputFourThree = chartData.infoView.touchOverlayInfo
+ let testAgainstFourThree = chartData.dataSets.dataSets[3].dataPoints
+ XCTAssertEqual(testOutputFourThree[0], testAgainstFourThree[2])
+ }
+
+ func testStackedBarGetPointLocation() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = StackedBarChartData(dataSets: data, groups: groups)
+
+ // Stack 1 - Point 2
+ let touchLocationOneTwo: CGPoint = CGPoint(x: 5, y: 95)
+ let testOneTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationOneTwo,
+ chartSize: rect)!
+ let testAgainstOneTwo = CGPoint(x: 12.50, y: 74.35)
+ XCTAssertEqual(testOneTwo.x, testAgainstOneTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testOneTwo.y, testAgainstOneTwo.y, accuracy: 0.01)
+
+ // Stack 1 - Point 4
+ let touchLocationOneFour: CGPoint = CGPoint(x: 5, y: 60)
+ let testOneFour: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationOneFour,
+ chartSize: rect)!
+ let testAgainstOneFour = CGPoint(x: 12.50, y: 44.44)
+ XCTAssertEqual(testOneFour.x, testAgainstOneFour.x, accuracy: 0.01)
+ XCTAssertEqual(testOneFour.y, testAgainstOneFour.y, accuracy: 0.01)
+
+ // Stack 2 - Point 1
+ let touchLocationTwoOne: CGPoint = CGPoint(x: 30, y: 95)
+ let testTwoOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationTwoOne,
+ chartSize: rect)!
+ let testAgainstTwoOne = CGPoint(x: 37.50, y: 92.59)
+ XCTAssertEqual(testTwoOne.x, testAgainstTwoOne.x, accuracy: 0.01)
+ XCTAssertEqual(testTwoOne.y, testAgainstTwoOne.y, accuracy: 0.01)
+
+ // Stack 2 - Point 3
+ let touchLocationTwoThree: CGPoint = CGPoint(x: 30, y: 66)
+ let testTwoThree: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationTwoThree,
+ chartSize: rect)!
+ let testAgainstTwoThree = CGPoint(x: 37.50, y: 55.55)
+ XCTAssertEqual(testTwoThree.x, testAgainstTwoThree.x, accuracy: 0.01)
+ XCTAssertEqual(testTwoThree.y, testAgainstTwoThree.y, accuracy: 0.01)
+
+ // Stack 3 - Point 1
+ let touchLocationThreeOne: CGPoint = CGPoint(x: 55, y: 95)
+ let testThreeOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationThreeOne,
+ chartSize: rect)!
+ let testAgainstThreeOne = CGPoint(x: 62.50, y: 86.36)
+ XCTAssertEqual(testThreeOne.x, testAgainstThreeOne.x, accuracy: 0.01)
+ XCTAssertEqual(testThreeOne.y, testAgainstThreeOne.y, accuracy: 0.01)
+
+ // Stack 3 - Point 4
+ let touchLocationThreeFour: CGPoint = CGPoint(x: 55, y: 10)
+ let testThreeFour: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationThreeFour,
+ chartSize: rect)!
+ let testAgainstThreeFour = CGPoint(x: 62.50, y: 0.00)
+ XCTAssertEqual(testThreeFour.x, testAgainstThreeFour.x, accuracy: 0.01)
+ XCTAssertEqual(testThreeFour.y, testAgainstThreeFour.y, accuracy: 0.01)
+
+ // Stack 4 - Point 2
+ let touchLocationFourTwo: CGPoint = CGPoint(x: 83, y: 50)
+ let testFourTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationFourTwo,
+ chartSize: rect)!
+ let testAgainstFourTwo = CGPoint(x: 87.50, y: 43.85)
+ XCTAssertEqual(testFourTwo.x, testAgainstFourTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testFourTwo.y, testAgainstFourTwo.y, accuracy: 0.01)
+
+ // Stack 4 - Point 3
+ let touchLocationFourThree: CGPoint = CGPoint(x: 83, y: 40)
+ let testFourThree: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationFourThree,
+ chartSize: rect)!
+ let testAgainstFourThree = CGPoint(x: 87.50, y: 34.50)
+ XCTAssertEqual(testFourThree.x, testAgainstFourThree.x, accuracy: 0.01)
+ XCTAssertEqual(testFourThree.y, testAgainstFourThree.y, accuracy: 0.01)
+ }
+
+ // MARK: - All Tests
+ static var allTests = [
+ // Data
+ ("testStackedBarMaxValue", testStackedBarMaxValue),
+ ("testStackedBarMinValue", testStackedBarMinValue),
+ ("testStackedBarAverage", testStackedBarAverage),
+ ("testStackedBarRange", testStackedBarRange),
+ // Greater
+ ("testStackedBarIsGreaterThanTwoTrue", testStackedBarIsGreaterThanTwoTrue),
+ ("testStackedBarIsGreaterThanTwoFalse", testStackedBarIsGreaterThanTwoFalse),
+ // Labels
+ ("testStackedBarGetYLabels", testStackedBarGetYLabels),
+ // Touch
+ ("testStackedBarGetDataPoint", testStackedBarGetDataPoint),
+ ("testStackedBarGetPointLocation", testStackedBarGetPointLocation),
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift
new file mode 100644
index 00000000..76f71674
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift
@@ -0,0 +1,127 @@
+import XCTest
+import SwiftUI
+@testable import SwiftUICharts
+
+final class LineChartPathTests: XCTestCase {
+
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 0),
+ LineChartDataPoint(value: 25),
+ LineChartDataPoint(value: 50),
+ LineChartDataPoint(value: 75),
+ LineChartDataPoint(value: 100)
+ ]))
+
+ let rect : CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let touchLocation: CGPoint = CGPoint(x: 25, y: 25)
+
+ func testGetIndicatorLocation() {
+
+ let test = LineChartData.getIndicatorLocation(rect: rect,
+ dataPoints: chartData.dataSets.dataPoints,
+ touchLocation: touchLocation,
+ lineType: .line,
+ minValue: chartData.minValue,
+ range: chartData.range)
+
+ XCTAssertEqual(test.x, 25, accuracy: 0.1)
+ XCTAssertEqual(test.y, 75, accuracy: 0.1)
+ }
+
+
+ func testGetPercentageOfPath() {
+
+ let path = Path.straightLine(rect : rect,
+ dataPoints : chartData.dataSets.dataPoints,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ isFilled : false)
+
+ let test = LineChartData.getPercentageOfPath(path: path, touchLocation: touchLocation)
+
+ XCTAssertEqual(test, 0.25, accuracy: 0.1)
+ }
+
+ func testGetTotalLength() {
+
+ let path = Path.straightLine(rect : rect,
+ dataPoints : chartData.dataSets.dataPoints,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ isFilled : false)
+
+ let test = LineChartData.getTotalLength(of: path)
+
+ XCTAssertEqual(test, 141.42, accuracy: 0.01)
+ }
+
+ func testGetLengthToTouch() {
+
+ let path = Path.straightLine(rect : rect,
+ dataPoints : chartData.dataSets.dataPoints,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ isFilled : false)
+
+ let test = LineChartData.getLength(to: touchLocation, on: path)
+
+ XCTAssertEqual(test, 35.35, accuracy: 0.01)
+ }
+
+ func testRelativePoint() {
+
+ let pointOne = CGPoint(x: 0.0, y: 0.0)
+ let pointTwo = CGPoint(x: 100, y: 100)
+
+ let test = LineChartData.relativePoint(from: pointOne, to: pointTwo, touchX: touchLocation.x)
+
+ XCTAssertEqual(test.x, 25, accuracy: 0.01)
+ XCTAssertEqual(test.y, 25, accuracy: 0.01)
+ }
+
+ func testDistanceToTouch() {
+
+ let pointOne = CGPoint(x: 0.0, y: 0.0)
+ let pointTwo = CGPoint(x: 100, y: 100)
+
+ let test = LineChartData.distanceToTouch(from: pointOne, to: pointTwo, touchX: touchLocation.x)
+
+ XCTAssertEqual(test, 35.355, accuracy: 0.01)
+ }
+
+ func testDistance() {
+
+ let pointOne = CGPoint(x: 0.0, y: 0.0)
+ let pointTwo = CGPoint(x: 100, y: 100)
+
+ let test = LineChartData.distance(from: pointOne, to: pointTwo)
+
+ XCTAssertEqual(test, 141.421356237309, accuracy: 0.01)
+ }
+
+ func testGetLocationOnPath() {
+
+ let path = Path.straightLine(rect : rect,
+ dataPoints : chartData.dataSets.dataPoints,
+ minValue : chartData.minValue,
+ range : chartData.range,
+ isFilled : false)
+
+
+ let test = LineChartData.locationOnPath(0.5, path)
+
+ XCTAssertEqual(test.x, 50, accuracy: 0.1)
+ XCTAssertEqual(test.y, 50, accuracy: 0.1)
+ }
+
+ static var allTests = [
+ ("testGetIndicatorLocation", testGetIndicatorLocation),
+ ("testGetPercentageOfPath", testGetPercentageOfPath),
+ ("testGetTotalLength", testGetTotalLength),
+ ("testGetLengthToTouch", testGetLengthToTouch),
+ ("testRelativePoint", testRelativePoint),
+ ("testDistanceToTouch", testDistanceToTouch),
+ ("testDistance", testDistance),
+ ("testGetLocationOnPath", testGetLocationOnPath)
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift
new file mode 100644
index 00000000..6dde6a4e
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift
@@ -0,0 +1,145 @@
+import XCTest
+@testable import SwiftUICharts
+
+final class LineChartTests: XCTestCase {
+
+ // MARK: - Set Up
+ let dataPoints = [
+ LineChartDataPoint(value: 10),
+ LineChartDataPoint(value: 50),
+ LineChartDataPoint(value: 40),
+ LineChartDataPoint(value: 80)
+ ]
+
+ // MARK: - Data
+ func testLineMaxValue() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.maxValue, 80)
+ }
+ func testLineMinValue() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.minValue, 10)
+ }
+ func testLineAverage() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.average, 45)
+ }
+ func testLineRange() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertEqual(chartData.range, 70.001)
+ }
+ func testLineIsGreaterThanTwoTrue() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertTrue(chartData.isGreaterThanTwo())
+ }
+ func testLineIsGreaterThanTwoFalse() {
+ let dataPoints = [
+ LineChartDataPoint(value: 10),
+ LineChartDataPoint(value: 60)
+ ]
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+ XCTAssertFalse(chartData.isGreaterThanTwo())
+ }
+
+ // MARK: - Labels
+ func testLineGetYLabels() {
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints),
+ chartStyle: LineChartStyle(yAxisNumberOfLabels: 3))
+
+ chartData.chartStyle.topLine = .maximumValue
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.000, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 40.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 80.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumValue
+ XCTAssertEqual(chartData.getYLabels()[0], 10.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 45.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 80.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumWithMaximum(of: 5)
+ XCTAssertEqual(chartData.getYLabels()[0], 5.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 42.50, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 80.00, accuracy: 0.01)
+
+ chartData.chartStyle.topLine = .maximum(of: 100)
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+ }
+
+
+ // MARK: - Touch
+ func testLineGetDataPoint() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+
+ let touchLocationOne: CGPoint = CGPoint(x: 5, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOne, chartSize: rect)
+ let testOutputOne = chartData.infoView.touchOverlayInfo
+ let testAgainstOne = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputOne[0], testAgainstOne[0])
+
+ let touchLocationTwo: CGPoint = CGPoint(x: 25, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwo, chartSize: rect)
+ let testOutputTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstTwo = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputTwo[0], testAgainstTwo[1])
+
+ let touchLocationThree: CGPoint = CGPoint(x: 50, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThree, chartSize: rect)
+ let testOutputThree = chartData.infoView.touchOverlayInfo
+ let testAgainstThree = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputThree[0], testAgainstThree[2])
+
+ let touchLocationFour: CGPoint = CGPoint(x: 85, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFour, chartSize: rect)
+ let testOutputFour = chartData.infoView.touchOverlayInfo
+ let testAgainstFour = chartData.dataSets.dataPoints
+ XCTAssertEqual(testOutputFour[0], testAgainstFour[3])
+ }
+
+ func testLineGetPointLocation() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints))
+
+ // Data point 1
+ let touchLocationOne: CGPoint = CGPoint(x: 5, y: 25)
+ let testOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationOne,
+ chartSize: rect)!
+ let testAgainstOne = CGPoint(x: 0, y: 100)
+ XCTAssertEqual(testOne.x, testAgainstOne.x)
+ XCTAssertEqual(testOne.y, testAgainstOne.y)
+
+ // Data point 3
+ let touchLocationTwo: CGPoint = CGPoint(x: 66, y: 25)
+ let testTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets,
+ touchLocation: touchLocationTwo,
+ chartSize: rect)!
+ let testAgainstTwo = CGPoint(x: 66.66, y: 57.14)
+ XCTAssertEqual(testTwo.x, testAgainstTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testTwo.y, testAgainstTwo.y, accuracy: 0.01)
+ }
+
+ // MARK: - All Tests
+ static var allTests = [
+ // Data
+ ("testLineMaxValue", testLineMaxValue),
+ ("testLineMinValue", testLineMinValue),
+ ("testLineAverage", testLineAverage),
+ ("testLineRange", testLineRange),
+ ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue),
+ ("testLineIsGreaterThanTwoFalse", testLineIsGreaterThanTwoFalse),
+ // Labels
+ ("testLineGetYLabels", testLineGetYLabels),
+ // Touch
+ ("testLineGetDataPoint", testLineGetDataPoint),
+ ("testLineGetPointLocation", testLineGetPointLocation),
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift
new file mode 100644
index 00000000..57997dfb
--- /dev/null
+++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift
@@ -0,0 +1,189 @@
+import XCTest
+@testable import SwiftUICharts
+
+final class MultiLineChartTest: XCTestCase {
+
+ // MARK: - Set Up
+ let dataSet = MultiLineDataSet(dataSets: [
+ LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 10),
+ LineChartDataPoint(value: 40),
+ LineChartDataPoint(value: 30),
+ LineChartDataPoint(value: 60)
+ ]),
+ LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 50),
+ LineChartDataPoint(value: 60),
+ LineChartDataPoint(value: 80),
+ LineChartDataPoint(value: 100)
+ ])
+ ])
+
+ // MARK: - Data
+ func testMultiLineMaxValue() {
+ let chartData = MultiLineChartData(dataSets: dataSet)
+
+ XCTAssertEqual(chartData.maxValue, 100)
+ }
+ func testMultiLineMinValue() {
+ let chartData = MultiLineChartData(dataSets: dataSet)
+ XCTAssertEqual(chartData.minValue, 10)
+ }
+ func testMultiLineAverage() {
+ let chartData = MultiLineChartData(dataSets: dataSet)
+ XCTAssertEqual(chartData.average, 53.75)
+ }
+ func testMultiLineRange() {
+ let chartData = MultiLineChartData(dataSets: dataSet)
+ XCTAssertEqual(chartData.range, 90.001)
+ }
+ // MARK: Greater
+ func testMultiIsGreaterThanTwoTrue() {
+ let chartData = MultiLineChartData(dataSets: dataSet)
+ XCTAssertTrue(chartData.isGreaterThanTwo())
+ }
+
+ func testMultiIsGreaterThanTwoFalse() {
+ let chartData = MultiLineChartData(dataSets:
+ MultiLineDataSet(dataSets: [
+ LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 10),
+ ]),
+ LineDataSet(dataPoints: [
+ LineChartDataPoint(value: 50)
+ ])
+ ]))
+ XCTAssertFalse(chartData.isGreaterThanTwo())
+ }
+
+ // MARK: - Labels
+ func testMultiLineGetYLabels() {
+ let chartData = MultiLineChartData(dataSets: dataSet,
+ chartStyle: LineChartStyle(yAxisNumberOfLabels: 3))
+
+ chartData.chartStyle.topLine = .maximumValue
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.000, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumValue
+ XCTAssertEqual(chartData.getYLabels()[0], 10.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 55.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+
+ chartData.chartStyle.baseline = .minimumWithMaximum(of: 5)
+ XCTAssertEqual(chartData.getYLabels()[0], 5.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 52.50, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+
+ chartData.chartStyle.topLine = .maximum(of: 100)
+ chartData.chartStyle.baseline = .zero
+ XCTAssertEqual(chartData.getYLabels()[0], 0.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[1], 50.00, accuracy: 0.01)
+ XCTAssertEqual(chartData.getYLabels()[2], 100.00, accuracy: 0.01)
+ }
+
+ // MARK: - Touch
+ func testMultiLineGetDataPoint() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = MultiLineChartData(dataSets: dataSet)
+
+ let touchLocationOne: CGPoint = CGPoint(x: 5, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationOne, chartSize: rect)
+ let testOutputOne = chartData.infoView.touchOverlayInfo
+ let testAgainstOneOne = chartData.dataSets.dataSets[0].dataPoints
+ let testAgainstOneTwo = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputOne[0], testAgainstOneOne[0])
+ XCTAssertEqual(testOutputOne[1], testAgainstOneTwo[0])
+
+ let touchLocationTwo: CGPoint = CGPoint(x: 25, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationTwo, chartSize: rect)
+ let testOutputTwo = chartData.infoView.touchOverlayInfo
+ let testAgainstTwoOne = chartData.dataSets.dataSets[0].dataPoints
+ let testAgainstTwoTwo = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputTwo[0], testAgainstTwoOne[1])
+ XCTAssertEqual(testOutputTwo[1], testAgainstTwoTwo[1])
+
+ let touchLocationThree: CGPoint = CGPoint(x: 50, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationThree, chartSize: rect)
+ let testOutputThree = chartData.infoView.touchOverlayInfo
+ let testAgainstThreeOne = chartData.dataSets.dataSets[0].dataPoints
+ let testAgainstThreeTwo = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputThree[0], testAgainstThreeOne[2])
+ XCTAssertEqual(testOutputThree[1], testAgainstThreeTwo[2])
+
+ let touchLocationFour: CGPoint = CGPoint(x: 85, y: 25)
+ chartData.infoView.touchOverlayInfo = []
+ chartData.getDataPoint(touchLocation: touchLocationFour, chartSize: rect)
+ let testOutputFour = chartData.infoView.touchOverlayInfo
+ let testAgainstFourOne = chartData.dataSets.dataSets[0].dataPoints
+ let testAgainstFourTwo = chartData.dataSets.dataSets[1].dataPoints
+ XCTAssertEqual(testOutputFour[0], testAgainstFourOne[3])
+ XCTAssertEqual(testOutputFour[1], testAgainstFourTwo[3])
+ }
+
+ func testMultiLineGetPointLocation() {
+ let rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100)
+ let chartData = MultiLineChartData(dataSets: dataSet)
+
+ // Data set 1 - point 1
+ let touchLocationOneOne: CGPoint = CGPoint(x: 5, y: 25)
+ let testOneOne: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets.dataSets[0],
+ touchLocation: touchLocationOneOne,
+ chartSize: rect)!
+ let testAgainstOneOne = CGPoint(x: 0, y: 100)
+ XCTAssertEqual(testOneOne.x, testAgainstOneOne.x)
+ XCTAssertEqual(testOneOne.y, testAgainstOneOne.y)
+
+ // Data set 1 - point 3
+ let touchLocationOneThree: CGPoint = CGPoint(x: 66, y: 25)
+ let testOneThree: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets.dataSets[0],
+ touchLocation: touchLocationOneThree,
+ chartSize: rect)!
+ let testAgainstOneThree = CGPoint(x: 66.66, y: 77.77)
+ XCTAssertEqual(testOneThree.x, testAgainstOneThree.x, accuracy: 0.01)
+ XCTAssertEqual(testOneThree.y, testAgainstOneThree.y, accuracy: 0.01)
+
+
+
+
+ // Data set 2 - point 2
+ let touchLocationTwoTwo: CGPoint = CGPoint(x: 66, y: 25)
+ let testTwoTwo: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets.dataSets[0],
+ touchLocation: touchLocationTwoTwo,
+ chartSize: rect)!
+ let testAgainstTwoTwo = CGPoint(x: 66.66, y: 77.77)
+ XCTAssertEqual(testTwoTwo.x, testAgainstTwoTwo.x, accuracy: 0.01)
+ XCTAssertEqual(testTwoTwo.y, testAgainstTwoTwo.y, accuracy: 0.01)
+
+ // Data set 2 - point 4
+ let touchLocationTwoFour: CGPoint = CGPoint(x: 5, y: 25)
+ let testTwoFour: CGPoint = chartData.getPointLocation(dataSet: chartData.dataSets.dataSets[0],
+ touchLocation: touchLocationTwoFour,
+ chartSize: rect)!
+ let testAgainstTwoFour = CGPoint(x: 0, y: 100)
+ XCTAssertEqual(testTwoFour.x, testAgainstTwoFour.x)
+ XCTAssertEqual(testTwoFour.y, testAgainstTwoFour.y)
+ }
+
+ // MARK: - All Tests
+ static var allTests = [
+ // Data
+ ("testMultiLineMaxValue", testMultiLineMaxValue),
+ ("testMultiLineMinValue", testMultiLineMinValue),
+ ("testMultiLineAverage" , testMultiLineAverage),
+ ("testMultiLineRange" , testMultiLineRange),
+ // Greater
+ ("testMultiLineIsGreaterThanTwoTrue" , testMultiIsGreaterThanTwoTrue),
+ ("testMultiLineIsGreaterThanTwoFalse", testMultiIsGreaterThanTwoFalse),
+ // Labels
+ ("testMultiLineGetYLabels" , testMultiLineGetYLabels),
+ // Touch
+ ("testMultiLineGetDataPoint", testMultiLineGetDataPoint),
+ ("testMultiLineGetPointLocation", testMultiLineGetPointLocation),
+ ]
+}
diff --git a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift
deleted file mode 100644
index 55d9318b..00000000
--- a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift
+++ /dev/null
@@ -1,120 +0,0 @@
-import XCTest
-@testable import SwiftUICharts
-
-final class SwiftUIChartsTests: XCTestCase {
-
- // MARK: - ChartData
- func testMaxValue() {
- let dataPoints = [
- ChartDataPoint(value: 10),
- ChartDataPoint(value: 40),
- ChartDataPoint(value: 30),
- ChartDataPoint(value: 60)
- ]
- let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle())
-
- XCTAssertEqual(chartData.maxValue(), 60)
- }
- func testMinValue() {
- let dataPoints = [
- ChartDataPoint(value: 10),
- ChartDataPoint(value: 40),
- ChartDataPoint(value: 30),
- ChartDataPoint(value: 60)
- ]
- let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle())
-
- XCTAssertEqual(chartData.minValue(), 10)
- }
- func testAverage() {
- let dataPoints = [
- ChartDataPoint(value: 10),
- ChartDataPoint(value: 40),
- ChartDataPoint(value: 30),
- ChartDataPoint(value: 60)
- ]
- let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle())
-
- XCTAssertEqual(chartData.average(), 35)
- }
- func testRange() {
- let dataPoints = [
- ChartDataPoint(value: 10),
- ChartDataPoint(value: 40),
- ChartDataPoint(value: 30),
- ChartDataPoint(value: 60)
- ]
- let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle())
-
- XCTAssertEqual(chartData.range(), 50.001)
- }
- // MARK: - Helper
- func testMonthlyAverage() {
- let calendar = Calendar.current
-
- let formatterForXAxisLabel = DateFormatter()
- formatterForXAxisLabel.locale = .current
- formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("MMM")
-
- let formatterForPointLabel = DateFormatter()
- formatterForXAxisLabel.locale = .current
- formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY")
-
- let components = DateComponents(year: 2021, month: 01, day: 01, hour: 10, minute: 0, second: 0)
- let date = calendar.date(from: components)!
-
- let monthOne = calendar.date(byAdding: .month, value: 0, to: date)!
- let monthTwo = calendar.date(byAdding: .month, value: 1, to: date)!
- let monthThree = calendar.date(byAdding: .month, value: 2, to: date)!
- let monthFour = calendar.date(byAdding: .month, value: 3, to: date)!
-
- let dataPoints = [
- ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 0, to: monthOne)),
- ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 5, to: monthOne)),
- ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 15, to: monthOne)),
- ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 25, to: monthOne)),
-
- ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 0, to: monthTwo)),
- ChartDataPoint(value: 50, date: calendar.date(byAdding: .day, value: 5, to: monthTwo)),
- ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 15, to: monthTwo)),
- ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 25, to: monthTwo)),
-
- ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 0, to: monthThree)),
- ChartDataPoint(value: 50, date: calendar.date(byAdding: .day, value: 5, to: monthThree)),
- ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 15, to: monthThree)),
- ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 25, to: monthThree)),
-
- ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 0, to: monthFour)),
- ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 5, to: monthFour)),
- ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 15, to: monthFour)),
- ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 25, to: monthFour))
- ]
-
- XCTAssertEqual(Calculations.monthlyAverage(dataPoints: dataPoints), [
- ChartDataPoint(value: 35,
- xAxisLabel: formatterForXAxisLabel.string(from: monthOne),
- pointLabel: formatterForPointLabel.string(from: monthOne)),
- ChartDataPoint(value: 52.50,
- xAxisLabel: formatterForXAxisLabel.string(from: monthTwo),
- pointLabel: formatterForPointLabel.string(from: monthTwo)),
- ChartDataPoint(value: 47.5,
- xAxisLabel: formatterForXAxisLabel.string(from: monthThree),
- pointLabel: formatterForPointLabel.string(from: monthThree)),
- ChartDataPoint(value: 35,
- xAxisLabel: formatterForXAxisLabel.string(from: monthFour),
- pointLabel: formatterForPointLabel.string(from: monthFour)),
- ])
- }
-
-
-
- static var allTests = [
- // Chart Data
- ("testMaxValue", testMaxValue),
- ("testMinValue", testMinValue),
- ("testAverage", testAverage),
- ("testRange", testRange),
- ("testMonthlyAverage", testMonthlyAverage)
- // Helper
- ]
-}
diff --git a/Tests/SwiftUIChartsTests/XCTestManifests.swift b/Tests/SwiftUIChartsTests/XCTestManifests.swift
index a3999a87..682d94a1 100644
--- a/Tests/SwiftUIChartsTests/XCTestManifests.swift
+++ b/Tests/SwiftUIChartsTests/XCTestManifests.swift
@@ -3,7 +3,13 @@ import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
- testCase(SwiftUIChartsTests.allTests),
+ testCase(LineChartTests.allTests),
+ testCase(MultiLineChartTests.allTests),
+ testCase(BarChartTests.allTests),
+ testCase(GroupedChartTests.allTests),
+ testCase(StackedChartTests.allTests),
+
+ testCase(LineChartPathTests.allTests),
]
}
#endif