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